UNPKG

@orbit/record-cache

Version:

Orbit base classes used to access and maintain a set of records.

186 lines (163 loc) 4.78 kB
import { Orbit, Evented } from '@orbit/core'; import { RecordQueryExpression, FindRecord, FindRecords, FindRelatedRecord, FindRelatedRecords, equalRecordIdentities, RecordQuery, RecordSchema, RecordOperation } from '@orbit/records'; import { RecordChange, recordOperationChange } from './record-change'; const { assert } = Orbit; export interface LiveQuerySettings { debounce: boolean; query: RecordQuery; } export abstract class LiveQuery { readonly debounce: boolean; protected abstract get cache(): Evented; protected abstract get schema(): RecordSchema; protected _query: RecordQuery; protected _subscribe(onNext: () => void): () => void { const execute = this.debounce ? onceTick(onNext) : onNext; const unsubscribePatch = this.cache.on( 'patch', (operation: RecordOperation) => { if (this.operationRelevantForQuery(operation)) { execute(); } } ); const unsubscribeReset = this.cache.on('reset', () => { execute(); }); function unsubscribe() { cancelTick(execute); unsubscribePatch(); unsubscribeReset(); } return unsubscribe; } constructor(settings: LiveQuerySettings) { assert( 'Only single expression queries are supported on LiveQuery', !Array.isArray(settings.query.expressions) ); this.debounce = settings.debounce; this._query = settings.query; } operationRelevantForQuery(operation: RecordOperation): boolean { const change = recordOperationChange(operation); const expression = this._query.expressions as RecordQueryExpression; return this.queryExpressionRelevantForChange(expression, change); } protected queryExpressionRelevantForChange( expression: RecordQueryExpression, change: RecordChange ): boolean { switch (expression.op) { case 'findRecord': return this.findRecordQueryExpressionRelevantForChange( expression as FindRecord, change ); case 'findRecords': return this.findRecordsQueryExpressionRelevantForChange( expression as FindRecords, change ); case 'findRelatedRecord': return this.findRelatedRecordQueryExpressionRelevantForChange( expression as FindRelatedRecord, change ); case 'findRelatedRecords': return this.findRelatedRecordsQueryExpressionRelevantForChange( expression as FindRelatedRecords, change ); default: return true; } } protected findRecordQueryExpressionRelevantForChange( expression: FindRecord, change: RecordChange ): boolean { return equalRecordIdentities(expression.record, change); } protected findRecordsQueryExpressionRelevantForChange( expression: FindRecords, change: RecordChange ): boolean { if (expression.type) { return expression.type === change.type; } else if (expression.records) { for (let record of expression.records) { if (record.type === change.type) { return true; } } return false; } return true; } protected findRelatedRecordQueryExpressionRelevantForChange( expression: FindRelatedRecord, change: RecordChange ): boolean { return ( equalRecordIdentities(expression.record, change) && (change.relationships.includes(expression.relationship) || change.remove) ); } protected findRelatedRecordsQueryExpressionRelevantForChange( expression: FindRelatedRecords, change: RecordChange ): boolean { const relationshipDef = this.schema.getRelationship( expression.record.type, expression.relationship ); const type = relationshipDef?.type; if (Array.isArray(type) && type.find((type) => type === change.type)) { return true; } else if (type === change.type) { return true; } return ( equalRecordIdentities(expression.record, change) && (change.relationships.includes(expression.relationship) || change.remove) ); } } const isNode = typeof Orbit.globals.process?.nextTick === 'function'; let resolvedPromise: Promise<void>; const nextTick = isNode ? function (fn: () => void) { if (!resolvedPromise) { resolvedPromise = Promise.resolve(); } resolvedPromise.then(() => { Orbit.globals.process.nextTick(fn); }); } : Orbit.globals.setImmediate ?? setTimeout; function onceTick(fn: () => void) { return function tick() { if (!ticks.has(tick)) { ticks.add(tick); nextTick(() => { fn(); cancelTick(tick); }); } }; } function cancelTick(tick: () => void) { ticks.delete(tick); } const ticks = new WeakSet();