UNPKG

jinaga

Version:

Data management for web and mobile applications.

377 lines 21.4 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ObserverImpl = void 0; const hash_1 = require("../fact/hash"); const description_1 = require("../specification/description"); const inverse_1 = require("../specification/inverse"); const storage_1 = require("../storage"); const encoding_1 = require("../util/encoding"); const trace_1 = require("../util/trace"); class ObserverImpl { constructor(factManager, given, specification, resultAdded) { this.factManager = factManager; this.given = given; this.specification = specification; this.listeners = []; this.removalsByTuple = {}; this.notifiedTuples = new Set(); this.addedHandlers = []; this.feeds = []; this.stopped = false; this.listenersAdded = false; /** * Tracks all pending notification promises to enable waiting for processing completion. */ this.pendingNotifications = new Set(); /** * Buffers results that are pending delivery to result handlers. * * The key is a string in the format `path|tupleHash`, where: * - `path` is a string representing the traversal path in the specification. * - `tupleHash` is the hash of the tuple of fact references for the current context. * * The value is an object containing: * - `projection`: The projection associated with the results. * - `parentSubset`: The parent subset of fact references. * - `results`: The buffered results (of type `ProjectedResult[]`) to be replayed to handlers when they are registered. * * This map is used to buffer results that are produced before any handlers are registered, * enabling replay of results to late-registered handlers. */ this.pendingAddsByKey = new Map(); // Map the given facts to a tuple. const tuple = specification.given.reduce((tuple, label, index) => (Object.assign(Object.assign({}, tuple), { [label.label.name]: given[index] })), {}); this.givenHash = (0, hash_1.computeObjectHash)(tuple); // Add the initial handler. this.addedHandlers.push({ path: "", tupleHash: this.givenHash, handler: resultAdded }); // Identify the specification by its hash. const declarationString = (0, description_1.describeDeclaration)(given, specification.given.map(g => g.label)); const specificationString = (0, description_1.describeSpecification)(specification, 0); const request = `${declarationString}\n${specificationString}`; this.specificationHash = (0, encoding_1.computeStringHash)(request); } start(keepAlive) { const givenTypes = this.given.map(g => g.type).join(', '); trace_1.Trace.info(`[Observer] START - Spec hash: ${this.specificationHash.substring(0, 8)}..., Given hash: ${this.givenHash.substring(0, 8)}..., Given types: [${givenTypes}], KeepAlive: ${keepAlive}`); this.cachedPromise = new Promise((cacheResolve, _) => { this.loadedPromise = new Promise((loadResolve, loadReject) => __awaiter(this, void 0, void 0, function* () { try { // Ensure listeners are added BEFORE any read/fetch to close T2–T3 window. if (!this.listenersAdded) { this.addSpecificationListeners(); } const mruDate = yield this.factManager.getMruDate(this.specificationHash); if (mruDate === null) { trace_1.Trace.info(`[Observer] Not cached - Spec hash: ${this.specificationHash.substring(0, 8)}..., will fetch then read`); // The data is not yet cached. cacheResolve(false); // Fetch from the server and then read from local storage. yield this.fetch(keepAlive); yield this.read(); loadResolve(); } else { trace_1.Trace.info(`[Observer] Cached (MRU: ${mruDate.toISOString()}) - Spec hash: ${this.specificationHash.substring(0, 8)}..., will read then fetch`); // Read from local storage into the cache. yield this.read(); cacheResolve(true); // Then fetch from the server to update the cache. yield this.fetch(keepAlive); loadResolve(); } yield this.factManager.setMruDate(this.specificationHash, new Date()); trace_1.Trace.info(`[Observer] COMPLETE - Spec hash: ${this.specificationHash.substring(0, 8)}...`); } catch (e) { trace_1.Trace.error(`[Observer] ERROR - Spec hash: ${this.specificationHash.substring(0, 8)}..., Error: ${e}`); cacheResolve(false); loadReject(e); } })); }); } addSpecificationListeners() { if (this.listenersAdded) { return; } trace_1.Trace.info(`[Observer] ADDING LISTENERS - Spec hash: ${this.specificationHash.substring(0, 8)}..., Given hash: ${this.givenHash.substring(0, 8)}...`); const inverses = (0, inverse_1.invertSpecification)(this.specification); trace_1.Trace.info(`[Observer] Generated ${inverses.length} inverse specifications`); inverses.forEach((inverse, index) => { const givenType = inverse.inverseSpecification.given[0].label.type; const path = inverse.path || "(root)"; const operation = inverse.operation; trace_1.Trace.info(`[Observer] Inverse ${index + 1}/${inverses.length} - Path: ${path}, Operation: ${operation}, Given type: ${givenType}, Given subset: [${inverse.givenSubset.join(', ')}], Parent subset: [${inverse.parentSubset.join(', ')}]`); }); const listeners = inverses.map((inverse, index) => { const listener = this.factManager.addSpecificationListener(inverse.inverseSpecification, (results) => this.onResult(inverse, results)); const path = inverse.path || "(root)"; trace_1.Trace.info(`[Observer] Registered listener ${index + 1}/${inverses.length} for path: ${path}`); return listener; }); this.listeners = listeners; trace_1.Trace.info(`[Observer] LISTENERS REGISTERED - Total: ${this.listeners.length}, Spec hash: ${this.specificationHash.substring(0, 8)}...`); this.listenersAdded = true; } cached() { if (this.cachedPromise === undefined) { throw new Error("The observer has not been started."); } return this.cachedPromise; } loaded() { if (this.loadedPromise === undefined) { throw new Error("The observer has not been started."); } return this.loadedPromise; } processed() { return __awaiter(this, void 0, void 0, function* () { // Keep waiting until no new notifications are created // This handles nested observers that register handlers which trigger more notifications while (this.pendingNotifications.size > 0) { // Create a snapshot of current pending notifications const currentNotifications = Array.from(this.pendingNotifications); yield Promise.all(currentNotifications); // Check if new notifications were added while we were waiting // If so, loop again to wait for those too } }); } stop() { this.stopped = true; for (const listener of this.listeners) { this.factManager.removeSpecificationListener(listener); } if (this.feeds.length > 0) { this.factManager.unsubscribe(this.feeds); } } fetch(keepAlive) { return __awaiter(this, void 0, void 0, function* () { if (keepAlive) { this.feeds = yield this.factManager.subscribe(this.given, this.specification); } else { yield this.factManager.fetch(this.given, this.specification); } }); } read() { return __awaiter(this, void 0, void 0, function* () { const projectedResults = yield this.factManager.read(this.given, this.specification); if (this.stopped) { // The observer was stopped before the read completed. return; } const givenSubset = this.specification.given.map(g => g.label.name); yield this.notifyAdded(projectedResults, this.specification.projection, "", givenSubset); }); } onResult(inverse, results) { return __awaiter(this, void 0, void 0, function* () { const path = inverse.path || "(root)"; trace_1.Trace.info(`[Observer] ON_RESULT - Path: ${path}, Operation: ${inverse.operation}, Results count: ${results.length}, Given hash: ${this.givenHash.substring(0, 8)}...`); // Track this notification for processing completion const processNotification = () => __awaiter(this, void 0, void 0, function* () { // Filter out results that do not match the given. const matchingResults = results.filter(pr => this.givenHash === (0, storage_1.computeTupleSubsetHash)(pr.tuple, inverse.givenSubset)); if (matchingResults.length === 0) { trace_1.Trace.info(`[Observer] No matching results after filtering - Path: ${path}, Given subset: [${inverse.givenSubset.join(', ')}]`); return; } trace_1.Trace.info(`[Observer] Matching results: ${matchingResults.length} - Path: ${path}, Operation: ${inverse.operation}`); if (inverse.operation === "add") { return yield this.notifyAdded(matchingResults, inverse.inverseSpecification.projection, inverse.path, inverse.parentSubset); } else if (inverse.operation === "remove") { return yield this.notifyRemoved(inverse.resultSubset, matchingResults); } else { const _exhaustiveCheck = inverse.operation; throw new Error(`Inverse operation ${_exhaustiveCheck} not implemented.`); } }); const notificationPromise = processNotification().finally(() => { // Remove from pending notifications when complete this.pendingNotifications.delete(notificationPromise); }); this.pendingNotifications.add(notificationPromise); yield notificationPromise; }); } notifyAdded(projectedResults, projection, path, parentSubset) { return __awaiter(this, void 0, void 0, function* () { const displayPath = path || "(root)"; trace_1.Trace.info(`[Observer] NOTIFY_ADDED - Path: ${displayPath}, Results: ${projectedResults.length}, Parent subset: [${parentSubset.join(', ')}]`); for (const pr of projectedResults) { const result = yield this.injectObservers(pr, projection, path); const parentTupleHash = (0, storage_1.computeTupleSubsetHash)(pr.tuple, parentSubset); const tupleHash = (0, hash_1.computeObjectHash)(pr.tuple); trace_1.Trace.info(`[Observer] Processing result - Path: ${displayPath}, Tuple hash: ${tupleHash.substring(0, 8)}..., Parent tuple hash: ${parentTupleHash.substring(0, 8)}...`); const addedHandler = this.addedHandlers.find(h => h.tupleHash === parentTupleHash && h.path === path); const resultAdded = addedHandler === null || addedHandler === void 0 ? void 0 : addedHandler.handler; if (!addedHandler) { trace_1.Trace.warn(`[Observer] NO HANDLER FOUND - Path: ${displayPath}, Parent tuple hash: ${parentTupleHash.substring(0, 8)}..., Available handlers: ${this.addedHandlers.length}`); this.addedHandlers.forEach((h, index) => { trace_1.Trace.warn(`[Observer] Handler ${index + 1}: Path="${h.path}", Tuple hash: ${h.tupleHash.substring(0, 8)}...`); }); // Buffer for replay when the handler registers later. this.bufferPendingNotification(path, pr, projection, parentSubset); // Skip deeper recursion until handler is registered. continue; } else if (!resultAdded) { trace_1.Trace.warn(`[Observer] Handler found but no callback - Path: ${displayPath}`); // Buffer for replay when the callback is attached. this.bufferPendingNotification(path, pr, projection, parentSubset); continue; } else { trace_1.Trace.info(`[Observer] Handler found - Path: ${displayPath}`); } // Don't call result added if we have already called it for this tuple. if (this.notifiedTuples.has(tupleHash) === false) { // Check if observer was stopped before calling the handler if (this.stopped) { trace_1.Trace.info(`[Observer] SKIPPING HANDLER - Observer stopped, Path: ${displayPath}, Tuple hash: ${tupleHash.substring(0, 8)}...`); continue; } trace_1.Trace.info(`[Observer] CALLING HANDLER - Path: ${displayPath}, Tuple hash: ${tupleHash.substring(0, 8)}...`); const promiseMaybe = resultAdded(result); this.notifiedTuples.add(tupleHash); if (promiseMaybe instanceof Promise) { const functionMaybe = yield promiseMaybe; if (functionMaybe instanceof Function) { this.removalsByTuple[tupleHash] = functionMaybe; } } else { const functionMaybe = promiseMaybe; if (functionMaybe instanceof Function) { this.removalsByTuple[tupleHash] = () => __awaiter(this, void 0, void 0, function* () { functionMaybe(); return Promise.resolve(); }); } } } else if (this.notifiedTuples.has(tupleHash)) { trace_1.Trace.info(`[Observer] Skipping already notified tuple - Path: ${displayPath}, Tuple hash: ${tupleHash.substring(0, 8)}...`); } // Recursively notify added for specification results. if (projection.type === "composite") { for (const component of projection.components) { if (component.type === "specification") { const childPath = path + "." + component.name; const childResults = pr.result[component.name]; trace_1.Trace.info(`[Observer] Processing nested spec - Parent path: ${displayPath}, Child path: ${childPath}, Child results: ${(childResults === null || childResults === void 0 ? void 0 : childResults.length) || 0}`); yield this.notifyAdded(childResults, component.projection, childPath, Object.keys(pr.tuple)); } } } } }); } notifyRemoved(resultSubset, projectedResult) { return __awaiter(this, void 0, void 0, function* () { for (const pr of projectedResult) { const resultTupleHash = (0, storage_1.computeTupleSubsetHash)(pr.tuple, resultSubset); const removal = this.removalsByTuple[resultTupleHash]; if (removal !== undefined) { yield removal(); delete this.removalsByTuple[resultTupleHash]; // After the tuple is removed, it can be re-added. this.notifiedTuples.delete(resultTupleHash); } } }); } /** * Buffers a pending notification for replay when a handler is registered later. * * @param path - The path in the specification * @param pr - The projected result to buffer * @param projection - The projection associated with the result * @param parentSubset - The parent subset of fact references */ bufferPendingNotification(path, pr, projection, parentSubset) { const parentTupleHash = (0, storage_1.computeTupleSubsetHash)(pr.tuple, parentSubset); const key = `${path}|${parentTupleHash}`; const existing = this.pendingAddsByKey.get(key); if (existing) { existing.results.push(pr); } else { this.pendingAddsByKey.set(key, { projection, parentSubset, results: [pr] }); } } injectObservers(pr, projection, parentPath) { return __awaiter(this, void 0, void 0, function* () { const displayPath = parentPath || "(root)"; if (projection.type === "composite") { const composite = {}; const tupleHash = (0, hash_1.computeObjectHash)(pr.tuple); for (const component of projection.components) { if (component.type === "specification") { const path = parentPath + "." + component.name; trace_1.Trace.info(`[Observer] INJECT_OBSERVER - Parent path: ${displayPath}, Component: ${component.name}, Full path: ${path}, Tuple hash: ${tupleHash.substring(0, 8)}...`); const observable = { onAdded: (handler) => __awaiter(this, void 0, void 0, function* () { this.addedHandlers.push({ tupleHash: tupleHash, path: path, handler: handler }); trace_1.Trace.info(`[Observer] HANDLER REGISTERED - Path: ${path}, Tuple hash: ${tupleHash.substring(0, 8)}..., Total handlers: ${this.addedHandlers.length}`); // Replay any buffered notifications now that the handler exists. const key = `${path}|${tupleHash}`; const pending = this.pendingAddsByKey.get(key); if (pending) { this.pendingAddsByKey.delete(key); // Track this replay as a pending notification const replayWork = () => __awaiter(this, void 0, void 0, function* () { try { yield this.notifyAdded(pending.results, pending.projection, path, pending.parentSubset); } catch (error) { trace_1.Trace.error(`[Observer] ERROR in buffered replay - Path: ${path}, Error: ${error}`); } }); const replayPromise = replayWork().finally(() => { this.pendingNotifications.delete(replayPromise); }); this.pendingNotifications.add(replayPromise); yield replayPromise; } }) }; composite[component.name] = observable; } else { composite[component.name] = pr.result[component.name]; } } return composite; } else { return pr.result; } }); } } exports.ObserverImpl = ObserverImpl; //# sourceMappingURL=observer.js.map