jinaga
Version:
Data management for web and mobile applications.
377 lines • 21.4 kB
JavaScript
"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