@catalyst-cloud/replicate 0.1.3 → 0.1.5

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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/src/replicate.ts +27 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@catalyst-cloud/replicate",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "Runtime-agnostic replica write path — applyDelta / truncateReplica / cursor over a portable ReplicaWriteDb (ADR-0002). Shared by the host-sync bun:sqlite replica and the browser OPFS replica so the two apply paths can't drift.",
6
6
  "license": "MIT",
@@ -25,7 +25,7 @@
25
25
  "build": "tsc -p tsconfig.build.json"
26
26
  },
27
27
  "dependencies": {
28
- "@catalyst-cloud/schema": "^0.1.3"
28
+ "@catalyst-cloud/schema": "0.1.10"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@catalyst-cloud/typescript-config": "workspace:*",
package/src/replicate.ts CHANGED
@@ -67,6 +67,12 @@ export interface ApplyOptions {
67
67
  * because the client's schema lacks them, so the caller can warn-once / surface the cloud→client
68
68
  * schema drift. Never fires when nothing was dropped. */
69
69
  onDroppedColumns?: (table: string, dropped: readonly string[]) => void;
70
+ /**
71
+ * CTC-393 — reported when `applyDelta` receives a delta for an entity this client's BUNDLED schema
72
+ * does not recognize AT ALL (the whole table is unknown, not merely a missing column). Never fires
73
+ * when the entity resolves. See {@link applyDelta}'s own doc for why this replaces a throw.
74
+ */
75
+ onUnknownEntity?: (entity: string) => void;
70
76
  }
71
77
 
72
78
  function quoteIdent(ident: string): string {
@@ -94,8 +100,23 @@ const KNOWN_COLUMNS: Record<string, ReadonlySet<string>> = Object.fromEntries(
94
100
 
95
101
  /**
96
102
  * Apply ONE change-feed record to the replica. Returns true iff a row was actually written (a stale
97
- * upsert rejected by the updated_at guard, or a delete of an already-absent row, returns false). Never
98
- * throws on a well-formed record; throws only on an unknown entity (malformed wire data).
103
+ * upsert rejected by the updated_at guard, or a delete of an already-absent row, returns false).
104
+ *
105
+ * ⛔ CTC-393 — NEVER THROWS, including on an unknown entity. It used to: an entity this client's
106
+ * bundled schema does not recognize AT ALL (the mirror started emitting a table this client predates
107
+ * — the exact shape of a NEW replicated entity rolling out to a fleet that upgrades independently)
108
+ * threw, and on the CURRENT SDK caller (catalyst-replica.ts) that throw propagates out of the SAME
109
+ * `engine.transaction(...)` callback that also calls `setCursor` — so the transaction rolls back,
110
+ * the cursor never advances past that seq, and the daemon's transport holds every LATER frame behind
111
+ * the hole (CTL-1402's own contiguity guarantee: "never delivers a frame beyond deliveredSeq+1").
112
+ * One new entity type permanently wedges an ENTIRE replica, silently, with no resync path — the
113
+ * exact review finding (CTC-393 / the closed PR #290) this fix closes.
114
+ *
115
+ * An unknown entity now returns `false` (nothing written — there is nowhere to put it) and reports
116
+ * via `opts.onUnknownEntity`, mirroring `onDroppedColumns`'s existing graceful-degrade posture. NO
117
+ * CALLER CHANGE IS REQUIRED for safety: `onUnknownEntity` is optional, so the EXISTING SDK code path
118
+ * (which doesn't set it) already gets the correct behavior — `setCursor` runs, the cursor advances,
119
+ * `recordApplyResult("skipped", ...)` fires — the moment this package version is on the fleet.
99
120
  */
100
121
  export function applyDelta<B>(
101
122
  db: ReplicaWriteDb<B>,
@@ -104,7 +125,10 @@ export function applyDelta<B>(
104
125
  opts?: ApplyOptions,
105
126
  ): boolean {
106
127
  const meta = metaFor(change.entity);
107
- if (!meta) throw new Error(`applyDelta: unknown entity ${String(change.entity)}`);
128
+ if (!meta) {
129
+ opts?.onUnknownEntity?.(change.entity);
130
+ return false;
131
+ }
108
132
  // A delete keys on PK columns only (always present in the local schema), so it needs no
109
133
  // forward-compat column filtering — only the upsert path binds the full wire row.
110
134
  if (change.op === "delete") return applyDelete(db, change, meta, toBindable);