UNPKG

jinaga

Version:

Data management for web and mobile applications.

160 lines (131 loc) 6.14 kB
import { FactEnvelope, FactRecord, FactReference, Queue } from '../storage'; import { execRequest, factKey, withDatabase, withTransaction } from './driver'; import { distinct, flattenAsync } from '../util/fn'; import { TopologicalSorter } from '../fact/sorter'; export class IndexedDBQueue implements Queue { constructor( private indexName: string ) { } peek(): Promise<FactEnvelope[]> { return withDatabase(this.indexName, db => withTransaction(db, ['queue', 'fact', 'ancestor'], 'readonly', async tx => { const queueObjectStore = tx.objectStore('queue'); const factObjectStore = tx.objectStore('fact'); const ancestorObjectStore = tx.objectStore('ancestor'); // Get all envelopes from the queue const queuedEnvelopes = await execRequest<FactEnvelope[]>(queueObjectStore.getAll()); // If queue is empty, return empty array if (queuedEnvelopes.length === 0) { return []; } // Get references to all queued facts const queuedReferences = queuedEnvelopes.map(envelope => ({ type: envelope.fact.type, hash: envelope.fact.hash })); // Get all ancestors of queued facts const allAncestors = await flattenAsync(queuedReferences, reference => execRequest<string[]>(ancestorObjectStore.get(factKey(reference)))); // Remove duplicates and queued facts from ancestors const queuedKeys = new Set(queuedReferences.map(factKey)); const distinctAncestorKeys = allAncestors .filter(distinct) .filter(key => !queuedKeys.has(key)); // Get fact records for all ancestors const ancestorEnvelopes = await Promise.all(distinctAncestorKeys.map<Promise<FactEnvelope>>(async key => ({ fact: await execRequest<FactRecord>(factObjectStore.get(key)), signatures: [] }))); // Combine queued envelopes and ancestor envelopes const allEnvelopes = [...queuedEnvelopes, ...ancestorEnvelopes]; // Sort envelopes in topological order return this.sortAndValidateEnvelopes(allEnvelopes); }) ); } private sortAndValidateEnvelopes(envelopes: FactEnvelope[]): FactEnvelope[] { // Extract facts from envelopes const facts = envelopes.map(envelope => envelope.fact); // Create a map from fact key to envelope for quick lookup const envelopeMap = new Map<string, FactEnvelope>(); envelopes.forEach(envelope => { envelopeMap.set(factKey(envelope.fact), envelope); }); // Use TopologicalSorter to sort facts const sorter = new TopologicalSorter<string>(); const sortedKeys = sorter.sort(facts, (_, fact) => factKey(fact)); // Check if sorting was successful (no circular dependencies) if (!sorter.finished()) { throw new Error(`Circular dependencies detected in the fact queue. Some facts have predecessors that depend on them, creating a cycle.`); } // Validate the topological ordering this.validateTopologicalOrdering(sortedKeys, facts); // Convert sorted keys back to envelopes return sortedKeys.map(key => envelopeMap.get(key)!); } private validateTopologicalOrdering(sortedKeys: string[], facts: FactRecord[]): void { // Create a map of fact keys to their positions in the sorted list const positionMap = new Map<string, number>(); sortedKeys.forEach((key, index) => { positionMap.set(key, index); }); // Create a map of fact keys to facts for quick lookup const factMap = new Map<string, FactRecord>(); facts.forEach(fact => { factMap.set(factKey(fact), fact); }); // Validate that for each fact, all its predecessors appear earlier in the list for (let i = 0; i < sortedKeys.length; i++) { const key = sortedKeys[i]; const fact = factMap.get(key); if (!fact) { throw new Error(`Internal error: Fact with key ${key} not found in fact map.`); } const predecessors = this.getAllPredecessors(fact); for (const predecessor of predecessors) { const predKey = factKey(predecessor); const predPosition = positionMap.get(predKey); // If predecessor is not in the list, it's an error if (predPosition === undefined) { throw new Error(`Missing predecessor: ${predKey} for fact ${key}`); } // If predecessor appears after the current fact, it's a topological ordering violation if (predPosition >= i) { throw new Error(`Topological ordering violation: ${predKey} (position ${predPosition}) should appear before ${key} (position ${i})`); } } } } private getAllPredecessors(fact: FactRecord): FactReference[] { const predecessors: FactReference[] = []; for (const role in fact.predecessors) { const references = fact.predecessors[role]; if (Array.isArray(references)) { predecessors.push(...references); } else { predecessors.push(references); } } return predecessors; } enqueue(envelopes: FactEnvelope[]): Promise<void> { return withDatabase(this.indexName, db => withTransaction(db, ['queue'], 'readwrite', async tx => { const queueObjectStore = tx.objectStore('queue'); await Promise.all(envelopes.map(envelope => execRequest(queueObjectStore.put(envelope, factKey(envelope.fact))) )); }) ); } dequeue(envelopes: FactEnvelope[]): Promise<void> { return withDatabase(this.indexName, db => withTransaction(db, ['queue'], 'readwrite', async tx => { const queueObjectStore = tx.objectStore('queue'); await Promise.all(envelopes.map(envelope => execRequest(queueObjectStore.delete(factKey(envelope.fact))) )); }) ); } }