@catalyst-cloud/replicate 0.1.2 → 0.1.3
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 +2 -2
- package/src/replicate.ts +39 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@catalyst-cloud/replicate",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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.
|
|
28
|
+
"@catalyst-cloud/schema": "^0.1.3"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@catalyst-cloud/typescript-config": "workspace:*",
|
package/src/replicate.ts
CHANGED
|
@@ -51,18 +51,21 @@ export interface ReplicaChange {
|
|
|
51
51
|
* `INSERT INTO <table> (…, <new_col>, …)` and SQLite throws `no such column` (SQLITE_ERROR / errno:1),
|
|
52
52
|
* leaving that row permanently stale in the replica.
|
|
53
53
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
54
|
+
* `applyUpsert` DROPS any row key the client's schema lacks — storing what it can and ignoring what it
|
|
55
|
+
* can't, so a server ahead of the client degrades gracefully instead of poisoning the apply.
|
|
56
|
+
* Client-ahead is already safe (a column the server hasn't started emitting is simply absent from the
|
|
57
|
+
* row → not set). This filtering is AUTOMATIC: `knownColumns` DEFAULTS to the bundled schema's columns
|
|
58
|
+
* (`TableMeta.columns`), so no caller has to wire anything — these options are purely for OVERRIDE
|
|
59
|
+
* (a caller that wants PRAGMA-accurate columns) and for the `onDroppedColumns` drift signal.
|
|
59
60
|
*/
|
|
60
61
|
export interface ApplyOptions {
|
|
61
|
-
/**
|
|
62
|
-
*
|
|
62
|
+
/** OVERRIDE the automatic default (the bundled schema's columns) with the columns the LOCAL table
|
|
63
|
+
* actually has, for the delta's `entity`. Row keys outside the set are dropped before the INSERT; PK
|
|
64
|
+
* columns are always local, so they are never dropped. Omit to use the bundled-schema default. */
|
|
63
65
|
knownColumns?: ReadonlySet<string>;
|
|
64
|
-
/** Reported (per apply
|
|
65
|
-
* can warn-once / surface the cloud→client
|
|
66
|
+
/** Reported (per apply, under the default OR an explicit `knownColumns`) with the row keys dropped
|
|
67
|
+
* because the client's schema lacks them, so the caller can warn-once / surface the cloud→client
|
|
68
|
+
* schema drift. Never fires when nothing was dropped. */
|
|
66
69
|
onDroppedColumns?: (table: string, dropped: readonly string[]) => void;
|
|
67
70
|
}
|
|
68
71
|
|
|
@@ -74,6 +77,21 @@ function metaFor(entity: string): TableMeta | undefined {
|
|
|
74
77
|
return (MIRROR_TABLE_META as Record<string, TableMeta>)[entity];
|
|
75
78
|
}
|
|
76
79
|
|
|
80
|
+
/**
|
|
81
|
+
* CTC-127: per-entity known-column Sets, built ONCE from the BUNDLED schema (`TableMeta.columns`, the
|
|
82
|
+
* same getTableConfig-derived columns the replica's `applyMigrations` migrates the DB to). This is the
|
|
83
|
+
* AUTOMATIC default `knownColumns` for `applyUpsert` — so a mirror emitting a column this client's
|
|
84
|
+
* schema doesn't have yet is handled with ZERO per-caller wiring (no call site can forget it). A client
|
|
85
|
+
* bundles its `@catalyst-cloud/replicate` and `@catalyst-cloud/schema` together, so this set is exactly
|
|
86
|
+
* "the columns this client knows".
|
|
87
|
+
*/
|
|
88
|
+
const KNOWN_COLUMNS: Record<string, ReadonlySet<string>> = Object.fromEntries(
|
|
89
|
+
Object.entries(MIRROR_TABLE_META).map(([entity, m]) => [
|
|
90
|
+
entity,
|
|
91
|
+
new Set((m as TableMeta).columns),
|
|
92
|
+
]),
|
|
93
|
+
);
|
|
94
|
+
|
|
77
95
|
/**
|
|
78
96
|
* Apply ONE change-feed record to the replica. Returns true iff a row was actually written (a stale
|
|
79
97
|
* upsert rejected by the updated_at guard, or a delete of an already-absent row, returns false). Never
|
|
@@ -102,17 +120,19 @@ function applyUpsert<B>(
|
|
|
102
120
|
): boolean {
|
|
103
121
|
const table = change.entity;
|
|
104
122
|
let cols = Object.keys(change.row);
|
|
105
|
-
// CTC-127 forward-compat:
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
123
|
+
// CTC-127 forward-compat (AUTOMATIC): drop any row key the local schema lacks — a column the mirror
|
|
124
|
+
// added ahead of this client's bundled schema. Without this the INSERT would name a nonexistent
|
|
125
|
+
// column and SQLite throws `no such column` (errno:1), stranding the row stale. `knownColumns`
|
|
126
|
+
// DEFAULTS to the bundled schema's columns (KNOWN_COLUMNS), so this protects EVERY caller with no
|
|
127
|
+
// wiring; an explicit `opts.knownColumns` OVERRIDES it (e.g. PRAGMA-accurate columns). `cols.every`
|
|
128
|
+
// short-circuits allocation-free in the common no-drift case. The `known.size > 0` guard falls back
|
|
129
|
+
// to legacy bind-all if the map is somehow empty (schema version skew) — the SAFE direction (never
|
|
130
|
+
// strips a row to PK-only). PK columns are always local, so the conflict target is never dropped.
|
|
131
|
+
const known = opts?.knownColumns ?? KNOWN_COLUMNS[table];
|
|
132
|
+
if (known && known.size > 0 && !cols.every((c) => known.has(c))) {
|
|
111
133
|
const dropped = cols.filter((c) => !known.has(c));
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
cols = cols.filter((c) => known.has(c));
|
|
115
|
-
}
|
|
134
|
+
opts?.onDroppedColumns?.(table, dropped);
|
|
135
|
+
cols = cols.filter((c) => known.has(c));
|
|
116
136
|
}
|
|
117
137
|
if (cols.length === 0) return false; // malformed wire upsert — skip rather than emit invalid SQL.
|
|
118
138
|
|