UNPKG

jinaga

Version:

Data management for web and mobile applications.

425 lines 17.1 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.Jinaga = void 0; const hydrate_1 = require("./fact/hydrate"); const UnionFind_1 = require("./specification/UnionFind"); const obj_1 = require("./util/obj"); const trace_1 = require("./util/trace"); class Jinaga { constructor(authentication, factManager, syncStatusNotifier) { this.authentication = authentication; this.factManager = factManager; this.syncStatusNotifier = syncStatusNotifier; this.errorHandlers = []; this.loadingHandlers = []; this.progressHandlers = []; } /** * Register an callback to receive error messages. * * @param handler A function to receive error messages */ onError(handler) { this.errorHandlers.push(handler); } /** * Register a callback to receive loading state notifications. * * @param handler A function to receive loading state */ onLoading(handler) { this.loadingHandlers.push(handler); } /** * Register a callback to receive outgoing fact count. * A count greater than 0 is an indication to the user that the application is saving. * * @param handler A function to receive the number of facts in the queue */ onProgress(handler) { this.progressHandlers.push(handler); } onSyncStatus(handler) { var _a; (_a = this.syncStatusNotifier) === null || _a === void 0 ? void 0 : _a.onSyncStatus(handler); } /** * Log the user in and return a fact that represents their identity. * This method is only valid in the browser. * * @returns A promise that resolves to a fact that represents the user's identity, and the user's profile as reported by the configured Passport strategy */ login() { return __awaiter(this, void 0, void 0, function* () { const { userFact, profile } = yield this.authentication.login(); return { userFact: (0, hydrate_1.hydrate)(userFact), profile }; }); } /** * Access the identity of the local machine. * This method is only valid for the server and clients with local storage. * The local machine's identity is not shared with remote machines. * * @returns A promise that resolves to the local machine's identity */ local() { return __awaiter(this, void 0, void 0, function* () { const deviceFact = yield this.authentication.local(); return (0, hydrate_1.hydrate)(deviceFact); }); } /** * Creates a new fact. * This method is asynchronous. * It will be resolved when the fact has been persisted. * * @param prototype The fact to save and share * @returns The fact that was just created */ fact(prototype) { return __awaiter(this, void 0, void 0, function* () { if (!prototype) { return prototype; } try { const fact = this.validateFact(prototype); const dehydration = new hydrate_1.Dehydration(); const reference = dehydration.dehydrate(fact); const factRecords = dehydration.factRecords(); const hydrated = (0, hydrate_1.hydrateFromTree)([reference], factRecords)[0]; const envelopes = factRecords.map(fact => { return { fact: fact, signatures: [] }; }); const authorized = yield this.authentication.authorize(envelopes); const saved = yield this.factManager.save(authorized); return hydrated; } catch (error) { this.error(error); throw error; } }); } /** * Execute a query for facts matching a specification. * * @param specification Use Model.given().match() to create a specification * @param given The fact or facts from which to begin the query * @returns A promise that resolves to an array of results */ query(specification, ...given) { return __awaiter(this, void 0, void 0, function* () { const innerSpecification = specification.specification; (0, UnionFind_1.detectDisconnectedSpecification)(innerSpecification); if (!given || given.some(g => !g)) { return []; } if (given.length !== innerSpecification.given.length) { throw new Error(`Expected ${innerSpecification.given.length} given facts, but received ${given.length}.`); } const references = given.map(g => this.prepareFactReference(g)); yield this.factManager.fetch(references, innerSpecification); const projectedResults = yield this.factManager.read(references, innerSpecification); const extracted = extractResults(projectedResults, innerSpecification.projection); trace_1.Trace.counter("facts_loaded", extracted.totalCount); return extracted.results; }); } /** * Receive notification when a projection changes. * The notification function will initially receive all matching results. * It will then subsequently receive new results as they are created. * Return a function to be called when the result is removed. * * @param specification Use Model.given().match() to create a specification * @param given The fact or facts from which to begin the query * @param resultAdded A function to receive the initial and new results * @returns An observer to control notifications */ watch(specification, ...args) { const given = args.slice(0, args.length - 1); const resultAdded = args[args.length - 1]; const innerSpecification = specification.specification; if (!given) { throw new Error("No given facts provided."); } if (given.some(g => !g)) { throw new Error("One or more given facts are null."); } if (!resultAdded || typeof resultAdded !== "function") { throw new Error("No resultAdded function provided."); } if (given.length !== innerSpecification.given.length) { throw new Error(`Expected ${innerSpecification.given.length} given facts, but received ${given.length}.`); } const references = given.map(g => this.prepareFactReference(g)); return this.factManager.startObserver(references, innerSpecification, resultAdded, false); } /** * Request server-sent events when a fact affects query results. * While the subscription is active, the server will push matching facts * to the client. Call Subscription.stop() to stop receiving events. * * @param specification Use Model.given().match() to create a specification * @param given The fact or facts from which to begin the subscription * @returns A subscription, which remains running until you call stop */ subscribe(specification, ...args) { const given = args.slice(0, args.length - 1); const resultAdded = args[args.length - 1]; const innerSpecification = specification.specification; if (!given) { throw new Error("No given facts provided."); } if (given.some(g => !g)) { throw new Error("One or more given facts are null."); } if (!resultAdded || typeof resultAdded !== "function") { throw new Error("No resultAdded function provided."); } if (given.length !== innerSpecification.given.length) { throw new Error(`Expected ${innerSpecification.given.length} given facts, but received ${given.length}.`); } const references = given.map(g => this.prepareFactReference(g)); return this.factManager.startObserver(references, innerSpecification, resultAdded, true); } /** * Compute the SHA-256 hash of a fact. * This is a deterministic hash that can be used to identify the fact. * @param fact The fact to hash * @returns The SHA-256 hash of the fact as a base-64 string */ static hash(fact) { const hash = (0, hydrate_1.lookupHash)(fact); if (hash) { return hash; } const error = this.getFactError(fact); if (error) { throw new Error(`Cannot hash the object. It is not a fact. ${error}: ${JSON.stringify(fact)}`); } const reference = (0, hydrate_1.dehydrateReference)(fact); return reference.hash; } /** * Create a strongly-typed fact reference from a type constructor and hash. * This allows you to create a minimal fact object that can be used with * query, watch, and subscribe APIs when you only have the hash. * * @param ctor The constructor function with a static Type property * @param hash The SHA-256 hash of the fact as a base-64 string * @returns A fact reference object typed as T */ static factReference(ctor, hash) { const type = ctor.Type; if (!type || typeof type !== 'string') { throw new Error(`Constructor must have a static Type property of type string. Found: ${typeof type}`); } const factRef = { type: type }; // Set the hash symbol if available if (hydrate_1.hashSymbol) { factRef[hydrate_1.hashSymbol] = hash; } return factRef; } /** * Compute the SHA-256 hash of a fact. * This is a deterministic hash that can be used to identify the fact. * @param fact The fact to hash * @returns The SHA-256 hash of the fact as a base-64 string */ hash(fact) { return Jinaga.hash(fact); } /** * Create a strongly-typed fact reference from a type constructor and hash. * This allows you to create a minimal fact object that can be used with * query, watch, and subscribe APIs when you only have the hash. * * @param ctor The constructor function with a static Type property * @param hash The SHA-256 hash of the fact as a base-64 string * @returns A fact reference object typed as T */ factReference(ctor, hash) { return Jinaga.factReference(ctor, hash); } /** * Purge the data store of all descendants of purge roots. * A purge root is a fact that satisfies a purge condition. * @returns Resolves when the data store has been purged. */ purge() { return this.factManager.purge(); } /** * Processes the queue immediately, bypassing any delay. * This allows you to ensure that all facts have been sent to the server. */ push() { return this.factManager.push(); } /** * Create some facts owned by a single-use principal. A key pair is * generated for the principal and used to sign the facts. The private * key is discarded after the facts are saved. * * @param func A function that saves a set of facts and returns one or more of them * @returns The result of the function */ singleUse(func) { return __awaiter(this, void 0, void 0, function* () { try { const { last } = yield this.factManager.beginSingleUse(); const principal = (0, hydrate_1.hydrate)(last); return yield func(principal); } finally { this.factManager.endSingleUse(); } }); } validateFact(prototype) { let fact = this.removeNullPredecessors(prototype); const error = Jinaga.getFactError(fact); if (error) { throw new Error(error); } return fact; } removeNullPredecessors(fact) { if (!fact) { return fact; } if (fact instanceof Date) { return fact; } if (typeof fact !== 'object') { return fact; } if (Array.isArray(fact)) { // Let the validator report the error return fact; } if ((0, hydrate_1.lookupHash)(fact)) { // If the fact has a hash symbol, then we need to retain its identity return fact; } const result = {}; for (const key in fact) { const value = fact[key]; if (value !== null && value !== undefined) { if (Array.isArray(value)) { result[key] = value.filter(v => v !== null && v !== undefined).map(v => this.removeNullPredecessors(v)); } else if (typeof value === 'object') { result[key] = this.removeNullPredecessors(value); } else { result[key] = value; } } } return result; } static getFactError(prototype) { if (!prototype) { return 'A fact cannot be null.'; } if (!('type' in prototype)) { return 'Specify the type of the fact and all of its predecessors.'; } for (const field in prototype) { const value = (0, obj_1.toJSON)(prototype[field]); if (typeof (value) === 'object') { if (Array.isArray(value)) { for (const element of value) { const error = this.getFactError(element); if (error) { return error; } } } else { const error = this.getFactError(value); if (error) { return error; } } } else if (typeof (value) === 'function') { return `A fact may not have any methods: ${field} in ${prototype.type} is a function.`; } } } error(error) { trace_1.Trace.error(error); this.errorHandlers.forEach((errorHandler) => { errorHandler(error); }); } prepareFactReference(g) { // Check if this is a factReference created by our helper // It should be an object with only a 'type' field and a hashSymbol if (typeof g === 'object' && g !== null) { const obj = g; const keys = Object.keys(obj); const hasType = typeof obj.type === 'string'; const hasHash = hydrate_1.hashSymbol && obj[hydrate_1.hashSymbol]; // If it only has a 'type' field and a hash symbol, treat it as a fact reference if (hasType && hasHash && keys.length === 1 && keys[0] === 'type') { return { type: obj.type, hash: hydrate_1.hashSymbol ? obj[hydrate_1.hashSymbol] : '' }; } } // Otherwise, process it as a normal fact const fact = JSON.parse(JSON.stringify(g)); const validatedFact = this.validateFact(fact); return (0, hydrate_1.dehydrateReference)(validatedFact); } } exports.Jinaga = Jinaga; function extractResults(projectedResults, projection) { const results = []; let totalCount = 0; for (const projectedResult of projectedResults) { let result = projectedResult.result; if (projection.type === "composite") { const obj = {}; for (const component of projection.components) { const value = result[component.name]; if (component.type === "specification") { const { results: nestedResults, totalCount: nestedCount } = extractResults(value, component.projection); obj[component.name] = nestedResults; totalCount += nestedCount; } else { obj[component.name] = value; } } result = obj; } results.push(result); totalCount++; } return { results, totalCount }; } //# sourceMappingURL=jinaga.js.map