@rocicorp/zero 1.11.0-canary.11 → 1.11.0-canary.12
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/out/shared/src/ring-buffer.d.ts +2 -0
- package/out/shared/src/ring-buffer.d.ts.map +1 -1
- package/out/shared/src/ring-buffer.js +4 -0
- package/out/shared/src/ring-buffer.js.map +1 -1
- package/out/zero/package.js +1 -1
- package/out/zero/package.js.map +1 -1
- package/out/zero-cache/src/server/replicator.d.ts.map +1 -1
- package/out/zero-cache/src/server/replicator.js +26 -13
- package/out/zero-cache/src/server/replicator.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts +45 -6
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +117 -32
- package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +2 -2
- package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts +10 -17
- package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/schema/tables.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/schema/tables.js +15 -4
- package/out/zero-cache/src/services/change-streamer/schema/tables.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/snapshot-message.d.ts +92 -0
- package/out/zero-cache/src/services/change-streamer/snapshot-message.d.ts.map +1 -0
- package/out/zero-cache/src/services/change-streamer/snapshot-message.js +42 -0
- package/out/zero-cache/src/services/change-streamer/snapshot-message.js.map +1 -0
- package/out/zero-cache/src/services/change-streamer/snapshot-reservations.d.ts +1 -1
- package/out/zero-cache/src/services/change-streamer/snapshot-reservations.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/snapshot-reservations.js +1 -1
- package/out/zero-cache/src/services/change-streamer/snapshot-reservations.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/snapshot.d.ts +7 -89
- package/out/zero-cache/src/services/change-streamer/snapshot.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-streamer/snapshot.js +26 -98
- package/out/zero-cache/src/services/change-streamer/snapshot.js.map +1 -1
- package/out/zero-cache/src/services/change-streamer/subscribe.d.ts +358 -0
- package/out/zero-cache/src/services/change-streamer/subscribe.d.ts.map +1 -0
- package/out/zero-cache/src/services/change-streamer/subscribe.js +68 -0
- package/out/zero-cache/src/services/change-streamer/subscribe.js.map +1 -0
- package/out/zero-cache/src/services/litestream/commands.d.ts +2 -3
- package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.js +3 -2
- package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
- package/out/zero-cache/src/services/replicator/incremental-sync.d.ts +2 -1
- package/out/zero-cache/src/services/replicator/incremental-sync.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/incremental-sync.js +13 -7
- package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
- package/out/zero-cache/src/services/replicator/replicator.d.ts +2 -1
- package/out/zero-cache/src/services/replicator/replicator.d.ts.map +1 -1
- package/out/zero-cache/src/services/replicator/replicator.js +2 -2
- package/out/zero-cache/src/services/replicator/replicator.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/client-handler.d.ts +19 -2
- package/out/zero-cache/src/services/view-syncer/client-handler.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/client-handler.js +8 -1
- package/out/zero-cache/src/services/view-syncer/client-handler.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr-store.d.ts +8 -1
- package/out/zero-cache/src/services/view-syncer/cvr-store.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr-store.js +14 -4
- package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr.d.ts +6 -2
- package/out/zero-cache/src/services/view-syncer/cvr.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/cvr.js +8 -4
- package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +13 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts +21 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.js +14 -6
- package/out/zero-cache/src/services/view-syncer/snapshotter.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.js +27 -4
- package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
- package/out/zero-cache/src/types/streams.d.ts +33 -7
- package/out/zero-cache/src/types/streams.d.ts.map +1 -1
- package/out/zero-cache/src/types/streams.js +93 -50
- package/out/zero-cache/src/types/streams.js.map +1 -1
- package/out/zero-cache/src/types/subscription.d.ts +11 -0
- package/out/zero-cache/src/types/subscription.d.ts.map +1 -1
- package/out/zero-cache/src/types/subscription.js +56 -35
- package/out/zero-cache/src/types/subscription.js.map +1 -1
- package/out/zero-cache/src/types/ws.js +1 -31
- package/out/zero-cache/src/types/ws.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zero-client/src/client/zero.d.ts.map +1 -1
- package/out/zero-client/src/client/zero.js +5 -5
- package/out/zero-client/src/client/zero.js.map +1 -1
- package/out/zero-protocol/src/inspect-down.d.ts +81 -81
- package/out/zql/src/builder/builder.d.ts.map +1 -1
- package/out/zql/src/builder/builder.js +5 -3
- package/out/zql/src/builder/builder.js.map +1 -1
- package/out/zql/src/ivm/cap.d.ts +2 -1
- package/out/zql/src/ivm/cap.d.ts.map +1 -1
- package/out/zql/src/ivm/cap.js +3 -0
- package/out/zql/src/ivm/cap.js.map +1 -1
- package/out/zql/src/ivm/exists.d.ts +2 -0
- package/out/zql/src/ivm/exists.d.ts.map +1 -1
- package/out/zql/src/ivm/exists.js +3 -0
- package/out/zql/src/ivm/exists.js.map +1 -1
- package/out/zql/src/ivm/fan-in.d.ts +5 -1
- package/out/zql/src/ivm/fan-in.d.ts.map +1 -1
- package/out/zql/src/ivm/fan-in.js +7 -0
- package/out/zql/src/ivm/fan-in.js.map +1 -1
- package/out/zql/src/ivm/fan-out.d.ts +3 -0
- package/out/zql/src/ivm/fan-out.d.ts.map +1 -1
- package/out/zql/src/ivm/fan-out.js +4 -0
- package/out/zql/src/ivm/fan-out.js.map +1 -1
- package/out/zql/src/ivm/filter-operators.d.ts +2 -0
- package/out/zql/src/ivm/filter-operators.d.ts.map +1 -1
- package/out/zql/src/ivm/filter-operators.js +9 -0
- package/out/zql/src/ivm/filter-operators.js.map +1 -1
- package/out/zql/src/ivm/filter.d.ts +4 -1
- package/out/zql/src/ivm/filter.d.ts.map +1 -1
- package/out/zql/src/ivm/filter.js +3 -0
- package/out/zql/src/ivm/filter.js.map +1 -1
- package/out/zql/src/ivm/flipped-join.d.ts +3 -2
- package/out/zql/src/ivm/flipped-join.d.ts.map +1 -1
- package/out/zql/src/ivm/flipped-join.js +70 -16
- package/out/zql/src/ivm/flipped-join.js.map +1 -1
- package/out/zql/src/ivm/join-utils.d.ts +24 -7
- package/out/zql/src/ivm/join-utils.d.ts.map +1 -1
- package/out/zql/src/ivm/join-utils.js +87 -4
- package/out/zql/src/ivm/join-utils.js.map +1 -1
- package/out/zql/src/ivm/join.d.ts +3 -3
- package/out/zql/src/ivm/join.d.ts.map +1 -1
- package/out/zql/src/ivm/join.js +33 -58
- package/out/zql/src/ivm/join.js.map +1 -1
- package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
- package/out/zql/src/ivm/memory-source.js +33 -23
- package/out/zql/src/ivm/memory-source.js.map +1 -1
- package/out/zql/src/ivm/operator.d.ts +39 -10
- package/out/zql/src/ivm/operator.d.ts.map +1 -1
- package/out/zql/src/ivm/operator.js +9 -4
- package/out/zql/src/ivm/operator.js.map +1 -1
- package/out/zql/src/ivm/push-accumulated.d.ts +6 -1
- package/out/zql/src/ivm/push-accumulated.d.ts.map +1 -1
- package/out/zql/src/ivm/push-accumulated.js +47 -2
- package/out/zql/src/ivm/push-accumulated.js.map +1 -1
- package/out/zql/src/ivm/skip.d.ts +2 -1
- package/out/zql/src/ivm/skip.d.ts.map +1 -1
- package/out/zql/src/ivm/skip.js +3 -0
- package/out/zql/src/ivm/skip.js.map +1 -1
- package/out/zql/src/ivm/take-gate.d.ts +1 -0
- package/out/zql/src/ivm/take-gate.d.ts.map +1 -1
- package/out/zql/src/ivm/take-gate.js +3 -0
- package/out/zql/src/ivm/take-gate.js.map +1 -1
- package/out/zql/src/ivm/take.d.ts +2 -1
- package/out/zql/src/ivm/take.d.ts.map +1 -1
- package/out/zql/src/ivm/take.js +59 -40
- package/out/zql/src/ivm/take.js.map +1 -1
- package/out/zql/src/ivm/union-fan-in.d.ts +3 -0
- package/out/zql/src/ivm/union-fan-in.d.ts.map +1 -1
- package/out/zql/src/ivm/union-fan-in.js +56 -5
- package/out/zql/src/ivm/union-fan-in.js.map +1 -1
- package/out/zql/src/ivm/union-fan-out.d.ts +2 -1
- package/out/zql/src/ivm/union-fan-out.d.ts.map +1 -1
- package/out/zql/src/ivm/union-fan-out.js +5 -0
- package/out/zql/src/ivm/union-fan-out.js.map +1 -1
- package/out/zql/src/query/measure-push-operator.d.ts +2 -1
- package/out/zql/src/query/measure-push-operator.d.ts.map +1 -1
- package/out/zql/src/query/measure-push-operator.js +7 -0
- package/out/zql/src/query/measure-push-operator.js.map +1 -1
- package/package.json +1 -1
|
@@ -14,6 +14,8 @@ export declare class RingBuffer<T> {
|
|
|
14
14
|
constructor(initialCapacity?: number);
|
|
15
15
|
get size(): number;
|
|
16
16
|
push(value: T): void;
|
|
17
|
+
/** Returns the front element without removing it, or `undefined` if empty. */
|
|
18
|
+
first(): T | undefined;
|
|
17
19
|
/** Returns the last element, or `undefined` if empty. */
|
|
18
20
|
last(): T | undefined;
|
|
19
21
|
/** Replaces the last element, or appends it if empty. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ring-buffer.d.ts","sourceRoot":"","sources":["../../../../shared/src/ring-buffer.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AACH,qBAAa,UAAU,CAAC,CAAC;;IAMvB,YAAY,eAAe,SAA2B,EAGrD;IAED,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,IAAI,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAMnB;IAED,yDAAyD;IACzD,IAAI,IAAI,CAAC,GAAG,SAAS,CAIpB;IAED,yDAAyD;IACzD,WAAW,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAM1B;IAED,sEAAsE;IACtE,KAAK,IAAI,CAAC,GAAG,SAAS,CAmBrB;IAED;;;;;;OAMG;IACH,MAAM,CAAC,KAAK,EAAE,CAAC,GAAG,MAAM,CAoBvB;IAED;;OAEG;IACH,KAAK,IAAI,CAAC,EAAE,CAMX;IAED,+DAA+D;IAC/D,OAAO,IAAI,CAAC,EAAE,CAMb;IAED,wDAAwD;IACxD,KAAK,IAAI,IAAI,CAKZ;CA0CF"}
|
|
1
|
+
{"version":3,"file":"ring-buffer.d.ts","sourceRoot":"","sources":["../../../../shared/src/ring-buffer.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AACH,qBAAa,UAAU,CAAC,CAAC;;IAMvB,YAAY,eAAe,SAA2B,EAGrD;IAED,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,IAAI,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAMnB;IAED,8EAA8E;IAC9E,KAAK,IAAI,CAAC,GAAG,SAAS,CAErB;IAED,yDAAyD;IACzD,IAAI,IAAI,CAAC,GAAG,SAAS,CAIpB;IAED,yDAAyD;IACzD,WAAW,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAM1B;IAED,sEAAsE;IACtE,KAAK,IAAI,CAAC,GAAG,SAAS,CAmBrB;IAED;;;;;;OAMG;IACH,MAAM,CAAC,KAAK,EAAE,CAAC,GAAG,MAAM,CAoBvB;IAED;;OAEG;IACH,KAAK,IAAI,CAAC,EAAE,CAMX;IAED,+DAA+D;IAC/D,OAAO,IAAI,CAAC,EAAE,CAMb;IAED,wDAAwD;IACxD,KAAK,IAAI,IAAI,CAKZ;CA0CF"}
|
|
@@ -29,6 +29,10 @@ var RingBuffer = class {
|
|
|
29
29
|
this.#buffer[(this.#head + this.#size) % this.#capacity] = value;
|
|
30
30
|
this.#size++;
|
|
31
31
|
}
|
|
32
|
+
/** Returns the front element without removing it, or `undefined` if empty. */
|
|
33
|
+
first() {
|
|
34
|
+
return this.#size === 0 ? void 0 : this.#buffer[this.#head];
|
|
35
|
+
}
|
|
32
36
|
/** Returns the last element, or `undefined` if empty. */
|
|
33
37
|
last() {
|
|
34
38
|
return this.#size === 0 ? void 0 : this.#buffer[(this.#head + this.#size - 1) % this.#capacity];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ring-buffer.js","names":["#capacity","#buffer","#size","#grow","#head","#shrink"],"sources":["../../../../shared/src/ring-buffer.ts"],"sourcesContent":["const DEFAULT_INITIAL_CAPACITY = 16;\nconst MIN_CAPACITY = 16;\n\n/**\n * A FIFO ring buffer backed by a circular array. Provides:\n *\n * - `push()` — O(1) amortized (O(n) on resize)\n * - `shift()` — O(1)\n * - `size` — O(1)\n * - `drain()` — O(n)\n *\n * The buffer grows (doubles) when full and shrinks (halves) when\n * utilization drops below 25%, bounded by {@link MIN_CAPACITY}.\n */\nexport class RingBuffer<T> {\n #buffer: (T | undefined)[];\n #head = 0;\n #size = 0;\n #capacity: number;\n\n constructor(initialCapacity = DEFAULT_INITIAL_CAPACITY) {\n this.#capacity = Math.max(MIN_CAPACITY, initialCapacity);\n this.#buffer = Array.from({length: initialCapacity});\n }\n\n get size(): number {\n return this.#size;\n }\n\n push(value: T): void {\n if (this.#size === this.#capacity) {\n this.#grow();\n }\n this.#buffer[(this.#head + this.#size) % this.#capacity] = value;\n this.#size++;\n }\n\n /** Returns the last element, or `undefined` if empty. */\n last(): T | undefined {\n return this.#size === 0\n ? undefined\n : this.#buffer[(this.#head + this.#size - 1) % this.#capacity];\n }\n\n /** Replaces the last element, or appends it if empty. */\n replaceLast(value: T): void {\n if (this.#size === 0) {\n this.push(value);\n } else {\n this.#buffer[(this.#head + this.#size - 1) % this.#capacity] = value;\n }\n }\n\n /** Removes and returns the front element, or `undefined` if empty. */\n shift(): T | undefined {\n if (this.#size === 0) {\n return undefined;\n }\n const value = this.#buffer[this.#head];\n this.#buffer[this.#head] = undefined; // allow GC of removed element\n this.#head = (this.#head + 1) % this.#capacity;\n this.#size--;\n\n // Shrink when utilization drops below 25%, keeping a minimum capacity.\n if (\n this.#size > 0 &&\n this.#size < this.#capacity >> 2 &&\n this.#capacity > MIN_CAPACITY\n ) {\n this.#shrink();\n }\n\n return value;\n }\n\n /**\n * Removes all elements matching `value` (by identity `===`).\n *\n * This is O(n) — suitable for rare operations like cancellation.\n *\n * @returns The number of elements removed.\n */\n delete(value: T): number {\n if (this.#size === 0) {\n return 0;\n }\n // Rebuild the buffer without matching elements.\n const newBuffer: (T | undefined)[] = Array.from({length: this.#capacity});\n let newSize = 0;\n let count = 0;\n for (let i = 0; i < this.#size; i++) {\n const item = this.#buffer[(this.#head + i) % this.#capacity];\n if (item === value) {\n count++;\n } else {\n newBuffer[newSize++] = item;\n }\n }\n this.#buffer = newBuffer;\n this.#head = 0;\n this.#size = newSize;\n return count;\n }\n\n /**\n * Removes and returns all elements in FIFO order, resetting the buffer.\n */\n drain(): T[] {\n const result = this.toArray();\n this.#head = 0;\n this.#size = 0;\n this.#buffer = Array.from({length: this.#capacity});\n return result;\n }\n\n /** Copies all elements in FIFO order without removing them. */\n toArray(): T[] {\n const result = Array.from<T>({length: this.#size});\n for (let i = 0; i < this.#size; i++) {\n result[i] = this.#buffer[(this.#head + i) % this.#capacity] as T;\n }\n return result;\n }\n\n /** Removes all elements and releases extra capacity. */\n clear(): void {\n this.#head = 0;\n this.#size = 0;\n this.#capacity = MIN_CAPACITY;\n this.#buffer = Array.from({length: this.#capacity});\n }\n\n /**\n * Grows the buffer in place by doubling its length and relocating\n * only the wrapped-around elements (those before #head) into the\n * newly available space. No new array is allocated.\n */\n #grow(): void {\n const oldLen = this.#buffer.length;\n const newLen = oldLen * 2;\n this.#buffer.length = newLen;\n this.#capacity = newLen;\n\n // Copy wrapped-around elements (indices 0..head-1) into the new space.\n for (let i = 0; i < this.#head; i++) {\n this.#buffer[oldLen + i] = this.#buffer[i];\n this.#buffer[i] = undefined; // ensure gc when the element is removed\n }\n }\n\n /**\n * Shrinks the buffer in place by halving its length. Elements whose\n * indices fall in the removed half are relocated into the lower half,\n * then the array is truncated.\n */\n #shrink(): void {\n const oldLen = this.#buffer.length;\n const newLen = Math.max(MIN_CAPACITY, oldLen >> 1);\n\n // Move elements that sit in the upper half down by newLen.\n const left = Math.max(newLen, this.#head);\n const right = Math.min(this.#head + this.#size, oldLen);\n for (let i = left; i < right; i++) {\n this.#buffer[i - newLen] = this.#buffer[i];\n }\n\n if (this.#head >= newLen) {\n this.#head -= newLen;\n }\n this.#buffer.length = newLen;\n this.#capacity = newLen;\n }\n}\n"],"mappings":";AAAA,IAAM,2BAA2B;AACjC,IAAM,eAAe;;;;;;;;;;;;AAarB,IAAa,aAAb,MAA2B;CACzB;CACA,QAAQ;CACR,QAAQ;CACR;CAEA,YAAY,kBAAkB,0BAA0B;EACtD,KAAKA,YAAY,KAAK,IAAI,cAAc,eAAe;EACvD,KAAKC,UAAU,MAAM,KAAK,EAAC,QAAQ,gBAAe,CAAC;CACrD;CAEA,IAAI,OAAe;EACjB,OAAO,KAAKC;CACd;CAEA,KAAK,OAAgB;EACnB,IAAI,KAAKA,UAAU,KAAKF,WACtB,KAAKG,MAAM;EAEb,KAAKF,SAAS,KAAKG,QAAQ,KAAKF,SAAS,KAAKF,aAAa;EAC3D,KAAKE;CACP;;CAGA,OAAsB;EACpB,OAAO,
|
|
1
|
+
{"version":3,"file":"ring-buffer.js","names":["#capacity","#buffer","#size","#grow","#head","#shrink"],"sources":["../../../../shared/src/ring-buffer.ts"],"sourcesContent":["const DEFAULT_INITIAL_CAPACITY = 16;\nconst MIN_CAPACITY = 16;\n\n/**\n * A FIFO ring buffer backed by a circular array. Provides:\n *\n * - `push()` — O(1) amortized (O(n) on resize)\n * - `shift()` — O(1)\n * - `size` — O(1)\n * - `drain()` — O(n)\n *\n * The buffer grows (doubles) when full and shrinks (halves) when\n * utilization drops below 25%, bounded by {@link MIN_CAPACITY}.\n */\nexport class RingBuffer<T> {\n #buffer: (T | undefined)[];\n #head = 0;\n #size = 0;\n #capacity: number;\n\n constructor(initialCapacity = DEFAULT_INITIAL_CAPACITY) {\n this.#capacity = Math.max(MIN_CAPACITY, initialCapacity);\n this.#buffer = Array.from({length: initialCapacity});\n }\n\n get size(): number {\n return this.#size;\n }\n\n push(value: T): void {\n if (this.#size === this.#capacity) {\n this.#grow();\n }\n this.#buffer[(this.#head + this.#size) % this.#capacity] = value;\n this.#size++;\n }\n\n /** Returns the front element without removing it, or `undefined` if empty. */\n first(): T | undefined {\n return this.#size === 0 ? undefined : this.#buffer[this.#head];\n }\n\n /** Returns the last element, or `undefined` if empty. */\n last(): T | undefined {\n return this.#size === 0\n ? undefined\n : this.#buffer[(this.#head + this.#size - 1) % this.#capacity];\n }\n\n /** Replaces the last element, or appends it if empty. */\n replaceLast(value: T): void {\n if (this.#size === 0) {\n this.push(value);\n } else {\n this.#buffer[(this.#head + this.#size - 1) % this.#capacity] = value;\n }\n }\n\n /** Removes and returns the front element, or `undefined` if empty. */\n shift(): T | undefined {\n if (this.#size === 0) {\n return undefined;\n }\n const value = this.#buffer[this.#head];\n this.#buffer[this.#head] = undefined; // allow GC of removed element\n this.#head = (this.#head + 1) % this.#capacity;\n this.#size--;\n\n // Shrink when utilization drops below 25%, keeping a minimum capacity.\n if (\n this.#size > 0 &&\n this.#size < this.#capacity >> 2 &&\n this.#capacity > MIN_CAPACITY\n ) {\n this.#shrink();\n }\n\n return value;\n }\n\n /**\n * Removes all elements matching `value` (by identity `===`).\n *\n * This is O(n) — suitable for rare operations like cancellation.\n *\n * @returns The number of elements removed.\n */\n delete(value: T): number {\n if (this.#size === 0) {\n return 0;\n }\n // Rebuild the buffer without matching elements.\n const newBuffer: (T | undefined)[] = Array.from({length: this.#capacity});\n let newSize = 0;\n let count = 0;\n for (let i = 0; i < this.#size; i++) {\n const item = this.#buffer[(this.#head + i) % this.#capacity];\n if (item === value) {\n count++;\n } else {\n newBuffer[newSize++] = item;\n }\n }\n this.#buffer = newBuffer;\n this.#head = 0;\n this.#size = newSize;\n return count;\n }\n\n /**\n * Removes and returns all elements in FIFO order, resetting the buffer.\n */\n drain(): T[] {\n const result = this.toArray();\n this.#head = 0;\n this.#size = 0;\n this.#buffer = Array.from({length: this.#capacity});\n return result;\n }\n\n /** Copies all elements in FIFO order without removing them. */\n toArray(): T[] {\n const result = Array.from<T>({length: this.#size});\n for (let i = 0; i < this.#size; i++) {\n result[i] = this.#buffer[(this.#head + i) % this.#capacity] as T;\n }\n return result;\n }\n\n /** Removes all elements and releases extra capacity. */\n clear(): void {\n this.#head = 0;\n this.#size = 0;\n this.#capacity = MIN_CAPACITY;\n this.#buffer = Array.from({length: this.#capacity});\n }\n\n /**\n * Grows the buffer in place by doubling its length and relocating\n * only the wrapped-around elements (those before #head) into the\n * newly available space. No new array is allocated.\n */\n #grow(): void {\n const oldLen = this.#buffer.length;\n const newLen = oldLen * 2;\n this.#buffer.length = newLen;\n this.#capacity = newLen;\n\n // Copy wrapped-around elements (indices 0..head-1) into the new space.\n for (let i = 0; i < this.#head; i++) {\n this.#buffer[oldLen + i] = this.#buffer[i];\n this.#buffer[i] = undefined; // ensure gc when the element is removed\n }\n }\n\n /**\n * Shrinks the buffer in place by halving its length. Elements whose\n * indices fall in the removed half are relocated into the lower half,\n * then the array is truncated.\n */\n #shrink(): void {\n const oldLen = this.#buffer.length;\n const newLen = Math.max(MIN_CAPACITY, oldLen >> 1);\n\n // Move elements that sit in the upper half down by newLen.\n const left = Math.max(newLen, this.#head);\n const right = Math.min(this.#head + this.#size, oldLen);\n for (let i = left; i < right; i++) {\n this.#buffer[i - newLen] = this.#buffer[i];\n }\n\n if (this.#head >= newLen) {\n this.#head -= newLen;\n }\n this.#buffer.length = newLen;\n this.#capacity = newLen;\n }\n}\n"],"mappings":"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"}
|
package/out/zero/package.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
var package_default = {
|
|
2
2
|
name: "@rocicorp/zero",
|
|
3
|
-
version: "1.11.0-canary.
|
|
3
|
+
version: "1.11.0-canary.12",
|
|
4
4
|
description: "Zero is a web framework for serverless web development.",
|
|
5
5
|
homepage: "https://zero.rocicorp.dev",
|
|
6
6
|
bugs: { "url": "https://bugs.rocicorp.dev" },
|
package/out/zero/package.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"package.js","names":[],"sources":["../../package.json"],"sourcesContent":["{\n \"name\": \"@rocicorp/zero\",\n \"version\": \"1.11.0-canary.
|
|
1
|
+
{"version":3,"file":"package.js","names":[],"sources":["../../package.json"],"sourcesContent":["{\n \"name\": \"@rocicorp/zero\",\n \"version\": \"1.11.0-canary.12\",\n \"description\": \"Zero is a web framework for serverless web development.\",\n \"homepage\": \"https://zero.rocicorp.dev\",\n \"bugs\": {\n \"url\": \"https://bugs.rocicorp.dev\"\n },\n \"license\": \"Apache-2.0\",\n \"author\": \"Rocicorp, Inc.\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/rocicorp/mono.git\",\n \"directory\": \"packages/zero\"\n },\n \"bin\": {\n \"analyze-query\": \"./out/zero/src/analyze-query.js\",\n \"ast-to-zql\": \"./out/zero/src/ast-to-zql.js\",\n \"transform-query\": \"./out/zero/src/transform-query.js\",\n \"zero-build-schema\": \"./out/zero/src/build-schema.js\",\n \"zero-cache\": \"./out/zero/src/cli.js\",\n \"zero-cache-dev\": \"./out/zero/src/zero-cache-dev.js\",\n \"zero-deploy-permissions\": \"./out/zero/src/deploy-permissions.js\",\n \"zero-out\": \"./out/zero/src/zero-out.js\"\n },\n \"files\": [\n \"out\",\n \"!*.tsbuildinfo\"\n ],\n \"type\": \"module\",\n \"main\": \"out/zero/src/zero.js\",\n \"module\": \"out/zero/src/zero.js\",\n \"types\": \"out/zero/src/zero.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./out/zero/src/zero.d.ts\",\n \"default\": \"./out/zero/src/zero.js\"\n },\n \"./analyze\": {\n \"types\": \"./out/zero/src/analyze.d.ts\",\n \"default\": \"./out/zero/src/analyze.js\"\n },\n \"./bindings\": {\n \"types\": \"./out/zero/src/bindings.d.ts\",\n \"default\": \"./out/zero/src/bindings.js\"\n },\n \"./change-protocol/v0\": {\n \"types\": \"./out/zero/src/change-protocol/v0.d.ts\",\n \"default\": \"./out/zero/src/change-protocol/v0.js\"\n },\n \"./expo-sqlite\": {\n \"types\": \"./out/zero/src/expo-sqlite.d.ts\",\n \"default\": \"./out/zero/src/expo-sqlite.js\"\n },\n \"./op-sqlite\": {\n \"types\": \"./out/zero/src/op-sqlite.d.ts\",\n \"default\": \"./out/zero/src/op-sqlite.js\"\n },\n \"./pg\": {\n \"types\": \"./out/zero/src/pg.d.ts\",\n \"default\": \"./out/zero/src/pg.js\"\n },\n \"./react\": {\n \"types\": \"./out/zero/src/react.d.ts\",\n \"default\": \"./out/zero/src/react.js\"\n },\n \"./react-native\": {\n \"types\": \"./out/zero/src/react-native.d.ts\",\n \"default\": \"./out/zero/src/react-native.js\"\n },\n \"./server\": {\n \"types\": \"./out/zero/src/server.d.ts\",\n \"default\": \"./out/zero/src/server.js\"\n },\n \"./server/adapters/drizzle\": {\n \"types\": \"./out/zero/src/adapters/drizzle.d.ts\",\n \"default\": \"./out/zero/src/adapters/drizzle.js\"\n },\n \"./server/adapters/kysely\": {\n \"types\": \"./out/zero/src/adapters/kysely.d.ts\",\n \"default\": \"./out/zero/src/adapters/kysely.js\"\n },\n \"./server/adapters/prisma\": {\n \"types\": \"./out/zero/src/adapters/prisma.d.ts\",\n \"default\": \"./out/zero/src/adapters/prisma.js\"\n },\n \"./server/adapters/pg\": {\n \"types\": \"./out/zero/src/adapters/pg.d.ts\",\n \"default\": \"./out/zero/src/adapters/pg.js\"\n },\n \"./server/adapters/postgresjs\": {\n \"types\": \"./out/zero/src/adapters/postgresjs.d.ts\",\n \"default\": \"./out/zero/src/adapters/postgresjs.js\"\n },\n \"./solid\": {\n \"types\": \"./out/zero/src/solid.d.ts\",\n \"default\": \"./out/zero/src/solid.js\"\n },\n \"./sqlite\": {\n \"types\": \"./out/zero/src/sqlite.d.ts\",\n \"default\": \"./out/zero/src/sqlite.js\"\n },\n \"./zqlite\": {\n \"types\": \"./out/zero/src/zqlite.d.ts\",\n \"default\": \"./out/zero/src/zqlite.js\"\n }\n },\n \"scripts\": {\n \"build\": \"node --experimental-strip-types --no-warnings tool/build.ts\",\n \"build:watch\": \"node --experimental-strip-types --no-warnings tool/build.ts --watch\",\n \"check-types\": \"tsc -p tsconfig.client.json && tsc -p tsconfig.server.json\",\n \"check-types:client:watch\": \"tsc -p tsconfig.client.json --watch\",\n \"check-types:server:watch\": \"tsc -p tsconfig.server.json --watch\",\n \"format\": \"oxfmt .\",\n \"check-format\": \"oxfmt --check .\",\n \"lint\": \"oxlint --quiet --config ../../oxlint.config.ts src/\",\n \"docs\": \"node --experimental-strip-types --no-warnings tool/generate-docs.ts\",\n \"docs:server\": \"node --watch --experimental-strip-types --no-warnings tool/generate-docs.ts --server\",\n \"fmt\": \"oxfmt .\",\n \"check-fmt\": \"oxfmt --check .\"\n },\n \"dependencies\": {\n \"@badrap/valita\": \"0.3.11\",\n \"@databases/escape-identifier\": \"^1.0.3\",\n \"@databases/sql\": \"^3.3.0\",\n \"@dotenvx/dotenvx\": \"^1.39.0\",\n \"@drdgvhbh/postgres-error-codes\": \"^0.0.6\",\n \"@fastify/cors\": \"^10.0.0\",\n \"@fastify/websocket\": \"^11.0.0\",\n \"@google-cloud/precise-date\": \"^4.0.0\",\n \"@opentelemetry/api\": \"^1.9.0\",\n \"@opentelemetry/api-logs\": \"^0.218.0\",\n \"@opentelemetry/auto-instrumentations-node\": \"^0.76.0\",\n \"@opentelemetry/exporter-metrics-otlp-http\": \"^0.218.0\",\n \"@opentelemetry/resources\": \"^2.7.1\",\n \"@opentelemetry/sdk-metrics\": \"^2.7.1\",\n \"@opentelemetry/sdk-node\": \"^0.218.0\",\n \"@opentelemetry/sdk-trace-node\": \"^2.7.1\",\n \"@opentelemetry/semantic-conventions\": \"^1.41.1\",\n \"@postgresql-typed/oids\": \"^0.2.0\",\n \"@rocicorp/lock\": \"^1.0.4\",\n \"@rocicorp/logger\": \"^6.1.0\",\n \"@rocicorp/resolver\": \"^1.0.2\",\n \"@rocicorp/zero-sqlite3\": \"^1.1.4\",\n \"@standard-schema/spec\": \"^1.0.0\",\n \"@types/basic-auth\": \"^1.1.8\",\n \"@types/ws\": \"^8.5.12\",\n \"basic-auth\": \"^2.0.1\",\n \"chalk-template\": \"^1.1.0\",\n \"chokidar\": \"^4.0.1\",\n \"cloudevents\": \"^10.0.0\",\n \"command-line-args\": \"^6.0.1\",\n \"command-line-usage\": \"^7.0.3\",\n \"compare-utf8\": \"^0.2.3\",\n \"defu\": \"^6.1.4\",\n \"eventemitter3\": \"^5.0.1\",\n \"fastify\": \"^5.0.0\",\n \"is-in-subnet\": \"^4.0.1\",\n \"jose\": \"^5.9.3\",\n \"json-custom-numbers\": \"^3.1.1\",\n \"kasi\": \"^1.1.0\",\n \"nanoid\": \"^5.1.2\",\n \"oxfmt\": \"^0.65.0\",\n \"parse-prometheus-text-format\": \"^1.1.1\",\n \"pg-format\": \"npm:pg-format-fix@^1.0.5\",\n \"postgres\": \"3.4.7\",\n \"semver\": \"^7.5.4\",\n \"url-pattern\": \"^1.0.3\",\n \"urlpattern-polyfill\": \"^10.1.0\",\n \"ws\": \"^8.18.1\"\n },\n \"devDependencies\": {\n \"@op-engineering/op-sqlite\": \">=15\",\n \"analyze-query\": \"workspace:*\",\n \"ast-to-zql\": \"workspace:*\",\n \"expo-sqlite\": \">=15\",\n \"oxc-parser\": \"^0.129.0\",\n \"replicache\": \"workspace:*\",\n \"shared\": \"workspace:*\",\n \"syncpack\": \"^14.3.0\",\n \"typedoc\": \"^0.28.17\",\n \"typescript\": \"~7.0.2\",\n \"vite\": \"^8.0.13\",\n \"vitest\": \"^5.0.0\",\n \"zero-cache\": \"workspace:*\",\n \"zero-client\": \"workspace:*\",\n \"zero-pg\": \"workspace:*\",\n \"zero-react\": \"workspace:*\",\n \"zero-server\": \"workspace:*\",\n \"zero-solid\": \"workspace:*\",\n \"zqlite\": \"workspace:*\"\n },\n \"engines\": {\n \"node\": \">=22\"\n },\n \"packageManager\": \"pnpm@11.11.0\"\n}\n"],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replicator.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/server/replicator.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"replicator.d.ts","sourceRoot":"","sources":["../../../../../zero-cache/src/server/replicator.ts"],"names":[],"mappings":"AA4CA,OAAO,EAGL,KAAK,MAAM,EACZ,MAAM,uBAAuB,CAAC;AAiB/B,wBAA8B,SAAS,CACrC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,GAAG,IAAI,EAAE,MAAM,EAAE,GAChB,OAAO,CAAC,IAAI,CAAC,CA4Hf"}
|
|
@@ -14,12 +14,12 @@ import { initEventSink } from "../observability/events.js";
|
|
|
14
14
|
import { ReplicationStatusPublisher } from "../services/replicator/replication-status.js";
|
|
15
15
|
import { litestreamRestoreDuration, litestreamRestoreMetricAttrs, litestreamRestoreRuns } from "../services/litestream/metrics.js";
|
|
16
16
|
import { tryRestore } from "../services/litestream/commands.js";
|
|
17
|
-
import { reserveAndGetSnapshotStatus } from "../services/change-streamer/snapshot.js";
|
|
18
17
|
import { ChangeStreamerHttpClient } from "../services/change-streamer/change-streamer-http.js";
|
|
19
18
|
import { sqliteFileBytes } from "../services/replicator/sqlite-change-log-observability.js";
|
|
20
19
|
import { startOtelAuto } from "./otel-start.js";
|
|
21
20
|
import { ThreadWriteWorkerClient } from "../services/replicator/write-worker-client.js";
|
|
22
21
|
import { deleteStaleChangeLog, getPragmaConfig, replicaFileModeSchema, setUpMessageHandlers, setupReplica } from "../workers/replicator.js";
|
|
22
|
+
import { reserveAndGetSnapshotStatus } from "../services/change-streamer/snapshot.js";
|
|
23
23
|
import { RestoreProgressReporter } from "../services/litestream/restore-progress.js";
|
|
24
24
|
import { ReplicatorService } from "../services/replicator/replicator.js";
|
|
25
25
|
import { LogContext, consoleLogSink } from "@rocicorp/logger";
|
|
@@ -45,7 +45,11 @@ async function runWorker(parent, env, ...args) {
|
|
|
45
45
|
dbPath: config.replica.file
|
|
46
46
|
}, config.sqliteCorruptionChecks);
|
|
47
47
|
initEventSink(lc, config);
|
|
48
|
-
|
|
48
|
+
const shard = getShardConfig(config);
|
|
49
|
+
const { taskID, change, changeStreamer: { port, uri: changeStreamerURI = runningLocalChangeStreamer ? `http://localhost:${port}/` : void 0 } } = config;
|
|
50
|
+
const changeStreamer = new ChangeStreamerHttpClient(lc, shard, change.db, changeStreamerURI);
|
|
51
|
+
let initialConnection;
|
|
52
|
+
if (fileMode === "serving" && !runningLocalChangeStreamer && (config.litestream.executable || config.litestream.executableV5)) initialConnection = await restoreReplica(lc, config, changeStreamer);
|
|
49
53
|
const { file: dbPath, walMode, pageSize } = await setupReplica(lc, fileMode, config.replica);
|
|
50
54
|
unregisterInitialCorruptionDiagnosticTarget();
|
|
51
55
|
registerSQLiteCorruptionDiagnosticTarget({
|
|
@@ -61,10 +65,7 @@ async function runWorker(parent, env, ...args) {
|
|
|
61
65
|
} : null;
|
|
62
66
|
const workerClient = new ThreadWriteWorkerClient();
|
|
63
67
|
await workerClient.init(dbPath, mode, pragmas, config.log, checkpoint);
|
|
64
|
-
const
|
|
65
|
-
const { taskID, change, changeStreamer: { port, uri: changeStreamerURI = runningLocalChangeStreamer ? `http://localhost:${port}/` : void 0 } } = config;
|
|
66
|
-
const changeStreamer = new ChangeStreamerHttpClient(lc, shard, change.db, changeStreamerURI);
|
|
67
|
-
const replicator = new ReplicatorService(lc, taskID, `${workerName}-${pid}`, mode, changeStreamer, workerClient, runningLocalChangeStreamer ? ReplicationStatusPublisher.forReplicaFile(dbPath) : null);
|
|
68
|
+
const replicator = new ReplicatorService(lc, taskID, `${workerName}-${pid}`, mode, changeStreamer, workerClient, runningLocalChangeStreamer ? ReplicationStatusPublisher.forReplicaFile(dbPath) : null, initialConnection);
|
|
68
69
|
setUpMessageHandlers(lc, replicator, parent);
|
|
69
70
|
const running = runUntilKilled(lc, parent, replicator);
|
|
70
71
|
for await (const _ of replicator.subscribe()) {
|
|
@@ -105,27 +106,39 @@ function observeSQLiteFileBytes(lc, file) {
|
|
|
105
106
|
return async (o) => o.observe(await sqliteFileBytes(lc, file));
|
|
106
107
|
}
|
|
107
108
|
var RETRY_INTERVAL_MS = 3e3;
|
|
108
|
-
async function restoreReplica(lc, config) {
|
|
109
|
+
async function restoreReplica(lc, config, changeStreamer) {
|
|
109
110
|
const start = performance.now();
|
|
110
111
|
let backupURL;
|
|
111
112
|
let result;
|
|
112
113
|
const progress = new RestoreProgressReporter(lc, config.replica.file);
|
|
113
114
|
try {
|
|
114
115
|
for (;;) {
|
|
115
|
-
const
|
|
116
|
-
({backupURL} =
|
|
116
|
+
const { reserved, followup } = await reserveAndGetSnapshotStatus(lc, config.taskID, changeStreamer);
|
|
117
|
+
({backupURL} = reserved);
|
|
117
118
|
const litestream = {
|
|
118
119
|
...config.litestream,
|
|
119
120
|
backupURL
|
|
120
121
|
};
|
|
121
|
-
progress.start(
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
progress.start(reserved.replicaSize);
|
|
123
|
+
let attempt;
|
|
124
|
+
try {
|
|
125
|
+
attempt = await tryRestore(lc, litestream, config.replica.file, reserved, "view_syncer", followup.signal);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
if (followup.signal.aborted) {
|
|
128
|
+
lc.info?.(`reservation disconnected during restore. retrying`, String(e));
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
followup.cancel();
|
|
132
|
+
throw e;
|
|
133
|
+
} finally {
|
|
134
|
+
progress.stop();
|
|
135
|
+
}
|
|
124
136
|
if (attempt.restored) {
|
|
125
137
|
result = attempt.result;
|
|
126
138
|
progress.done();
|
|
127
|
-
return;
|
|
139
|
+
return followup;
|
|
128
140
|
}
|
|
141
|
+
followup.cancel();
|
|
129
142
|
lc.info?.(`replica not found. retrying in ${RETRY_INTERVAL_MS / 1e3} seconds`);
|
|
130
143
|
await sleep(RETRY_INTERVAL_MS);
|
|
131
144
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replicator.js","names":[],"sources":["../../../../../zero-cache/src/server/replicator.ts"],"sourcesContent":["import {stat} from 'node:fs/promises';\nimport {pid} from 'node:process';\nimport type {ObservableCallback} from '@opentelemetry/api';\nimport {consoleLogSink, LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport {sleep} from '../../../shared/src/sleep.ts';\nimport * as v from '../../../shared/src/valita.ts';\nimport type {\n LitestreamConfig,\n NormalizedZeroConfig,\n} from '../config/normalize.ts';\nimport {getNormalizedZeroConfig} from '../config/zero-config.ts';\nimport {registerSQLiteCorruptionDiagnosticTarget} from '../db/sqlite-corruption.ts';\nimport {initEventSink} from '../observability/events.ts';\nimport {getOrCreateGauge} from '../observability/metrics.ts';\nimport {ChangeStreamerHttpClient} from '../services/change-streamer/change-streamer-http.ts';\nimport {reserveAndGetSnapshotStatus} from '../services/change-streamer/snapshot.ts';\nimport {exitAfter, runUntilKilled} from '../services/life-cycle.ts';\nimport {\n tryRestore,\n type RestoreResult,\n} from '../services/litestream/commands.ts';\nimport {\n litestreamRestoreDuration,\n litestreamRestoreMetricAttrs,\n litestreamRestoreRuns,\n} from '../services/litestream/metrics.ts';\nimport {RestoreProgressReporter} from '../services/litestream/restore-progress.ts';\nimport {ReplicationStatusPublisher} from '../services/replicator/replication-status.ts';\nimport {\n ReplicatorService,\n type ReplicatorMode,\n} from '../services/replicator/replicator.ts';\nimport {sqliteFileBytes} from '../services/replicator/sqlite-change-log-observability.ts';\nimport {\n ThreadWriteWorkerClient,\n type ForceCheckpointConfig,\n} from '../services/replicator/write-worker-client.ts';\nimport {\n parentWorker,\n singleProcessMode,\n type Worker,\n} from '../types/processes.ts';\nimport {installProfileHandler} from '../types/profiler.ts';\nimport {getShardConfig} from '../types/shards.ts';\nimport {\n deleteStaleChangeLog,\n getPragmaConfig,\n replicaFileModeSchema,\n setUpMessageHandlers,\n setupReplica,\n type WalMode,\n} from '../workers/replicator.ts';\nimport {createLogContext} from './logging.ts';\nimport {startOtelAuto} from './otel-start.ts';\n\n// Default LogContext, overridden in runWorker\nlet lc = new LogContext('info', {}, consoleLogSink);\n\nexport default async function runWorker(\n parent: Worker,\n env: NodeJS.ProcessEnv,\n ...args: string[]\n): Promise<void> {\n assert(args.length > 0, `replicator mode not specified`);\n const fileMode = v.parse(args[0], replicaFileModeSchema);\n\n const config = getNormalizedZeroConfig({env, argv: args.slice(1)});\n const mode: ReplicatorMode = fileMode === 'backup' ? 'backup' : 'serving';\n const runningLocalChangeStreamer =\n config.changeStreamer.mode === 'dedicated' && !config.changeStreamer.uri;\n const workerName = `${mode}-replicator`;\n installProfileHandler(parent, workerName);\n startOtelAuto(createLogContext(config, workerName, 0, false), workerName, 0);\n lc = createLogContext(config, workerName);\n const unregisterInitialCorruptionDiagnosticTarget =\n registerSQLiteCorruptionDiagnosticTarget(\n {\n debugName: `${workerName} replica`,\n dbPath: config.replica.file,\n },\n config.sqliteCorruptionChecks,\n );\n initEventSink(lc, config);\n\n // Remote view-syncers restore from the replication-manager, which supplies\n // the backup URL. A local change-streamer owns the canonical replica and\n // must not infer backup configuration from bundled executable paths.\n if (\n fileMode === 'serving' &&\n !runningLocalChangeStreamer &&\n (config.litestream.executable || config.litestream.executableV5)\n ) {\n await restoreReplica(lc, config);\n }\n\n const {\n file: dbPath,\n walMode,\n pageSize,\n } = await setupReplica(lc, fileMode, config.replica);\n unregisterInitialCorruptionDiagnosticTarget();\n registerSQLiteCorruptionDiagnosticTarget(\n {\n debugName: `${workerName} replica`,\n dbPath,\n },\n config.sqliteCorruptionChecks,\n );\n\n // A file left behind by a run with the writer enabled would otherwise hold\n // local disk indefinitely. See `replicatorDeletesStaleChangeLog` for why\n // this is keyed on the config flag and nothing else.\n deleteStaleChangeLog(config.changeStreamer.sqliteChangeLogMode, dbPath);\n\n setupMetrics(lc, dbPath, walMode);\n\n // Create the write worker for async SQLite writes. Enable write-path\n // checkpoint backpressure only for the backup replicator on litestream v5,\n // which owns the replica litestream backs up.\n const pragmas = getPragmaConfig(fileMode);\n const checkpoint: ForceCheckpointConfig | null =\n mode === 'backup' &&\n config.litestream.backupURL &&\n config.litestream.backupUsingV5 &&\n config.litestream.forceCheckpointThresholdMB\n ? {\n checkpointThresholdPages:\n (config.litestream.forceCheckpointThresholdMB * 1024 ** 2) /\n pageSize,\n maxWalPages: !config.litestream.maxWalSizeMB\n ? undefined\n : (config.litestream.maxWalSizeMB * 1024 ** 2) / pageSize,\n }\n : null;\n const workerClient = new ThreadWriteWorkerClient();\n await workerClient.init(dbPath, mode, pragmas, config.log, checkpoint);\n\n const shard = getShardConfig(config);\n const {\n taskID,\n change,\n changeStreamer: {\n port,\n uri: changeStreamerURI = runningLocalChangeStreamer\n ? `http://localhost:${port}/`\n : undefined,\n },\n } = config;\n const changeStreamer = new ChangeStreamerHttpClient(\n lc,\n shard,\n change.db,\n changeStreamerURI,\n );\n\n const replicator = new ReplicatorService(\n lc,\n taskID,\n `${workerName}-${pid}`,\n mode,\n changeStreamer,\n workerClient,\n runningLocalChangeStreamer\n ? // publish ReplicationStatusEvents from backup-replicator only\n ReplicationStatusPublisher.forReplicaFile(dbPath)\n : null,\n );\n\n setUpMessageHandlers(lc, replicator, parent);\n\n const running = runUntilKilled(lc, parent, replicator);\n\n // Signal readiness once the first ReplicaVersionReady notification is received.\n for await (const _ of replicator.subscribe()) {\n parent.send(['ready', {ready: true}]);\n break;\n }\n\n return running;\n}\n\nfunction setupMetrics(lc: LogContext, file: string, walMode: WalMode) {\n getOrCreateGauge('replica', 'db_size', {\n description:\n `The size of the replica's main db file, ` +\n `which does not include the wal file(s)`,\n unit: 'bytes',\n }).addCallback(observeFileSize(lc, file));\n\n getOrCreateGauge('replica', 'wal_size', {\n description: `The size of the replica's wal file`,\n unit: 'bytes',\n }).addCallback(observeFileSize(lc, `${file}-wal`));\n\n if (walMode === 'wal2') {\n getOrCreateGauge('replica', 'wal2_size', {\n description: `The size of the replica's wal2 file`,\n unit: 'bytes',\n }).addCallback(observeFileSize(lc, `${file}-wal2`));\n }\n\n // A whole-database total rather than a per-file one. Its counterpart,\n // `sqlite_change_log.file_bytes`, is emitted by the change-streamer, which is\n // the process that writes the log.\n getOrCreateGauge('replica', 'file_bytes', {\n description:\n `The replica's total on-disk footprint: its main db file plus its ` +\n `wal sidecars.`,\n unit: 'By',\n }).addCallback(observeSQLiteFileBytes(lc, file));\n}\n\nfunction observeFileSize(lc: LogContext, file: string): ObservableCallback {\n return async o => {\n try {\n const stats = await stat(file);\n o.observe(stats.size);\n } catch (e) {\n lc.warn?.(`unable to stat ${file} for size metrics`, e);\n }\n };\n}\n\nfunction observeSQLiteFileBytes(\n lc: LogContext,\n file: string,\n): ObservableCallback {\n return async o => o.observe(await sqliteFileBytes(lc, file));\n}\n\nconst RETRY_INTERVAL_MS = 3000;\n\n// View-syncers (no replicaConstraints) wait indefinitely for the\n// replication-manager to publish a restorable backup. On a fresh stack the\n// first backup is not durable until the initial sync completes and litestream\n// uploads the initial snapshot, which can take many minutes for a large\n// replica. The platform's startup probe budget (which scales with replica\n// size) is the backstop, so restoreReplica must not impose its own shorter\n// cap and self-terminate while the backup is still being produced.\nasync function restoreReplica(lc: LogContext, config: NormalizedZeroConfig) {\n const start = performance.now();\n let backupURL: string | undefined;\n let result: RestoreResult | undefined;\n const progress = new RestoreProgressReporter(lc, config.replica.file);\n try {\n for (;;) {\n const snapshotStatus = await reserveAndGetSnapshotStatus(lc, config);\n // The backupURL comes from the replication-manager's snapshot response.\n ({backupURL} = snapshotStatus);\n const litestream: LitestreamConfig = {...config.litestream, backupURL};\n progress.start(snapshotStatus.replicaSize);\n const attempt = await tryRestore(\n lc,\n litestream,\n config.replica.file,\n snapshotStatus,\n 'view_syncer',\n );\n progress.stop();\n if (attempt.restored) {\n result = attempt.result;\n progress.done();\n return;\n }\n lc.info?.(\n `replica not found. retrying in ${RETRY_INTERVAL_MS / 1000} seconds`,\n );\n await sleep(RETRY_INTERVAL_MS);\n }\n } finally {\n progress.stop();\n const attrs = litestreamRestoreMetricAttrs(\n config.litestream,\n 'view_syncer',\n backupURL,\n );\n const labels = {...attrs, result: result ?? 'error'};\n litestreamRestoreRuns().add(1, labels);\n litestreamRestoreDuration().recordMs(performance.now() - start, labels);\n }\n}\n\n// fork()\nif (!singleProcessMode()) {\n void exitAfter(\n () => lc,\n () => runWorker(must(parentWorker), process.env, ...process.argv.slice(2)),\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"replicator.js","names":[],"sources":["../../../../../zero-cache/src/server/replicator.ts"],"sourcesContent":["import {stat} from 'node:fs/promises';\nimport {pid} from 'node:process';\nimport type {ObservableCallback} from '@opentelemetry/api';\nimport {consoleLogSink, LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../shared/src/asserts.ts';\nimport {must} from '../../../shared/src/must.ts';\nimport {sleep} from '../../../shared/src/sleep.ts';\nimport * as v from '../../../shared/src/valita.ts';\nimport type {\n LitestreamConfig,\n NormalizedZeroConfig,\n} from '../config/normalize.ts';\nimport {getNormalizedZeroConfig} from '../config/zero-config.ts';\nimport {registerSQLiteCorruptionDiagnosticTarget} from '../db/sqlite-corruption.ts';\nimport {initEventSink} from '../observability/events.ts';\nimport {getOrCreateGauge} from '../observability/metrics.ts';\nimport {\n ChangeStreamerHttpClient,\n type ReservationFollowup,\n type SnapshotReserver,\n} from '../services/change-streamer/change-streamer-http.ts';\nimport {reserveAndGetSnapshotStatus} from '../services/change-streamer/snapshot.ts';\nimport {exitAfter, runUntilKilled} from '../services/life-cycle.ts';\nimport {\n tryRestore,\n type RestoreAttempt,\n type RestoreResult,\n} from '../services/litestream/commands.ts';\nimport {\n litestreamRestoreDuration,\n litestreamRestoreMetricAttrs,\n litestreamRestoreRuns,\n} from '../services/litestream/metrics.ts';\nimport {RestoreProgressReporter} from '../services/litestream/restore-progress.ts';\nimport {ReplicationStatusPublisher} from '../services/replicator/replication-status.ts';\nimport {\n ReplicatorService,\n type ReplicatorMode,\n} from '../services/replicator/replicator.ts';\nimport {sqliteFileBytes} from '../services/replicator/sqlite-change-log-observability.ts';\nimport {\n ThreadWriteWorkerClient,\n type ForceCheckpointConfig,\n} from '../services/replicator/write-worker-client.ts';\nimport {\n parentWorker,\n singleProcessMode,\n type Worker,\n} from '../types/processes.ts';\nimport {installProfileHandler} from '../types/profiler.ts';\nimport {getShardConfig} from '../types/shards.ts';\nimport {\n deleteStaleChangeLog,\n getPragmaConfig,\n replicaFileModeSchema,\n setUpMessageHandlers,\n setupReplica,\n type WalMode,\n} from '../workers/replicator.ts';\nimport {createLogContext} from './logging.ts';\nimport {startOtelAuto} from './otel-start.ts';\n\n// Default LogContext, overridden in runWorker\nlet lc = new LogContext('info', {}, consoleLogSink);\n\nexport default async function runWorker(\n parent: Worker,\n env: NodeJS.ProcessEnv,\n ...args: string[]\n): Promise<void> {\n assert(args.length > 0, `replicator mode not specified`);\n const fileMode = v.parse(args[0], replicaFileModeSchema);\n\n const config = getNormalizedZeroConfig({env, argv: args.slice(1)});\n const mode: ReplicatorMode = fileMode === 'backup' ? 'backup' : 'serving';\n const runningLocalChangeStreamer =\n config.changeStreamer.mode === 'dedicated' && !config.changeStreamer.uri;\n const workerName = `${mode}-replicator`;\n installProfileHandler(parent, workerName);\n startOtelAuto(createLogContext(config, workerName, 0, false), workerName, 0);\n lc = createLogContext(config, workerName);\n const unregisterInitialCorruptionDiagnosticTarget =\n registerSQLiteCorruptionDiagnosticTarget(\n {\n debugName: `${workerName} replica`,\n dbPath: config.replica.file,\n },\n config.sqliteCorruptionChecks,\n );\n initEventSink(lc, config);\n\n const shard = getShardConfig(config);\n const {\n taskID,\n change,\n changeStreamer: {\n port,\n uri: changeStreamerURI = runningLocalChangeStreamer\n ? `http://localhost:${port}/`\n : undefined,\n },\n } = config;\n const changeStreamer = new ChangeStreamerHttpClient(\n lc,\n shard,\n change.db,\n changeStreamerURI,\n );\n\n // Remote view-syncers restore from the replication-manager, which supplies\n // the backup URL. A local change-streamer owns the canonical replica and\n // must not infer backup configuration from bundled executable paths.\n //\n // The reservation's connection (`initialConnection`) is threaded through to\n // IncrementalSyncer's first subscribe(), so the replication-manager that\n // reserved the change log for this restore is the one that serves the\n // subscription (single-connection handoff).\n let initialConnection: ReservationFollowup | undefined;\n if (\n fileMode === 'serving' &&\n !runningLocalChangeStreamer &&\n (config.litestream.executable || config.litestream.executableV5)\n ) {\n initialConnection = await restoreReplica(lc, config, changeStreamer);\n }\n\n const {\n file: dbPath,\n walMode,\n pageSize,\n } = await setupReplica(lc, fileMode, config.replica);\n unregisterInitialCorruptionDiagnosticTarget();\n registerSQLiteCorruptionDiagnosticTarget(\n {\n debugName: `${workerName} replica`,\n dbPath,\n },\n config.sqliteCorruptionChecks,\n );\n\n // A file left behind by a run with the writer enabled would otherwise hold\n // local disk indefinitely. See `replicatorDeletesStaleChangeLog` for why\n // this is keyed on the config flag and nothing else.\n deleteStaleChangeLog(config.changeStreamer.sqliteChangeLogMode, dbPath);\n\n setupMetrics(lc, dbPath, walMode);\n\n // Create the write worker for async SQLite writes. Enable write-path\n // checkpoint backpressure only for the backup replicator on litestream v5,\n // which owns the replica litestream backs up.\n const pragmas = getPragmaConfig(fileMode);\n const checkpoint: ForceCheckpointConfig | null =\n mode === 'backup' &&\n config.litestream.backupURL &&\n config.litestream.backupUsingV5 &&\n config.litestream.forceCheckpointThresholdMB\n ? {\n checkpointThresholdPages:\n (config.litestream.forceCheckpointThresholdMB * 1024 ** 2) /\n pageSize,\n maxWalPages: !config.litestream.maxWalSizeMB\n ? undefined\n : (config.litestream.maxWalSizeMB * 1024 ** 2) / pageSize,\n }\n : null;\n const workerClient = new ThreadWriteWorkerClient();\n await workerClient.init(dbPath, mode, pragmas, config.log, checkpoint);\n\n const replicator = new ReplicatorService(\n lc,\n taskID,\n `${workerName}-${pid}`,\n mode,\n changeStreamer,\n workerClient,\n runningLocalChangeStreamer\n ? // publish ReplicationStatusEvents from backup-replicator only\n ReplicationStatusPublisher.forReplicaFile(dbPath)\n : null,\n initialConnection,\n );\n\n setUpMessageHandlers(lc, replicator, parent);\n\n const running = runUntilKilled(lc, parent, replicator);\n\n // Signal readiness once the first ReplicaVersionReady notification is received.\n for await (const _ of replicator.subscribe()) {\n parent.send(['ready', {ready: true}]);\n break;\n }\n\n return running;\n}\n\nfunction setupMetrics(lc: LogContext, file: string, walMode: WalMode) {\n getOrCreateGauge('replica', 'db_size', {\n description:\n `The size of the replica's main db file, ` +\n `which does not include the wal file(s)`,\n unit: 'bytes',\n }).addCallback(observeFileSize(lc, file));\n\n getOrCreateGauge('replica', 'wal_size', {\n description: `The size of the replica's wal file`,\n unit: 'bytes',\n }).addCallback(observeFileSize(lc, `${file}-wal`));\n\n if (walMode === 'wal2') {\n getOrCreateGauge('replica', 'wal2_size', {\n description: `The size of the replica's wal2 file`,\n unit: 'bytes',\n }).addCallback(observeFileSize(lc, `${file}-wal2`));\n }\n\n // A whole-database total rather than a per-file one. Its counterpart,\n // `sqlite_change_log.file_bytes`, is emitted by the change-streamer, which is\n // the process that writes the log.\n getOrCreateGauge('replica', 'file_bytes', {\n description:\n `The replica's total on-disk footprint: its main db file plus its ` +\n `wal sidecars.`,\n unit: 'By',\n }).addCallback(observeSQLiteFileBytes(lc, file));\n}\n\nfunction observeFileSize(lc: LogContext, file: string): ObservableCallback {\n return async o => {\n try {\n const stats = await stat(file);\n o.observe(stats.size);\n } catch (e) {\n lc.warn?.(`unable to stat ${file} for size metrics`, e);\n }\n };\n}\n\nfunction observeSQLiteFileBytes(\n lc: LogContext,\n file: string,\n): ObservableCallback {\n return async o => o.observe(await sqliteFileBytes(lc, file));\n}\n\nconst RETRY_INTERVAL_MS = 3000;\n\n// View-syncers wait indefinitely for the replication-manager to publish a\n// restorable backup. On a fresh stack the first backup is not durable until\n// the initial sync completes and litestream uploads the initial snapshot,\n// which can take many minutes for a large replica. The platform's startup\n// probe budget (which scales with replica size) is the backstop, so\n// restoreReplica must not impose its own shorter cap and self-terminate\n// while the backup is still being produced.\n//\n// Returns the `followup` of the reservation whose backup was actually\n// restored, still open, so the caller can hand it to IncrementalSyncer's\n// first subscribe() via `followup.subscribe()`: the same replication-manager\n// that reserved the change log is the one that serves the subscription.\nasync function restoreReplica(\n lc: LogContext,\n config: NormalizedZeroConfig,\n changeStreamer: SnapshotReserver,\n): Promise<ReservationFollowup> {\n const start = performance.now();\n let backupURL: string | undefined;\n let result: RestoreResult | undefined;\n const progress = new RestoreProgressReporter(lc, config.replica.file);\n try {\n for (;;) {\n const {reserved, followup} = await reserveAndGetSnapshotStatus(\n lc,\n config.taskID,\n changeStreamer,\n );\n // The backupURL comes from the replication-manager's snapshot response.\n ({backupURL} = reserved);\n const litestream: LitestreamConfig = {...config.litestream, backupURL};\n progress.start(reserved.replicaSize);\n let attempt: RestoreAttempt;\n try {\n attempt = await tryRestore(\n lc,\n litestream,\n config.replica.file,\n reserved,\n 'view_syncer',\n followup.signal, // abort the restore if the reservation disconnects\n );\n } catch (e) {\n if (followup.signal.aborted) {\n lc.info?.(\n `reservation disconnected during restore. retrying`,\n String(e),\n );\n continue;\n }\n followup.cancel();\n throw e;\n } finally {\n progress.stop();\n }\n if (attempt.restored) {\n result = attempt.result;\n progress.done();\n return followup; // connection to be converted to a subscription\n }\n // This attempt's reservation is stale; a fresh one is made on retry.\n followup.cancel();\n lc.info?.(\n `replica not found. retrying in ${RETRY_INTERVAL_MS / 1000} seconds`,\n );\n await sleep(RETRY_INTERVAL_MS);\n }\n } finally {\n progress.stop();\n const attrs = litestreamRestoreMetricAttrs(\n config.litestream,\n 'view_syncer',\n backupURL,\n );\n const labels = {...attrs, result: result ?? 'error'};\n litestreamRestoreRuns().add(1, labels);\n litestreamRestoreDuration().recordMs(performance.now() - start, labels);\n }\n}\n\n// fork()\nif (!singleProcessMode()) {\n void exitAfter(\n () => lc,\n () => runWorker(must(parentWorker), process.env, ...process.argv.slice(2)),\n );\n}\n"],"mappings":"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"}
|
|
@@ -3,10 +3,11 @@ import type { LogContext } from '@rocicorp/logger';
|
|
|
3
3
|
import type { NormalizedZeroConfig } from '../../config/normalize.ts';
|
|
4
4
|
import { type Worker } from '../../types/processes.ts';
|
|
5
5
|
import { type ShardID } from '../../types/shards.ts';
|
|
6
|
-
import { type Source } from '../../types/streams.ts';
|
|
6
|
+
import { type Sized, type Source } from '../../types/streams.ts';
|
|
7
|
+
import { Subscription } from '../../types/subscription.ts';
|
|
7
8
|
import { HttpService, type Options as HttpOptions } from '../http-service.ts';
|
|
8
|
-
import { type ChangeStreamer, type ChangeStreamerService, type
|
|
9
|
-
import { type
|
|
9
|
+
import { type ChangeStreamer, type ChangeStreamerService, type SubscriberContext } from './change-streamer.ts';
|
|
10
|
+
import { type SnapshotStatus, type SubscribeDownstream, type SubscribeUpstream } from './subscribe.ts';
|
|
10
11
|
type Options = HttpOptions & {
|
|
11
12
|
startupDelayMs: number;
|
|
12
13
|
config?: Pick<NormalizedZeroConfig, 'adminPassword'> | undefined;
|
|
@@ -19,11 +20,49 @@ export declare class ChangeStreamerHttpServer extends HttpService {
|
|
|
19
20
|
protected _onStart(): void;
|
|
20
21
|
protected _onStop(): Promise<void>;
|
|
21
22
|
}
|
|
22
|
-
export
|
|
23
|
+
export interface SnapshotReserver {
|
|
24
|
+
reserveSnapshot(taskID: string): Promise<{
|
|
25
|
+
reserved: SnapshotStatus;
|
|
26
|
+
followup: ReservationFollowup;
|
|
27
|
+
}>;
|
|
28
|
+
}
|
|
29
|
+
export interface SubscriptionStarter {
|
|
30
|
+
subscribe(ctx: SubscriberContext): Promise<Source<Sized<SubscribeDownstream>>>;
|
|
31
|
+
}
|
|
32
|
+
export interface ReservationFollowup extends SubscriptionStarter {
|
|
33
|
+
/**
|
|
34
|
+
* A signal to indicate that the followup is no longer valid (i.e. the
|
|
35
|
+
* connection to the reserving task has been severed).
|
|
36
|
+
*/
|
|
37
|
+
readonly signal: AbortSignal;
|
|
38
|
+
/**
|
|
39
|
+
* Abandons the reservation without subscribing, closing the connection that
|
|
40
|
+
* was held open to pin the change-log floor. Used when the caller cannot
|
|
41
|
+
* proceed to `subscribe()` (e.g. process shutdown while a restore is in
|
|
42
|
+
* progress) and must not leave the reservation's connection open forever.
|
|
43
|
+
*/
|
|
44
|
+
cancel(reason?: Error): void;
|
|
45
|
+
}
|
|
46
|
+
export declare class ChangeStreamerHttpClient implements ChangeStreamer, SnapshotReserver, SubscriptionStarter {
|
|
23
47
|
#private;
|
|
24
48
|
constructor(lc: LogContext, shardID: ShardID, changeDB: string, changeStreamerURI: string | undefined);
|
|
25
|
-
reserveSnapshot(taskID: string): Promise<
|
|
26
|
-
|
|
49
|
+
reserveSnapshot(taskID: string): Promise<{
|
|
50
|
+
reserved: SnapshotStatus;
|
|
51
|
+
followup: ReservationFollowup;
|
|
52
|
+
}>;
|
|
53
|
+
subscribe(ctx: SubscriberContext): Promise<Source<Sized<SubscribeDownstream>>>;
|
|
54
|
+
/**
|
|
55
|
+
* Opens a merged v7 `/subscribe` connection (see subscribe.ts). Returns the
|
|
56
|
+
* inbound change/reservation stream and an `outbound` sink into which the
|
|
57
|
+
* caller pushes the `reserve-snapshot` / `start-subscription` control
|
|
58
|
+
* messages that drive the connection's phases. The single connection spans
|
|
59
|
+
* the reservation, backup restore, and subscription, so the reserving
|
|
60
|
+
* replication-manager is the one that serves the subscription.
|
|
61
|
+
*/
|
|
62
|
+
connect(taskID: string): Promise<{
|
|
63
|
+
instream: Source<Sized<SubscribeDownstream>>;
|
|
64
|
+
outbound: Subscription<SubscribeUpstream>;
|
|
65
|
+
}>;
|
|
27
66
|
}
|
|
28
67
|
type RequestHeaders = Pick<IncomingMessage, 'url' | 'headers'>;
|
|
29
68
|
export declare function getSubscriberContext(req: RequestHeaders): SubscriberContext;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"change-streamer-http.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/change-streamer-http.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,WAAW,CAAC;AAE/C,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAIjD,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,2BAA2B,CAAC;AAGpE,OAAO,EAAC,KAAK,MAAM,EAAC,MAAM,0BAA0B,CAAC;AACrD,OAAO,EAAC,KAAK,OAAO,EAAC,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAKL,KAAK,MAAM,EACZ,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"change-streamer-http.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/change-streamer/change-streamer-http.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { assert } from "../../../../shared/src/asserts.js";
|
|
2
1
|
import { promiseVoid } from "../../../../shared/src/resolved-promises.js";
|
|
2
|
+
import { BigIntJSON } from "../../../../shared/src/bigint-json.js";
|
|
3
3
|
import "../../types/processes.js";
|
|
4
4
|
import { pgClient } from "../../types/pg.js";
|
|
5
5
|
import { HttpService } from "../http-service.js";
|
|
@@ -7,19 +7,22 @@ import { URLParams } from "../../types/url-params.js";
|
|
|
7
7
|
import { handleProfzRequest } from "../profz.js";
|
|
8
8
|
import { PROTOCOL_ERROR, closeWithError } from "../../types/ws.js";
|
|
9
9
|
import { installWebSocketReceiver } from "../../types/websocket-handoff.js";
|
|
10
|
-
import {
|
|
10
|
+
import { Subscription } from "../../types/subscription.js";
|
|
11
|
+
import { readHead, streamInternal, streamInternalStringified, streamInternalWithSize } from "../../types/streams.js";
|
|
11
12
|
import { discoverChangeStreamerAddress } from "./schema/tables.js";
|
|
12
|
-
import
|
|
13
|
-
import
|
|
13
|
+
import "./change-streamer.js";
|
|
14
|
+
import "./snapshot-message.js";
|
|
15
|
+
import { subscribeDownstreamSchema, subscribeUpstreamSchema } from "./subscribe.js";
|
|
14
16
|
import WebSocket$1 from "ws";
|
|
15
17
|
import websocket from "@fastify/websocket";
|
|
16
18
|
//#region ../zero-cache/src/services/change-streamer/change-streamer-http.ts
|
|
17
19
|
var MIN_SUPPORTED_PROTOCOL_VERSION = 4;
|
|
20
|
+
var MIN_SUBSCRIBE_PROTOCOL_VERSION = 7;
|
|
18
21
|
var SNAPSHOT_PATH_PATTERN = "/replication/:version/snapshot";
|
|
19
22
|
var CHANGES_PATH_PATTERN = "/replication/:version/changes";
|
|
20
|
-
var
|
|
21
|
-
var
|
|
22
|
-
var
|
|
23
|
+
var SUBSCRIBE_PATH_PATTERN = "/replication/:version/subscribe";
|
|
24
|
+
var PATH_REGEX = /\/replication\/v(?<version>\d+)\/(changes|snapshot|subscribe)$/;
|
|
25
|
+
var SUBSCRIBE_PATH = `/replication/v7/subscribe`;
|
|
23
26
|
var ChangeStreamerHttpServer = class extends HttpService {
|
|
24
27
|
id = "change-streamer-http-server";
|
|
25
28
|
#lc;
|
|
@@ -28,8 +31,9 @@ var ChangeStreamerHttpServer = class extends HttpService {
|
|
|
28
31
|
constructor(lc, opts, parent, changeStreamer) {
|
|
29
32
|
super("change-streamer-http-server", lc, opts, async (fastify) => {
|
|
30
33
|
await fastify.register(websocket);
|
|
31
|
-
fastify.get(CHANGES_PATH_PATTERN, { websocket: true }, this.#
|
|
34
|
+
fastify.get(CHANGES_PATH_PATTERN, { websocket: true }, this.#changes);
|
|
32
35
|
fastify.get(SNAPSHOT_PATH_PATTERN, { websocket: true }, this.#reserveSnapshot);
|
|
36
|
+
fastify.get(SUBSCRIBE_PATH_PATTERN, { websocket: true }, this.#subscribe);
|
|
33
37
|
fastify.get("/profz", (req, res) => handleProfzRequest(lc, opts.config ?? { adminPassword: void 0 }, req, res, opts.getProfileWorker, void 0, "change-streamer"));
|
|
34
38
|
installWebSocketReceiver(lc, fastify.websocketServer, this.#receiveWebsocket, parent);
|
|
35
39
|
});
|
|
@@ -40,7 +44,8 @@ var ChangeStreamerHttpServer = class extends HttpService {
|
|
|
40
44
|
#receiveWebsocket = (ws, action, msg) => {
|
|
41
45
|
switch (action) {
|
|
42
46
|
case "snapshot": return this.#reserveSnapshot(ws, msg);
|
|
43
|
-
case "changes": return this.#
|
|
47
|
+
case "changes": return this.#changes(ws, msg);
|
|
48
|
+
case "subscribe": return this.#subscribe(ws, msg);
|
|
44
49
|
default:
|
|
45
50
|
closeWithError(this._lc, ws, `invalid action "${action}" received in handoff`);
|
|
46
51
|
return;
|
|
@@ -54,21 +59,74 @@ var ChangeStreamerHttpServer = class extends HttpService {
|
|
|
54
59
|
const taskID = url.searchParams.get("taskID");
|
|
55
60
|
if (!taskID) throw new Error("Missing taskID in snapshot request");
|
|
56
61
|
const downstream = await this.#changeStreamer.startSnapshotReservation(taskID);
|
|
57
|
-
|
|
62
|
+
streamInternal(this._lc, ws, void 0, downstream);
|
|
58
63
|
} catch (err) {
|
|
59
64
|
closeWithError(this._lc, ws, err, PROTOCOL_ERROR);
|
|
60
65
|
}
|
|
61
66
|
};
|
|
62
|
-
#
|
|
67
|
+
#changes = async (ws, req) => {
|
|
63
68
|
try {
|
|
64
69
|
const ctx = getSubscriberContext(req);
|
|
65
70
|
if (ctx.mode === "serving") this.#ensureChangeStreamerStarted("incoming subscription");
|
|
66
71
|
const downstream = await this.#changeStreamer.subscribe(ctx);
|
|
67
|
-
|
|
72
|
+
streamInternalStringified(this._lc, ws, void 0, downstream, { batched: ctx.wsBatched });
|
|
73
|
+
} catch (err) {
|
|
74
|
+
closeWithError(this._lc, ws, err, PROTOCOL_ERROR);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* The merged v7 `/subscribe` handler. A single bidirectional connection
|
|
79
|
+
* carries, in sequence: an optional reservation phase (`reserve-snapshot` →
|
|
80
|
+
* `reserved`) and then the subscription phase (`start-subscription` → change
|
|
81
|
+
* stream). This ensures that the subscriber subscribes to the same task from
|
|
82
|
+
* which it restored the backup.
|
|
83
|
+
*/
|
|
84
|
+
#subscribe = async (ws, req) => {
|
|
85
|
+
let reservation;
|
|
86
|
+
const outbound = Subscription.create();
|
|
87
|
+
try {
|
|
88
|
+
const protocolVersion = checkProtocolVersion(new URL(req.url ?? "", req.headers.origin ?? "http://localhost").pathname);
|
|
89
|
+
if (protocolVersion < MIN_SUBSCRIBE_PROTOCOL_VERSION) throw new Error(`the /subscribe endpoint requires protocol v${MIN_SUBSCRIBE_PROTOCOL_VERSION} (client is at v${protocolVersion})`);
|
|
90
|
+
const instream = await streamInternalStringified(this._lc, ws, subscribeUpstreamSchema, outbound, { batched: true });
|
|
91
|
+
for await (const msg of instream) if (msg[0] === "reserve-snapshot") {
|
|
92
|
+
const { taskID } = msg[1];
|
|
93
|
+
this.#ensureChangeStreamerStarted("incoming snapshot reservation");
|
|
94
|
+
reservation = await this.#changeStreamer.startSnapshotReservation(taskID);
|
|
95
|
+
this.#forwardReservation(reservation, outbound);
|
|
96
|
+
} else if (msg[0] === "start-subscription") {
|
|
97
|
+
const ctx = {
|
|
98
|
+
...msg[1],
|
|
99
|
+
protocolVersion,
|
|
100
|
+
wsBatched: true
|
|
101
|
+
};
|
|
102
|
+
if (ctx.mode === "serving") this.#ensureChangeStreamerStarted("incoming subscription");
|
|
103
|
+
reservation = void 0;
|
|
104
|
+
await this.#changeStreamer.subscribe(ctx, outbound);
|
|
105
|
+
}
|
|
68
106
|
} catch (err) {
|
|
69
107
|
closeWithError(this._lc, ws, err, PROTOCOL_ERROR);
|
|
108
|
+
} finally {
|
|
109
|
+
reservation?.cancel();
|
|
70
110
|
}
|
|
71
111
|
};
|
|
112
|
+
/**
|
|
113
|
+
* Forwards a snapshot reservation's single `['status', ...]` message onto
|
|
114
|
+
* the merged connection's `outbound` sink as a `['reserved', ...]` message.
|
|
115
|
+
* The loop then blocks on the held-open reservation until it is cancelled,
|
|
116
|
+
* by `subscribe()` at the subscription handoff, or by the reservation-phase
|
|
117
|
+
* teardown on disconnect.
|
|
118
|
+
*/
|
|
119
|
+
async #forwardReservation(reservation, outbound) {
|
|
120
|
+
try {
|
|
121
|
+
for await (const [, status] of reservation) outbound.push(BigIntJSON.stringify(["reserved", {
|
|
122
|
+
...status,
|
|
123
|
+
tag: "snapshot"
|
|
124
|
+
}]));
|
|
125
|
+
} catch (e) {
|
|
126
|
+
this._lc.warn?.(`error forwarding snapshot reservation`, e);
|
|
127
|
+
outbound.fail(e instanceof Error ? e : new Error(String(e)));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
72
130
|
#changeStreamerStarted = false;
|
|
73
131
|
#ensureChangeStreamerStarted(reason) {
|
|
74
132
|
if (!this.#changeStreamerStarted && this._state.shouldRun()) {
|
|
@@ -115,15 +173,45 @@ var ChangeStreamerHttpClient = class {
|
|
|
115
173
|
return uri;
|
|
116
174
|
}
|
|
117
175
|
async reserveSnapshot(taskID) {
|
|
118
|
-
const
|
|
119
|
-
|
|
176
|
+
const { instream, outbound } = await this.connect(taskID);
|
|
177
|
+
outbound.push(["reserve-snapshot", { taskID }]);
|
|
178
|
+
const { head, rest } = readHead(instream);
|
|
179
|
+
const item = await head.next();
|
|
180
|
+
if (!item.value) throw new Error("stream closed without snapshot reservation");
|
|
181
|
+
const { data: msg } = item.value;
|
|
182
|
+
if (msg[0] !== "reserved") throw new Error(`expected "reserved" message but got ${msg[0]}`);
|
|
183
|
+
return {
|
|
184
|
+
reserved: msg[1],
|
|
185
|
+
followup: {
|
|
186
|
+
subscribe: (ctx) => {
|
|
187
|
+
outbound.push(["start-subscription", toStartSubscriptionContext(ctx)]);
|
|
188
|
+
return Promise.resolve(rest);
|
|
189
|
+
},
|
|
190
|
+
signal: outbound.signal,
|
|
191
|
+
cancel: (reason) => outbound.cancel(reason)
|
|
192
|
+
}
|
|
193
|
+
};
|
|
120
194
|
}
|
|
121
195
|
async subscribe(ctx) {
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
196
|
+
const { instream, outbound } = await this.connect(ctx.taskID);
|
|
197
|
+
outbound.push(["start-subscription", toStartSubscriptionContext(ctx)]);
|
|
198
|
+
return instream;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Opens a merged v7 `/subscribe` connection (see subscribe.ts). Returns the
|
|
202
|
+
* inbound change/reservation stream and an `outbound` sink into which the
|
|
203
|
+
* caller pushes the `reserve-snapshot` / `start-subscription` control
|
|
204
|
+
* messages that drive the connection's phases. The single connection spans
|
|
205
|
+
* the reservation, backup restore, and subscription, so the reserving
|
|
206
|
+
* replication-manager is the one that serves the subscription.
|
|
207
|
+
*/
|
|
208
|
+
async connect(taskID) {
|
|
209
|
+
const ws = new WebSocket$1(await this.#resolveChangeStreamer(SUBSCRIBE_PATH) + `?${new URLSearchParams({ taskID }).toString()}`);
|
|
210
|
+
const outbound = Subscription.create();
|
|
211
|
+
return {
|
|
212
|
+
instream: await streamInternalWithSize(this.#lc, ws, subscribeDownstreamSchema, outbound),
|
|
213
|
+
outbound
|
|
214
|
+
};
|
|
127
215
|
}
|
|
128
216
|
};
|
|
129
217
|
function getSubscriberContext(req) {
|
|
@@ -137,8 +225,6 @@ function getSubscriberContext(req) {
|
|
|
137
225
|
mode: params.get("mode", false) === "backup" ? "backup" : "serving",
|
|
138
226
|
replicaVersion: params.get("replicaVersion", true),
|
|
139
227
|
watermark: params.get("watermark", true),
|
|
140
|
-
initial: params.getBoolean("initial"),
|
|
141
|
-
logsChangeStream: params.getBoolean("logsChangeStream"),
|
|
142
228
|
wsBatched: params.getBoolean("wsBatched")
|
|
143
229
|
};
|
|
144
230
|
}
|
|
@@ -146,19 +232,18 @@ function checkProtocolVersion(pathname) {
|
|
|
146
232
|
const match = PATH_REGEX.exec(pathname);
|
|
147
233
|
if (!match) throw new Error(`invalid path: ${pathname}`);
|
|
148
234
|
const v = Number(match.groups?.version);
|
|
149
|
-
if (Number.isNaN(v) || v >
|
|
235
|
+
if (Number.isNaN(v) || v > 7 || v < MIN_SUPPORTED_PROTOCOL_VERSION) throw new Error(`Cannot service client at protocol v${v}. Supported protocols: [v${MIN_SUPPORTED_PROTOCOL_VERSION} ... v7]`);
|
|
150
236
|
return v;
|
|
151
237
|
}
|
|
152
|
-
function
|
|
153
|
-
const {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
return params;
|
|
238
|
+
function toStartSubscriptionContext(ctx) {
|
|
239
|
+
const { taskID, id, mode, replicaVersion, watermark } = ctx;
|
|
240
|
+
return {
|
|
241
|
+
taskID,
|
|
242
|
+
id,
|
|
243
|
+
mode,
|
|
244
|
+
replicaVersion,
|
|
245
|
+
watermark
|
|
246
|
+
};
|
|
162
247
|
}
|
|
163
248
|
//#endregion
|
|
164
249
|
export { ChangeStreamerHttpClient, ChangeStreamerHttpServer };
|