UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

246 lines (245 loc) 7.66 kB
"use strict"; Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const errors = require("../errors.cjs"); const browserPolyfills = require("../utils/browser-polyfills.cjs"); const callbacks = require("../utils/callbacks.cjs"); const cleanupQueue = require("./cleanup-queue.cjs"); class CollectionLifecycleManager { /** * Creates a new CollectionLifecycleManager instance */ constructor(config, id) { this.status = `idle`; this.hasBeenReady = false; this.hasReceivedFirstCommit = false; this.onFirstReadyCallbacks = []; this.idleCallbackId = null; this.statusRevision = 0; this.cleaningUp = false; this.config = config; this.id = id; } setDeps(deps) { this.indexes = deps.indexes; this.events = deps.events; this.changes = deps.changes; this.sync = deps.sync; this.state = deps.state; } /** * Validates state transitions to prevent invalid status changes */ validateStatusTransition(from, to) { if (from === to) { return; } const validTransitions = { idle: [`loading`, `error`, `cleaned-up`], loading: [`ready`, `error`, `cleaned-up`], ready: [`cleaned-up`, `error`], error: [`ready`, `cleaned-up`, `idle`], "cleaned-up": [`loading`, `error`] }; if (!validTransitions[from].includes(to)) { throw new errors.InvalidCollectionStatusTransitionError(from, to, this.id); } } /** * Safely update the collection status with validation * @private */ setStatus(newStatus, allowReady = false) { if (newStatus === `ready` && !allowReady) { throw new errors.CollectionStateError( `You can't directly call "setStatus('ready'). You must use markReady instead.` ); } this.validateStatusTransition(this.status, newStatus); const revision = ++this.statusRevision; const previousStatus = this.status; this.status = newStatus; this.events.emitStatusChange( newStatus, previousStatus, () => this.statusRevision === revision ); } /** * Validates that the collection is in a usable state for data operations * @private */ validateCollectionUsable(operation) { switch (this.status) { case `error`: throw new errors.CollectionInErrorStateError(operation, this.id); case `cleaned-up`: this.sync.startSync(); break; } } /** * Mark the collection as ready for use * This is called by sync implementations to explicitly signal that the collection is ready, * providing a more intuitive alternative to using commits for readiness signaling * @private - Should only be called by sync implementations */ markReady() { const failure = this.applyReadyTransition(); if (failure) throw failure.error; } /** @internal Capture ready-effect failures while the sync entry completes. */ markReadyDuringSyncStart() { return this.applyReadyTransition(); } applyReadyTransition() { this.validateStatusTransition(this.status, `ready`); if (this.status === `loading` || this.status === `error`) { this.syncError = void 0; const readyRevision = this.statusRevision + 1; this.setStatus(`ready`, true); if (this.status !== `ready` || this.statusRevision !== readyRevision) { return void 0; } const readyEffects = []; if (!this.hasBeenReady) { this.hasBeenReady = true; if (!this.hasReceivedFirstCommit) { this.hasReceivedFirstCommit = true; } readyEffects.push(...this.onFirstReadyCallbacks); this.onFirstReadyCallbacks = []; } readyEffects.push(() => this.changes.emitEmptyReadyEvent()); try { callbacks.runAllCallbacks(readyEffects); } catch (error) { return { error }; } } return void 0; } /** Mark an asynchronous sync failure after sync has started. */ markError(error) { this.validateStatusTransition(this.status, `error`); this.syncError = error; this.setStatus(`error`); } /** Return the cause supplied by the current sync session, if any. */ getSyncError() { return this.syncError; } assertCanStartSync() { if (this.cleaningUp) { throw new errors.CollectionStateError( `Cannot start collection "${this.id}" during cleanup. Restart after cleanup() completes.` ); } } /** * Start the garbage collection timer * Called when the collection becomes inactive (no subscribers) */ startGCTimer() { const gcTime = this.config.gcTime ?? 3e5; if (gcTime <= 0 || !Number.isFinite(gcTime)) { return; } cleanupQueue.CleanupQueue.getInstance().schedule(this, gcTime, () => { if (this.changes.activeSubscribersCount === 0) { this.scheduleIdleCleanup(); } }); } /** * Cancel the garbage collection timer * Called when the collection becomes active again */ cancelGCTimer() { cleanupQueue.CleanupQueue.getInstance().cancel(this); if (this.idleCallbackId !== null) { browserPolyfills.safeCancelIdleCallback(this.idleCallbackId); this.idleCallbackId = null; } } /** * Schedule cleanup to run during browser idle time * This prevents blocking the UI thread during cleanup operations */ scheduleIdleCleanup() { if (this.idleCallbackId !== null) { browserPolyfills.safeCancelIdleCallback(this.idleCallbackId); } this.idleCallbackId = browserPolyfills.safeRequestIdleCallback( (deadline) => { if (this.changes.activeSubscribersCount === 0) { const cleanupCompleted = this.performCleanup(deadline); if (cleanupCompleted) { this.idleCallbackId = null; } } else { this.idleCallbackId = null; } }, { timeout: 1e3 } ); } /** * Perform cleanup operations, optionally in chunks during idle time * @returns true if cleanup was completed, false if it was rescheduled */ performCleanup(deadline) { if (this.cleaningUp) return true; const hasTime = !deadline || deadline.timeRemaining() > 0 || deadline.didTimeout; if (hasTime) { this.cleaningUp = true; try { this.sync.cleanup(); this.state.cleanup(); this.changes.cleanup(); this.indexes.cleanup(); cleanupQueue.CleanupQueue.getInstance().cancel(this); this.hasBeenReady = false; this.syncError = void 0; this.onFirstReadyCallbacks = []; } finally { this.cleaningUp = false; } this.setStatus(`cleaned-up`); if (this.changes.activeSubscribersCount === 0) { this.events.cleanup(); } return true; } else { this.scheduleIdleCleanup(); return false; } } /** * Register a callback to be executed when the collection first becomes ready * Useful for preloading collections * @param callback Function to call when the collection first becomes ready */ onFirstReady(callback) { if (this.hasBeenReady) { callback(); return () => { }; } this.onFirstReadyCallbacks.push(callback); return () => { const index = this.onFirstReadyCallbacks.indexOf(callback); if (index !== -1) { this.onFirstReadyCallbacks.splice(index, 1); } }; } cleanup() { if (this.idleCallbackId !== null) { browserPolyfills.safeCancelIdleCallback(this.idleCallbackId); this.idleCallbackId = null; } this.performCleanup(); } } exports.CollectionLifecycleManager = CollectionLifecycleManager; //# sourceMappingURL=lifecycle.cjs.map