jinaga
Version:
Data management for web and mobile applications.
203 lines • 13.3 kB
JavaScript
;
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.ObservableSource = void 0;
const description_1 = require("../specification/description");
const encoding_1 = require("../util/encoding");
const trace_1 = require("../util/trace");
class ObservableSource {
constructor(store) {
this.store = store;
this.listenersByTypeAndSpecification = new Map();
}
notify(saved) {
return __awaiter(this, void 0, void 0, function* () {
// Collect all notification promises to ensure all callbacks complete
const notificationPromises = [];
for (let index = 0; index < saved.length; index++) {
const envelope = saved[index];
notificationPromises.push(this.notifyFactSaved(envelope.fact));
}
// Wait for all notifications to complete before resolving
yield Promise.all(notificationPromises);
});
}
addSpecificationListener(specification, onResult) {
if (specification.given.length !== 1) {
throw new Error("Specification must have exactly one given fact");
}
const givenType = specification.given[0].label.type;
const givenName = specification.given[0].label.name;
const specificationKey = (0, encoding_1.computeStringHash)((0, description_1.describeSpecification)(specification, 0));
const hasNestedSpecs = specification.projection.type === "composite" &&
specification.projection.components.some(c => c.type === "specification");
trace_1.Trace.info(`[ObservableSource] ADD_LISTENER REQUEST - Type: ${givenType}, Name: ${givenName}, Spec key: ${specificationKey.substring(0, 8)}..., Has nested specs: ${hasNestedSpecs}`);
let listenersBySpecification = this.listenersByTypeAndSpecification.get(givenType);
if (!listenersBySpecification) {
listenersBySpecification = new Map();
this.listenersByTypeAndSpecification.set(givenType, listenersBySpecification);
trace_1.Trace.info(`[ObservableSource] Created new listener map for type: ${givenType}`);
}
let listeners = listenersBySpecification.get(specificationKey);
if (!listeners) {
listeners = {
specification,
listeners: []
};
listenersBySpecification.set(specificationKey, listeners);
trace_1.Trace.info(`[ObservableSource] Created new listener group for spec: ${specificationKey.substring(0, 8)}... (type: ${givenType})`);
}
const specificationListener = {
onResult
};
listeners.listeners.push(specificationListener);
const listenerCount = listeners.listeners.length;
const totalListeners = Array.from(this.listenersByTypeAndSpecification.values())
.reduce((total, map) => total + Array.from(map.values()).reduce((sum, l) => sum + l.listeners.length, 0), 0);
trace_1.Trace.info(`[ObservableSource] LISTENER ADDED - Spec: ${specificationKey.substring(0, 8)}..., Type: ${givenType}, Count for spec: ${listenerCount}, Total listeners: ${totalListeners}, Nested specs: ${hasNestedSpecs}`);
return specificationListener;
}
removeSpecificationListener(specificationListener) {
const startTime = Date.now();
let found = false;
let removedFromSpec = '';
let removedFromType = '';
for (const [givenType, listenersBySpecification] of this.listenersByTypeAndSpecification) {
for (const [specificationKey, listeners] of listenersBySpecification) {
const beforeCount = listeners.listeners.length;
const index = listeners.listeners.indexOf(specificationListener);
if (index >= 0) {
trace_1.Trace.info(`[ObservableSource] REMOVING listener - Spec: ${specificationKey.substring(0, 8)}..., Type: ${givenType}, Index: ${index}, Before count: ${beforeCount}`);
listeners.listeners.splice(index, 1);
found = true;
removedFromSpec = specificationKey;
removedFromType = givenType;
const afterCount = listeners.listeners.length;
trace_1.Trace.info(`[ObservableSource] REMOVED listener - After count: ${afterCount}`);
if (listeners.listeners.length === 0) {
listenersBySpecification.delete(specificationKey);
trace_1.Trace.info(`[ObservableSource] Deleted empty spec group: ${specificationKey.substring(0, 8)}...`);
if (listenersBySpecification.size === 0) {
this.listenersByTypeAndSpecification.delete(givenType);
trace_1.Trace.info(`[ObservableSource] Deleted empty type group: ${givenType}`);
}
}
break;
}
}
if (found)
break;
}
const totalListeners = Array.from(this.listenersByTypeAndSpecification.values())
.reduce((total, map) => total + Array.from(map.values()).reduce((sum, l) => sum + l.listeners.length, 0), 0);
const duration = Date.now() - startTime;
if (found) {
trace_1.Trace.info(`[ObservableSource] Listener removal completed - Spec: ${removedFromSpec.substring(0, 8)}..., Type: ${removedFromType}, Total remaining: ${totalListeners}, Duration: ${duration}ms`);
}
else {
trace_1.Trace.warn(`[ObservableSource] Listener NOT FOUND during removal - Total listeners: ${totalListeners}, Duration: ${duration}ms`);
}
}
notifyFactSaved(fact) {
return __awaiter(this, void 0, void 0, function* () {
const startTime = Date.now();
const listenersBySpecification = this.listenersByTypeAndSpecification.get(fact.type);
if (listenersBySpecification) {
trace_1.Trace.info(`[ObservableSource] NOTIFY START - Fact type: ${fact.type}, Hash: ${fact.hash.substring(0, 8)}..., Spec groups: ${listenersBySpecification.size}`);
let totalNotifications = 0;
let specCount = 0;
let nestedSpecCount = 0;
for (const [specificationKey, listeners] of listenersBySpecification) {
specCount++;
if (listeners && listeners.listeners.length > 0) {
const listenerCount = listeners.listeners.length;
const specification = listeners.specification;
const hasNestedSpecs = specification.projection.type === "composite" &&
specification.projection.components.some(c => c.type === "specification");
if (hasNestedSpecs) {
nestedSpecCount++;
const nestedSpecNames = specification.projection.type === "composite"
? specification.projection.components
.filter(c => c.type === "specification")
.map(c => c.name)
.join(', ')
: '';
trace_1.Trace.info(`[ObservableSource] NESTED SPEC DETECTED - Spec ${specCount}/${listenersBySpecification.size}, Key: ${specificationKey.substring(0, 8)}..., Nested components: [${nestedSpecNames}], Listeners: ${listenerCount}`);
}
else {
trace_1.Trace.info(`[ObservableSource] Processing spec ${specCount}/${listenersBySpecification.size} - Key: ${specificationKey.substring(0, 8)}..., Listeners: ${listenerCount}`);
}
const givenReference = {
type: fact.type,
hash: fact.hash
};
const readStart = Date.now();
const results = yield this.store.read([givenReference], specification);
const readDuration = Date.now() - readStart;
if (hasNestedSpecs) {
trace_1.Trace.info(`[ObservableSource] Store read for NESTED spec - Results: ${results.length}, Duration: ${readDuration}ms`);
// Log nested result structure if present
if (results.length > 0 && specification.projection.type === "composite") {
const nestedResults = specification.projection.components
.filter(c => c.type === "specification")
.map(c => { var _a; return `${c.name}: ${((_a = results[0].result[c.name]) === null || _a === void 0 ? void 0 : _a.length) || 0}`; })
.join(', ');
trace_1.Trace.info(`[ObservableSource] Nested results structure: {${nestedResults}}`);
}
}
else {
trace_1.Trace.info(`[ObservableSource] Store read completed - Results: ${results.length}, Duration: ${readDuration}ms`);
}
// Create a snapshot of listeners to avoid modification during iteration
const listenerSnapshot = [...listeners.listeners];
if (listenerSnapshot.length !== listeners.listeners.length) {
trace_1.Trace.warn(`[ObservableSource] RACE CONDITION DETECTED - Listener count changed during snapshot: ${listenerSnapshot.length} vs ${listeners.listeners.length}`);
}
for (let i = 0; i < listenerSnapshot.length; i++) {
const specificationListener = listenerSnapshot[i];
if (specificationListener) {
try {
const notifyStart = Date.now();
trace_1.Trace.info(`[ObservableSource] Calling listener ${i + 1}/${listenerSnapshot.length} - Nested: ${hasNestedSpecs}`);
yield specificationListener.onResult(results);
const notifyDuration = Date.now() - notifyStart;
totalNotifications++;
if (notifyDuration > 100) {
trace_1.Trace.warn(`[ObservableSource] SLOW notification - Listener ${i + 1}/${listenerSnapshot.length}, Duration: ${notifyDuration}ms, Nested: ${hasNestedSpecs}`);
}
else {
trace_1.Trace.info(`[ObservableSource] Listener completed - ${i + 1}/${listenerSnapshot.length}, Duration: ${notifyDuration}ms`);
}
}
catch (error) {
trace_1.Trace.error(`[ObservableSource] ERROR in listener notification - Listener ${i + 1}/${listenerSnapshot.length}, Nested: ${hasNestedSpecs}, Error: ${error}`);
}
}
else {
trace_1.Trace.warn(`[ObservableSource] NULL listener encountered at index ${i}`);
}
}
}
else {
trace_1.Trace.info(`[ObservableSource] Skipping spec ${specCount}/${listenersBySpecification.size} - No listeners or null group`);
}
}
const totalDuration = Date.now() - startTime;
trace_1.Trace.info(`[ObservableSource] NOTIFY COMPLETE - Fact: ${fact.hash.substring(0, 8)}..., Type: ${fact.type}, Specs processed: ${specCount} (${nestedSpecCount} nested), Total notifications: ${totalNotifications}, Duration: ${totalDuration}ms`);
}
else {
trace_1.Trace.info(`[ObservableSource] No listeners for fact type: ${fact.type} - Available types: [${Array.from(this.listenersByTypeAndSpecification.keys()).join(', ')}]`);
}
});
}
}
exports.ObservableSource = ObservableSource;
//# sourceMappingURL=observable.js.map