@prisma/streams-server 0.1.1 → 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 (91) 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/host_runtime.ts +5 -0
  45. package/src/runtime_memory.ts +200 -0
  46. package/src/runtime_memory_sampler.ts +235 -0
  47. package/src/schema/read_json.ts +43 -0
  48. package/src/schema/registry.ts +563 -59
  49. package/src/search/agg_format.ts +638 -0
  50. package/src/search/aggregate.ts +389 -0
  51. package/src/search/binary/codec.ts +162 -0
  52. package/src/search/binary/docset.ts +67 -0
  53. package/src/search/binary/restart_strings.ts +181 -0
  54. package/src/search/binary/varint.ts +34 -0
  55. package/src/search/bitset.ts +19 -0
  56. package/src/search/col_format.ts +382 -0
  57. package/src/search/col_runtime.ts +59 -0
  58. package/src/search/column_encoding.ts +43 -0
  59. package/src/search/companion_file_cache.ts +319 -0
  60. package/src/search/companion_format.ts +313 -0
  61. package/src/search/companion_manager.ts +1086 -0
  62. package/src/search/companion_plan.ts +218 -0
  63. package/src/search/fts_format.ts +423 -0
  64. package/src/search/fts_runtime.ts +333 -0
  65. package/src/search/query.ts +875 -0
  66. package/src/search/schema.ts +245 -0
  67. package/src/segment/cache.ts +93 -2
  68. package/src/segment/cached_segment.ts +89 -0
  69. package/src/segment/format.ts +108 -36
  70. package/src/segment/segmenter.ts +79 -5
  71. package/src/segment/segmenter_worker.ts +35 -6
  72. package/src/segment/segmenter_workers.ts +42 -12
  73. package/src/server.ts +150 -36
  74. package/src/sqlite/adapter.ts +185 -14
  75. package/src/sqlite/runtime_stats.ts +163 -0
  76. package/src/stats.ts +3 -3
  77. package/src/stream_size_reconciler.ts +100 -0
  78. package/src/touch/canonical_change.ts +7 -0
  79. package/src/touch/live_metrics.ts +94 -64
  80. package/src/touch/live_templates.ts +15 -1
  81. package/src/touch/manager.ts +166 -88
  82. package/src/touch/{interpreter_worker.ts → processor_worker.ts} +19 -14
  83. package/src/touch/spec.ts +95 -92
  84. package/src/touch/touch_journal.ts +4 -0
  85. package/src/touch/worker_pool.ts +8 -14
  86. package/src/touch/worker_protocol.ts +3 -3
  87. package/src/uploader.ts +77 -6
  88. package/src/util/bloom256.ts +2 -2
  89. package/src/util/byte_lru.ts +73 -0
  90. package/src/util/lru.ts +8 -0
  91. package/src/util/stream_paths.ts +19 -0
@@ -1,3 +1,6 @@
1
+ import { createRequire } from "node:module";
2
+ import type { HostRuntime } from "../runtime/host_runtime.ts";
3
+ import { detectHostRuntime } from "../runtime/host_runtime.ts";
1
4
  import { dsError } from "../util/ds_error.ts";
2
5
  export interface SqliteStatement {
3
6
  get(...params: any[]): any;
@@ -10,15 +13,49 @@ export interface SqliteStatement {
10
13
  export interface SqliteDatabase {
11
14
  exec(sql: string): void;
12
15
  query(sql: string): SqliteStatement;
16
+ prepare(sql: string): SqliteStatement;
13
17
  transaction<T>(fn: () => T): () => T;
14
18
  close(): void;
15
19
  }
16
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
+
17
51
  class BunStatementAdapter implements SqliteStatement {
18
52
  private readonly stmt: any;
53
+ private readonly onFinalize?: () => void;
54
+ private finalized = false;
19
55
 
20
- constructor(stmt: any) {
56
+ constructor(stmt: any, onFinalize?: () => void) {
21
57
  this.stmt = stmt;
58
+ this.onFinalize = onFinalize;
22
59
  }
23
60
 
24
61
  get(...params: any[]): any {
@@ -38,23 +75,58 @@ class BunStatementAdapter implements SqliteStatement {
38
75
  }
39
76
 
40
77
  finalize(): void {
41
- 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
+ }
42
85
  }
43
86
  }
44
87
 
45
88
  class BunDatabaseAdapter implements SqliteDatabase {
46
89
  private readonly db: any;
90
+ private preparedStatementCount = 0;
91
+ private closed = false;
92
+ private readonly statementCache = new Map<string, BunStatementAdapter>();
47
93
 
48
94
  constructor(db: any) {
49
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
+ };
50
109
  }
51
110
 
52
111
  exec(sql: string): void {
53
112
  this.db.exec(sql);
54
113
  }
55
114
 
115
+ prepare(sql: string): SqliteStatement {
116
+ return new BunStatementAdapter(this.db.query(sql), this.trackStatement());
117
+ }
118
+
56
119
  query(sql: string): SqliteStatement {
57
- 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;
58
130
  }
59
131
 
60
132
  transaction<T>(fn: () => T): () => T {
@@ -62,15 +134,37 @@ class BunDatabaseAdapter implements SqliteDatabase {
62
134
  }
63
135
 
64
136
  close(): void {
65
- 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
+ }
66
157
  }
67
158
  }
68
159
 
69
160
  class NodeStatementAdapter implements SqliteStatement {
70
161
  private readonly stmt: any;
162
+ private readonly onFinalize?: () => void;
163
+ private finalized = false;
71
164
 
72
- constructor(stmt: any) {
165
+ constructor(stmt: any, onFinalize?: () => void) {
73
166
  this.stmt = stmt;
167
+ this.onFinalize = onFinalize;
74
168
  }
75
169
 
76
170
  get(...params: any[]): any {
@@ -90,7 +184,13 @@ class NodeStatementAdapter implements SqliteStatement {
90
184
  }
91
185
 
92
186
  finalize(): void {
93
- 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
+ }
94
194
  }
95
195
  }
96
196
 
@@ -98,19 +198,50 @@ class NodeDatabaseAdapter implements SqliteDatabase {
98
198
  private txDepth = 0;
99
199
  private txCounter = 0;
100
200
  private readonly db: any;
201
+ private preparedStatementCount = 0;
202
+ private closed = false;
203
+ private readonly statementCache = new Map<string, NodeStatementAdapter>();
101
204
 
102
205
  constructor(db: any) {
103
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
+ };
104
220
  }
105
221
 
106
222
  exec(sql: string): void {
107
223
  this.db.exec(sql);
108
224
  }
109
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
+
110
232
  query(sql: string): SqliteStatement {
233
+ const cached = this.statementCache.get(sql);
234
+ if (cached) return cached;
111
235
  const stmt = this.db.prepare(sql);
112
236
  if (typeof stmt?.setReadBigInts === "function") stmt.setReadBigInts(true);
113
- 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;
114
245
  }
115
246
 
116
247
  transaction<T>(fn: () => T): () => T {
@@ -144,21 +275,61 @@ class NodeDatabaseAdapter implements SqliteDatabase {
144
275
  }
145
276
 
146
277
  close(): void {
147
- 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
+ }
148
298
  }
149
299
  }
150
300
 
151
301
  let openImpl: ((path: string) => SqliteDatabase) | null = null;
302
+ let openImplRuntime: HostRuntime | null = null;
303
+ let runtimeOverride: HostRuntime | null = null;
304
+ const require = createRequire(import.meta.url);
152
305
 
153
- if (typeof (globalThis as any).Bun !== "undefined") {
154
- const { Database } = await import("bun:sqlite");
155
- openImpl = (path: string) => new BunDatabaseAdapter(new Database(path));
156
- } else {
157
- const { DatabaseSync } = await import("node:sqlite");
158
- openImpl = (path: string) => new NodeDatabaseAdapter(new DatabaseSync(path));
306
+ function selectedRuntime(): HostRuntime {
307
+ return runtimeOverride ?? detectHostRuntime();
308
+ }
309
+
310
+ function buildOpenImpl(runtime: HostRuntime): (path: string) => SqliteDatabase {
311
+ if (runtime === "bun") {
312
+ const { Database } = require("bun:sqlite") as { Database: new (path: string) => any };
313
+ return (path: string) => new BunDatabaseAdapter(new Database(path));
314
+ }
315
+ const { DatabaseSync } = require("node:sqlite") as { DatabaseSync: new (path: string) => any };
316
+ return (path: string) => new NodeDatabaseAdapter(new DatabaseSync(path));
317
+ }
318
+
319
+ export function setSqliteRuntimeOverride(runtime: HostRuntime | null): void {
320
+ runtimeOverride = runtime;
321
+ if (runtimeOverride && openImplRuntime && runtimeOverride !== openImplRuntime) {
322
+ openImpl = null;
323
+ openImplRuntime = null;
324
+ }
159
325
  }
160
326
 
161
327
  export function openSqliteDatabase(path: string): SqliteDatabase {
328
+ const runtime = selectedRuntime();
329
+ if (!openImpl || openImplRuntime !== runtime) {
330
+ openImpl = buildOpenImpl(runtime);
331
+ openImplRuntime = runtime;
332
+ }
162
333
  if (!openImpl) throw dsError("sqlite adapter not initialized");
163
334
  return openImpl(path);
164
335
  }
@@ -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
+ };