@catalyst-cloud/sdk 0.14.0 → 0.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/node.js CHANGED
@@ -7305,7 +7305,8 @@ function searchIndexesOver(db, tables) {
7305
7305
  );
7306
7306
  const pairs = JSON.parse(String(row?.["j"] ?? "[]"));
7307
7307
  return pairs.filter(([, ddl]) => {
7308
- const content = /content\s*=\s*'([^']*)'/i.exec(ddl)?.[1];
7308
+ const m = /content\s*=\s*(?:'([^']*)'|"([^"]*)"|([A-Za-z_]\w*))/i.exec(ddl);
7309
+ const content = m?.[1] ?? m?.[2] ?? m?.[3];
7309
7310
  return content !== void 0 && tables.has(content);
7310
7311
  }).map(([name]) => name);
7311
7312
  }
@@ -7619,6 +7620,9 @@ var CatalystReplica = class _CatalystReplica {
7619
7620
  fetchImpl;
7620
7621
  log;
7621
7622
  engine = null;
7623
+ /** The dynamic engine open is owned until it settles; closeAndWait must not release the lock first. */
7624
+ engineOpening = null;
7625
+ shutdownTask = null;
7622
7626
  writeDb = null;
7623
7627
  sqlExecutor = null;
7624
7628
  client = null;
@@ -7632,11 +7636,9 @@ var CatalystReplica = class _CatalystReplica {
7632
7636
  readonlyMode = false;
7633
7637
  /** The claimed single-writer lock (writers only; null for readers / `:memory:` / disabled). */
7634
7638
  writerLock = null;
7635
- /** The in-flight seed's AbortController (CTC-281), or null when no seed is running. close() aborts
7636
- * it SYNCHRONOUSLY so the pending /snapshot fetch + body read release their connection instead of
7637
- * keeping the event loop referenced after close() resolved — the "close() succeeded but the
7638
- * process can't exit" hostage mechanism from the incident chain. Also the seed idle-timeout's
7639
- * abort seam. One controller per seed RUN (a retried seed gets a fresh one). */
7639
+ /** The in-flight seed's AbortController (CTC-281), synchronously aborted by closeAndWait before it
7640
+ * joins the fetch/body-reader unwind. Also the seed idle-timeout's abort seam. One controller per
7641
+ * seed RUN (a retried seed gets a fresh one). */
7640
7642
  seedAbort = null;
7641
7643
  /** Resolved on the first 'live' status (start() = caught-up + ready to read); rejected on close /
7642
7644
  * an initial seed failure. */
@@ -7728,7 +7730,18 @@ var CatalystReplica = class _CatalystReplica {
7728
7730
  if (this.started) throw new Error("CatalystReplica: start() already called");
7729
7731
  this.started = true;
7730
7732
  this.writerLock = claimWriterLock(this.opts.dbPath, this.opts.writerGuard ?? {}, this.log);
7731
- const engine = await this.resolveEngine();
7733
+ const engineOpening = this.resolveEngine();
7734
+ this.engineOpening = engineOpening;
7735
+ let engine;
7736
+ try {
7737
+ engine = await engineOpening;
7738
+ } finally {
7739
+ if (this.engineOpening === engineOpening) this.engineOpening = null;
7740
+ }
7741
+ if (this.closed) {
7742
+ this.engine = engine;
7743
+ throw new Error("CatalystReplica: start cancelled by close()");
7744
+ }
7732
7745
  this.engine = engine;
7733
7746
  const stored = readAccountStamp(engine);
7734
7747
  const mySource = this.opts.accountSource ?? DEFAULT_ACCOUNT_SOURCE;
@@ -7827,13 +7840,14 @@ var CatalystReplica = class _CatalystReplica {
7827
7840
  tracerName: DEFAULT_SCOPE_NAME,
7828
7841
  meterName: DEFAULT_SCOPE_NAME
7829
7842
  });
7843
+ if (this.closed) throw new Error("CatalystReplica: start cancelled by close()");
7830
7844
  this.registerMetrics();
7831
7845
  this.client = new LiveSyncClient({
7832
7846
  baseUrl: this.baseUrl,
7833
7847
  accountId: this.opts.account,
7834
7848
  connectPath: this.opts.connectPath,
7835
7849
  auth: this.opts.auth,
7836
- reseed: () => this.seedFromSnapshot(),
7850
+ reseed: (signal) => this.seedFromSnapshot(signal),
7837
7851
  getCursor: () => getCursor(this.writeDb),
7838
7852
  connectParams: () => this.workflowRevParams(),
7839
7853
  onChange: (frame) => this.applyFrame(frame),
@@ -7868,13 +7882,11 @@ var CatalystReplica = class _CatalystReplica {
7868
7882
  const timer = setTimeout(() => {
7869
7883
  if (settled) return;
7870
7884
  settled = true;
7871
- void this.close().finally(() => {
7872
- reject(
7873
- new Error(
7874
- `CatalystReplica.start() did not reach 'live' within ${timeoutMs}ms (account=${this.opts.account}, dbPath=${this.opts.dbPath})`
7875
- )
7876
- );
7877
- });
7885
+ const timeoutMessage = `CatalystReplica.start() did not reach 'live' within ${timeoutMs}ms (account=${this.opts.account}, dbPath=${this.opts.dbPath})`;
7886
+ void this.close().then(
7887
+ () => reject(new Error(timeoutMessage)),
7888
+ (cleanupError) => reject(new Error(timeoutMessage, { cause: cleanupError }))
7889
+ );
7878
7890
  }, timeoutMs);
7879
7891
  timer.unref?.();
7880
7892
  livePromise.then(
@@ -7906,14 +7918,23 @@ var CatalystReplica = class _CatalystReplica {
7906
7918
  if (!this.client) throw new Error("CatalystReplica: resume() before start()");
7907
7919
  this.client.resume();
7908
7920
  }
7909
- /** Stop the socket, release the writer lock, close the DB. Idempotent. Rejects a still-pending
7910
- * start(). On a reader: no socket/lock to release — just closes the read-only handle. */
7911
- async close() {
7912
- if (this.closed) return;
7921
+ /**
7922
+ * Compatibility shutdown entrypoint. It now acknowledges actual seed/engine settlement before
7923
+ * releasing the writer guard or closing SQLite; callers that need an explicit lifecycle name can
7924
+ * use closeAndWait().
7925
+ */
7926
+ close() {
7927
+ return this.closeAndWait();
7928
+ }
7929
+ /** Stop the socket and join engine-open and transport seed/reseed work before releasing ownership. */
7930
+ async closeAndWait() {
7931
+ if (this.shutdownTask) return this.shutdownTask;
7913
7932
  this.closed = true;
7914
7933
  this.client?.stop();
7915
7934
  try {
7916
- this.seedAbort?.abort(new Error("CatalystReplica closed"));
7935
+ const abortError = new Error("CatalystReplica closed");
7936
+ abortError.name = "AbortError";
7937
+ this.seedAbort?.abort(abortError);
7917
7938
  } catch (err) {
7918
7939
  this.log("warn", "seed abort threw", err);
7919
7940
  }
@@ -7921,25 +7942,45 @@ var CatalystReplica = class _CatalystReplica {
7921
7942
  const rej = this.liveReject;
7922
7943
  this.clearLiveDeferred();
7923
7944
  rej?.(new Error("CatalystReplica: closed before first 'live'"));
7924
- try {
7925
- this.writerLock?.release();
7926
- } catch (err) {
7927
- this.log("warn", "writer-lock release threw", err);
7928
- }
7929
- this.writerLock = null;
7930
- for (const reg of this.metricRegs) {
7945
+ const client = this.client;
7946
+ const shutdown = async () => {
7947
+ const failures = [];
7948
+ if (this.engineOpening) await this.engineOpening.catch(() => void 0);
7949
+ let seedIoAcknowledged = false;
7931
7950
  try {
7932
- reg.remove();
7951
+ await client?.stopAndWait();
7952
+ seedIoAcknowledged = true;
7933
7953
  } catch (err) {
7934
- this.log("warn", "metric unregister threw", err);
7954
+ failures.push(err);
7935
7955
  }
7936
- }
7937
- this.metricRegs = [];
7938
- try {
7939
- this.engine?.close();
7940
- } catch (err) {
7941
- this.log("warn", "engine close threw", err);
7942
- }
7956
+ for (const reg of this.metricRegs) {
7957
+ try {
7958
+ reg.remove();
7959
+ } catch (err) {
7960
+ failures.push(err);
7961
+ }
7962
+ }
7963
+ this.metricRegs = [];
7964
+ let engineClosed = this.engine == null;
7965
+ try {
7966
+ this.engine?.close();
7967
+ engineClosed = true;
7968
+ this.engine = null;
7969
+ } catch (err) {
7970
+ failures.push(err);
7971
+ }
7972
+ if (engineClosed && seedIoAcknowledged) {
7973
+ try {
7974
+ this.writerLock?.release();
7975
+ this.writerLock = null;
7976
+ } catch (err) {
7977
+ failures.push(err);
7978
+ }
7979
+ }
7980
+ if (failures.length) throw new AggregateError(failures, "CatalystReplica shutdown failed");
7981
+ };
7982
+ this.shutdownTask = shutdown();
7983
+ return this.shutdownTask;
7943
7984
  }
7944
7985
  // ── Reads ───────────────────────────────────────────────────────────────────────────────────────
7945
7986
  /** The read-model SqlExecutor over the replica. `buildIssuesView(replica.sql, …)` is unchanged. */
@@ -8258,16 +8299,19 @@ var CatalystReplica = class _CatalystReplica {
8258
8299
  * interrupted seed self-heals (getCursor → null → re-seed on the next start), preserving host-sync's
8259
8300
  * atomic truncate+apply+setCursor safety without holding one giant transaction.
8260
8301
  */
8261
- async seedFromSnapshot() {
8302
+ async seedFromSnapshot(attemptSignal) {
8262
8303
  return this.telemetry.withActiveSpan(
8263
8304
  REPLICA_SPAN.seed,
8264
8305
  { [CATALYST_ATTR.tenant]: this.opts.account },
8265
8306
  async (span) => {
8266
8307
  const engine = this.engine;
8267
8308
  const writeDb = this.writeDb;
8309
+ if (attemptSignal?.aborted) throw attemptSignal.reason;
8268
8310
  if (this.closed) throw new Error("CatalystReplica: seed aborted (already closed)");
8269
8311
  const abort = new AbortController();
8270
8312
  this.seedAbort = abort;
8313
+ const onAttemptAbort = () => abort.abort(attemptSignal?.reason);
8314
+ attemptSignal?.addEventListener("abort", onAttemptAbort, { once: true });
8271
8315
  const idleMs = this.opts.snapshotIdleTimeoutMs ?? 3e4;
8272
8316
  let idleTimer = null;
8273
8317
  const clearIdle = () => {
@@ -8377,6 +8421,7 @@ var CatalystReplica = class _CatalystReplica {
8377
8421
  throw err;
8378
8422
  } finally {
8379
8423
  clearIdle();
8424
+ attemptSignal?.removeEventListener("abort", onAttemptAbort);
8380
8425
  if (this.seedAbort === abort) this.seedAbort = null;
8381
8426
  }
8382
8427
  }
@@ -177,6 +177,9 @@ export declare class CatalystReplica {
177
177
  private readonly fetchImpl;
178
178
  private readonly log;
179
179
  private engine;
180
+ /** The dynamic engine open is owned until it settles; closeAndWait must not release the lock first. */
181
+ private engineOpening;
182
+ private shutdownTask;
180
183
  private writeDb;
181
184
  private sqlExecutor;
182
185
  private client;
@@ -190,11 +193,9 @@ export declare class CatalystReplica {
190
193
  private readonlyMode;
191
194
  /** The claimed single-writer lock (writers only; null for readers / `:memory:` / disabled). */
192
195
  private writerLock;
193
- /** The in-flight seed's AbortController (CTC-281), or null when no seed is running. close() aborts
194
- * it SYNCHRONOUSLY so the pending /snapshot fetch + body read release their connection instead of
195
- * keeping the event loop referenced after close() resolved — the "close() succeeded but the
196
- * process can't exit" hostage mechanism from the incident chain. Also the seed idle-timeout's
197
- * abort seam. One controller per seed RUN (a retried seed gets a fresh one). */
196
+ /** The in-flight seed's AbortController (CTC-281), synchronously aborted by closeAndWait before it
197
+ * joins the fetch/body-reader unwind. Also the seed idle-timeout's abort seam. One controller per
198
+ * seed RUN (a retried seed gets a fresh one). */
198
199
  private seedAbort;
199
200
  /** Resolved on the first 'live' status (start() = caught-up + ready to read); rejected on close /
200
201
  * an initial seed failure. */
@@ -263,9 +264,14 @@ export declare class CatalystReplica {
263
264
  * after `close()`.
264
265
  */
265
266
  resume(): void;
266
- /** Stop the socket, release the writer lock, close the DB. Idempotent. Rejects a still-pending
267
- * start(). On a reader: no socket/lock to release — just closes the read-only handle. */
267
+ /**
268
+ * Compatibility shutdown entrypoint. It now acknowledges actual seed/engine settlement before
269
+ * releasing the writer guard or closing SQLite; callers that need an explicit lifecycle name can
270
+ * use closeAndWait().
271
+ */
268
272
  close(): Promise<void>;
273
+ /** Stop the socket and join engine-open and transport seed/reseed work before releasing ownership. */
274
+ closeAndWait(): Promise<void>;
269
275
  /** The read-model SqlExecutor over the replica. `buildIssuesView(replica.sql, …)` is unchanged. */
270
276
  get sql(): SqlExecutor;
271
277
  issues(opts?: {
@@ -1 +1 @@
1
- {"version":3,"file":"catalyst-replica.d.ts","sourceRoot":"","sources":["../../src/replica/catalyst-replica.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAYhE,OAAO,EAQL,KAAK,WAAW,EAEhB,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,KAAK,QAAQ,EACb,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,oBAAoB,EAC1B,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EACL,SAAS,EAGT,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,gBAAgB,EACtB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,aAAa,EACnB,MAAM,aAAa,CAAC;AACrB,OAAO,EAEL,KAAK,kBAAkB,EAExB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAYL,KAAK,eAAe,EAIrB,MAAM,YAAY,CAAC;AA4BpB;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,SAAS,CAAC;AAanD;;;;;;GAMG;AACH,qBAAa,2BAA4B,SAAQ,KAAK;IAElD,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,aAAa,EAAE,MAAM;IAC9B,QAAQ,CAAC,iBAAiB,EAAE,MAAM;IAClC;;+EAE2E;IAC3E,QAAQ,CAAC,mBAAmB,EAAE,aAAa;gBANlC,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,EACrB,iBAAiB,EAAE,MAAM;IAClC;;+EAE2E;IAClE,mBAAmB,GAAE,aAA0B;CAa3D;AAiCD;;;;;GAKG;AACH,OAAO,EAAE,wBAAwB,EAAE,CAAC;AAEpC,MAAM,WAAW,sBAAsB;IACrC,oGAAoG;IACpG,OAAO,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;;;;;;;;;;;OAeG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;0GAEsG;IACtG,IAAI,EAAE,YAAY,CAAC;IACnB,+BAA+B;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,8GAA8G;IAC9G,MAAM,CAAC,EAAE,aAAa,GAAG,aAAa,CAAC;IACvC,6CAA6C;IAC7C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oDAAoD;IACpD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,wFAAwF;IACxF,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,CAAC;IAC5C;;;;6DAIyD;IACzD,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,SAAS,KAAK,IAAI,CAAC;IACvC,kDAAkD;IAClD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yGAAyG;IACzG,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;4GACwG;IACxG,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;;OAQG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;sFAIkF;IAClF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;2GACuG;IACvG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;0CACsC;IACtC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,sFAAsF;IACtF,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B,uDAAuD;IACvD,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAC9D;;;;;OAKG;IACH,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;;;;;;;;;OAcG;IACH,SAAS,CAAC,EAAE,eAAe,CAAC;CAC7B;AAED;;;;GAIG;AACH,MAAM,WAAW,8BAA8B;IAC7C;qDACiD;IACjD,MAAM,EAAE,MAAM,CAAC;IACf;yGACqG;IACrG,MAAM,CAAC,EAAE,aAAa,GAAG,aAAa,CAAC;IACvC,uDAAuD;IACvD,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;CAC/D;AAUD,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAyB;IAC9C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;IACzC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA6C;IAEjE,OAAO,CAAC,MAAM,CAA8B;IAC5C,OAAO,CAAC,OAAO,CAAwC;IACvD,OAAO,CAAC,WAAW,CAA4B;IAC/C,OAAO,CAAC,MAAM,CAA+B;IAE7C,sGAAsG;IACtG,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,OAAO,CAAgC;IAC/C,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,MAAM,CAAS;IAEvB;0EACsE;IACtE,OAAO,CAAC,YAAY,CAAS;IAC7B,+FAA+F;IAC/F,OAAO,CAAC,UAAU,CAAiC;IAEnD;;;;qFAIiF;IACjF,OAAO,CAAC,SAAS,CAAgC;IAEjD;mCAC+B;IAC/B,OAAO,CAAC,WAAW,CAA6B;IAChD,OAAO,CAAC,UAAU,CAAyC;IAG3D,4FAA4F;IAC5F,OAAO,CAAC,SAAS,CAA6B;IAC9C,yGAAyG;IACzG,OAAO,CAAC,UAAU,CAA2B;IAC7C,sGAAsG;IACtG,OAAO,CAAC,cAAc,CAA2D;IACjF,OAAO,CAAC,mBAAmB,CAAsE;IACjG;kGAC8F;IAC9F,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,gBAAgB,CAAuB;IAC/C,oGAAoG;IACpG,OAAO,CAAC,eAAe,CAAuB;IAC9C;yGACqG;IACrG,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqB;IACjD;;;;;OAKG;IACH,OAAO,CAAC,oBAAoB,CAAuD;IACnF;;;wEAGoE;IACpE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAExB;IAEF;;2GAEuG;IACvG,OAAO,CAAC,YAAY;gBAKR,IAAI,EAAE,sBAAsB;IAUxC;;;;;;;;;;;OAWG;WACU,YAAY,CAAC,IAAI,EAAE,8BAA8B,GAAG,OAAO,CAAC,eAAe,CAAC;IAqBzF;;;;;;OAMG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA4T5B;;;;;;;OAOG;IACH,MAAM,IAAI,IAAI;IAMd;8FAC0F;IACpF,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAyC5B,mGAAmG;IACnG,IAAI,GAAG,IAAI,WAAW,CAGrB;IAED,MAAM,CAAC,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,EAAE;IAG/D,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI;IAGjD,KAAK,CAAC,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,QAAQ,EAAE;IAG7D,QAAQ,CAAC,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,WAAW,EAAE;IAGnE,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI;IAG7C,WAAW,CAAC,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,cAAc,EAAE;IAGzE,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,oBAAoB,GAAG,IAAI;IAInD,iFAAiF;IACjF,IAAI,MAAM,IAAI,MAAM,GAAG,IAAI,CAE1B;IAED,uCAAuC;IACvC,IAAI,MAAM,IAAI,cAAc,CAE3B;IAED;;wGAEoG;IACpG,IAAI,WAAW,IAAI,MAAM,GAAG,IAAI,CAE/B;IAED;;;kGAG8F;IAC9F,IAAI,iBAAiB,IAAI,MAAM,GAAG,IAAI,CAErC;IAED,sGAAsG;IACtG,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED;kGAC8F;IAC9F,IAAI,WAAW,IAAI,MAAM,GAAG,IAAI,CAE/B;IAED;0GACsG;IACtG,IAAI,OAAO,IAAI,MAAM,GAAG,IAAI,CAE3B;IAED;mGAC+F;IAC/F,IAAI,YAAY,IAAI,MAAM,GAAG,IAAI,CAEhC;YAIa,aAAa;IAO3B;;;;;OAKG;YACW,oBAAoB;IA6BlC,OAAO,CAAC,YAAY;IAcpB;yFACqF;IACrF,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,iBAAiB;IAKzB;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IA8CvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACH,OAAO,CAAC,iBAAiB;IAazB,sGAAsG;IACtG,OAAO,CAAC,UAAU;IAoElB,gFAAgF;IAChF,OAAO,CAAC,SAAS;IASjB;;;;;;;;;;;;;;;OAeG;IACH;;;;;;OAMG;IACH,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,iBAAiB;IAkBzB;;;;;;OAMG;YACW,gBAAgB;IA2L9B;+EAC2E;IAC3E,OAAO,CAAC,kBAAkB;IAgB1B;;sEAEkE;YACpD,WAAW;CAmB1B"}
1
+ {"version":3,"file":"catalyst-replica.d.ts","sourceRoot":"","sources":["../../src/replica/catalyst-replica.ts"],"names":[],"mappings":"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"}
@@ -132,6 +132,9 @@ export class CatalystReplica {
132
132
  fetchImpl;
133
133
  log;
134
134
  engine = null;
135
+ /** The dynamic engine open is owned until it settles; closeAndWait must not release the lock first. */
136
+ engineOpening = null;
137
+ shutdownTask = null;
135
138
  writeDb = null;
136
139
  sqlExecutor = null;
137
140
  client = null;
@@ -145,11 +148,9 @@ export class CatalystReplica {
145
148
  readonlyMode = false;
146
149
  /** The claimed single-writer lock (writers only; null for readers / `:memory:` / disabled). */
147
150
  writerLock = null;
148
- /** The in-flight seed's AbortController (CTC-281), or null when no seed is running. close() aborts
149
- * it SYNCHRONOUSLY so the pending /snapshot fetch + body read release their connection instead of
150
- * keeping the event loop referenced after close() resolved — the "close() succeeded but the
151
- * process can't exit" hostage mechanism from the incident chain. Also the seed idle-timeout's
152
- * abort seam. One controller per seed RUN (a retried seed gets a fresh one). */
151
+ /** The in-flight seed's AbortController (CTC-281), synchronously aborted by closeAndWait before it
152
+ * joins the fetch/body-reader unwind. Also the seed idle-timeout's abort seam. One controller per
153
+ * seed RUN (a retried seed gets a fresh one). */
153
154
  seedAbort = null;
154
155
  /** Resolved on the first 'live' status (start() = caught-up + ready to read); rejected on close /
155
156
  * an initial seed failure. */
@@ -250,7 +251,22 @@ export class CatalystReplica {
250
251
  // Single-writer guard: claim sole ownership of the file BEFORE opening it, so a second concurrent
251
252
  // writer rejects here instead of racing the cursor/seed. A no-op for ':memory:' or when disabled.
252
253
  this.writerLock = claimWriterLock(this.opts.dbPath, this.opts.writerGuard ?? {}, this.log);
253
- const engine = await this.resolveEngine();
254
+ const engineOpening = this.resolveEngine();
255
+ this.engineOpening = engineOpening;
256
+ let engine;
257
+ try {
258
+ engine = await engineOpening;
259
+ }
260
+ finally {
261
+ if (this.engineOpening === engineOpening)
262
+ this.engineOpening = null;
263
+ }
264
+ // close() may have been requested while the sqlite driver was opening. The lock stays held until
265
+ // that opener settles, and a late opener is closed before migrations or any other writes begin.
266
+ if (this.closed) {
267
+ this.engine = engine;
268
+ throw new Error("CatalystReplica: start cancelled by close()");
269
+ }
254
270
  this.engine = engine;
255
271
  // ── CTC-582: THE ACCOUNT FENCE ────────────────────────────────────────────────────────────────
256
272
  //
@@ -446,13 +462,15 @@ export class CatalystReplica {
446
462
  tracerName: DEFAULT_SCOPE_NAME,
447
463
  meterName: DEFAULT_SCOPE_NAME,
448
464
  });
465
+ if (this.closed)
466
+ throw new Error("CatalystReplica: start cancelled by close()");
449
467
  this.registerMetrics();
450
468
  this.client = new LiveSyncClient({
451
469
  baseUrl: this.baseUrl,
452
470
  accountId: this.opts.account,
453
471
  connectPath: this.opts.connectPath,
454
472
  auth: this.opts.auth,
455
- reseed: () => this.seedFromSnapshot(),
473
+ reseed: (signal) => this.seedFromSnapshot(signal),
456
474
  getCursor: () => getCursor(this.writeDb),
457
475
  connectParams: () => this.workflowRevParams(),
458
476
  onChange: (frame) => this.applyFrame(frame),
@@ -496,10 +514,11 @@ export class CatalystReplica {
496
514
  if (settled)
497
515
  return;
498
516
  settled = true;
499
- void this.close().finally(() => {
500
- reject(new Error(`CatalystReplica.start() did not reach 'live' within ${timeoutMs}ms ` +
501
- `(account=${this.opts.account}, dbPath=${this.opts.dbPath})`));
502
- });
517
+ const timeoutMessage = `CatalystReplica.start() did not reach 'live' within ${timeoutMs}ms ` +
518
+ `(account=${this.opts.account}, dbPath=${this.opts.dbPath})`;
519
+ // Consume both shutdown outcomes. An ignored finally() promise would reject independently
520
+ // when cleanup fails, leaving the caller with only the timeout and an unhandled rejection.
521
+ void this.close().then(() => reject(new Error(timeoutMessage)), (cleanupError) => reject(new Error(timeoutMessage, { cause: cleanupError })));
503
522
  }, timeoutMs);
504
523
  timer.unref?.();
505
524
  livePromise.then(() => {
@@ -532,19 +551,25 @@ export class CatalystReplica {
532
551
  throw new Error("CatalystReplica: resume() before start()");
533
552
  this.client.resume();
534
553
  }
535
- /** Stop the socket, release the writer lock, close the DB. Idempotent. Rejects a still-pending
536
- * start(). On a reader: no socket/lock to release — just closes the read-only handle. */
537
- async close() {
538
- if (this.closed)
539
- return;
554
+ /**
555
+ * Compatibility shutdown entrypoint. It now acknowledges actual seed/engine settlement before
556
+ * releasing the writer guard or closing SQLite; callers that need an explicit lifecycle name can
557
+ * use closeAndWait().
558
+ */
559
+ close() {
560
+ return this.closeAndWait();
561
+ }
562
+ /** Stop the socket and join engine-open and transport seed/reseed work before releasing ownership. */
563
+ async closeAndWait() {
564
+ if (this.shutdownTask)
565
+ return this.shutdownTask;
540
566
  this.closed = true;
541
567
  this.client?.stop();
542
- // Abort any in-flight seed SYNCHRONOUSLY (CTC-281) — never await its settlement (close() must
543
- // stay one-tick). Without this, the pending /snapshot fetch + un-cancelled body reader kept the
544
- // TCP connection referenced after close() resolved, holding a supervised process's exit hostage.
545
- // stop() ran first, so the seed's rejection lands in paths that already refuse to reopen.
568
+ // Abort any in-flight seed synchronously, then join its actual I/O in LiveSyncClient.stopAndWait.
546
569
  try {
547
- this.seedAbort?.abort(new Error("CatalystReplica closed"));
570
+ const abortError = new Error("CatalystReplica closed");
571
+ abortError.name = "AbortError";
572
+ this.seedAbort?.abort(abortError);
548
573
  }
549
574
  catch (err) {
550
575
  this.log("warn", "seed abort threw", err);
@@ -553,29 +578,55 @@ export class CatalystReplica {
553
578
  const rej = this.liveReject;
554
579
  this.clearLiveDeferred();
555
580
  rej?.(new Error("CatalystReplica: closed before first 'live'"));
556
- try {
557
- this.writerLock?.release();
558
- }
559
- catch (err) {
560
- this.log("warn", "writer-lock release threw", err);
561
- }
562
- this.writerLock = null;
563
- // Unregister the observable-gauge callbacks so a closed replica stops being scraped + can be GC'd.
564
- for (const reg of this.metricRegs) {
581
+ const client = this.client;
582
+ const shutdown = async () => {
583
+ const failures = [];
584
+ // The replica lock remains held while either an engine opener or client-owned seed can still
585
+ // write. Startup checks `closed` immediately after the opener and before client construction.
586
+ if (this.engineOpening)
587
+ await this.engineOpening.catch(() => undefined);
588
+ let seedIoAcknowledged = false;
565
589
  try {
566
- reg.remove();
590
+ await client?.stopAndWait();
591
+ seedIoAcknowledged = true;
567
592
  }
568
593
  catch (err) {
569
- this.log("warn", "metric unregister threw", err);
594
+ failures.push(err);
570
595
  }
571
- }
572
- this.metricRegs = [];
573
- try {
574
- this.engine?.close();
575
- }
576
- catch (err) {
577
- this.log("warn", "engine close threw", err);
578
- }
596
+ for (const reg of this.metricRegs) {
597
+ try {
598
+ reg.remove();
599
+ }
600
+ catch (err) {
601
+ failures.push(err);
602
+ }
603
+ }
604
+ this.metricRegs = [];
605
+ let engineClosed = this.engine == null;
606
+ try {
607
+ this.engine?.close();
608
+ engineClosed = true;
609
+ this.engine = null;
610
+ }
611
+ catch (err) {
612
+ failures.push(err);
613
+ }
614
+ // Do not admit a successor while the old handle may still be live. Keep the sidecar on an
615
+ // engine-close or seed-I/O acknowledgement failure so ownership remains conservatively held.
616
+ if (engineClosed && seedIoAcknowledged) {
617
+ try {
618
+ this.writerLock?.release();
619
+ this.writerLock = null;
620
+ }
621
+ catch (err) {
622
+ failures.push(err);
623
+ }
624
+ }
625
+ if (failures.length)
626
+ throw new AggregateError(failures, "CatalystReplica shutdown failed");
627
+ };
628
+ this.shutdownTask = shutdown();
629
+ return this.shutdownTask;
579
630
  }
580
631
  // ── Reads ───────────────────────────────────────────────────────────────────────────────────────
581
632
  /** The read-model SqlExecutor over the replica. `buildIssuesView(replica.sql, …)` is unchanged. */
@@ -919,7 +970,7 @@ export class CatalystReplica {
919
970
  * interrupted seed self-heals (getCursor → null → re-seed on the next start), preserving host-sync's
920
971
  * atomic truncate+apply+setCursor safety without holding one giant transaction.
921
972
  */
922
- async seedFromSnapshot() {
973
+ async seedFromSnapshot(attemptSignal) {
923
974
  // The whole seed (fetch + truncate + batched apply + setCursor) runs inside ONE active span so it
924
975
  // nests under the resync span when triggered by a {type:"resync"} frame, and stands alone on a cold
925
976
  // start. Active so the per-batch apply spans auto-parent under it.
@@ -930,10 +981,16 @@ export class CatalystReplica {
930
981
  // it synchronously. Every await below (fetch, each body chunk) is bounded by it — a hung
931
982
  // /snapshot can no longer wedge the transport in "resyncing" (no socket, no timers) or keep
932
983
  // the event loop referenced after close().
984
+ if (attemptSignal?.aborted)
985
+ throw attemptSignal.reason;
933
986
  if (this.closed)
934
987
  throw new Error("CatalystReplica: seed aborted (already closed)");
935
988
  const abort = new AbortController();
936
989
  this.seedAbort = abort;
990
+ // Carry the transport's exact owned cancellation into fetch/body reads. A distinct cleanup
991
+ // error, even one named AbortError, must remain distinguishable at stopAndWait().
992
+ const onAttemptAbort = () => abort.abort(attemptSignal?.reason);
993
+ attemptSignal?.addEventListener("abort", onAttemptAbort, { once: true });
937
994
  // The idle (NO-PROGRESS) deadline: (re)armed before the fetch and on every body chunk, so a
938
995
  // big-but-flowing seed never trips it while a silent half-open connection always does.
939
996
  // Unref'd — the timer must never itself hold the process open.
@@ -1085,6 +1142,7 @@ export class CatalystReplica {
1085
1142
  }
1086
1143
  finally {
1087
1144
  clearIdle();
1145
+ attemptSignal?.removeEventListener("abort", onAttemptAbort);
1088
1146
  if (this.seedAbort === abort)
1089
1147
  this.seedAbort = null;
1090
1148
  }