jinaga
Version:
Data management for web and mobile applications.
425 lines • 17.1 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.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