@tanstack/db
Version:
A reactive client store for building super fast apps on sync
1 lines • 10.6 kB
Source Map (JSON)
{"version":3,"file":"scheduler.cjs","sources":["../../src/scheduler.ts"],"sourcesContent":["import { runAllCallbacks } from './utils/callbacks.js'\n\n/**\n * Identifier used to scope scheduled work. Maps to a transaction id for live queries.\n */\nexport type SchedulerContextId = string | symbol\n\n/**\n * Options for {@link Scheduler.schedule}. Jobs are identified by `jobId` within a context\n * and may declare dependencies.\n */\ninterface ScheduleOptions {\n contextId?: SchedulerContextId\n jobId: unknown\n dependencies?: Iterable<unknown>\n run: () => void\n}\n\n/**\n * State per context. Queue preserves order, jobs hold run functions, dependencies track\n * prerequisites. A job leaves the pending map before its callback runs, so work\n * queued by that callback is a new pending dependency.\n */\ninterface SchedulerContextState {\n queue: Array<unknown>\n jobs: Map<unknown, () => void>\n dependencies: Map<unknown, Set<unknown>>\n}\n\ninterface PendingAwareJob {\n hasPendingGraphRun: (contextId: SchedulerContextId) => boolean\n}\n\nfunction isPendingAwareJob(dep: any): dep is PendingAwareJob {\n return (\n typeof dep === `object` &&\n dep !== null &&\n typeof dep.hasPendingGraphRun === `function`\n )\n}\n\n/**\n * Scoped scheduler that coalesces work by context and job.\n *\n * - **context** (e.g. transaction id) defines the batching boundary; work is queued until flushed.\n * - **job id** deduplicates work within a context; scheduling the same job replaces the previous run function.\n * - Without a context id, work executes immediately.\n *\n * Callers manage their own state; the scheduler only orchestrates execution order.\n */\nexport class Scheduler {\n private contexts = new Map<SchedulerContextId, SchedulerContextState>()\n private clearListeners = new Set<(contextId: SchedulerContextId) => void>()\n\n /**\n * Get or create the state bucket for a context.\n */\n private getOrCreateContext(\n contextId: SchedulerContextId,\n ): SchedulerContextState {\n let context = this.contexts.get(contextId)\n if (!context) {\n context = {\n queue: [],\n jobs: new Map(),\n dependencies: new Map(),\n }\n this.contexts.set(contextId, context)\n }\n return context\n }\n\n /**\n * Schedule work. Without a context id, executes immediately.\n * Otherwise queues the job to be flushed once dependencies are satisfied.\n * Scheduling the same jobId again replaces the previous run function.\n */\n schedule({ contextId, jobId, dependencies, run }: ScheduleOptions): void {\n if (typeof contextId === `undefined`) {\n run()\n return\n }\n\n const context = this.getOrCreateContext(contextId)\n\n // If this is a new job, add it to the queue\n if (!context.jobs.has(jobId)) {\n context.queue.push(jobId)\n }\n\n // Store or replace the run function\n context.jobs.set(jobId, run)\n\n // Update dependencies\n if (dependencies) {\n const depSet = new Set<unknown>(dependencies)\n depSet.delete(jobId)\n context.dependencies.set(jobId, depSet)\n } else if (!context.dependencies.has(jobId)) {\n context.dependencies.set(jobId, new Set())\n }\n }\n\n /**\n * Flush all queued work for a context. Jobs with unmet dependencies are retried.\n * Throws if a pass completes without running any job (dependency cycle).\n */\n flush(contextId: SchedulerContextId): void {\n const context = this.contexts.get(contextId)\n if (!context) return\n\n const { queue, jobs, dependencies } = context\n\n while (queue.length > 0) {\n let ranThisPass = false\n const jobsThisPass = queue.length\n\n for (let i = 0; i < jobsThisPass; i++) {\n const jobId = queue.shift()!\n const run = jobs.get(jobId)\n if (!run) {\n dependencies.delete(jobId)\n continue\n }\n\n const deps = dependencies.get(jobId)\n let ready = !deps\n if (deps) {\n ready = true\n for (const dep of deps) {\n if (dep === jobId) continue\n\n const depHasPending =\n isPendingAwareJob(dep) && dep.hasPendingGraphRun(contextId)\n\n // Treat dependencies as blocking if the dep has a pending run in this\n // context or if it's enqueued. If the dep is\n // neither pending nor enqueued, consider it satisfied to avoid deadlocks\n // on lazy sources that never schedule work.\n if (jobs.has(dep) || depHasPending) {\n ready = false\n break\n }\n }\n }\n\n if (ready) {\n jobs.delete(jobId)\n dependencies.delete(jobId)\n // A reentrant schedule now owns a fresh pending job; finishing this\n // callback must not mark that replacement as complete.\n run()\n ranThisPass = true\n } else {\n queue.push(jobId)\n }\n }\n\n if (!ranThisPass) {\n throw new Error(\n `Scheduler detected unresolved dependencies for context ${String(\n contextId,\n )}.`,\n )\n }\n }\n\n this.contexts.delete(contextId)\n }\n\n /** Clear all scheduled jobs for a context. */\n clear(contextId: SchedulerContextId): void {\n this.contexts.delete(contextId)\n runAllCallbacks(\n [...this.clearListeners].map((listener) => () => listener(contextId)),\n )\n }\n\n /** Register a listener to be notified when a context is cleared. */\n onClear(listener: (contextId: SchedulerContextId) => void): () => void {\n this.clearListeners.add(listener)\n return () => this.clearListeners.delete(listener)\n }\n}\n\nexport const transactionScopedScheduler = new Scheduler()\n\nlet activePublicationContext: SchedulerContextId | undefined\nlet activePublicationFailure: { error: unknown } | undefined\n\nfunction getActivePublicationFailure(): { error: unknown } | undefined {\n return activePublicationFailure\n}\n\n/**\n * Returns the Collection publication that currently owns synchronous change\n * delivery. Live-query jobs use it to coalesce all source subscriptions that\n * observe one committed batch.\n */\nexport function getActivePublicationContext(): SchedulerContextId | undefined {\n return activePublicationContext\n}\n\n/** Report a listener failure after the whole publication graph has drained. */\nexport function recordPublicationError(error: unknown): void {\n if (activePublicationContext === undefined) throw error\n activePublicationFailure ??= { error }\n}\n\n/**\n * Runs one synchronous Collection publication inside a scheduler context.\n * Nested publications share the outer context, so downstream live queries run\n * only after every subscriber to the original committed batch has observed it.\n */\nexport function withPublicationContext<T>(publish: () => T): T {\n if (activePublicationContext !== undefined) return publish()\n\n const contextId = Symbol(`collection-publication`)\n activePublicationContext = contextId\n activePublicationFailure = undefined\n let result!: T\n let listenerFailure: { error: unknown } | undefined\n try {\n result = publish()\n transactionScopedScheduler.flush(contextId)\n listenerFailure = getActivePublicationFailure()\n } catch (error) {\n try {\n transactionScopedScheduler.clear(contextId)\n } catch {\n // Keep the earlier publication or graph failure.\n }\n // Keep the first reported failure, including one from an earlier listener.\n const publicationFailure = getActivePublicationFailure()\n if (publicationFailure) {\n throw publicationFailure.error\n }\n throw error\n } finally {\n activePublicationContext = undefined\n activePublicationFailure = undefined\n }\n if (listenerFailure) throw listenerFailure.error\n return result\n}\n"],"names":["runAllCallbacks"],"mappings":"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"}