@prisma/streams-server 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CONTRIBUTING.md +8 -0
  2. package/package.json +2 -1
  3. package/src/app.ts +290 -17
  4. package/src/app_core.ts +1833 -698
  5. package/src/app_local.ts +144 -4
  6. package/src/auto_tune.ts +62 -0
  7. package/src/bootstrap.ts +159 -1
  8. package/src/concurrency_gate.ts +108 -0
  9. package/src/config.ts +116 -14
  10. package/src/db/db.ts +1201 -131
  11. package/src/db/schema.ts +308 -8
  12. package/src/foreground_activity.ts +55 -0
  13. package/src/index/indexer.ts +254 -124
  14. package/src/index/lexicon_file_cache.ts +261 -0
  15. package/src/index/lexicon_format.ts +93 -0
  16. package/src/index/lexicon_indexer.ts +789 -0
  17. package/src/index/secondary_indexer.ts +824 -0
  18. package/src/index/secondary_schema.ts +105 -0
  19. package/src/ingest.ts +10 -12
  20. package/src/manifest.ts +143 -8
  21. package/src/memory.ts +183 -8
  22. package/src/metrics.ts +15 -29
  23. package/src/metrics_emitter.ts +26 -3
  24. package/src/notifier.ts +121 -5
  25. package/src/objectstore/accounting.ts +92 -0
  26. package/src/objectstore/mock_r2.ts +1 -1
  27. package/src/objectstore/r2.ts +17 -1
  28. package/src/profiles/evlog/schema.ts +234 -0
  29. package/src/profiles/evlog.ts +299 -0
  30. package/src/profiles/generic.ts +47 -0
  31. package/src/profiles/index.ts +205 -0
  32. package/src/profiles/metrics/block_format.ts +109 -0
  33. package/src/profiles/metrics/normalize.ts +366 -0
  34. package/src/profiles/metrics/schema.ts +319 -0
  35. package/src/profiles/metrics.ts +85 -0
  36. package/src/profiles/profile.ts +225 -0
  37. package/src/{touch/engine.ts → profiles/stateProtocol/changes.ts} +3 -20
  38. package/src/profiles/stateProtocol/routes.ts +389 -0
  39. package/src/profiles/stateProtocol/types.ts +6 -0
  40. package/src/profiles/stateProtocol/validation.ts +51 -0
  41. package/src/profiles/stateProtocol.ts +100 -0
  42. package/src/read_filter.ts +468 -0
  43. package/src/reader.ts +2151 -164
  44. package/src/runtime_memory.ts +200 -0
  45. package/src/runtime_memory_sampler.ts +235 -0
  46. package/src/schema/read_json.ts +43 -0
  47. package/src/schema/registry.ts +563 -59
  48. package/src/search/agg_format.ts +638 -0
  49. package/src/search/aggregate.ts +389 -0
  50. package/src/search/binary/codec.ts +162 -0
  51. package/src/search/binary/docset.ts +67 -0
  52. package/src/search/binary/restart_strings.ts +181 -0
  53. package/src/search/binary/varint.ts +34 -0
  54. package/src/search/bitset.ts +19 -0
  55. package/src/search/col_format.ts +382 -0
  56. package/src/search/col_runtime.ts +59 -0
  57. package/src/search/column_encoding.ts +43 -0
  58. package/src/search/companion_file_cache.ts +319 -0
  59. package/src/search/companion_format.ts +313 -0
  60. package/src/search/companion_manager.ts +1086 -0
  61. package/src/search/companion_plan.ts +218 -0
  62. package/src/search/fts_format.ts +423 -0
  63. package/src/search/fts_runtime.ts +333 -0
  64. package/src/search/query.ts +875 -0
  65. package/src/search/schema.ts +245 -0
  66. package/src/segment/cache.ts +93 -2
  67. package/src/segment/cached_segment.ts +89 -0
  68. package/src/segment/format.ts +108 -36
  69. package/src/segment/segmenter.ts +79 -5
  70. package/src/segment/segmenter_worker.ts +31 -5
  71. package/src/segment/segmenter_workers.ts +40 -12
  72. package/src/server.ts +150 -36
  73. package/src/sqlite/adapter.ts +155 -8
  74. package/src/sqlite/runtime_stats.ts +163 -0
  75. package/src/stats.ts +3 -3
  76. package/src/stream_size_reconciler.ts +100 -0
  77. package/src/touch/canonical_change.ts +7 -0
  78. package/src/touch/live_metrics.ts +94 -64
  79. package/src/touch/live_templates.ts +15 -1
  80. package/src/touch/manager.ts +166 -88
  81. package/src/touch/{interpreter_worker.ts → processor_worker.ts} +13 -13
  82. package/src/touch/spec.ts +95 -92
  83. package/src/touch/touch_journal.ts +4 -0
  84. package/src/touch/worker_pool.ts +6 -13
  85. package/src/touch/worker_protocol.ts +3 -3
  86. package/src/uploader.ts +77 -6
  87. package/src/util/bloom256.ts +2 -2
  88. package/src/util/byte_lru.ts +73 -0
  89. package/src/util/lru.ts +8 -0
  90. package/src/util/stream_paths.ts +19 -0
@@ -13,15 +13,49 @@ export interface SqliteStatement {
13
13
  export interface SqliteDatabase {
14
14
  exec(sql: string): void;
15
15
  query(sql: string): SqliteStatement;
16
+ prepare(sql: string): SqliteStatement;
16
17
  transaction<T>(fn: () => T): () => T;
17
18
  close(): void;
18
19
  }
19
20
 
21
+ type SqliteAdapterRuntimeCounts = {
22
+ open_connections: number;
23
+ prepared_statements: number;
24
+ };
25
+
26
+ const sqliteAdapterRuntimeCounts: SqliteAdapterRuntimeCounts = {
27
+ open_connections: 0,
28
+ prepared_statements: 0,
29
+ };
30
+
31
+ function incrementSqliteConnection(): void {
32
+ sqliteAdapterRuntimeCounts.open_connections += 1;
33
+ }
34
+
35
+ function decrementSqliteConnection(): void {
36
+ sqliteAdapterRuntimeCounts.open_connections = Math.max(0, sqliteAdapterRuntimeCounts.open_connections - 1);
37
+ }
38
+
39
+ function incrementPreparedStatement(): void {
40
+ sqliteAdapterRuntimeCounts.prepared_statements += 1;
41
+ }
42
+
43
+ function decrementPreparedStatement(): void {
44
+ sqliteAdapterRuntimeCounts.prepared_statements = Math.max(0, sqliteAdapterRuntimeCounts.prepared_statements - 1);
45
+ }
46
+
47
+ export function getSqliteAdapterRuntimeCounts(): SqliteAdapterRuntimeCounts {
48
+ return { ...sqliteAdapterRuntimeCounts };
49
+ }
50
+
20
51
  class BunStatementAdapter implements SqliteStatement {
21
52
  private readonly stmt: any;
53
+ private readonly onFinalize?: () => void;
54
+ private finalized = false;
22
55
 
23
- constructor(stmt: any) {
56
+ constructor(stmt: any, onFinalize?: () => void) {
24
57
  this.stmt = stmt;
58
+ this.onFinalize = onFinalize;
25
59
  }
26
60
 
27
61
  get(...params: any[]): any {
@@ -41,23 +75,58 @@ class BunStatementAdapter implements SqliteStatement {
41
75
  }
42
76
 
43
77
  finalize(): void {
44
- if (typeof this.stmt.finalize === "function") this.stmt.finalize();
78
+ if (this.finalized) return;
79
+ this.finalized = true;
80
+ try {
81
+ if (typeof this.stmt.finalize === "function") this.stmt.finalize();
82
+ } finally {
83
+ this.onFinalize?.();
84
+ }
45
85
  }
46
86
  }
47
87
 
48
88
  class BunDatabaseAdapter implements SqliteDatabase {
49
89
  private readonly db: any;
90
+ private preparedStatementCount = 0;
91
+ private closed = false;
92
+ private readonly statementCache = new Map<string, BunStatementAdapter>();
50
93
 
51
94
  constructor(db: any) {
52
95
  this.db = db;
96
+ incrementSqliteConnection();
97
+ }
98
+
99
+ private trackStatement(): () => void {
100
+ let released = false;
101
+ this.preparedStatementCount += 1;
102
+ incrementPreparedStatement();
103
+ return () => {
104
+ if (released) return;
105
+ released = true;
106
+ this.preparedStatementCount = Math.max(0, this.preparedStatementCount - 1);
107
+ decrementPreparedStatement();
108
+ };
53
109
  }
54
110
 
55
111
  exec(sql: string): void {
56
112
  this.db.exec(sql);
57
113
  }
58
114
 
115
+ prepare(sql: string): SqliteStatement {
116
+ return new BunStatementAdapter(this.db.query(sql), this.trackStatement());
117
+ }
118
+
59
119
  query(sql: string): SqliteStatement {
60
- return new BunStatementAdapter(this.db.query(sql));
120
+ const cached = this.statementCache.get(sql);
121
+ if (cached) return cached;
122
+ let adapter: BunStatementAdapter;
123
+ const release = this.trackStatement();
124
+ adapter = new BunStatementAdapter(this.db.query(sql), () => {
125
+ this.statementCache.delete(sql);
126
+ release();
127
+ });
128
+ this.statementCache.set(sql, adapter);
129
+ return adapter;
61
130
  }
62
131
 
63
132
  transaction<T>(fn: () => T): () => T {
@@ -65,15 +134,37 @@ class BunDatabaseAdapter implements SqliteDatabase {
65
134
  }
66
135
 
67
136
  close(): void {
68
- this.db.close();
137
+ if (this.closed) return;
138
+ this.closed = true;
139
+ const cachedStatements = Array.from(this.statementCache.values());
140
+ this.statementCache.clear();
141
+ for (const stmt of cachedStatements) {
142
+ try {
143
+ stmt.finalize?.();
144
+ } catch {
145
+ // Ignore finalizer failures during shutdown.
146
+ }
147
+ }
148
+ while (this.preparedStatementCount > 0) {
149
+ this.preparedStatementCount -= 1;
150
+ decrementPreparedStatement();
151
+ }
152
+ try {
153
+ this.db.close();
154
+ } finally {
155
+ decrementSqliteConnection();
156
+ }
69
157
  }
70
158
  }
71
159
 
72
160
  class NodeStatementAdapter implements SqliteStatement {
73
161
  private readonly stmt: any;
162
+ private readonly onFinalize?: () => void;
163
+ private finalized = false;
74
164
 
75
- constructor(stmt: any) {
165
+ constructor(stmt: any, onFinalize?: () => void) {
76
166
  this.stmt = stmt;
167
+ this.onFinalize = onFinalize;
77
168
  }
78
169
 
79
170
  get(...params: any[]): any {
@@ -93,7 +184,13 @@ class NodeStatementAdapter implements SqliteStatement {
93
184
  }
94
185
 
95
186
  finalize(): void {
96
- if (typeof this.stmt.finalize === "function") this.stmt.finalize();
187
+ if (this.finalized) return;
188
+ this.finalized = true;
189
+ try {
190
+ if (typeof this.stmt.finalize === "function") this.stmt.finalize();
191
+ } finally {
192
+ this.onFinalize?.();
193
+ }
97
194
  }
98
195
  }
99
196
 
@@ -101,19 +198,50 @@ class NodeDatabaseAdapter implements SqliteDatabase {
101
198
  private txDepth = 0;
102
199
  private txCounter = 0;
103
200
  private readonly db: any;
201
+ private preparedStatementCount = 0;
202
+ private closed = false;
203
+ private readonly statementCache = new Map<string, NodeStatementAdapter>();
104
204
 
105
205
  constructor(db: any) {
106
206
  this.db = db;
207
+ incrementSqliteConnection();
208
+ }
209
+
210
+ private trackStatement(): () => void {
211
+ let released = false;
212
+ this.preparedStatementCount += 1;
213
+ incrementPreparedStatement();
214
+ return () => {
215
+ if (released) return;
216
+ released = true;
217
+ this.preparedStatementCount = Math.max(0, this.preparedStatementCount - 1);
218
+ decrementPreparedStatement();
219
+ };
107
220
  }
108
221
 
109
222
  exec(sql: string): void {
110
223
  this.db.exec(sql);
111
224
  }
112
225
 
226
+ prepare(sql: string): SqliteStatement {
227
+ const stmt = this.db.prepare(sql);
228
+ if (typeof stmt?.setReadBigInts === "function") stmt.setReadBigInts(true);
229
+ return new NodeStatementAdapter(stmt, this.trackStatement());
230
+ }
231
+
113
232
  query(sql: string): SqliteStatement {
233
+ const cached = this.statementCache.get(sql);
234
+ if (cached) return cached;
114
235
  const stmt = this.db.prepare(sql);
115
236
  if (typeof stmt?.setReadBigInts === "function") stmt.setReadBigInts(true);
116
- return new NodeStatementAdapter(stmt);
237
+ let adapter: NodeStatementAdapter;
238
+ const release = this.trackStatement();
239
+ adapter = new NodeStatementAdapter(stmt, () => {
240
+ this.statementCache.delete(sql);
241
+ release();
242
+ });
243
+ this.statementCache.set(sql, adapter);
244
+ return adapter;
117
245
  }
118
246
 
119
247
  transaction<T>(fn: () => T): () => T {
@@ -147,7 +275,26 @@ class NodeDatabaseAdapter implements SqliteDatabase {
147
275
  }
148
276
 
149
277
  close(): void {
150
- this.db.close();
278
+ if (this.closed) return;
279
+ this.closed = true;
280
+ const cachedStatements = Array.from(this.statementCache.values());
281
+ this.statementCache.clear();
282
+ for (const stmt of cachedStatements) {
283
+ try {
284
+ stmt.finalize?.();
285
+ } catch {
286
+ // Ignore finalizer failures during shutdown.
287
+ }
288
+ }
289
+ while (this.preparedStatementCount > 0) {
290
+ this.preparedStatementCount -= 1;
291
+ decrementPreparedStatement();
292
+ }
293
+ try {
294
+ this.db.close();
295
+ } finally {
296
+ decrementSqliteConnection();
297
+ }
151
298
  }
152
299
  }
153
300
 
@@ -0,0 +1,163 @@
1
+ import { createRequire } from "node:module";
2
+ import { detectHostRuntime } from "../runtime/host_runtime.ts";
3
+ import { getSqliteAdapterRuntimeCounts } from "./adapter.ts";
4
+ import type { SqliteRuntimeMemoryStats } from "../runtime_memory.ts";
5
+
6
+ const SQLITE_STATUS_MEMORY_USED = 0;
7
+ const SQLITE_STATUS_PAGECACHE_USED = 1;
8
+ const SQLITE_STATUS_PAGECACHE_OVERFLOW = 2;
9
+ const SQLITE_STATUS_MALLOC_COUNT = 9;
10
+
11
+ type SqliteStatus64Fn = (op: number, currentPtr: number, highwaterPtr: number, resetFlag: number) => number;
12
+ type BunFfiModule = {
13
+ dlopen: (
14
+ path: string,
15
+ symbols: {
16
+ sqlite3_status64: {
17
+ args: ["i32", "ptr", "ptr", "i32"];
18
+ returns: "i32";
19
+ };
20
+ }
21
+ ) => { symbols: { sqlite3_status64: SqliteStatus64Fn }; close: () => void };
22
+ ptr: (value: TypedArray) => number;
23
+ };
24
+
25
+ type TypedArray = BigInt64Array | BigUint64Array | Int32Array | Uint32Array;
26
+
27
+ type SqliteLibraryHandle = {
28
+ sqlite3_status64: SqliteStatus64Fn;
29
+ close: () => void;
30
+ };
31
+
32
+ type CachedSqliteRuntimeStats = {
33
+ at_ms: number;
34
+ value: SqliteRuntimeMemoryStats;
35
+ };
36
+
37
+ let ffiModule: BunFfiModule | null | undefined;
38
+ let sqliteLibrary: SqliteLibraryHandle | null | undefined;
39
+ let cachedRuntimeStats: CachedSqliteRuntimeStats | null = null;
40
+ const require = createRequire(import.meta.url);
41
+
42
+ function sqliteLibraryNames(): string[] {
43
+ if (process.platform === "darwin") {
44
+ return ["libsqlite3.dylib", "/usr/lib/libsqlite3.dylib"];
45
+ }
46
+ if (process.platform === "linux") {
47
+ return ["libsqlite3.so.0", "libsqlite3.so", "/usr/lib/x86_64-linux-gnu/libsqlite3.so.0", "/lib/x86_64-linux-gnu/libsqlite3.so.0"];
48
+ }
49
+ return ["libsqlite3.so", "libsqlite3.dylib"];
50
+ }
51
+
52
+ function loadBunFfi(): BunFfiModule | null {
53
+ if (ffiModule !== undefined) return ffiModule;
54
+ if (detectHostRuntime() !== "bun") {
55
+ ffiModule = null;
56
+ return ffiModule;
57
+ }
58
+ try {
59
+ ffiModule = require("bun:ffi") as BunFfiModule;
60
+ } catch {
61
+ ffiModule = null;
62
+ }
63
+ return ffiModule;
64
+ }
65
+
66
+ function loadSqliteLibrary(): SqliteLibraryHandle | null {
67
+ if (sqliteLibrary !== undefined) return sqliteLibrary;
68
+ const ffi = loadBunFfi();
69
+ if (!ffi) {
70
+ sqliteLibrary = null;
71
+ return sqliteLibrary;
72
+ }
73
+ for (const name of sqliteLibraryNames()) {
74
+ try {
75
+ const lib = ffi.dlopen(name, {
76
+ sqlite3_status64: {
77
+ args: ["i32", "ptr", "ptr", "i32"],
78
+ returns: "i32",
79
+ },
80
+ });
81
+ sqliteLibrary = {
82
+ sqlite3_status64: lib.symbols.sqlite3_status64,
83
+ close: () => lib.close(),
84
+ };
85
+ return sqliteLibrary;
86
+ } catch {
87
+ // Try the next library name.
88
+ }
89
+ }
90
+ sqliteLibrary = null;
91
+ return sqliteLibrary;
92
+ }
93
+
94
+ function readStatus64(
95
+ lib: SqliteLibraryHandle,
96
+ ffi: BunFfiModule,
97
+ op: number
98
+ ): { current: number; highwater: number } | null {
99
+ const current = new BigInt64Array(1);
100
+ const highwater = new BigInt64Array(1);
101
+ try {
102
+ const rc = lib.sqlite3_status64(op, ffi.ptr(current), ffi.ptr(highwater), 0);
103
+ if (rc !== 0) return null;
104
+ return {
105
+ current: Number(current[0]),
106
+ highwater: Number(highwater[0]),
107
+ };
108
+ } catch {
109
+ return null;
110
+ }
111
+ }
112
+
113
+ export function readSqliteRuntimeMemoryStats(ttlMs = 1_000): SqliteRuntimeMemoryStats {
114
+ const now = Date.now();
115
+ if (cachedRuntimeStats && now - cachedRuntimeStats.at_ms < Math.max(0, ttlMs)) {
116
+ return cachedRuntimeStats.value;
117
+ }
118
+
119
+ const adapterCounts = getSqliteAdapterRuntimeCounts();
120
+ const unavailable: SqliteRuntimeMemoryStats = {
121
+ available: false,
122
+ source: "unavailable",
123
+ memory_used_bytes: 0,
124
+ memory_highwater_bytes: 0,
125
+ pagecache_used_slots: 0,
126
+ pagecache_used_slots_highwater: 0,
127
+ pagecache_overflow_bytes: 0,
128
+ pagecache_overflow_highwater_bytes: 0,
129
+ malloc_count: 0,
130
+ malloc_count_highwater: 0,
131
+ open_connections: adapterCounts.open_connections,
132
+ prepared_statements: adapterCounts.prepared_statements,
133
+ };
134
+
135
+ const ffi = loadBunFfi();
136
+ const lib = loadSqliteLibrary();
137
+ if (!ffi || !lib) {
138
+ cachedRuntimeStats = { at_ms: now, value: unavailable };
139
+ return unavailable;
140
+ }
141
+
142
+ const memoryUsed = readStatus64(lib, ffi, SQLITE_STATUS_MEMORY_USED);
143
+ const pagecacheUsed = readStatus64(lib, ffi, SQLITE_STATUS_PAGECACHE_USED);
144
+ const pagecacheOverflow = readStatus64(lib, ffi, SQLITE_STATUS_PAGECACHE_OVERFLOW);
145
+ const mallocCount = readStatus64(lib, ffi, SQLITE_STATUS_MALLOC_COUNT);
146
+
147
+ const stats: SqliteRuntimeMemoryStats = {
148
+ available: memoryUsed != null,
149
+ source: memoryUsed != null ? "sqlite3_status64" : "unavailable",
150
+ memory_used_bytes: Math.max(0, Math.floor(memoryUsed?.current ?? 0)),
151
+ memory_highwater_bytes: Math.max(0, Math.floor(memoryUsed?.highwater ?? 0)),
152
+ pagecache_used_slots: Math.max(0, Math.floor(pagecacheUsed?.current ?? 0)),
153
+ pagecache_used_slots_highwater: Math.max(0, Math.floor(pagecacheUsed?.highwater ?? 0)),
154
+ pagecache_overflow_bytes: Math.max(0, Math.floor(pagecacheOverflow?.current ?? 0)),
155
+ pagecache_overflow_highwater_bytes: Math.max(0, Math.floor(pagecacheOverflow?.highwater ?? 0)),
156
+ malloc_count: Math.max(0, Math.floor(mallocCount?.current ?? 0)),
157
+ malloc_count_highwater: Math.max(0, Math.floor(mallocCount?.highwater ?? 0)),
158
+ open_connections: adapterCounts.open_connections,
159
+ prepared_statements: adapterCounts.prepared_statements,
160
+ };
161
+ cachedRuntimeStats = { at_ms: now, value: stats };
162
+ return stats;
163
+ }
package/src/stats.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { SqliteDurableStore } from "./db/db";
2
2
  import type { UploaderController } from "./uploader";
3
- import type { MemoryGuard } from "./memory";
3
+ import type { MemoryPressureMonitor } from "./memory";
4
4
  import type { BackpressureGate } from "./backpressure";
5
5
  import type { IngestQueue } from "./ingest";
6
6
 
@@ -110,7 +110,7 @@ export class StatsReporter {
110
110
  private readonly uploader: UploaderController;
111
111
  private readonly ingest?: IngestQueue;
112
112
  private readonly backpressure?: BackpressureGate;
113
- private readonly memory?: MemoryGuard;
113
+ private readonly memory?: MemoryPressureMonitor;
114
114
 
115
115
  constructor(
116
116
  stats: StatsCollector,
@@ -118,7 +118,7 @@ export class StatsReporter {
118
118
  uploader: UploaderController,
119
119
  ingest?: IngestQueue,
120
120
  backpressure?: BackpressureGate,
121
- memory?: MemoryGuard,
121
+ memory?: MemoryPressureMonitor,
122
122
  intervalMs = 60_000
123
123
  ) {
124
124
  this.stats = stats;
@@ -0,0 +1,100 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { Result } from "better-result";
4
+ import type { SqliteDurableStore, SegmentRow } from "./db/db";
5
+ import type { ObjectStore } from "./objectstore/interface";
6
+ import type { SegmentDiskCache } from "./segment/cache";
7
+ import { loadSegmentBytesCached } from "./segment/cached_segment";
8
+ import { iterateBlocksResult } from "./segment/format";
9
+ import { dsError } from "./util/ds_error";
10
+ import { yieldToEventLoop } from "./util/yield";
11
+
12
+ export class StreamSizeReconciler {
13
+ private stopped = false;
14
+ private running: Promise<void> | null = null;
15
+
16
+ constructor(
17
+ private readonly db: SqliteDurableStore,
18
+ private readonly os: ObjectStore,
19
+ private readonly segmentCache?: SegmentDiskCache,
20
+ private readonly onMetadataChanged?: (stream: string) => void
21
+ ) {}
22
+
23
+ start(): void {
24
+ if (this.running) return;
25
+ this.running = this.run().finally(() => {
26
+ this.running = null;
27
+ });
28
+ }
29
+
30
+ stop(): void {
31
+ this.stopped = true;
32
+ }
33
+
34
+ private async run(): Promise<void> {
35
+ while (!this.stopped) {
36
+ const streams = this.db.listStreamsMissingLogicalSize(8);
37
+ if (streams.length === 0) return;
38
+ for (const stream of streams) {
39
+ if (this.stopped) return;
40
+ try {
41
+ await this.reconcileStream(stream);
42
+ } catch (e) {
43
+ const msg = String((e as any)?.message ?? e);
44
+ if (!this.stopped && !msg.includes("Statement has finalized")) {
45
+ // eslint-disable-next-line no-console
46
+ console.error("stream size reconcile failed", stream, e);
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
52
+
53
+ private async reconcileStream(stream: string): Promise<void> {
54
+ for (let attempt = 0; attempt < 3 && !this.stopped; attempt++) {
55
+ const before = this.db.getStream(stream);
56
+ if (!before || this.db.isDeleted(before) || before.next_offset <= 0n) return;
57
+ if (before.logical_size_bytes > 0n) return;
58
+
59
+ const segments = this.db.listSegmentsForStream(stream);
60
+ let total = 0n;
61
+ for (const segment of segments) {
62
+ if (this.stopped) return;
63
+ total += await this.sumSegmentPayloadBytes(segment);
64
+ await yieldToEventLoop();
65
+ }
66
+
67
+ const after = this.db.getStream(stream);
68
+ if (!after || this.db.isDeleted(after)) return;
69
+ if (after.logical_size_bytes > 0n) return;
70
+
71
+ if (segments.length !== this.db.countSegmentsForStream(stream)) continue;
72
+
73
+ const finalTotal = total + after.wal_bytes;
74
+ if (finalTotal > after.logical_size_bytes) {
75
+ this.db.setStreamLogicalSizeBytes(stream, finalTotal);
76
+ this.onMetadataChanged?.(stream);
77
+ }
78
+ return;
79
+ }
80
+ }
81
+
82
+ private async sumSegmentPayloadBytes(segment: SegmentRow): Promise<bigint> {
83
+ const bytes = await this.loadSegmentBytes(segment);
84
+ let total = 0n;
85
+ for (const blockRes of iterateBlocksResult(bytes)) {
86
+ if (Result.isError(blockRes)) throw dsError(blockRes.error.message);
87
+ for (const record of blockRes.value.decoded.records) {
88
+ total += BigInt(record.payload.byteLength);
89
+ }
90
+ }
91
+ return total;
92
+ }
93
+
94
+ private async loadSegmentBytes(segment: SegmentRow): Promise<Uint8Array> {
95
+ if (existsSync(segment.local_path)) {
96
+ return new Uint8Array(await readFile(segment.local_path));
97
+ }
98
+ return loadSegmentBytesCached(this.os, segment, this.segmentCache);
99
+ }
100
+ }
@@ -0,0 +1,7 @@
1
+ export type CanonicalChange = {
2
+ entity: string;
3
+ key?: string;
4
+ op: "insert" | "update" | "delete";
5
+ before?: unknown;
6
+ after?: unknown;
7
+ };