orez 0.4.30 → 0.4.32

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 (39) hide show
  1. package/dist/config.js +3 -3
  2. package/dist/fk-cascade.d.ts +102 -0
  3. package/dist/fk-cascade.d.ts.map +1 -0
  4. package/dist/fk-cascade.js +233 -0
  5. package/dist/fk-cascade.js.map +1 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +65 -39
  8. package/dist/index.js.map +1 -1
  9. package/dist/pg-proxy-do-backend.d.ts +1 -0
  10. package/dist/pg-proxy-do-backend.d.ts.map +1 -1
  11. package/dist/pg-proxy-do-backend.js +114 -0
  12. package/dist/pg-proxy-do-backend.js.map +1 -1
  13. package/dist/pglite-manager.d.ts +12 -9
  14. package/dist/pglite-manager.d.ts.map +1 -1
  15. package/dist/pglite-manager.js +61 -31
  16. package/dist/pglite-manager.js.map +1 -1
  17. package/dist/port.d.ts +6 -1
  18. package/dist/port.d.ts.map +1 -1
  19. package/dist/port.js +54 -19
  20. package/dist/port.js.map +1 -1
  21. package/dist/recovery.d.ts +7 -6
  22. package/dist/recovery.d.ts.map +1 -1
  23. package/dist/recovery.js +5 -4
  24. package/dist/recovery.js.map +1 -1
  25. package/dist/worker/cf-patches.d.ts +4 -2
  26. package/dist/worker/cf-patches.d.ts.map +1 -1
  27. package/dist/worker/cf-patches.js +15 -2
  28. package/dist/worker/cf-patches.js.map +1 -1
  29. package/dist/worker/shims/postgres.d.ts.map +1 -1
  30. package/dist/worker/shims/postgres.js +112 -0
  31. package/dist/worker/shims/postgres.js.map +1 -1
  32. package/dist/worker/zero-cache-embed.d.ts.map +1 -1
  33. package/dist/worker/zero-cache-embed.js +4 -0
  34. package/dist/worker/zero-cache-embed.js.map +1 -1
  35. package/dist/zero-changelog-cleanup-patch.d.ts +18 -0
  36. package/dist/zero-changelog-cleanup-patch.d.ts.map +1 -0
  37. package/dist/zero-changelog-cleanup-patch.js +63 -0
  38. package/dist/zero-changelog-cleanup-patch.js.map +1 -0
  39. package/package.json +2 -2
package/dist/config.js CHANGED
@@ -6,9 +6,9 @@ export function defineConfig(config) {
6
6
  export function getConfig(overrides = {}) {
7
7
  return {
8
8
  dataDir: overrides.dataDir || '.orez',
9
- pgPort: overrides.pgPort || 6434,
10
- zeroPort: overrides.zeroPort || 5849,
11
- adminPort: overrides.adminPort || 0,
9
+ pgPort: overrides.pgPort ?? 6434,
10
+ zeroPort: overrides.zeroPort ?? 5849,
11
+ adminPort: overrides.adminPort ?? 0,
12
12
  pgUser: overrides.pgUser || 'user',
13
13
  pgPassword: overrides.pgPassword || 'password',
14
14
  migrationsDir: overrides.migrationsDir || '',
@@ -0,0 +1,102 @@
1
+ /**
2
+ * fk-cascade — pg→sqlite compat for ON DELETE actions.
3
+ *
4
+ * orez's backends drop every FOREIGN KEY constraint when translating PG DDL to
5
+ * their store (the PGlite shim regex-strips them; the DO backend filters the
6
+ * CONSTR_FOREIGN AST nodes). that leaves the store with no FK enforcement and,
7
+ * crucially, no `ON DELETE CASCADE` / `ON DELETE SET NULL` — so a plain
8
+ * `DELETE FROM parent` orphans every child row instead of cascading.
9
+ *
10
+ * native engine cascade is not an option here: change capture is trigger-based
11
+ * (`_zero_change_trigger` / `_zero_track_change`), and SQLite does NOT fire
12
+ * triggers for native foreign-key cascade actions unless `recursive_triggers`
13
+ * is on. an engine-level cascade would delete child rows invisibly, so zero
14
+ * clients would never learn the rows are gone — strictly worse than orphaning.
15
+ *
16
+ * instead we restore faithful PG semantics by EXPANSION: capture the FK edges
17
+ * the backends were about to drop, and rewrite each `DELETE FROM parent` into
18
+ * an ordered set of explicit, set-based child statements (leaves-first) plus
19
+ * the original parent delete. every emitted statement is a real DELETE/UPDATE,
20
+ * so it goes through the same change-tracking trigger and replicates to zero
21
+ * clients exactly like any other write. one implementation, both backends.
22
+ *
23
+ * this module is pure: it operates on already-parsed libpg_query CREATE TABLE
24
+ * AST nodes (for edge capture) and plain SQL strings (for expansion). the
25
+ * caller owns naming — it passes a `resolveTable` that produces the canonical
26
+ * SQL identifier for a table (schema-qualified PG name). that identifier doubles
27
+ * as the registry key, so capture and expansion stay consistent. the expansion
28
+ * emits PG SQL under those names; the DO backend re-runs each child through its
29
+ * normal rewrite (flatten + change-track), while PGlite runs them directly.
30
+ */
31
+ /** the ON DELETE actions we can faithfully expand. restrict/no-action/set-default are left alone (no enforcement, matching today). */
32
+ export type FkDeleteAction = 'cascade' | 'set-null';
33
+ export interface FkChild {
34
+ /** canonical SQL identifier of the child (referencing) table; also its registry key. */
35
+ table: string;
36
+ /** child FK column identifiers (already quoted), in FK order. */
37
+ columns: string[];
38
+ /** referenced parent column identifiers (already quoted), aligned with `columns`. */
39
+ refColumns: string[];
40
+ onDelete: FkDeleteAction;
41
+ }
42
+ /**
43
+ * parent-table → children that reference it. built once from DDL, consulted on
44
+ * every DELETE. keyed by the canonical table identifier `resolveTable` produces.
45
+ */
46
+ export declare class FkCascadeRegistry {
47
+ private byParent;
48
+ add(parentKey: string, child: FkChild): void;
49
+ childrenOf(parentKey: string): readonly FkChild[];
50
+ /** every (parentKey, child) edge — for persisting the registry to durable storage. */
51
+ entries(): IterableIterator<readonly [string, FkChild]>;
52
+ /** true once any cascade/set-null edge has been captured — lets backends skip the DELETE parse on FK-free schemas. */
53
+ get hasEdges(): boolean;
54
+ clear(): void;
55
+ }
56
+ export interface TableRef {
57
+ schemaname?: string;
58
+ relname: string;
59
+ }
60
+ export type ResolveTable = (ref: TableRef) => string;
61
+ export declare function quoteIdent(name: string): string;
62
+ /**
63
+ * capture every cascade/set-null FK edge declared by a CREATE TABLE statement
64
+ * into `registry`. handles both inline column FKs (`col … REFERENCES parent(c)`)
65
+ * and table-level FKs (`FOREIGN KEY (col) REFERENCES parent(c)`). call this at
66
+ * the exact site each backend drops the constraint, BEFORE it is discarded.
67
+ */
68
+ export declare function recordCreateTableForeignKeys(createStmt: {
69
+ relation?: TableRef;
70
+ tableElts?: unknown[];
71
+ } | undefined, registry: FkCascadeRegistry, resolveTable: ResolveTable): number;
72
+ /**
73
+ * capture cascade/set-null FK edges from an `ALTER TABLE … ADD CONSTRAINT …
74
+ * FOREIGN KEY …` statement (drizzle's standard FK form — it emits FKs as a
75
+ * separate ALTER, not inline in CREATE TABLE). call at the site the backend
76
+ * drops the constraint. returns the number of edges recorded.
77
+ */
78
+ export declare function recordAlterTableForeignKeys(alterStmt: {
79
+ relation?: TableRef;
80
+ cmds?: unknown[];
81
+ } | undefined, registry: FkCascadeRegistry, resolveTable: ResolveTable): number;
82
+ export interface ExpandOptions {
83
+ /** guard against pathological / cyclic graphs. */
84
+ maxDepth?: number;
85
+ }
86
+ /**
87
+ * expand a `DELETE FROM target [WHERE wherePredicate]` into the ordered list of
88
+ * statements that reproduces PG's ON DELETE semantics. children are emitted
89
+ * leaves-first (grandchildren before children) so every delete runs before the
90
+ * rows it depends on disappear; the returned list does NOT include the original
91
+ * parent delete — the caller still runs that, after these.
92
+ *
93
+ * each statement embeds the full original predicate via nested subqueries, so
94
+ * for a parameterized DELETE the SAME bound params apply unchanged to every
95
+ * emitted statement.
96
+ *
97
+ * cascade → `DELETE FROM child WHERE childCols IN (SELECT … )`
98
+ * set-null → `UPDATE child SET childCols = NULL WHERE childCols IN (SELECT … )`
99
+ * (SET NULL does not delete the child, so its subtree is not recursed)
100
+ */
101
+ export declare function expandDelete(target: string, wherePredicate: string | null, registry: FkCascadeRegistry, options?: ExpandOptions): string[];
102
+ //# sourceMappingURL=fk-cascade.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fk-cascade.d.ts","sourceRoot":"","sources":["../src/fk-cascade.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,sIAAsI;AACtI,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,UAAU,CAAA;AAEnD,MAAM,WAAW,OAAO;IACtB,wFAAwF;IACxF,KAAK,EAAE,MAAM,CAAA;IACb,iEAAiE;IACjE,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,qFAAqF;IACrF,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,QAAQ,EAAE,cAAc,CAAA;CACzB;AAED;;;GAGG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAA+B;IAE/C,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAY5C,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,OAAO,EAAE;IAIjD,sFAAsF;IACrF,OAAO,IAAI,gBAAgB,CAAC,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAMxD,sHAAsH;IACtH,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,KAAK,IAAI,IAAI;CAGd;AAcD,MAAM,WAAW,QAAQ;IACvB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,QAAQ,KAAK,MAAM,CAAA;AASpD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAaD;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAC1C,UAAU,EAAE;IAAE,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,EAAE,CAAA;CAAE,GAAG,SAAS,EACtE,QAAQ,EAAE,iBAAiB,EAC3B,YAAY,EAAE,YAAY,GACzB,MAAM,CAyCR;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACzC,SAAS,EAAE;IAAE,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,EAAE,CAAA;CAAE,GAAG,SAAS,EAChE,QAAQ,EAAE,iBAAiB,EAC3B,YAAY,EAAE,YAAY,GACzB,MAAM,CAwBR;AAqDD,MAAM,WAAW,aAAa;IAC5B,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,GAAG,IAAI,EAC7B,QAAQ,EAAE,iBAAiB,EAC3B,OAAO,GAAE,aAAkB,GAC1B,MAAM,EAAE,CA6BV"}
@@ -0,0 +1,233 @@
1
+ /**
2
+ * fk-cascade — pg→sqlite compat for ON DELETE actions.
3
+ *
4
+ * orez's backends drop every FOREIGN KEY constraint when translating PG DDL to
5
+ * their store (the PGlite shim regex-strips them; the DO backend filters the
6
+ * CONSTR_FOREIGN AST nodes). that leaves the store with no FK enforcement and,
7
+ * crucially, no `ON DELETE CASCADE` / `ON DELETE SET NULL` — so a plain
8
+ * `DELETE FROM parent` orphans every child row instead of cascading.
9
+ *
10
+ * native engine cascade is not an option here: change capture is trigger-based
11
+ * (`_zero_change_trigger` / `_zero_track_change`), and SQLite does NOT fire
12
+ * triggers for native foreign-key cascade actions unless `recursive_triggers`
13
+ * is on. an engine-level cascade would delete child rows invisibly, so zero
14
+ * clients would never learn the rows are gone — strictly worse than orphaning.
15
+ *
16
+ * instead we restore faithful PG semantics by EXPANSION: capture the FK edges
17
+ * the backends were about to drop, and rewrite each `DELETE FROM parent` into
18
+ * an ordered set of explicit, set-based child statements (leaves-first) plus
19
+ * the original parent delete. every emitted statement is a real DELETE/UPDATE,
20
+ * so it goes through the same change-tracking trigger and replicates to zero
21
+ * clients exactly like any other write. one implementation, both backends.
22
+ *
23
+ * this module is pure: it operates on already-parsed libpg_query CREATE TABLE
24
+ * AST nodes (for edge capture) and plain SQL strings (for expansion). the
25
+ * caller owns naming — it passes a `resolveTable` that produces the canonical
26
+ * SQL identifier for a table (schema-qualified PG name). that identifier doubles
27
+ * as the registry key, so capture and expansion stay consistent. the expansion
28
+ * emits PG SQL under those names; the DO backend re-runs each child through its
29
+ * normal rewrite (flatten + change-track), while PGlite runs them directly.
30
+ */
31
+ /**
32
+ * parent-table → children that reference it. built once from DDL, consulted on
33
+ * every DELETE. keyed by the canonical table identifier `resolveTable` produces.
34
+ */
35
+ export class FkCascadeRegistry {
36
+ byParent = new Map();
37
+ add(parentKey, child) {
38
+ const list = this.byParent.get(parentKey);
39
+ if (!list) {
40
+ this.byParent.set(parentKey, [child]);
41
+ return;
42
+ }
43
+ // dedupe: the same DDL can be re-applied (CREATE TABLE IF NOT EXISTS, a boot
44
+ // load followed by a re-sent migration) — a duplicate edge doubles the cascade.
45
+ if (list.some((existing) => sameEdge(existing, child)))
46
+ return;
47
+ list.push(child);
48
+ }
49
+ childrenOf(parentKey) {
50
+ return this.byParent.get(parentKey) ?? EMPTY;
51
+ }
52
+ /** every (parentKey, child) edge — for persisting the registry to durable storage. */
53
+ *entries() {
54
+ for (const [parentKey, children] of this.byParent) {
55
+ for (const child of children)
56
+ yield [parentKey, child];
57
+ }
58
+ }
59
+ /** true once any cascade/set-null edge has been captured — lets backends skip the DELETE parse on FK-free schemas. */
60
+ get hasEdges() {
61
+ return this.byParent.size > 0;
62
+ }
63
+ clear() {
64
+ this.byParent.clear();
65
+ }
66
+ }
67
+ const EMPTY = Object.freeze([]);
68
+ function sameEdge(a, b) {
69
+ return (a.table === b.table &&
70
+ a.onDelete === b.onDelete &&
71
+ a.columns.length === b.columns.length &&
72
+ a.columns.every((c, i) => c === b.columns[i]) &&
73
+ a.refColumns.every((c, i) => c === b.refColumns[i]));
74
+ }
75
+ /** libpg_query fk_del_action codes → the action we expand (others: no enforcement, skip). */
76
+ const DELETE_ACTION = {
77
+ c: 'cascade', // CASCADE
78
+ n: 'set-null', // SET NULL
79
+ // a: NO ACTION, r: RESTRICT, d: SET DEFAULT — no faithful expansion, leave unenforced
80
+ };
81
+ export function quoteIdent(name) {
82
+ return '"' + name.replace(/"/g, '""') + '"';
83
+ }
84
+ function stringValues(nodes) {
85
+ if (!Array.isArray(nodes))
86
+ return [];
87
+ const out = [];
88
+ for (const node of nodes) {
89
+ const sval = node?.String;
90
+ const value = sval?.sval ?? sval?.str;
91
+ if (typeof value === 'string')
92
+ out.push(value);
93
+ }
94
+ return out;
95
+ }
96
+ /**
97
+ * capture every cascade/set-null FK edge declared by a CREATE TABLE statement
98
+ * into `registry`. handles both inline column FKs (`col … REFERENCES parent(c)`)
99
+ * and table-level FKs (`FOREIGN KEY (col) REFERENCES parent(c)`). call this at
100
+ * the exact site each backend drops the constraint, BEFORE it is discarded.
101
+ */
102
+ export function recordCreateTableForeignKeys(createStmt, registry, resolveTable) {
103
+ const relation = createStmt?.relation;
104
+ if (!relation?.relname)
105
+ return 0;
106
+ const childTable = resolveTable(relation);
107
+ let recorded = 0;
108
+ for (const elt of createStmt?.tableElts ?? []) {
109
+ const node = elt;
110
+ // table-level: FOREIGN KEY (a, b) REFERENCES parent (x, y)
111
+ if (node.Constraint?.contype === 'CONSTR_FOREIGN') {
112
+ if (addForeignKey(registry, childTable, node.Constraint, stringValues(node.Constraint.fk_attrs), resolveTable)) {
113
+ recorded++;
114
+ }
115
+ continue;
116
+ }
117
+ // inline column: col … REFERENCES parent (x)
118
+ const col = node.ColumnDef;
119
+ if (col?.colname && Array.isArray(col.constraints)) {
120
+ for (const c of col.constraints) {
121
+ const constraint = c.Constraint;
122
+ if (constraint?.contype === 'CONSTR_FOREIGN') {
123
+ if (addForeignKey(registry, childTable, constraint, [col.colname], resolveTable)) {
124
+ recorded++;
125
+ }
126
+ }
127
+ }
128
+ }
129
+ }
130
+ return recorded;
131
+ }
132
+ /**
133
+ * capture cascade/set-null FK edges from an `ALTER TABLE … ADD CONSTRAINT …
134
+ * FOREIGN KEY …` statement (drizzle's standard FK form — it emits FKs as a
135
+ * separate ALTER, not inline in CREATE TABLE). call at the site the backend
136
+ * drops the constraint. returns the number of edges recorded.
137
+ */
138
+ export function recordAlterTableForeignKeys(alterStmt, registry, resolveTable) {
139
+ const relation = alterStmt?.relation;
140
+ if (!relation?.relname)
141
+ return 0;
142
+ const childTable = resolveTable(relation);
143
+ let recorded = 0;
144
+ for (const cmd of alterStmt?.cmds ?? []) {
145
+ const command = cmd
146
+ ?.AlterTableCmd;
147
+ if (command?.subtype !== 'AT_AddConstraint')
148
+ continue;
149
+ const constraint = command.def?.Constraint;
150
+ if (constraint?.contype !== 'CONSTR_FOREIGN')
151
+ continue;
152
+ if (addForeignKey(registry, childTable, constraint, stringValues(constraint.fk_attrs), resolveTable)) {
153
+ recorded++;
154
+ }
155
+ }
156
+ return recorded;
157
+ }
158
+ function addForeignKey(registry, childTable, constraint, childColumns, resolveTable) {
159
+ const onDelete = DELETE_ACTION[constraint.fk_del_action ?? 'a'];
160
+ if (!onDelete)
161
+ return false; // restrict / no-action / set-default: nothing to expand
162
+ const parent = constraint.pktable;
163
+ if (!parent?.relname)
164
+ return false;
165
+ const refColumns = stringValues(constraint.pk_attrs);
166
+ // need an explicit referenced column to build the IN-subquery; drizzle always
167
+ // emits one. without it we can't faithfully expand, so skip rather than guess.
168
+ if (childColumns.length === 0 || refColumns.length !== childColumns.length)
169
+ return false;
170
+ registry.add(resolveTable(parent), {
171
+ table: childTable,
172
+ columns: childColumns.map(quoteIdent),
173
+ refColumns: refColumns.map(quoteIdent),
174
+ onDelete,
175
+ });
176
+ return true;
177
+ }
178
+ function columnTuple(columns) {
179
+ return columns.length === 1 ? columns[0] : `(${columns.join(', ')})`;
180
+ }
181
+ /**
182
+ * build the set-based predicate selecting child rows whose FK points at the
183
+ * parent rows matched by `parentPredicate`:
184
+ * childCols IN (SELECT refCols FROM parentTable WHERE parentPredicate)
185
+ */
186
+ function childMatchPredicate(child, parentTable, parentPredicate) {
187
+ const where = parentPredicate ? ` WHERE ${parentPredicate}` : '';
188
+ const select = `SELECT ${child.refColumns.join(', ')} FROM ${parentTable}${where}`;
189
+ return `${columnTuple(child.columns)} IN (${select})`;
190
+ }
191
+ /**
192
+ * expand a `DELETE FROM target [WHERE wherePredicate]` into the ordered list of
193
+ * statements that reproduces PG's ON DELETE semantics. children are emitted
194
+ * leaves-first (grandchildren before children) so every delete runs before the
195
+ * rows it depends on disappear; the returned list does NOT include the original
196
+ * parent delete — the caller still runs that, after these.
197
+ *
198
+ * each statement embeds the full original predicate via nested subqueries, so
199
+ * for a parameterized DELETE the SAME bound params apply unchanged to every
200
+ * emitted statement.
201
+ *
202
+ * cascade → `DELETE FROM child WHERE childCols IN (SELECT … )`
203
+ * set-null → `UPDATE child SET childCols = NULL WHERE childCols IN (SELECT … )`
204
+ * (SET NULL does not delete the child, so its subtree is not recursed)
205
+ */
206
+ export function expandDelete(target, wherePredicate, registry, options = {}) {
207
+ if (!registry.hasEdges)
208
+ return [];
209
+ const maxDepth = options.maxDepth ?? 32;
210
+ const out = [];
211
+ const visit = (table, predicate, depth, path) => {
212
+ if (depth > maxDepth)
213
+ return;
214
+ for (const child of registry.childrenOf(table)) {
215
+ if (path.has(child.table))
216
+ continue; // cycle guard — self-ref / mutual FK
217
+ const match = childMatchPredicate(child, table, predicate);
218
+ if (child.onDelete === 'cascade') {
219
+ // delete grandchildren first, keyed off the rows this child is about to lose
220
+ visit(child.table, match, depth + 1, new Set(path).add(child.table));
221
+ out.push(`DELETE FROM ${child.table} WHERE ${match}`);
222
+ }
223
+ else {
224
+ // set-null: null the link, leave the child row (and its subtree) intact
225
+ const assignments = child.columns.map((c) => `${c} = NULL`).join(', ');
226
+ out.push(`UPDATE ${child.table} SET ${assignments} WHERE ${match}`);
227
+ }
228
+ }
229
+ };
230
+ visit(target, wherePredicate, 0, new Set([target]));
231
+ return out;
232
+ }
233
+ //# sourceMappingURL=fk-cascade.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fk-cascade.js","sourceRoot":"","sources":["../src/fk-cascade.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAeH,OAAO,EAGL,KAAK,YAAY,EAClB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAoDpE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AA0DjD,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAC1E,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAC7E,OAAO,EAAE,+BAA+B,EAAE,MAAM,0BAA0B,CAAA;AAC1E,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAA;AAoEvE,wBAAsB,aAAa,CAAC,SAAS,GAAE,OAAO,CAAC,cAAc,CAAM;;;;;;;;;;;;;;GAu9B1E"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAeH,OAAO,EAGL,KAAK,YAAY,EAClB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAsDpE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AA0DjD,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAC1E,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAC7E,OAAO,EAAE,+BAA+B,EAAE,MAAM,0BAA0B,CAAA;AAC1E,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAA;AAoEvE,wBAAsB,aAAa,CAAC,SAAS,GAAE,OAAO,CAAC,cAAc,CAAM;;;;;;;;;;;;;;GA0+B1E"}
package/dist/index.js CHANGED
@@ -17,14 +17,15 @@ import { getConfig, getConnectionString } from './config.js';
17
17
  import { log, port, setLogLevel, setLogStore } from './log.js';
18
18
  import { DoBackend } from './pg-proxy-do-backend.js';
19
19
  import { startPgProxy } from './pg-proxy.js';
20
- import { createPGliteInstances, createPGliteWorkerInstances, createSinglePGliteInstance, createSinglePGliteWorkerInstance, createPGliteWorker, runMigrations, startPeriodicCheckpoint, startPeriodicVacuum, } from './pglite-manager.js';
21
- import { findPort } from './port.js';
20
+ import { createPGliteInstances, createPGliteWorkerInstances, createSinglePGliteInstance, createSinglePGliteWorkerInstance, createPGliteWorker, runMigrations, startPeriodicCheckpoint, startPeriodicVacuum, vacuumPGliteChurnTables, } from './pglite-manager.js';
21
+ import { findPort, findPortBlock } from './port.js';
22
22
  import { orezTitle } from './process-title.js';
23
23
  import { ensurePublicationHasTables, syncManagedPublications } from './publications.js';
24
24
  import { classifyZeroCrashRecovery, classifyZeroStartupRecovery, hasRecoverableZeroStateSignature, hasZeroReplicaMonitorWarmupSignature, getZeroReplicaStartupResetReason, recoverZeroState, zeroInconsistencyResetMode, } from './recovery.js';
25
25
  import { installChangeTracking } from './replication/change-tracker.js';
26
26
  import { resetReplicationState } from './replication/handler.js';
27
27
  import { applySqliteMode, cleanupShim, formatNativeBootstrapInstructions, hasMissingNativeBinarySignature, inspectNativeSqliteBinary, resolveSqliteMode, resolveSqliteModeConfig, } from './sqlite-mode/index.js';
28
+ import { enableZeroChangeLogCleanupRetry } from './zero-changelog-cleanup-patch.js';
28
29
  import { enableZeroReplicaCheckpoint } from './zero-checkpoint-patch.js';
29
30
  import { probeZeroCacheHttp } from './zero-health.js';
30
31
  import { disableZeroLitestreamRestore } from './zero-litestream-patch.js';
@@ -140,7 +141,7 @@ export async function startZeroLite(overrides = {}) {
140
141
  const pgPort = await findPort(config.pgPort);
141
142
  const zeroPort = config.skipZeroCache
142
143
  ? config.zeroPort
143
- : await findPort(config.zeroPort);
144
+ : await findPortBlock(config.zeroPort, 2, { host: '::' });
144
145
  const adminPort = config.adminPort > 0 ? await findPort(config.adminPort) : 0;
145
146
  if (pgPort !== config.pgPort)
146
147
  log.debug.orez(`port ${config.pgPort} in use, using ${pgPort}`);
@@ -240,6 +241,19 @@ export async function startZeroLite(overrides = {}) {
240
241
  let instances, db, stopCheckpoint, stopVacuum = () => { };
241
242
  let migrationsApplied = 0;
242
243
  let isDoBackend = false;
244
+ let pgliteVacuumMs = 0;
245
+ let delayedVacuumTimer;
246
+ const queuePgliteVacuum = (delayMs = 15_000) => {
247
+ if (isDoBackend || pgliteVacuumMs <= 0)
248
+ return;
249
+ if (delayedVacuumTimer)
250
+ clearTimeout(delayedVacuumTimer);
251
+ delayedVacuumTimer = setTimeout(() => {
252
+ delayedVacuumTimer = undefined;
253
+ void vacuumPGliteChurnTables(instances);
254
+ }, delayMs);
255
+ delayedVacuumTimer.unref?.();
256
+ };
243
257
  if (config.doBackendUrl) {
244
258
  isDoBackend = true;
245
259
  log.orez(`using DO backend: ${config.doBackendUrl}`);
@@ -275,12 +289,6 @@ export async function startZeroLite(overrides = {}) {
275
289
  config.checkpointIntervalMs > 0
276
290
  ? startPeriodicCheckpoint(instances, config.checkpointIntervalMs)
277
291
  : () => { };
278
- // periodic VACUUM of the change-tracking churn tables — PGlite has no
279
- // effective autovacuum, so without this the change buffer bloats until the
280
- // change-streamer scan times out and Zero stops sending live updates. Interval
281
- // via OREZ_VACUUM_MS (default 10min, 0 disables).
282
- const vacuumMs = Number(process.env.OREZ_VACUUM_MS ?? 10 * 60 * 1000);
283
- stopVacuum = vacuumMs > 0 ? startPeriodicVacuum(instances, vacuumMs) : () => { };
284
292
  // config-based publications
285
293
  if (config.zeroPublications && !process.env.ZERO_APP_PUBLICATIONS) {
286
294
  process.env.ZERO_APP_PUBLICATIONS = config.zeroPublications;
@@ -289,6 +297,16 @@ export async function startZeroLite(overrides = {}) {
289
297
  migrationsApplied = await runMigrations(db, config);
290
298
  log.debug.orez('installing change tracking');
291
299
  await installChangeTracking(db);
300
+ // periodic VACUUM of the change-tracking churn tables — PGlite has no
301
+ // effective autovacuum, so without this the change buffers bloat until the
302
+ // change-streamer scan times out and Zero stops sending live updates. Start it
303
+ // only after Orez's own tracking tables exist; zero-cache's CDB changeLog is
304
+ // vacuumed once more after zero-cache has started.
305
+ pgliteVacuumMs = Number(process.env.OREZ_VACUUM_MS ?? 60 * 1000);
306
+ if (pgliteVacuumMs > 0) {
307
+ await vacuumPGliteChurnTables(instances);
308
+ stopVacuum = startPeriodicVacuum(instances, pgliteVacuumMs);
309
+ }
292
310
  }
293
311
  // shared: publications config
294
312
  if (config.zeroPublications && !process.env.ZERO_APP_PUBLICATIONS) {
@@ -325,6 +343,9 @@ export async function startZeroLite(overrides = {}) {
325
343
  await ensurePublicationHasTables(db, managedPub.names);
326
344
  log.debug.orez('re-installing change tracking after on-db-ready');
327
345
  await installChangeTracking(db);
346
+ if (!isDoBackend && pgliteVacuumMs > 0) {
347
+ await vacuumPGliteChurnTables(instances);
348
+ }
328
349
  }
329
350
  if (isDoBackend) {
330
351
  await installChangeTracking(db);
@@ -343,26 +364,22 @@ export async function startZeroLite(overrides = {}) {
343
364
  instances.postgresReplicas = await createReadReplicas(db, config.readReplicas, config);
344
365
  }
345
366
  // clean up stale lock files from previous crash. if lock files were present,
346
- // the previous shutdown was unclean and the replica, change DB, and CVR DB can
347
- // disagree about the same client/version history, so rebuild all zero state.
367
+ // the previous shutdown was unclean and the replica file may not be safe to
368
+ // reuse, but wiping CVR/CDB here evicts every persisted client with
369
+ // ClientNotFound. rebuild only the replica; true CDC/CVR corruption is handled
370
+ // by the crash classifiers below.
348
371
  const hadStaleLocks = cleanupStaleLockFiles(config);
349
372
  if (hadStaleLocks) {
350
- log.debug.orez('unclean shutdown detected, resetting zero state');
351
- await recoverZeroState({
352
- config,
353
- instances,
354
- zeroCacheProcess: null,
355
- });
373
+ log.debug.orez('unclean shutdown detected, rebuilding replica (CVR preserved)');
374
+ cleanupStaleReplica(config);
375
+ resetReplicationState();
356
376
  }
357
377
  if (!config.skipZeroCache) {
358
378
  const replicaResetReason = getZeroReplicaStartupResetReason(getReplicaDir(config));
359
379
  if (replicaResetReason) {
360
- log.orez(`detected invalid zero replica (${replicaResetReason}), resetting zero state`);
361
- await recoverZeroState({
362
- config,
363
- instances,
364
- zeroCacheProcess: null,
365
- });
380
+ log.orez(`detected invalid zero replica (${replicaResetReason}), rebuilding replica (CVR preserved)`);
381
+ cleanupStaleReplica(config);
382
+ resetReplicationState();
366
383
  }
367
384
  }
368
385
  // when admin is enabled, zero-cache runs on internal port with http proxy in front
@@ -394,6 +411,10 @@ export async function startZeroLite(overrides = {}) {
394
411
  const details = tail?.length ? tail.slice(-20).join('\n') : '';
395
412
  throw new Error(`zero-cache crashed during startup stability check\n${details}`);
396
413
  }
414
+ if (!isDoBackend && pgliteVacuumMs > 0) {
415
+ await vacuumPGliteChurnTables(instances);
416
+ queuePgliteVacuum();
417
+ }
397
418
  };
398
419
  // zero-cache can die during startup for a few distinct reasons; the policy
399
420
  // for each lives in classifyZeroStartupRecovery (pure + unit-tested) and
@@ -410,7 +431,7 @@ export async function startZeroLite(overrides = {}) {
410
431
  plainRestarts: 0,
411
432
  maxRestarts: 3,
412
433
  didRecoverState: false,
413
- didFullReset: false,
434
+ didCacheReset: false,
414
435
  canWasmFallback: sqliteMode === 'native' && !config.disableWasmSqlite,
415
436
  didWasmFallback: false,
416
437
  nativeBinaryMissing: false,
@@ -456,10 +477,11 @@ export async function startZeroLite(overrides = {}) {
456
477
  sqliteModeConfig = resolveSqliteModeConfig(false, true); // force wasm
457
478
  continue;
458
479
  }
459
- if (action === 'full-reset') {
460
- startupRetry.didFullReset = true;
461
- log.orez('zero-cache still crashing after restarts — resetting zero state and retrying once more...');
462
- await recoverZeroState({ config, instances, zeroCacheProcess });
480
+ if (action === 'cache-reset') {
481
+ startupRetry.didCacheReset = true;
482
+ log.orez('zero-cache still crashing after restarts — rebuilding replica and retrying once more (CVR preserved)...');
483
+ cleanupStaleReplica(config);
484
+ resetReplicationState();
463
485
  continue;
464
486
  }
465
487
  // action === 'restart': transient / unexpected crash, plain relaunch.
@@ -471,7 +493,7 @@ export async function startZeroLite(overrides = {}) {
471
493
  // visible in logs rather than silently swallowed.
472
494
  if (startupRetry.plainRestarts > 0 ||
473
495
  startupRetry.didRecoverState ||
474
- startupRetry.didFullReset ||
496
+ startupRetry.didCacheReset ||
475
497
  startupRetry.didWasmFallback) {
476
498
  log.orez('zero-cache started after recovery');
477
499
  }
@@ -820,16 +842,14 @@ export async function startZeroLite(overrides = {}) {
820
842
  installCrashWatcher();
821
843
  })
822
844
  .catch((err) => {
823
- // if a plain restart can't bring it back, the local state may be
824
- // worse than the tail suggested — fall back to a full reset once.
825
- log.orez(`zero-cache restart recovery failed (${err?.message || err}) — falling back to full reset`);
826
- resetZeroState('full')
845
+ log.orez(`zero-cache restart recovery failed (${err?.message || err}) rebuilding replica (CVR preserved)`);
846
+ resetZeroState('cache-only')
827
847
  .then(() => {
828
- log.orez('zero-cache full-reset recovery successful');
848
+ log.orez('zero-cache replica rebuild recovery successful');
829
849
  installCrashWatcher();
830
850
  })
831
851
  .catch((resetErr) => {
832
- log.orez(`zero-cache full-reset recovery failed: ${resetErr?.message || resetErr}`);
852
+ log.orez(`zero-cache replica rebuild recovery failed: ${resetErr?.message || resetErr}`);
833
853
  });
834
854
  });
835
855
  });
@@ -894,14 +914,14 @@ export async function startZeroLite(overrides = {}) {
894
914
  installCrashWatcher();
895
915
  }
896
916
  catch (err) {
897
- log.orez(`zero-cache HTTP liveness restart failed (${err?.message || err}) — falling back to full reset`);
917
+ log.orez(`zero-cache HTTP liveness restart failed (${err?.message || err}) — rebuilding replica (CVR preserved)`);
898
918
  try {
899
- await resetZeroState('full');
900
- log.orez('zero-cache HTTP liveness full-reset recovery successful');
919
+ await resetZeroState('cache-only');
920
+ log.orez('zero-cache HTTP liveness replica rebuild recovery successful');
901
921
  installCrashWatcher();
902
922
  }
903
923
  catch (resetErr) {
904
- log.orez(`zero-cache HTTP liveness full-reset recovery failed: ${resetErr?.message || resetErr}`);
924
+ log.orez(`zero-cache HTTP liveness replica rebuild recovery failed: ${resetErr?.message || resetErr}`);
905
925
  }
906
926
  }
907
927
  finally {
@@ -923,6 +943,8 @@ export async function startZeroLite(overrides = {}) {
923
943
  shuttingDown = true;
924
944
  if (zeroHttpHealthTimer)
925
945
  clearInterval(zeroHttpHealthTimer);
946
+ if (delayedVacuumTimer)
947
+ clearTimeout(delayedVacuumTimer);
926
948
  stopCheckpoint();
927
949
  stopVacuum();
928
950
  httpProxyServer?.close();
@@ -1067,6 +1089,10 @@ async function startZeroCache(config, logStore, sqliteMode = resolveSqliteMode(c
1067
1089
  // orez owns the replica on disk and has no litestream backup; stop zero 1.5's
1068
1090
  // change-streamer from erroring + resyncing on every restart (see the patch).
1069
1091
  disableZeroLitestreamRestore();
1092
+ // zero-cache's changeLog cleanup can strand pending cleanup watermarks when
1093
+ // it runs before any subscriber is connected. Keep the cleanup timer alive so
1094
+ // local/ephemeral Orez instances do not accumulate a huge CDB catchup log.
1095
+ enableZeroChangeLogCleanupRetry();
1070
1096
  // litestream also checkpointed the replica WAL in stock zero-cache; with it gone,
1071
1097
  // nothing reclaims the wal2 (PASSIVE autocheckpoint can't pass the view-syncer's
1072
1098
  // held readers), so it grows unbounded and reads slow down. Inject a periodic