@tungthedev/streams-server 0.2.0

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 (183) hide show
  1. package/CODE_OF_CONDUCT.md +45 -0
  2. package/CONTRIBUTING.md +76 -0
  3. package/LICENSE +201 -0
  4. package/README.md +58 -0
  5. package/SECURITY.md +42 -0
  6. package/bin/prisma-streams-server +2 -0
  7. package/package.json +46 -0
  8. package/src/app.ts +583 -0
  9. package/src/app_core.ts +3144 -0
  10. package/src/app_local.ts +206 -0
  11. package/src/auth.ts +124 -0
  12. package/src/auto_tune.ts +69 -0
  13. package/src/backpressure.ts +66 -0
  14. package/src/bootstrap.ts +613 -0
  15. package/src/compute/demo_entry.ts +415 -0
  16. package/src/compute/demo_site.ts +1242 -0
  17. package/src/compute/entry.ts +19 -0
  18. package/src/compute/package_entry.ts +4 -0
  19. package/src/compute/virtual-modules.d.ts +15 -0
  20. package/src/compute/worker_module_url.ts +9 -0
  21. package/src/concurrency_gate.ts +108 -0
  22. package/src/config.ts +402 -0
  23. package/src/db/bootstrap_store.ts +9 -0
  24. package/src/db/db.ts +2424 -0
  25. package/src/db/schema.ts +925 -0
  26. package/src/db/sqlite_manifest_snapshot.ts +81 -0
  27. package/src/db/sqlite_touch_store.ts +491 -0
  28. package/src/db/sqlite_wal_store.ts +472 -0
  29. package/src/details/full_mode_details.ts +568 -0
  30. package/src/expiry_sweeper.ts +47 -0
  31. package/src/foreground_activity.ts +55 -0
  32. package/src/hist.ts +169 -0
  33. package/src/index/binary_fuse.ts +379 -0
  34. package/src/index/indexer.ts +947 -0
  35. package/src/index/lexicon_file_cache.ts +261 -0
  36. package/src/index/lexicon_format.ts +93 -0
  37. package/src/index/lexicon_indexer.ts +863 -0
  38. package/src/index/run_cache.ts +84 -0
  39. package/src/index/run_format.ts +213 -0
  40. package/src/index/schedule.ts +28 -0
  41. package/src/index/secondary_indexer.ts +901 -0
  42. package/src/index/secondary_schema.ts +105 -0
  43. package/src/ingest.ts +309 -0
  44. package/src/lens/lens.ts +501 -0
  45. package/src/manifest.ts +249 -0
  46. package/src/memory.ts +334 -0
  47. package/src/metrics.ts +147 -0
  48. package/src/metrics_emitter.ts +83 -0
  49. package/src/notifier.ts +180 -0
  50. package/src/objectstore/accounting.ts +151 -0
  51. package/src/objectstore/interface.ts +13 -0
  52. package/src/objectstore/mock_r2.ts +269 -0
  53. package/src/objectstore/null.ts +32 -0
  54. package/src/objectstore/r2.ts +318 -0
  55. package/src/observe/pairing.ts +61 -0
  56. package/src/observe/request.ts +772 -0
  57. package/src/offset.ts +70 -0
  58. package/src/postgres/bootstrap.ts +269 -0
  59. package/src/postgres/companions.ts +197 -0
  60. package/src/postgres/control_restore.ts +109 -0
  61. package/src/postgres/details.ts +189 -0
  62. package/src/postgres/lexicon_index.ts +260 -0
  63. package/src/postgres/routing_index.ts +189 -0
  64. package/src/postgres/rows.ts +132 -0
  65. package/src/postgres/schema.ts +355 -0
  66. package/src/postgres/secondary_index.ts +238 -0
  67. package/src/postgres/segments.ts +900 -0
  68. package/src/postgres/stats.ts +103 -0
  69. package/src/postgres/store.ts +947 -0
  70. package/src/postgres/touch.ts +591 -0
  71. package/src/postgres/types.ts +32 -0
  72. package/src/profiles/evlog/schema.ts +234 -0
  73. package/src/profiles/evlog.ts +473 -0
  74. package/src/profiles/generic.ts +51 -0
  75. package/src/profiles/index.ts +237 -0
  76. package/src/profiles/metrics/block_format.ts +109 -0
  77. package/src/profiles/metrics/normalize.ts +366 -0
  78. package/src/profiles/metrics/schema.ts +319 -0
  79. package/src/profiles/metrics.ts +83 -0
  80. package/src/profiles/otelTraces/normalize.ts +955 -0
  81. package/src/profiles/otelTraces/otlp.ts +1002 -0
  82. package/src/profiles/otelTraces/schema.ts +408 -0
  83. package/src/profiles/otelTraces.ts +390 -0
  84. package/src/profiles/profile.ts +284 -0
  85. package/src/profiles/stateProtocol/change_event_conformance.typecheck.ts +35 -0
  86. package/src/profiles/stateProtocol/changes.ts +24 -0
  87. package/src/profiles/stateProtocol/ingest.ts +115 -0
  88. package/src/profiles/stateProtocol/routes.ts +511 -0
  89. package/src/profiles/stateProtocol/types.ts +6 -0
  90. package/src/profiles/stateProtocol/validation.ts +51 -0
  91. package/src/profiles/stateProtocol.ts +107 -0
  92. package/src/read_filter.ts +468 -0
  93. package/src/reader.ts +2986 -0
  94. package/src/runtime/hash.ts +156 -0
  95. package/src/runtime/hash_vendor/LICENSE.hash-wasm +38 -0
  96. package/src/runtime/hash_vendor/NOTICE.md +8 -0
  97. package/src/runtime/hash_vendor/xxhash3.umd.min.cjs +7 -0
  98. package/src/runtime/hash_vendor/xxhash32.umd.min.cjs +7 -0
  99. package/src/runtime/hash_vendor/xxhash64.umd.min.cjs +7 -0
  100. package/src/runtime/host_runtime.ts +5 -0
  101. package/src/runtime_memory.ts +200 -0
  102. package/src/runtime_memory_sampler.ts +237 -0
  103. package/src/schema/lens_schema.ts +290 -0
  104. package/src/schema/proof.ts +547 -0
  105. package/src/schema/read_json.ts +51 -0
  106. package/src/schema/registry.ts +966 -0
  107. package/src/search/agg_format.ts +638 -0
  108. package/src/search/aggregate.ts +409 -0
  109. package/src/search/binary/codec.ts +162 -0
  110. package/src/search/binary/docset.ts +67 -0
  111. package/src/search/binary/restart_strings.ts +181 -0
  112. package/src/search/binary/varint.ts +34 -0
  113. package/src/search/bitset.ts +19 -0
  114. package/src/search/col_format.ts +382 -0
  115. package/src/search/col_runtime.ts +59 -0
  116. package/src/search/column_encoding.ts +43 -0
  117. package/src/search/companion_file_cache.ts +319 -0
  118. package/src/search/companion_format.ts +327 -0
  119. package/src/search/companion_manager.ts +1305 -0
  120. package/src/search/companion_plan.ts +229 -0
  121. package/src/search/exact_format.ts +281 -0
  122. package/src/search/exact_runtime.ts +55 -0
  123. package/src/search/fts_format.ts +423 -0
  124. package/src/search/fts_runtime.ts +333 -0
  125. package/src/search/query.ts +875 -0
  126. package/src/search/schema.ts +245 -0
  127. package/src/segment/cache.ts +270 -0
  128. package/src/segment/cached_segment.ts +89 -0
  129. package/src/segment/format.ts +403 -0
  130. package/src/segment/segmenter.ts +412 -0
  131. package/src/segment/segmenter_worker.ts +72 -0
  132. package/src/segment/segmenter_workers.ts +130 -0
  133. package/src/server.ts +264 -0
  134. package/src/server_auto_tune.ts +158 -0
  135. package/src/sqlite/adapter.ts +335 -0
  136. package/src/sqlite/runtime_stats.ts +163 -0
  137. package/src/stats.ts +205 -0
  138. package/src/store/append.ts +50 -0
  139. package/src/store/bootstrap_restore_store.ts +71 -0
  140. package/src/store/capabilities.ts +86 -0
  141. package/src/store/full_mode_details_store.ts +71 -0
  142. package/src/store/index_store.ts +104 -0
  143. package/src/store/profile_touch_store.ts +1 -0
  144. package/src/store/rows.ts +144 -0
  145. package/src/store/schema_profile_store.ts +73 -0
  146. package/src/store/schema_publication.ts +6 -0
  147. package/src/store/segment_manifest_store.ts +129 -0
  148. package/src/store/segment_read_store.ts +22 -0
  149. package/src/store/stats_accounting_store.ts +83 -0
  150. package/src/store/touch_store.ts +98 -0
  151. package/src/store/wal_store.ts +21 -0
  152. package/src/stream_size_reconciler.ts +100 -0
  153. package/src/touch/canonical_change.ts +7 -0
  154. package/src/touch/live_keys.ts +158 -0
  155. package/src/touch/live_metrics.ts +841 -0
  156. package/src/touch/live_templates.ts +449 -0
  157. package/src/touch/manager.ts +1292 -0
  158. package/src/touch/process_batch.ts +576 -0
  159. package/src/touch/processor_worker.ts +85 -0
  160. package/src/touch/spec.ts +459 -0
  161. package/src/touch/touch_journal.ts +771 -0
  162. package/src/touch/touch_key_id.ts +20 -0
  163. package/src/touch/worker_pool.ts +191 -0
  164. package/src/touch/worker_protocol.ts +57 -0
  165. package/src/types/proper-lockfile.d.ts +1 -0
  166. package/src/uploader.ts +358 -0
  167. package/src/util/base32_crockford.ts +81 -0
  168. package/src/util/bloom256.ts +67 -0
  169. package/src/util/byte_lru.ts +73 -0
  170. package/src/util/cleanup.ts +22 -0
  171. package/src/util/crc32c.ts +29 -0
  172. package/src/util/ds_error.ts +15 -0
  173. package/src/util/duration.ts +17 -0
  174. package/src/util/endian.ts +53 -0
  175. package/src/util/json_pointer.ts +148 -0
  176. package/src/util/log.ts +25 -0
  177. package/src/util/lru.ts +53 -0
  178. package/src/util/retry.ts +35 -0
  179. package/src/util/siphash.ts +71 -0
  180. package/src/util/stream_paths.ts +50 -0
  181. package/src/util/time.ts +14 -0
  182. package/src/util/yield.ts +3 -0
  183. package/src/util/zstd.ts +24 -0
@@ -0,0 +1,472 @@
1
+ import { Result } from "better-result";
2
+ import type { SqliteDatabase, SqliteStatement } from "../sqlite/adapter";
3
+ import {
4
+ type StoreAppendBatch,
5
+ type StoreAppendError,
6
+ type StoreAppendResult,
7
+ type StoreAppendTask,
8
+ } from "../store/append";
9
+ import type { WalReadRow, WalStore } from "../store/wal_store";
10
+
11
+ type StreamState = {
12
+ nextOffset: bigint;
13
+ lastAppendMs: bigint;
14
+ expiresAtMs: bigint | null;
15
+ contentType: string;
16
+ streamSeq: string | null;
17
+ closed: boolean;
18
+ closedProducerId: string | null;
19
+ closedProducerEpoch: number | null;
20
+ closedProducerSeq: number | null;
21
+ };
22
+
23
+ type ProducerState = { epoch: number; lastSeq: number };
24
+
25
+ export class SqliteWalStore implements WalStore {
26
+ private readonly stmts: {
27
+ getStream: SqliteStatement;
28
+ insertWal: SqliteStatement;
29
+ updateStreamAppend: SqliteStatement;
30
+ updateStreamCloseOnly: SqliteStatement;
31
+ getProducerState: SqliteStatement;
32
+ upsertProducerState: SqliteStatement;
33
+ };
34
+
35
+ constructor(
36
+ private readonly db: SqliteDatabase,
37
+ private readonly nowMs: () => bigint,
38
+ private readonly deletedFlag: number
39
+ ) {
40
+ this.stmts = {
41
+ getStream: this.db.query(
42
+ `SELECT stream, epoch, next_offset, last_append_ms, expires_at_ms, stream_flags,
43
+ content_type, stream_seq, closed, closed_producer_id, closed_producer_epoch, closed_producer_seq
44
+ FROM streams WHERE stream=? LIMIT 1;`
45
+ ),
46
+ insertWal: this.db.query(
47
+ `INSERT INTO wal(stream, offset, ts_ms, payload, payload_len, routing_key, content_type, flags)
48
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?);`
49
+ ),
50
+ updateStreamAppend: this.db.query(
51
+ `UPDATE streams
52
+ SET next_offset=?, updated_at_ms=?, last_append_ms=?,
53
+ pending_rows=pending_rows+?, pending_bytes=pending_bytes+?,
54
+ logical_size_bytes=logical_size_bytes+?,
55
+ wal_rows=wal_rows+?, wal_bytes=wal_bytes+?,
56
+ stream_seq=?,
57
+ closed=CASE WHEN ? THEN 1 ELSE closed END,
58
+ closed_producer_id=CASE WHEN ? THEN ? ELSE closed_producer_id END,
59
+ closed_producer_epoch=CASE WHEN ? THEN ? ELSE closed_producer_epoch END,
60
+ closed_producer_seq=CASE WHEN ? THEN ? ELSE closed_producer_seq END
61
+ WHERE stream=? AND (stream_flags & ?) = 0;`
62
+ ),
63
+ updateStreamCloseOnly: this.db.query(
64
+ `UPDATE streams
65
+ SET closed=1,
66
+ closed_producer_id=?,
67
+ closed_producer_epoch=?,
68
+ closed_producer_seq=?,
69
+ updated_at_ms=?,
70
+ stream_seq=?
71
+ WHERE stream=? AND (stream_flags & ?) = 0;`
72
+ ),
73
+ getProducerState: this.db.query(
74
+ `SELECT epoch, last_seq FROM producer_state WHERE stream=? AND producer_id=? LIMIT 1;`
75
+ ),
76
+ upsertProducerState: this.db.query(
77
+ `INSERT INTO producer_state(stream, producer_id, epoch, last_seq, updated_at_ms)
78
+ VALUES(?, ?, ?, ?, ?)
79
+ ON CONFLICT(stream, producer_id) DO UPDATE SET
80
+ epoch=excluded.epoch,
81
+ last_seq=excluded.last_seq,
82
+ updated_at_ms=excluded.updated_at_ms;`
83
+ ),
84
+ };
85
+ }
86
+
87
+ async appendBatch(batch: StoreAppendTask[]): Promise<StoreAppendBatch> {
88
+ if (batch.length === 0) return Result.ok({ results: [], walBytesCommitted: 0 });
89
+
90
+ const nowMs = this.nowMs();
91
+ let walBytesCommitted = 0;
92
+ const results: StoreAppendResult[] = new Array(batch.length);
93
+
94
+ const tx = this.db.transaction(() => {
95
+ const perStream = new Map<string, StreamState>();
96
+ const perProducer = new Map<string, ProducerState | null>();
97
+
98
+ const loadStream = (stream: string): StreamState | null => {
99
+ const cached = perStream.get(stream);
100
+ if (cached) return cached;
101
+ const row = this.stmts.getStream.get(stream) as any;
102
+ if (!row || (Number(row.stream_flags) & this.deletedFlag) !== 0) return null;
103
+ const st: StreamState = {
104
+ nextOffset: BigInt(row.next_offset),
105
+ lastAppendMs: BigInt(row.last_append_ms),
106
+ expiresAtMs: row.expires_at_ms == null ? null : BigInt(row.expires_at_ms),
107
+ contentType: String(row.content_type),
108
+ streamSeq: row.stream_seq == null ? null : String(row.stream_seq),
109
+ closed: Number(row.closed) !== 0,
110
+ closedProducerId: row.closed_producer_id == null ? null : String(row.closed_producer_id),
111
+ closedProducerEpoch: row.closed_producer_epoch == null ? null : Number(row.closed_producer_epoch),
112
+ closedProducerSeq: row.closed_producer_seq == null ? null : Number(row.closed_producer_seq),
113
+ };
114
+ perStream.set(stream, st);
115
+ return st;
116
+ };
117
+
118
+ const loadProducerState = (stream: string, producerId: string): ProducerState | null => {
119
+ const key = `${stream}\u0000${producerId}`;
120
+ if (perProducer.has(key)) return perProducer.get(key)!;
121
+ const row = this.stmts.getProducerState.get(stream, producerId) as any;
122
+ const state = row ? { epoch: Number(row.epoch), lastSeq: Number(row.last_seq) } : null;
123
+ perProducer.set(key, state);
124
+ return state;
125
+ };
126
+
127
+ const checkProducer = (
128
+ task: StoreAppendTask
129
+ ): Result<{ duplicate: boolean; update: boolean; epoch: number; seq: number }, StoreAppendError> => {
130
+ const producer = task.producer!;
131
+ const key = `${task.stream}\u0000${producer.id}`;
132
+ const state = loadProducerState(task.stream, producer.id);
133
+ if (!state) {
134
+ if (producer.seq !== 0) return Result.err({ kind: "producer_epoch_seq" });
135
+ perProducer.set(key, { epoch: producer.epoch, lastSeq: producer.seq });
136
+ return Result.ok({ duplicate: false, update: true, epoch: producer.epoch, seq: producer.seq });
137
+ }
138
+ if (producer.epoch < state.epoch) {
139
+ return Result.err({ kind: "producer_stale_epoch", producerEpoch: state.epoch });
140
+ }
141
+ if (producer.epoch > state.epoch) {
142
+ if (producer.seq !== 0) return Result.err({ kind: "producer_epoch_seq" });
143
+ perProducer.set(key, { epoch: producer.epoch, lastSeq: producer.seq });
144
+ return Result.ok({ duplicate: false, update: true, epoch: producer.epoch, seq: producer.seq });
145
+ }
146
+ if (producer.seq <= state.lastSeq) {
147
+ return Result.ok({ duplicate: true, update: false, epoch: state.epoch, seq: state.lastSeq });
148
+ }
149
+ if (producer.seq === state.lastSeq + 1) {
150
+ perProducer.set(key, { epoch: state.epoch, lastSeq: producer.seq });
151
+ return Result.ok({ duplicate: false, update: true, epoch: state.epoch, seq: producer.seq });
152
+ }
153
+ return Result.err({ kind: "producer_gap", expected: state.lastSeq + 1, received: producer.seq });
154
+ };
155
+
156
+ const checkStreamSeq = (
157
+ task: StoreAppendTask,
158
+ st: StreamState
159
+ ): Result<{ nextSeq: string | null }, StoreAppendError> => {
160
+ if (task.streamSeq == null) return Result.ok({ nextSeq: st.streamSeq });
161
+ if (st.streamSeq != null && task.streamSeq <= st.streamSeq) {
162
+ return Result.err({ kind: "stream_seq", expected: st.streamSeq, received: task.streamSeq });
163
+ }
164
+ return Result.ok({ nextSeq: task.streamSeq });
165
+ };
166
+
167
+ for (let idx = 0; idx < batch.length; idx++) {
168
+ const task = batch[idx]!;
169
+ const st = loadStream(task.stream);
170
+ if (!st) {
171
+ results[idx] = Result.err({ kind: "not_found" });
172
+ continue;
173
+ }
174
+ if (st.expiresAtMs != null && nowMs > st.expiresAtMs) {
175
+ results[idx] = Result.err({ kind: "gone" });
176
+ continue;
177
+ }
178
+
179
+ const tailOffset = st.nextOffset - 1n;
180
+ const isCloseOnly = task.close && task.rows.length === 0;
181
+
182
+ if (st.closed) {
183
+ if (isCloseOnly) {
184
+ results[idx] = Result.ok({ lastOffset: tailOffset, appendedRows: 0, closed: true, duplicate: true });
185
+ continue;
186
+ }
187
+ if (
188
+ task.producer &&
189
+ task.close &&
190
+ st.closedProducerId != null &&
191
+ st.closedProducerEpoch != null &&
192
+ st.closedProducerSeq != null &&
193
+ st.closedProducerId === task.producer.id &&
194
+ st.closedProducerEpoch === task.producer.epoch &&
195
+ st.closedProducerSeq === task.producer.seq
196
+ ) {
197
+ results[idx] = Result.ok({
198
+ lastOffset: tailOffset,
199
+ appendedRows: 0,
200
+ closed: true,
201
+ duplicate: true,
202
+ producer: { epoch: st.closedProducerEpoch, seq: st.closedProducerSeq },
203
+ });
204
+ continue;
205
+ }
206
+ results[idx] = Result.err({ kind: "closed", lastOffset: tailOffset });
207
+ continue;
208
+ }
209
+
210
+ if (isCloseOnly) {
211
+ const closeRes = this.applyCloseOnly(task, st, checkProducer, checkStreamSeq, nowMs, tailOffset);
212
+ results[idx] = closeRes.result;
213
+ continue;
214
+ }
215
+
216
+ if (!task.contentType || task.contentType !== st.contentType) {
217
+ results[idx] = Result.err({ kind: "content_type_mismatch" });
218
+ continue;
219
+ }
220
+
221
+ const appendRes = this.applyAppend(task, st, checkProducer, checkStreamSeq, nowMs, tailOffset);
222
+ results[idx] = appendRes.result;
223
+ walBytesCommitted += appendRes.walBytesCommitted;
224
+ }
225
+ });
226
+
227
+ try {
228
+ tx();
229
+ } catch (e) {
230
+ if (isSqliteBusy(e)) {
231
+ return Result.err({ kind: "retryable" });
232
+ }
233
+ throw e;
234
+ }
235
+ return Result.ok({ results, walBytesCommitted });
236
+ }
237
+
238
+ async *readWalRange(stream: string, startOffset: bigint, endOffset: bigint, routingKey?: Uint8Array): AsyncIterable<WalReadRow> {
239
+ yield* this.iterWalRange(stream, startOffset, endOffset, routingKey);
240
+ }
241
+
242
+ async *readWalRangeDesc(stream: string, startOffset: bigint, endOffset: bigint, routingKey?: Uint8Array): AsyncIterable<WalReadRow> {
243
+ yield* this.iterWalRangeDesc(stream, startOffset, endOffset, routingKey);
244
+ }
245
+
246
+ *iterWalRange(stream: string, startOffset: bigint, endOffset: bigint, routingKey?: Uint8Array): Generator<WalReadRow, void, void> {
247
+ yield* this.readWalRows(stream, startOffset, endOffset, "asc", routingKey);
248
+ }
249
+
250
+ *iterWalRangeDesc(stream: string, startOffset: bigint, endOffset: bigint, routingKey?: Uint8Array): Generator<WalReadRow, void, void> {
251
+ yield* this.readWalRows(stream, startOffset, endOffset, "desc", routingKey);
252
+ }
253
+
254
+ async getWalOldestTimestampMsForRead(stream: string): Promise<bigint | null> {
255
+ const row = this.db.query(`SELECT MIN(ts_ms) as min_ts FROM wal WHERE stream=?;`).get(stream) as any;
256
+ if (!row || row.min_ts == null) return null;
257
+ return toBigInt(row.min_ts);
258
+ }
259
+
260
+ private *readWalRows(
261
+ stream: string,
262
+ startOffset: bigint,
263
+ endOffset: bigint,
264
+ order: "asc" | "desc",
265
+ routingKey?: Uint8Array
266
+ ): Generator<WalReadRow, void, void> {
267
+ const start = bindSqliteInt(startOffset);
268
+ const end = bindSqliteInt(endOffset);
269
+ const sqlOrder = order === "asc" ? "ASC" : "DESC";
270
+ const stmt = routingKey
271
+ ? this.db.prepare(
272
+ `SELECT offset, ts_ms, routing_key, content_type, payload
273
+ FROM wal
274
+ WHERE stream = ? AND offset >= ? AND offset <= ? AND routing_key = ?
275
+ ORDER BY offset ${sqlOrder};`
276
+ )
277
+ : this.db.prepare(
278
+ `SELECT offset, ts_ms, routing_key, content_type, payload
279
+ FROM wal
280
+ WHERE stream = ? AND offset >= ? AND offset <= ?
281
+ ORDER BY offset ${sqlOrder};`
282
+ );
283
+ try {
284
+ const it = routingKey ? (stmt.iterate(stream, start, end, routingKey) as any) : (stmt.iterate(stream, start, end) as any);
285
+ for (const row of it) yield coerceWalReadRow(row);
286
+ } finally {
287
+ try {
288
+ stmt.finalize?.();
289
+ } catch {
290
+ // ignore
291
+ }
292
+ }
293
+ }
294
+
295
+ private applyCloseOnly(
296
+ task: StoreAppendTask,
297
+ st: StreamState,
298
+ checkProducer: (task: StoreAppendTask) => Result<{ duplicate: boolean; update: boolean; epoch: number; seq: number }, StoreAppendError>,
299
+ checkStreamSeq: (task: StoreAppendTask, st: StreamState) => Result<{ nextSeq: string | null }, StoreAppendError>,
300
+ nowMs: bigint,
301
+ tailOffset: bigint
302
+ ): { result: StoreAppendResult } {
303
+ let producerInfo: { epoch: number; seq: number } | undefined;
304
+ let duplicate = false;
305
+ if (task.producer) {
306
+ const prodCheck = checkProducer(task);
307
+ if (Result.isError(prodCheck)) return { result: Result.err(prodCheck.error) };
308
+ duplicate = prodCheck.value.duplicate;
309
+ producerInfo = { epoch: prodCheck.value.epoch, seq: prodCheck.value.seq };
310
+ if (prodCheck.value.update) {
311
+ this.stmts.upsertProducerState.run(task.stream, task.producer.id, prodCheck.value.epoch, prodCheck.value.seq, nowMs);
312
+ }
313
+ }
314
+ if (!duplicate) {
315
+ const seqCheck = checkStreamSeq(task, st);
316
+ if (Result.isError(seqCheck)) return { result: Result.err(seqCheck.error) };
317
+ st.streamSeq = seqCheck.value.nextSeq;
318
+ const closedProducer = task.producer ?? null;
319
+ this.stmts.updateStreamCloseOnly.run(
320
+ closedProducer ? closedProducer.id : null,
321
+ closedProducer ? closedProducer.epoch : null,
322
+ closedProducer ? closedProducer.seq : null,
323
+ nowMs,
324
+ st.streamSeq,
325
+ task.stream,
326
+ this.deletedFlag
327
+ );
328
+ st.closed = true;
329
+ st.closedProducerId = closedProducer ? closedProducer.id : null;
330
+ st.closedProducerEpoch = closedProducer ? closedProducer.epoch : null;
331
+ st.closedProducerSeq = closedProducer ? closedProducer.seq : null;
332
+ }
333
+ return {
334
+ result: Result.ok({
335
+ lastOffset: tailOffset,
336
+ appendedRows: 0,
337
+ closed: st.closed,
338
+ duplicate,
339
+ producer: producerInfo,
340
+ }),
341
+ };
342
+ }
343
+
344
+ private applyAppend(
345
+ task: StoreAppendTask,
346
+ st: StreamState,
347
+ checkProducer: (task: StoreAppendTask) => Result<{ duplicate: boolean; update: boolean; epoch: number; seq: number }, StoreAppendError>,
348
+ checkStreamSeq: (task: StoreAppendTask, st: StreamState) => Result<{ nextSeq: string | null }, StoreAppendError>,
349
+ nowMs: bigint,
350
+ tailOffset: bigint
351
+ ): { result: StoreAppendResult; walBytesCommitted: number } {
352
+ let producerInfo: { epoch: number; seq: number } | undefined;
353
+ if (task.producer) {
354
+ const prodCheck = checkProducer(task);
355
+ if (Result.isError(prodCheck)) return { result: Result.err(prodCheck.error), walBytesCommitted: 0 };
356
+ if (prodCheck.value.duplicate) {
357
+ return {
358
+ result: Result.ok({
359
+ lastOffset: tailOffset,
360
+ appendedRows: 0,
361
+ closed: false,
362
+ duplicate: true,
363
+ producer: { epoch: prodCheck.value.epoch, seq: prodCheck.value.seq },
364
+ }),
365
+ walBytesCommitted: 0,
366
+ };
367
+ }
368
+ producerInfo = { epoch: prodCheck.value.epoch, seq: prodCheck.value.seq };
369
+ if (prodCheck.value.update) {
370
+ this.stmts.upsertProducerState.run(task.stream, task.producer.id, prodCheck.value.epoch, prodCheck.value.seq, nowMs);
371
+ }
372
+ }
373
+
374
+ const seqCheck = checkStreamSeq(task, st);
375
+ if (Result.isError(seqCheck)) return { result: Result.err(seqCheck.error), walBytesCommitted: 0 };
376
+ st.streamSeq = seqCheck.value.nextSeq;
377
+
378
+ let appendMs = task.baseAppendMs;
379
+ if (appendMs <= st.lastAppendMs) appendMs = st.lastAppendMs + 1n;
380
+
381
+ let offset = st.nextOffset;
382
+ let totalBytes = 0n;
383
+ for (const r of task.rows) {
384
+ const payloadLen = r.payload.byteLength;
385
+ totalBytes += BigInt(payloadLen);
386
+ this.stmts.insertWal.run(task.stream, offset, appendMs, r.payload, payloadLen, r.routingKey, r.contentType, 0);
387
+ offset += 1n;
388
+ }
389
+
390
+ const lastOffset = offset - 1n;
391
+ st.nextOffset = offset;
392
+ st.lastAppendMs = appendMs;
393
+ if (task.close) {
394
+ st.closed = true;
395
+ if (task.producer) {
396
+ st.closedProducerId = task.producer.id;
397
+ st.closedProducerEpoch = task.producer.epoch;
398
+ st.closedProducerSeq = task.producer.seq;
399
+ } else {
400
+ st.closedProducerId = null;
401
+ st.closedProducerEpoch = null;
402
+ st.closedProducerSeq = null;
403
+ }
404
+ }
405
+
406
+ const closedProducer = task.close && task.producer ? task.producer : null;
407
+ const closeFlag = task.close ? 1 : 0;
408
+ this.stmts.updateStreamAppend.run(
409
+ st.nextOffset,
410
+ nowMs,
411
+ st.lastAppendMs,
412
+ BigInt(task.rows.length),
413
+ totalBytes,
414
+ totalBytes,
415
+ BigInt(task.rows.length),
416
+ totalBytes,
417
+ st.streamSeq,
418
+ closeFlag,
419
+ closeFlag,
420
+ closedProducer ? closedProducer.id : null,
421
+ closeFlag,
422
+ closedProducer ? closedProducer.epoch : null,
423
+ closeFlag,
424
+ closedProducer ? closedProducer.seq : null,
425
+ task.stream,
426
+ this.deletedFlag
427
+ );
428
+
429
+ return {
430
+ result: Result.ok({
431
+ lastOffset,
432
+ appendedRows: task.rows.length,
433
+ closed: task.close,
434
+ duplicate: false,
435
+ producer: producerInfo,
436
+ }),
437
+ walBytesCommitted: Number(totalBytes),
438
+ };
439
+ }
440
+ }
441
+
442
+ function isSqliteBusy(e: unknown): boolean {
443
+ const err = e as { code?: unknown; errno?: unknown };
444
+ const code = String(err?.code ?? "");
445
+ const errno = Number(err?.errno ?? -1);
446
+ return code === "SQLITE_BUSY" || code === "SQLITE_BUSY_SNAPSHOT" || errno === 5 || errno === 517;
447
+ }
448
+
449
+ function bindSqliteInt(value: bigint): number | string {
450
+ const min = BigInt(Number.MIN_SAFE_INTEGER);
451
+ const max = BigInt(Number.MAX_SAFE_INTEGER);
452
+ return value >= min && value <= max ? Number(value) : value.toString();
453
+ }
454
+
455
+ function toBigInt(value: unknown): bigint {
456
+ return typeof value === "bigint" ? value : BigInt(value as any);
457
+ }
458
+
459
+ function toBytes(value: unknown): Uint8Array | null {
460
+ if (value == null) return null;
461
+ return value instanceof Uint8Array ? value : new Uint8Array(value as ArrayBuffer);
462
+ }
463
+
464
+ function coerceWalReadRow(row: any): WalReadRow {
465
+ return {
466
+ offset: toBigInt(row.offset),
467
+ tsMs: toBigInt(row.ts_ms),
468
+ routingKey: toBytes(row.routing_key),
469
+ contentType: row.content_type == null ? null : String(row.content_type),
470
+ payload: toBytes(row.payload) ?? new Uint8Array(),
471
+ };
472
+ }