UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

275 lines (274 loc) • 9.19 kB
"use strict"; Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const errors = require("../errors.cjs"); const scheduler = require("../scheduler.cjs"); const callbacks = require("../utils/callbacks.cjs"); const refProxy = require("../query/builder/ref-proxy.cjs"); const subscription = require("./subscription.cjs"); class CollectionChangesManager { /** * Creates a new CollectionChangesManager instance */ constructor() { this.activeSubscribersCount = 0; this.changeSubscriptions = /* @__PURE__ */ new Set(); this.batchedEvents = []; this.shouldBatchEvents = false; this.layoutChangeListeners = /* @__PURE__ */ new Set(); this.stateRevision = 0; this.layoutRevision = 0; } setDeps(deps) { this.lifecycle = deps.lifecycle; this.sync = deps.sync; this.events = deps.events; this.collection = deps.collection; this.state = deps.state; } /** * Emit an empty ready event to notify subscribers that the collection is ready * This bypasses the normal empty array check in emitEvents */ emitEmptyReadyEvent() { scheduler.withPublicationContext(() => { try { callbacks.runAllCallbacks( [...this.changeSubscriptions].map( (subscription2) => () => subscription2.emitEvents([]) ) ); } catch (error) { scheduler.recordPublicationError(error); } }); } /** * Enriches a change message with virtual properties ($synced, $origin, $key, $collectionId). * Uses the "add-if-missing" pattern to preserve virtual properties from upstream collections. */ enrichChangeWithVirtualProps(change) { return this.state.enrichChangeMessage(change); } // Reduce unpublished same-key changes relative to the subscriber's last // visible row: keep the earliest previous row and latest value, and cancel // an insert followed by a delete. composeBatchedChange(pending, change) { if (!pending) return change; if (pending.type === `insert`) { if (change.type === `delete`) return void 0; return { ...change, type: `insert`, previousValue: void 0 }; } const previousValue = pending.type === `update` ? pending.previousValue ?? pending.value : pending.value; if (change.type === `delete`) { return { ...change, value: previousValue, previousValue: void 0 }; } return { ...change, type: `update`, previousValue }; } /** * Emit events either immediately or batch them for later emission */ emitEvents(changes, forceEmit = false, layoutChanged = false) { if (changes.length > 0) this.stateRevision++; if (layoutChanged) this.layoutRevision++; if (this.shouldBatchEvents && !forceEmit) { this.batchedEvents.push( ...changes.map((change) => this.enrichChangeWithVirtualProps(change)) ); return; } let rawEvents = changes; if (forceEmit) { if (this.batchedEvents.length > 0) { const combined = /* @__PURE__ */ new Map(); for (const change of [...this.batchedEvents, ...changes]) { combined.set( change.key, this.composeBatchedChange(combined.get(change.key), change) ); } rawEvents = [...combined.values()].flatMap((change) => { return change ? [change] : []; }); } this.batchedEvents = []; this.shouldBatchEvents = false; } if (this.deferral) { this.deferral.publications.push({ changes: rawEvents, layoutChanged }); return; } this.publishEvents(rawEvents, layoutChanged); } /** * Defers subscriber delivery while a coherent multi-Collection publication * installs all of its visible state. State and indexes still commit at their * normal transaction boundaries. */ deferPublication() { const deferral = this.deferral ??= { depth: 0, discard: false, stateRevision: this.stateRevision, layoutRevision: this.layoutRevision, publications: [] }; deferral.depth++; let closed = false; const close = (discard) => { if (closed || this.deferral !== deferral) return; closed = true; deferral.discard ||= discard; if (--deferral.depth > 0) return; const publications = deferral.publications; deferral.publications = []; this.deferral = void 0; if (deferral.discard) { this.stateRevision = deferral.stateRevision; this.layoutRevision = deferral.layoutRevision; return; } this.publishEvents( publications.flatMap(({ changes }) => changes), publications.some(({ layoutChanged }) => layoutChanged) ); }; return { publish: () => close(false), discard: () => close(true) }; } publishEvents(rawEvents, layoutChanged) { if (rawEvents.length === 0 && !layoutChanged) { return; } const enrichedEvents = rawEvents.map((change) => this.enrichChangeWithVirtualProps(change)); const layoutListeners = [...this.layoutChangeListeners]; const subscriptions = [...this.changeSubscriptions]; scheduler.withPublicationContext(() => { const callbacks$1 = subscriptions.map( (subscription2) => () => subscription2.emitEvents(enrichedEvents) ); if (rawEvents.length === 0) { callbacks$1.unshift(...layoutListeners); } try { callbacks.runAllCallbacks(callbacks$1); } catch (error) { scheduler.recordPublicationError(error); } }); } /** Subscribe to layout-only publications. Internal observer channel. */ subscribeLayoutChanges(listener) { this.layoutChangeListeners.add(listener); return () => this.layoutChangeListeners.delete(listener); } /** * Subscribe to changes in the collection */ subscribeChanges(callback, options = {}) { if (options.where && options.whereExpression) { throw new Error( `Cannot specify both 'where' and 'whereExpression' options. Use one or the other.` ); } const { where, ...opts } = options; let whereExpression = opts.whereExpression; if (where) { const proxy = refProxy.createSingleRowRefProxy(); const result = where(proxy); whereExpression = refProxy.toExpression(result); } this.addSubscriber(); let subscription$1; const setupState = { closed: false }; try { subscription$1 = new subscription.CollectionSubscription(this.collection, callback, { ...opts, whereExpression, onUnsubscribe: () => { setupState.closed = true; this.removeSubscriber(); if (subscription$1) this.changeSubscriptions.delete(subscription$1); } }); if (options.onStatusChange) { subscription$1.on(`status:change`, options.onStatusChange); } if (options.includeInitialState) { subscription$1.requestSnapshot({ trackLoadSubsetPromise: false, orderBy: options.orderBy, limit: options.limit, onLoadSubsetResult: options.onLoadSubsetResult }); } else if (options.includeInitialState === false) { subscription$1.markAllStateAsSeen(); } if (!setupState.closed) this.changeSubscriptions.add(subscription$1); } catch (error) { if (subscription$1) { try { subscription$1.unsubscribe(); } catch { } } else { this.removeSubscriber(); } throw error; } return subscription$1; } /** * Increment the active subscribers count and start sync if needed */ addSubscriber() { const previousSubscriberCount = this.activeSubscribersCount; this.activeSubscribersCount++; this.lifecycle.cancelGCTimer(); try { if (this.lifecycle.status === `cleaned-up` || this.lifecycle.status === `idle`) { this.sync.startSync(); } } catch (error) { this.activeSubscribersCount = previousSubscriberCount; if (this.activeSubscribersCount === 0) { this.lifecycle.startGCTimer(); } throw error; } this.events.emitSubscribersChange( this.activeSubscribersCount, previousSubscriberCount ); } /** * Decrement the active subscribers count and start GC timer if needed */ removeSubscriber() { const previousSubscriberCount = this.activeSubscribersCount; this.activeSubscribersCount--; if (this.activeSubscribersCount === 0) { this.lifecycle.startGCTimer(); } else if (this.activeSubscribersCount < 0) { throw new errors.NegativeActiveSubscribersError(); } this.events.emitSubscribersChange( this.activeSubscribersCount, previousSubscriberCount ); } /** * Clean up the collection by stopping sync and clearing data * This can be called manually or automatically by garbage collection */ cleanup() { this.stateRevision++; this.batchedEvents = []; this.shouldBatchEvents = false; if (this.deferral) this.deferral.publications.length = 0; this.deferral = void 0; } } exports.CollectionChangesManager = CollectionChangesManager; //# sourceMappingURL=changes.cjs.map