{"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":";;;AAiCA,SAAS,kBAAkB,KAAkC;AAC3D,SACE,OAAO,QAAQ,YACf,QAAQ,QACR,OAAO,IAAI,uBAAuB;AAEtC;AAWO,MAAM,UAAU;AAAA,EAAhB,cAAA;AACL,SAAQ,+BAAe,IAAA;AACvB,SAAQ,qCAAqB,IAAA;AAAA,EAA6C;AAAA;AAAA;AAAA;AAAA,EAKlE,mBACN,WACuB;AACvB,QAAI,UAAU,KAAK,SAAS,IAAI,SAAS;AACzC,QAAI,CAAC,SAAS;AACZ,gBAAU;AAAA,QACR,OAAO,CAAA;AAAA,QACP,0BAAU,IAAA;AAAA,QACV,kCAAkB,IAAA;AAAA,MAAI;AAExB,WAAK,SAAS,IAAI,WAAW,OAAO;AAAA,IACtC;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,SAAS,EAAE,WAAW,OAAO,cAAc,OAA8B;AACvE,QAAI,OAAO,cAAc,aAAa;AACpC,UAAA;AACA;AAAA,IACF;AAEA,UAAM,UAAU,KAAK,mBAAmB,SAAS;AAGjD,QAAI,CAAC,QAAQ,KAAK,IAAI,KAAK,GAAG;AAC5B,cAAQ,MAAM,KAAK,KAAK;AAAA,IAC1B;AAGA,YAAQ,KAAK,IAAI,OAAO,GAAG;AAG3B,QAAI,cAAc;AAChB,YAAM,SAAS,IAAI,IAAa,YAAY;AAC5C,aAAO,OAAO,KAAK;AACnB,cAAQ,aAAa,IAAI,OAAO,MAAM;AAAA,IACxC,WAAW,CAAC,QAAQ,aAAa,IAAI,KAAK,GAAG;AAC3C,cAAQ,aAAa,IAAI,OAAO,oBAAI,KAAK;AAAA,IAC3C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,WAAqC;AACzC,UAAM,UAAU,KAAK,SAAS,IAAI,SAAS;AAC3C,QAAI,CAAC,QAAS;AAEd,UAAM,EAAE,OAAO,MAAM,aAAA,IAAiB;AAEtC,WAAO,MAAM,SAAS,GAAG;AACvB,UAAI,cAAc;AAClB,YAAM,eAAe,MAAM;AAE3B,eAAS,IAAI,GAAG,IAAI,cAAc,KAAK;AACrC,cAAM,QAAQ,MAAM,MAAA;AACpB,cAAM,MAAM,KAAK,IAAI,KAAK;AAC1B,YAAI,CAAC,KAAK;AACR,uBAAa,OAAO,KAAK;AACzB;AAAA,QACF;AAEA,cAAM,OAAO,aAAa,IAAI,KAAK;AACnC,YAAI,QAAQ,CAAC;AACb,YAAI,MAAM;AACR,kBAAQ;AACR,qBAAW,OAAO,MAAM;AACtB,gBAAI,QAAQ,MAAO;AAEnB,kBAAM,gBACJ,kBAAkB,GAAG,KAAK,IAAI,mBAAmB,SAAS;AAM5D,gBAAI,KAAK,IAAI,GAAG,KAAK,eAAe;AAClC,sBAAQ;AACR;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAEA,YAAI,OAAO;AACT,eAAK,OAAO,KAAK;AACjB,uBAAa,OAAO,KAAK;AAGzB,cAAA;AACA,wBAAc;AAAA,QAChB,OAAO;AACL,gBAAM,KAAK,KAAK;AAAA,QAClB;AAAA,MACF;AAEA,UAAI,CAAC,aAAa;AAChB,cAAM,IAAI;AAAA,UACR,0DAA0D;AAAA,YACxD;AAAA,UAAA,CACD;AAAA,QAAA;AAAA,MAEL;AAAA,IACF;AAEA,SAAK,SAAS,OAAO,SAAS;AAAA,EAChC;AAAA;AAAA,EAGA,MAAM,WAAqC;AACzC,SAAK,SAAS,OAAO,SAAS;AAC9BA,cAAAA;AAAAA,MACE,CAAC,GAAG,KAAK,cAAc,EAAE,IAAI,CAAC,aAAa,MAAM,SAAS,SAAS,CAAC;AAAA,IAAA;AAAA,EAExE;AAAA;AAAA,EAGA,QAAQ,UAA+D;AACrE,SAAK,eAAe,IAAI,QAAQ;AAChC,WAAO,MAAM,KAAK,eAAe,OAAO,QAAQ;AAAA,EAClD;AACF;AAEO,MAAM,6BAA6B,IAAI,UAAA;AAE9C,IAAI;AACJ,IAAI;AAEJ,SAAS,8BAA8D;AACrE,SAAO;AACT;AAOO,SAAS,8BAA8D;AAC5E,SAAO;AACT;AAGO,SAAS,uBAAuB,OAAsB;AAC3D,MAAI,6BAA6B,OAAW,OAAM;AAClD,+BAA6B,EAAE,MAAA;AACjC;AAOO,SAAS,uBAA0B,SAAqB;AAC7D,MAAI,6BAA6B,OAAW,QAAO,QAAA;AAEnD,QAAM,mCAAmB,wBAAwB;AACjD,6BAA2B;AAC3B,6BAA2B;AAC3B,MAAI;AACJ,MAAI;AACJ,MAAI;AACF,aAAS,QAAA;AACT,+BAA2B,MAAM,SAAS;AAC1C,sBAAkB,4BAAA;AAAA,EACpB,SAAS,OAAO;AACd,QAAI;AACF,iCAA2B,MAAM,SAAS;AAAA,IAC5C,QAAQ;AAAA,IAER;AAEA,UAAM,qBAAqB,4BAAA;AAC3B,QAAI,oBAAoB;AACtB,YAAM,mBAAmB;AAAA,IAC3B;AACA,UAAM;AAAA,EACR,UAAA;AACE,+BAA2B;AAC3B,+BAA2B;AAAA,EAC7B;AACA,MAAI,uBAAuB,gBAAgB;AAC3C,SAAO;AACT;;;;;;"}