@catalyst-cloud/replicate 0.1.0 → 0.1.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/package.json +3 -2
- package/src/index.ts +2 -2
- package/src/replicate.ts +40 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@catalyst-cloud/replicate",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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",
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"scripts": {
|
|
22
22
|
"typecheck": "tsc --noEmit",
|
|
23
23
|
"test": "vitest run",
|
|
24
|
-
"test:coverage": "vitest run --coverage"
|
|
24
|
+
"test:coverage": "vitest run --coverage",
|
|
25
|
+
"build": "tsc -p tsconfig.build.json"
|
|
25
26
|
},
|
|
26
27
|
"dependencies": {
|
|
27
28
|
"@catalyst-cloud/schema": "^0.1.0"
|
package/src/index.ts
CHANGED
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
// the IDENTICAL apply logic runs unchanged in the host-sync bun:sqlite replica and the browser OPFS
|
|
6
6
|
// wasm replica — collapsing the two hand-maintained `apply.ts` twins into one source of truth.
|
|
7
7
|
|
|
8
|
-
export { applyDelta, truncateReplica, getCursor, setCursor } from "./replicate";
|
|
9
|
-
export type { ReplicaWriteDb, ReplicaChange, ToBindable } from "./replicate";
|
|
8
|
+
export { applyDelta, truncateReplica, getCursor, setCursor } from "./replicate.js";
|
|
9
|
+
export type { ReplicaWriteDb, ReplicaChange, ToBindable, ApplyOptions } from "./replicate.js";
|
package/src/replicate.ts
CHANGED
|
@@ -44,6 +44,28 @@ export interface ReplicaChange {
|
|
|
44
44
|
entityId?: string;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Forward-compatibility options for applying a delta (CTC-127). The mirror (server) and the replica
|
|
49
|
+
* (client) upgrade INDEPENDENTLY — the server is often AHEAD, emitting a normalized row with columns a
|
|
50
|
+
* migration added that this client's schema doesn't have yet. Without this, `applyUpsert` would
|
|
51
|
+
* `INSERT INTO <table> (…, <new_col>, …)` and SQLite throws `no such column` (SQLITE_ERROR / errno:1),
|
|
52
|
+
* leaving that row permanently stale in the replica.
|
|
53
|
+
*
|
|
54
|
+
* When `knownColumns` is supplied (the columns the LOCAL table actually has), `applyUpsert` DROPS any
|
|
55
|
+
* row key not in the set — storing what it can and ignoring what it can't, so a server ahead of the
|
|
56
|
+
* client degrades gracefully instead of poisoning the apply. Client-ahead is already safe (a column
|
|
57
|
+
* the server hasn't started emitting is simply absent from the row → not set). Omit `knownColumns` for
|
|
58
|
+
* the legacy behavior (bind every key; a drifted column throws).
|
|
59
|
+
*/
|
|
60
|
+
export interface ApplyOptions {
|
|
61
|
+
/** The columns the LOCAL replica table has, for the delta's `entity`. Row keys outside this set are
|
|
62
|
+
* dropped before the INSERT. PK columns are always local, so they are never dropped. */
|
|
63
|
+
knownColumns?: ReadonlySet<string>;
|
|
64
|
+
/** Reported (per apply) with the row keys dropped because the local table lacks them, so the caller
|
|
65
|
+
* can warn-once / surface the cloud→client schema drift. Never fires when nothing was dropped. */
|
|
66
|
+
onDroppedColumns?: (table: string, dropped: readonly string[]) => void;
|
|
67
|
+
}
|
|
68
|
+
|
|
47
69
|
function quoteIdent(ident: string): string {
|
|
48
70
|
return `"${ident.replace(/"/g, '""')}"`;
|
|
49
71
|
}
|
|
@@ -61,11 +83,14 @@ export function applyDelta<B>(
|
|
|
61
83
|
db: ReplicaWriteDb<B>,
|
|
62
84
|
change: ReplicaChange,
|
|
63
85
|
toBindable: ToBindable<B>,
|
|
86
|
+
opts?: ApplyOptions,
|
|
64
87
|
): boolean {
|
|
65
88
|
const meta = metaFor(change.entity);
|
|
66
89
|
if (!meta) throw new Error(`applyDelta: unknown entity ${String(change.entity)}`);
|
|
90
|
+
// A delete keys on PK columns only (always present in the local schema), so it needs no
|
|
91
|
+
// forward-compat column filtering — only the upsert path binds the full wire row.
|
|
67
92
|
if (change.op === "delete") return applyDelete(db, change, meta, toBindable);
|
|
68
|
-
return applyUpsert(db, change, meta, toBindable);
|
|
93
|
+
return applyUpsert(db, change, meta, toBindable, opts);
|
|
69
94
|
}
|
|
70
95
|
|
|
71
96
|
function applyUpsert<B>(
|
|
@@ -73,9 +98,22 @@ function applyUpsert<B>(
|
|
|
73
98
|
change: ReplicaChange,
|
|
74
99
|
meta: TableMeta,
|
|
75
100
|
toBindable: ToBindable<B>,
|
|
101
|
+
opts?: ApplyOptions,
|
|
76
102
|
): boolean {
|
|
77
103
|
const table = change.entity;
|
|
78
|
-
|
|
104
|
+
let cols = Object.keys(change.row);
|
|
105
|
+
// CTC-127 forward-compat: when the caller passes the local table's columns, drop any row key the
|
|
106
|
+
// local schema lacks (a mirror ahead of this client emits columns a migration added). Dropping them
|
|
107
|
+
// stores what we can instead of throwing `no such column`. PK columns are always local, so this can
|
|
108
|
+
// never strip the conflict target. Reported so the caller can warn-once about the drift.
|
|
109
|
+
if (opts?.knownColumns) {
|
|
110
|
+
const known = opts.knownColumns;
|
|
111
|
+
const dropped = cols.filter((c) => !known.has(c));
|
|
112
|
+
if (dropped.length > 0) {
|
|
113
|
+
opts.onDroppedColumns?.(table, dropped);
|
|
114
|
+
cols = cols.filter((c) => known.has(c));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
79
117
|
if (cols.length === 0) return false; // malformed wire upsert — skip rather than emit invalid SQL.
|
|
80
118
|
|
|
81
119
|
const pkSet = new Set<string>(meta.pk);
|