@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.
Files changed (161) hide show
  1. package/out/shared/src/ring-buffer.d.ts +2 -0
  2. package/out/shared/src/ring-buffer.d.ts.map +1 -1
  3. package/out/shared/src/ring-buffer.js +4 -0
  4. package/out/shared/src/ring-buffer.js.map +1 -1
  5. package/out/zero/package.js +1 -1
  6. package/out/zero/package.js.map +1 -1
  7. package/out/zero-cache/src/server/replicator.d.ts.map +1 -1
  8. package/out/zero-cache/src/server/replicator.js +26 -13
  9. package/out/zero-cache/src/server/replicator.js.map +1 -1
  10. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts +45 -6
  11. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts.map +1 -1
  12. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +117 -32
  13. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
  14. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js +2 -2
  15. package/out/zero-cache/src/services/change-streamer/change-streamer-service.js.map +1 -1
  16. package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts +10 -17
  17. package/out/zero-cache/src/services/change-streamer/change-streamer.d.ts.map +1 -1
  18. package/out/zero-cache/src/services/change-streamer/change-streamer.js.map +1 -1
  19. package/out/zero-cache/src/services/change-streamer/schema/tables.d.ts.map +1 -1
  20. package/out/zero-cache/src/services/change-streamer/schema/tables.js +15 -4
  21. package/out/zero-cache/src/services/change-streamer/schema/tables.js.map +1 -1
  22. package/out/zero-cache/src/services/change-streamer/snapshot-message.d.ts +92 -0
  23. package/out/zero-cache/src/services/change-streamer/snapshot-message.d.ts.map +1 -0
  24. package/out/zero-cache/src/services/change-streamer/snapshot-message.js +42 -0
  25. package/out/zero-cache/src/services/change-streamer/snapshot-message.js.map +1 -0
  26. package/out/zero-cache/src/services/change-streamer/snapshot-reservations.d.ts +1 -1
  27. package/out/zero-cache/src/services/change-streamer/snapshot-reservations.d.ts.map +1 -1
  28. package/out/zero-cache/src/services/change-streamer/snapshot-reservations.js +1 -1
  29. package/out/zero-cache/src/services/change-streamer/snapshot-reservations.js.map +1 -1
  30. package/out/zero-cache/src/services/change-streamer/snapshot.d.ts +7 -89
  31. package/out/zero-cache/src/services/change-streamer/snapshot.d.ts.map +1 -1
  32. package/out/zero-cache/src/services/change-streamer/snapshot.js +26 -98
  33. package/out/zero-cache/src/services/change-streamer/snapshot.js.map +1 -1
  34. package/out/zero-cache/src/services/change-streamer/subscribe.d.ts +358 -0
  35. package/out/zero-cache/src/services/change-streamer/subscribe.d.ts.map +1 -0
  36. package/out/zero-cache/src/services/change-streamer/subscribe.js +68 -0
  37. package/out/zero-cache/src/services/change-streamer/subscribe.js.map +1 -0
  38. package/out/zero-cache/src/services/litestream/commands.d.ts +2 -3
  39. package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
  40. package/out/zero-cache/src/services/litestream/commands.js +3 -2
  41. package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
  42. package/out/zero-cache/src/services/replicator/incremental-sync.d.ts +2 -1
  43. package/out/zero-cache/src/services/replicator/incremental-sync.d.ts.map +1 -1
  44. package/out/zero-cache/src/services/replicator/incremental-sync.js +13 -7
  45. package/out/zero-cache/src/services/replicator/incremental-sync.js.map +1 -1
  46. package/out/zero-cache/src/services/replicator/replicator.d.ts +2 -1
  47. package/out/zero-cache/src/services/replicator/replicator.d.ts.map +1 -1
  48. package/out/zero-cache/src/services/replicator/replicator.js +2 -2
  49. package/out/zero-cache/src/services/replicator/replicator.js.map +1 -1
  50. package/out/zero-cache/src/services/view-syncer/client-handler.d.ts +19 -2
  51. package/out/zero-cache/src/services/view-syncer/client-handler.d.ts.map +1 -1
  52. package/out/zero-cache/src/services/view-syncer/client-handler.js +8 -1
  53. package/out/zero-cache/src/services/view-syncer/client-handler.js.map +1 -1
  54. package/out/zero-cache/src/services/view-syncer/cvr-store.d.ts +8 -1
  55. package/out/zero-cache/src/services/view-syncer/cvr-store.d.ts.map +1 -1
  56. package/out/zero-cache/src/services/view-syncer/cvr-store.js +14 -4
  57. package/out/zero-cache/src/services/view-syncer/cvr-store.js.map +1 -1
  58. package/out/zero-cache/src/services/view-syncer/cvr.d.ts +6 -2
  59. package/out/zero-cache/src/services/view-syncer/cvr.d.ts.map +1 -1
  60. package/out/zero-cache/src/services/view-syncer/cvr.js +8 -4
  61. package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
  62. package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
  63. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +13 -1
  64. package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
  65. package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts +21 -1
  66. package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts.map +1 -1
  67. package/out/zero-cache/src/services/view-syncer/snapshotter.js +14 -6
  68. package/out/zero-cache/src/services/view-syncer/snapshotter.js.map +1 -1
  69. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
  70. package/out/zero-cache/src/services/view-syncer/view-syncer.js +27 -4
  71. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  72. package/out/zero-cache/src/types/streams.d.ts +33 -7
  73. package/out/zero-cache/src/types/streams.d.ts.map +1 -1
  74. package/out/zero-cache/src/types/streams.js +93 -50
  75. package/out/zero-cache/src/types/streams.js.map +1 -1
  76. package/out/zero-cache/src/types/subscription.d.ts +11 -0
  77. package/out/zero-cache/src/types/subscription.d.ts.map +1 -1
  78. package/out/zero-cache/src/types/subscription.js +56 -35
  79. package/out/zero-cache/src/types/subscription.js.map +1 -1
  80. package/out/zero-cache/src/types/ws.js +1 -31
  81. package/out/zero-cache/src/types/ws.js.map +1 -1
  82. package/out/zero-client/src/client/version.js +1 -1
  83. package/out/zero-client/src/client/zero.d.ts.map +1 -1
  84. package/out/zero-client/src/client/zero.js +5 -5
  85. package/out/zero-client/src/client/zero.js.map +1 -1
  86. package/out/zero-protocol/src/inspect-down.d.ts +81 -81
  87. package/out/zql/src/builder/builder.d.ts.map +1 -1
  88. package/out/zql/src/builder/builder.js +5 -3
  89. package/out/zql/src/builder/builder.js.map +1 -1
  90. package/out/zql/src/ivm/cap.d.ts +2 -1
  91. package/out/zql/src/ivm/cap.d.ts.map +1 -1
  92. package/out/zql/src/ivm/cap.js +3 -0
  93. package/out/zql/src/ivm/cap.js.map +1 -1
  94. package/out/zql/src/ivm/exists.d.ts +2 -0
  95. package/out/zql/src/ivm/exists.d.ts.map +1 -1
  96. package/out/zql/src/ivm/exists.js +3 -0
  97. package/out/zql/src/ivm/exists.js.map +1 -1
  98. package/out/zql/src/ivm/fan-in.d.ts +5 -1
  99. package/out/zql/src/ivm/fan-in.d.ts.map +1 -1
  100. package/out/zql/src/ivm/fan-in.js +7 -0
  101. package/out/zql/src/ivm/fan-in.js.map +1 -1
  102. package/out/zql/src/ivm/fan-out.d.ts +3 -0
  103. package/out/zql/src/ivm/fan-out.d.ts.map +1 -1
  104. package/out/zql/src/ivm/fan-out.js +4 -0
  105. package/out/zql/src/ivm/fan-out.js.map +1 -1
  106. package/out/zql/src/ivm/filter-operators.d.ts +2 -0
  107. package/out/zql/src/ivm/filter-operators.d.ts.map +1 -1
  108. package/out/zql/src/ivm/filter-operators.js +9 -0
  109. package/out/zql/src/ivm/filter-operators.js.map +1 -1
  110. package/out/zql/src/ivm/filter.d.ts +4 -1
  111. package/out/zql/src/ivm/filter.d.ts.map +1 -1
  112. package/out/zql/src/ivm/filter.js +3 -0
  113. package/out/zql/src/ivm/filter.js.map +1 -1
  114. package/out/zql/src/ivm/flipped-join.d.ts +3 -2
  115. package/out/zql/src/ivm/flipped-join.d.ts.map +1 -1
  116. package/out/zql/src/ivm/flipped-join.js +70 -16
  117. package/out/zql/src/ivm/flipped-join.js.map +1 -1
  118. package/out/zql/src/ivm/join-utils.d.ts +24 -7
  119. package/out/zql/src/ivm/join-utils.d.ts.map +1 -1
  120. package/out/zql/src/ivm/join-utils.js +87 -4
  121. package/out/zql/src/ivm/join-utils.js.map +1 -1
  122. package/out/zql/src/ivm/join.d.ts +3 -3
  123. package/out/zql/src/ivm/join.d.ts.map +1 -1
  124. package/out/zql/src/ivm/join.js +33 -58
  125. package/out/zql/src/ivm/join.js.map +1 -1
  126. package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
  127. package/out/zql/src/ivm/memory-source.js +33 -23
  128. package/out/zql/src/ivm/memory-source.js.map +1 -1
  129. package/out/zql/src/ivm/operator.d.ts +39 -10
  130. package/out/zql/src/ivm/operator.d.ts.map +1 -1
  131. package/out/zql/src/ivm/operator.js +9 -4
  132. package/out/zql/src/ivm/operator.js.map +1 -1
  133. package/out/zql/src/ivm/push-accumulated.d.ts +6 -1
  134. package/out/zql/src/ivm/push-accumulated.d.ts.map +1 -1
  135. package/out/zql/src/ivm/push-accumulated.js +47 -2
  136. package/out/zql/src/ivm/push-accumulated.js.map +1 -1
  137. package/out/zql/src/ivm/skip.d.ts +2 -1
  138. package/out/zql/src/ivm/skip.d.ts.map +1 -1
  139. package/out/zql/src/ivm/skip.js +3 -0
  140. package/out/zql/src/ivm/skip.js.map +1 -1
  141. package/out/zql/src/ivm/take-gate.d.ts +1 -0
  142. package/out/zql/src/ivm/take-gate.d.ts.map +1 -1
  143. package/out/zql/src/ivm/take-gate.js +3 -0
  144. package/out/zql/src/ivm/take-gate.js.map +1 -1
  145. package/out/zql/src/ivm/take.d.ts +2 -1
  146. package/out/zql/src/ivm/take.d.ts.map +1 -1
  147. package/out/zql/src/ivm/take.js +59 -40
  148. package/out/zql/src/ivm/take.js.map +1 -1
  149. package/out/zql/src/ivm/union-fan-in.d.ts +3 -0
  150. package/out/zql/src/ivm/union-fan-in.d.ts.map +1 -1
  151. package/out/zql/src/ivm/union-fan-in.js +56 -5
  152. package/out/zql/src/ivm/union-fan-in.js.map +1 -1
  153. package/out/zql/src/ivm/union-fan-out.d.ts +2 -1
  154. package/out/zql/src/ivm/union-fan-out.d.ts.map +1 -1
  155. package/out/zql/src/ivm/union-fan-out.js +5 -0
  156. package/out/zql/src/ivm/union-fan-out.js.map +1 -1
  157. package/out/zql/src/query/measure-push-operator.d.ts +2 -1
  158. package/out/zql/src/query/measure-push-operator.d.ts.map +1 -1
  159. package/out/zql/src/query/measure-push-operator.js +7 -0
  160. package/out/zql/src/query/measure-push-operator.js.map +1 -1
  161. 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":"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"}
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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"}
@@ -1,6 +1,6 @@
1
1
  var package_default = {
2
2
  name: "@rocicorp/zero",
3
- version: "1.11.0-canary.11",
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" },
@@ -1 +1 @@
1
- {"version":3,"file":"package.js","names":[],"sources":["../../package.json"],"sourcesContent":["{\n \"name\": \"@rocicorp/zero\",\n \"version\": \"1.11.0-canary.11\",\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
+ {"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":"AAuCA,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,CAqHf"}
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
- if (fileMode === "serving" && !runningLocalChangeStreamer && (config.litestream.executable || config.litestream.executableV5)) await restoreReplica(lc, config);
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 shard = getShardConfig(config);
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 snapshotStatus = await reserveAndGetSnapshotStatus(lc, config);
116
- ({backupURL} = snapshotStatus);
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(snapshotStatus.replicaSize);
122
- const attempt = await tryRestore(lc, litestream, config.replica.file, snapshotStatus, "view_syncer");
123
- progress.stop();
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 SizedDownstream, type SubscriberContext } from './change-streamer.ts';
9
- import { type SnapshotMessage } from './snapshot.ts';
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 declare class ChangeStreamerHttpClient implements ChangeStreamer {
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<Source<SnapshotMessage>>;
26
- subscribe(ctx: SubscriberContext): Promise<Source<SizedDownstream>>;
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;AAIhC,OAAO,EAAC,WAAW,EAAE,KAAK,OAAO,IAAI,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAE5E,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACvB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAwB,KAAK,eAAe,EAAC,MAAM,eAAe,CAAC;AAW1E,KAAK,OAAO,GAAG,WAAW,GAAG;IAC3B,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,IAAI,CAAC,oBAAoB,EAAE,eAAe,CAAC,GAAG,SAAS,CAAC;IACjE,gBAAgB,CAAC,EAAE,CAAC,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC;CACxD,CAAC;AAEF,qBAAa,wBAAyB,SAAQ,WAAW;;IACvD,QAAQ,CAAC,EAAE,iCAAiC;IAK5C,YACE,EAAE,EAAE,UAAU,EACd,IAAI,EAAE,OAAO,EACb,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,qBAAqB,EAmCtC;IA6ED,UAAmB,QAAQ,IAAI,IAAI,CA2BlC;IAED,UAAyB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAIhD;CACF;AAED,qBAAa,wBAAyB,YAAW,cAAc;;IAM7D,YACE,EAAE,EAAE,UAAU,EACd,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,MAAM,EAChB,iBAAiB,EAAE,MAAM,GAAG,SAAS,EAWtC;IAmBK,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,CAOtE;IAEK,SAAS,CAAC,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,CAOxE;CACF;AAED,KAAK,cAAc,GAAG,IAAI,CAAC,eAAe,EAAE,KAAK,GAAG,SAAS,CAAC,CAAC;AAE/D,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,cAAc,GAAG,iBAAiB,CAmB3E"}
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 { streamIn, streamInWithSize, streamOut, streamOutStringified } from "../../types/streams.js";
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 { downstreamSchema } from "./change-streamer.js";
13
- import { snapshotMessageSchema } from "./snapshot.js";
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 PATH_REGEX = /\/replication\/v(?<version>\d+)\/(changes|snapshot)$/;
21
- var SNAPSHOT_PATH = `/replication/v6/snapshot`;
22
- var CHANGES_PATH = `/replication/v6/changes`;
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.#subscribe);
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.#subscribe(ws, msg);
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
- streamOut(this._lc, downstream, ws);
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
- #subscribe = async (ws, req) => {
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
- streamOutStringified(this._lc, downstream, ws, { batched: ctx.wsBatched });
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 ws = new WebSocket$1(await this.#resolveChangeStreamer(SNAPSHOT_PATH) + `?${new URLSearchParams({ taskID }).toString()}`);
119
- return streamIn(this.#lc, ws, snapshotMessageSchema);
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 ws = new WebSocket$1(await this.#resolveChangeStreamer(CHANGES_PATH) + `?${getParams({
123
- wsBatched: true,
124
- ...ctx
125
- }).toString()}`);
126
- return streamInWithSize(this.#lc, ws, downstreamSchema);
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 > 6 || v < MIN_SUPPORTED_PROTOCOL_VERSION) throw new Error(`Cannot service client at protocol v${v}. Supported protocols: [v${MIN_SUPPORTED_PROTOCOL_VERSION} ... v6]`);
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 getParams(ctx) {
153
- const { protocolVersion, wsBatched, ...stringParams } = ctx;
154
- assert(protocolVersion === 6, `replicator should be setting protocolVersion to 6`);
155
- const params = new URLSearchParams({
156
- ...stringParams,
157
- initial: ctx.initial ? "true" : "false",
158
- logsChangeStream: ctx.logsChangeStream ? "true" : "false"
159
- });
160
- if (wsBatched) params.set("wsBatched", "true");
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 };