@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,81 @@
1
+ import type { SqliteDurableStore } from "./db";
2
+ import type { ManifestPublicationSnapshot } from "../store/segment_manifest_store";
3
+ import { dsError } from "../util/ds_error.ts";
4
+ import { readU64LE } from "../util/endian";
5
+
6
+ export function loadSqliteManifestPublicationSnapshot(db: SqliteDurableStore, stream: string): ManifestPublicationSnapshot | null {
7
+ const streamRow = db.getStream(stream);
8
+ if (!streamRow) return null;
9
+
10
+ const prevUploadedSegmentCount = streamRow.uploaded_segment_count ?? 0;
11
+ let uploadedPrefixCount = db.advanceUploadedSegmentCount(stream);
12
+
13
+ const segmentCount = db.countSegmentsForStream(stream);
14
+ let segmentMeta = db.getSegmentMeta(stream);
15
+ const needsRebuild =
16
+ !segmentMeta ||
17
+ segmentMeta.segment_count !== segmentCount ||
18
+ segmentMeta.segment_offsets.byteLength !== segmentCount * 8 ||
19
+ segmentMeta.segment_blocks.byteLength !== segmentCount * 4 ||
20
+ segmentMeta.segment_last_ts.byteLength !== segmentCount * 8;
21
+ if (needsRebuild) {
22
+ segmentMeta = db.rebuildSegmentMeta(stream);
23
+ }
24
+ if (!segmentMeta) return null;
25
+ if (uploadedPrefixCount > segmentMeta.segment_count) {
26
+ uploadedPrefixCount = segmentMeta.segment_count;
27
+ db.setUploadedSegmentCount(stream, uploadedPrefixCount);
28
+ }
29
+
30
+ const uploadedThrough =
31
+ uploadedPrefixCount === 0 ? -1n : readU64LE(segmentMeta.segment_offsets, (uploadedPrefixCount - 1) * 8) - 1n;
32
+ const unpublishedWalBytes = db.getWalBytesAfterOffset(stream, uploadedThrough);
33
+ const publishedLogicalSizeBytes =
34
+ streamRow.logical_size_bytes > unpublishedWalBytes ? streamRow.logical_size_bytes - unpublishedWalBytes : 0n;
35
+
36
+ const manifestRow = db.getManifestRow(stream);
37
+ const secondaryIndexStates = db.listSecondaryIndexStates(stream);
38
+ const secondaryIndexRuns = secondaryIndexStates.flatMap((state) => db.listSecondaryIndexRuns(stream, state.index_name));
39
+ const retiredSecondaryIndexRuns = secondaryIndexStates.flatMap((state) =>
40
+ db.listRetiredSecondaryIndexRuns(stream, state.index_name)
41
+ );
42
+ const lexiconIndexStates = db.listLexiconIndexStates(stream);
43
+ const lexiconIndexRuns = lexiconIndexStates.flatMap((state) =>
44
+ db.listLexiconIndexRuns(stream, state.source_kind, state.source_name)
45
+ );
46
+ const retiredLexiconIndexRuns = lexiconIndexStates.flatMap((state) =>
47
+ db.listRetiredLexiconIndexRuns(stream, state.source_kind, state.source_name)
48
+ );
49
+
50
+ let profileJson: Record<string, any> | null = null;
51
+ const profileRow = db.getStreamProfile(stream);
52
+ if (profileRow) {
53
+ try {
54
+ profileJson = JSON.parse(profileRow.profile_json);
55
+ } catch {
56
+ throw dsError(`invalid profile_json for ${stream}`);
57
+ }
58
+ }
59
+
60
+ return {
61
+ streamRow,
62
+ prevUploadedSegmentCount,
63
+ uploadedPrefixCount,
64
+ uploadedThrough,
65
+ publishedLogicalSizeBytes,
66
+ generation: manifestRow.generation + 1,
67
+ segmentMeta,
68
+ profileJson,
69
+ indexState: db.getIndexState(stream),
70
+ indexRuns: db.listIndexRuns(stream),
71
+ retiredRuns: db.listRetiredIndexRuns(stream),
72
+ secondaryIndexStates,
73
+ secondaryIndexRuns,
74
+ retiredSecondaryIndexRuns,
75
+ lexiconIndexStates,
76
+ lexiconIndexRuns,
77
+ retiredLexiconIndexRuns,
78
+ searchCompanionPlan: db.getSearchCompanionPlan(stream),
79
+ searchSegmentCompanions: db.listSearchSegmentCompanions(stream),
80
+ };
81
+ }
@@ -0,0 +1,491 @@
1
+ import type { SqliteDatabase, SqliteStatement } from "../sqlite/adapter";
2
+ import type { StreamRow } from "../store/rows";
3
+ import type {
4
+ LiveTemplateActivationInput,
5
+ LiveTemplateActivationResult,
6
+ LiveTemplateIdentityRow,
7
+ LiveTemplateLastSeenUpdate,
8
+ LiveTemplateStoreRow,
9
+ StreamTouchStateRow,
10
+ TouchProcessorStore,
11
+ } from "../store/touch_store";
12
+ import type { WalReadRow } from "../store/wal_store";
13
+
14
+ type SqliteTouchStoreDelegates = {
15
+ nowMs(): bigint;
16
+ getStream(stream: string): StreamRow | null;
17
+ ensureStream(stream: string, opts?: { contentType?: string; streamFlags?: number }): StreamRow;
18
+ addStreamFlags(stream: string, flags: number): void;
19
+ isDeleted(row: StreamRow): boolean;
20
+ readWalRange(stream: string, startOffset: bigint, endOffset: bigint, routingKey?: Uint8Array): AsyncIterable<WalReadRow>;
21
+ deleteWalThrough(stream: string, uploadedThrough: bigint): { deletedRows: number; deletedBytes: number };
22
+ getWalOldestOffset(stream: string): bigint | null;
23
+ trimWalByAge(stream: string, maxAgeMs: number): { trimmedRows: number; trimmedBytes: number; keptFromOffset: bigint | null };
24
+ };
25
+
26
+ function toBigInt(v: unknown): bigint {
27
+ return typeof v === "bigint" ? v : BigInt(v as any);
28
+ }
29
+
30
+ function bindInt(v: bigint): number | string {
31
+ const max = BigInt(Number.MAX_SAFE_INTEGER);
32
+ const min = BigInt(Number.MIN_SAFE_INTEGER);
33
+ if (v <= max && v >= min) return Number(v);
34
+ return v.toString();
35
+ }
36
+
37
+ export class SqliteTouchStore implements TouchProcessorStore {
38
+ private readonly stmts: {
39
+ getStreamTouchState: ReturnType<SqliteDatabase["query"]>;
40
+ upsertStreamTouchState: ReturnType<SqliteDatabase["query"]>;
41
+ deleteStreamTouchState: ReturnType<SqliteDatabase["query"]>;
42
+ listStreamTouchStates: ReturnType<SqliteDatabase["query"]>;
43
+ listStreamsByProfile: ReturnType<SqliteDatabase["query"]>;
44
+ countActiveLiveTemplates: SqliteStatement;
45
+ listActiveLiveTemplates: SqliteStatement;
46
+ getLiveTemplate: SqliteStatement;
47
+ updateLiveTemplateHeartbeat: SqliteStatement;
48
+ reactivateLiveTemplate: SqliteStatement;
49
+ insertLiveTemplate: SqliteStatement;
50
+ updateLiveTemplateLastSeen: SqliteStatement;
51
+ listExpiredLiveTemplates: SqliteStatement;
52
+ retireLiveTemplateForInactivity: SqliteStatement;
53
+ listActiveLiveTemplateEntityCounts: SqliteStatement;
54
+ retireLiveTemplateForCap: SqliteStatement;
55
+ };
56
+
57
+ constructor(
58
+ private readonly db: SqliteDatabase,
59
+ private readonly delegates: SqliteTouchStoreDelegates
60
+ ) {
61
+ this.stmts = {
62
+ getStreamTouchState: this.db.query(
63
+ `SELECT stream, processed_through, updated_at_ms
64
+ FROM stream_touch_state WHERE stream=? LIMIT 1;`
65
+ ),
66
+ upsertStreamTouchState: this.db.query(
67
+ `INSERT INTO stream_touch_state(stream, processed_through, updated_at_ms)
68
+ VALUES(?, ?, ?)
69
+ ON CONFLICT(stream) DO UPDATE SET
70
+ processed_through=excluded.processed_through,
71
+ updated_at_ms=excluded.updated_at_ms;`
72
+ ),
73
+ deleteStreamTouchState: this.db.query(`DELETE FROM stream_touch_state WHERE stream=?;`),
74
+ listStreamTouchStates: this.db.query(
75
+ `SELECT stream, processed_through, updated_at_ms
76
+ FROM stream_touch_state
77
+ ORDER BY stream ASC;`
78
+ ),
79
+ listStreamsByProfile: this.db.query(`SELECT stream FROM streams WHERE profile=? ORDER BY stream ASC;`),
80
+ countActiveLiveTemplates: this.db.query(`SELECT COUNT(*) as cnt FROM live_templates WHERE stream=? AND state='active';`),
81
+ listActiveLiveTemplates: this.db.query(
82
+ `SELECT stream, template_id, entity, fields_json, encodings_json, state, created_at_ms, last_seen_at_ms,
83
+ inactivity_ttl_ms, active_from_source_offset, retired_at_ms, retired_reason
84
+ FROM live_templates
85
+ WHERE stream=? AND state='active'
86
+ ORDER BY entity ASC, template_id ASC;`
87
+ ),
88
+ getLiveTemplate: this.db.query(
89
+ `SELECT stream, template_id, entity, fields_json, encodings_json, state, created_at_ms, last_seen_at_ms,
90
+ inactivity_ttl_ms, active_from_source_offset, retired_at_ms, retired_reason
91
+ FROM live_templates
92
+ WHERE stream=? AND template_id=? LIMIT 1;`
93
+ ),
94
+ updateLiveTemplateHeartbeat: this.db.query(
95
+ `UPDATE live_templates
96
+ SET last_seen_at_ms=?, inactivity_ttl_ms=?
97
+ WHERE stream=? AND template_id=?;`
98
+ ),
99
+ reactivateLiveTemplate: this.db.query(
100
+ `UPDATE live_templates
101
+ SET state='active',
102
+ last_seen_at_ms=?,
103
+ inactivity_ttl_ms=?,
104
+ active_from_source_offset=?,
105
+ retired_at_ms=NULL,
106
+ retired_reason=NULL
107
+ WHERE stream=? AND template_id=?;`
108
+ ),
109
+ insertLiveTemplate: this.db.query(
110
+ `INSERT INTO live_templates(
111
+ stream, template_id, entity, fields_json, encodings_json,
112
+ state, created_at_ms, last_seen_at_ms, inactivity_ttl_ms, active_from_source_offset,
113
+ retired_at_ms, retired_reason
114
+ ) VALUES(?, ?, ?, ?, ?, 'active', ?, ?, ?, ?, NULL, NULL);`
115
+ ),
116
+ updateLiveTemplateLastSeen: this.db.query(
117
+ `UPDATE live_templates
118
+ SET last_seen_at_ms=?
119
+ WHERE stream=? AND template_id=? AND state='active';`
120
+ ),
121
+ listExpiredLiveTemplates: this.db.query(
122
+ `SELECT template_id, entity, fields_json, encodings_json, last_seen_at_ms, inactivity_ttl_ms
123
+ FROM live_templates
124
+ WHERE stream=? AND state='active' AND (last_seen_at_ms + inactivity_ttl_ms) < ?
125
+ ORDER BY last_seen_at_ms ASC
126
+ LIMIT ?;`
127
+ ),
128
+ retireLiveTemplateForInactivity: this.db.query(
129
+ `UPDATE live_templates
130
+ SET state='retired', retired_reason='inactivity', retired_at_ms=?
131
+ WHERE stream=? AND template_id=? AND state='active';`
132
+ ),
133
+ listActiveLiveTemplateEntityCounts: this.db.query(
134
+ `SELECT entity, COUNT(*) as cnt
135
+ FROM live_templates
136
+ WHERE stream=? AND state='active'
137
+ GROUP BY entity;`
138
+ ),
139
+ retireLiveTemplateForCap: this.db.query(
140
+ `UPDATE live_templates
141
+ SET state='retired', retired_reason='cap_exceeded', retired_at_ms=?
142
+ WHERE stream=? AND template_id=? AND state='active';`
143
+ ),
144
+ };
145
+ }
146
+
147
+ nowMs(): bigint {
148
+ return this.delegates.nowMs();
149
+ }
150
+
151
+ getStream(stream: string): StreamRow | null {
152
+ return this.delegates.getStream(stream);
153
+ }
154
+
155
+ ensureStream(stream: string, opts?: { contentType?: string; streamFlags?: number }): StreamRow {
156
+ return this.delegates.ensureStream(stream, opts);
157
+ }
158
+
159
+ addStreamFlags(stream: string, flags: number): void {
160
+ this.delegates.addStreamFlags(stream, flags);
161
+ }
162
+
163
+ isDeleted(row: StreamRow): boolean {
164
+ return this.delegates.isDeleted(row);
165
+ }
166
+
167
+ readWalRange(stream: string, startOffset: bigint, endOffset: bigint, routingKey?: Uint8Array): AsyncIterable<WalReadRow> {
168
+ return this.delegates.readWalRange(stream, startOffset, endOffset, routingKey);
169
+ }
170
+
171
+ deleteWalThrough(stream: string, uploadedThrough: bigint): { deletedRows: number; deletedBytes: number } {
172
+ return this.delegates.deleteWalThrough(stream, uploadedThrough);
173
+ }
174
+
175
+ getWalOldestOffset(stream: string): bigint | null {
176
+ return this.delegates.getWalOldestOffset(stream);
177
+ }
178
+
179
+ trimWalByAge(stream: string, maxAgeMs: number): { trimmedRows: number; trimmedBytes: number; keptFromOffset: bigint | null } {
180
+ return this.delegates.trimWalByAge(stream, maxAgeMs);
181
+ }
182
+
183
+ getStreamTouchState(stream: string): StreamTouchStateRow | null {
184
+ const row = this.stmts.getStreamTouchState.get(stream) as any;
185
+ if (!row) return null;
186
+ return {
187
+ stream: String(row.stream),
188
+ processed_through: toBigInt(row.processed_through),
189
+ updated_at_ms: toBigInt(row.updated_at_ms),
190
+ };
191
+ }
192
+
193
+ listStreamTouchStates(): StreamTouchStateRow[] {
194
+ const rows = this.stmts.listStreamTouchStates.all() as any[];
195
+ return rows.map((row) => ({
196
+ stream: String(row.stream),
197
+ processed_through: toBigInt(row.processed_through),
198
+ updated_at_ms: toBigInt(row.updated_at_ms),
199
+ }));
200
+ }
201
+
202
+ listStreamsByProfile(kind: string): string[] {
203
+ const rows = this.stmts.listStreamsByProfile.all(kind) as any[];
204
+ return rows.map((row) => String(row.stream));
205
+ }
206
+
207
+ ensureStreamTouchState(stream: string): void {
208
+ const existing = this.getStreamTouchState(stream);
209
+ if (existing) return;
210
+ const srow = this.getStream(stream);
211
+ const initialThrough = srow ? srow.next_offset - 1n : -1n;
212
+ this.stmts.upsertStreamTouchState.run(stream, bindInt(initialThrough), this.nowMs());
213
+ }
214
+
215
+ updateStreamTouchStateThrough(stream: string, processedThrough: bigint): void {
216
+ this.stmts.upsertStreamTouchState.run(stream, bindInt(processedThrough), this.nowMs());
217
+ }
218
+
219
+ deleteStreamTouchState(stream: string): void {
220
+ this.stmts.deleteStreamTouchState.run(stream);
221
+ }
222
+
223
+ countActiveLiveTemplates(stream: string): number {
224
+ const row = this.stmts.countActiveLiveTemplates.get(stream) as any;
225
+ return Number(row?.cnt ?? 0);
226
+ }
227
+
228
+ activateLiveTemplates(args: {
229
+ stream: string;
230
+ templates: LiveTemplateActivationInput[];
231
+ maxActiveTemplatesPerStream: number;
232
+ maxActiveTemplatesPerEntity: number;
233
+ maxActivationTokens: number;
234
+ }): LiveTemplateActivationResult {
235
+ if (args.templates.length === 0) return { activated: [], invalid: [], rateLimited: [], activationTokensUsed: 0, evicted: [] };
236
+ const tx = this.db.transaction(() => {
237
+ const activated: string[] = [];
238
+ const invalid: string[] = [];
239
+ const rateLimited: string[] = [];
240
+ let activationTokensUsed = 0;
241
+ for (const template of args.templates) {
242
+ const existing = this.stmts.getLiveTemplate.get(args.stream, template.templateId) as any;
243
+ if (
244
+ existing &&
245
+ (String(existing.entity) !== template.entity ||
246
+ String(existing.fields_json) !== template.fieldsJson ||
247
+ String(existing.encodings_json) !== template.encodingsJson)
248
+ ) {
249
+ invalid.push(template.templateId);
250
+ continue;
251
+ }
252
+ const alreadyActive = existing && String(existing.state) === "active";
253
+ if (!alreadyActive && activationTokensUsed >= args.maxActivationTokens) {
254
+ rateLimited.push(template.templateId);
255
+ continue;
256
+ }
257
+ if (existing) {
258
+ const state = String(existing.state);
259
+ if (state === "active") {
260
+ this.stmts.updateLiveTemplateHeartbeat.run(template.nowMs, template.inactivityTtlMs, args.stream, template.templateId);
261
+ } else {
262
+ this.stmts.reactivateLiveTemplate.run(
263
+ template.nowMs,
264
+ template.inactivityTtlMs,
265
+ bindInt(template.activeFromSourceOffset),
266
+ args.stream,
267
+ template.templateId
268
+ );
269
+ }
270
+ } else {
271
+ this.stmts.insertLiveTemplate.run(
272
+ args.stream,
273
+ template.templateId,
274
+ template.entity,
275
+ template.fieldsJson,
276
+ template.encodingsJson,
277
+ template.nowMs,
278
+ template.nowMs,
279
+ template.inactivityTtlMs,
280
+ bindInt(template.activeFromSourceOffset)
281
+ );
282
+ }
283
+ if (!alreadyActive) activationTokensUsed += 1;
284
+ activated.push(template.templateId);
285
+ }
286
+ const evicted = this.evictToCapsInTransaction({
287
+ stream: args.stream,
288
+ protectedIds: new Set(activated),
289
+ maxActiveTemplatesPerStream: args.maxActiveTemplatesPerStream,
290
+ maxActiveTemplatesPerEntity: args.maxActiveTemplatesPerEntity,
291
+ nowMs: args.templates.reduce((max, template) => Math.max(max, template.nowMs), 0),
292
+ });
293
+ return { activated, invalid, rateLimited, activationTokensUsed, evicted };
294
+ });
295
+ return tx();
296
+ }
297
+
298
+ listActiveLiveTemplates(stream: string): LiveTemplateStoreRow[] {
299
+ const rows = this.stmts.listActiveLiveTemplates.all(stream) as any[];
300
+ return rows.map((row) => this.coerceLiveTemplateRow(row));
301
+ }
302
+
303
+ private getLiveTemplate(stream: string, templateId: string): LiveTemplateStoreRow | null {
304
+ const row = this.stmts.getLiveTemplate.get(stream, templateId) as any;
305
+ return row ? this.coerceLiveTemplateRow(row) : null;
306
+ }
307
+
308
+ private updateLiveTemplateHeartbeat(stream: string, templateId: string, nowMs: number, inactivityTtlMs: number): void {
309
+ this.stmts.updateLiveTemplateHeartbeat.run(nowMs, inactivityTtlMs, stream, templateId);
310
+ }
311
+
312
+ private reactivateLiveTemplate(stream: string, templateId: string, nowMs: number, inactivityTtlMs: number, activeFromSourceOffset: bigint): void {
313
+ this.stmts.reactivateLiveTemplate.run(nowMs, inactivityTtlMs, activeFromSourceOffset, stream, templateId);
314
+ }
315
+
316
+ private insertLiveTemplate(args: {
317
+ stream: string;
318
+ templateId: string;
319
+ entity: string;
320
+ fieldsJson: string;
321
+ encodingsJson: string;
322
+ nowMs: number;
323
+ inactivityTtlMs: number;
324
+ activeFromSourceOffset: bigint;
325
+ }): void {
326
+ this.stmts.insertLiveTemplate.run(
327
+ args.stream,
328
+ args.templateId,
329
+ args.entity,
330
+ args.fieldsJson,
331
+ args.encodingsJson,
332
+ args.nowMs,
333
+ args.nowMs,
334
+ args.inactivityTtlMs,
335
+ args.activeFromSourceOffset
336
+ );
337
+ }
338
+
339
+ updateLiveTemplateLastSeenBatch(updates: LiveTemplateLastSeenUpdate[]): void {
340
+ for (const update of updates) {
341
+ this.stmts.updateLiveTemplateLastSeen.run(update.lastSeenAtMs, update.stream, update.templateId);
342
+ }
343
+ }
344
+
345
+ listExpiredLiveTemplates(stream: string, nowMs: number, limit: number): LiveTemplateIdentityRow[] {
346
+ const rows = this.stmts.listExpiredLiveTemplates.all(stream, nowMs, Math.max(1, Math.floor(limit))) as any[];
347
+ return rows.map((row) => ({
348
+ template_id: String(row.template_id),
349
+ entity: String(row.entity),
350
+ fields_json: String(row.fields_json),
351
+ encodings_json: String(row.encodings_json),
352
+ last_seen_at_ms: toBigInt(row.last_seen_at_ms),
353
+ inactivity_ttl_ms: toBigInt(row.inactivity_ttl_ms),
354
+ }));
355
+ }
356
+
357
+ retireLiveTemplatesForInactivity(stream: string, templateIds: string[], nowMs: number): void {
358
+ for (const templateId of templateIds) {
359
+ this.stmts.retireLiveTemplateForInactivity.run(nowMs, stream, templateId);
360
+ }
361
+ }
362
+
363
+ private listActiveLiveTemplateEntityCounts(stream: string): Array<{ entity: string; count: number }> {
364
+ const rows = this.stmts.listActiveLiveTemplateEntityCounts.all(stream) as any[];
365
+ return rows.map((row) => ({ entity: String(row.entity), count: Number(row.cnt) }));
366
+ }
367
+
368
+ private listLiveTemplateLruIds(args: { stream: string; entity?: string; excludeTemplateIds?: string[]; limit: number }): string[] {
369
+ const params: any[] = [args.stream];
370
+ let where = `stream=? AND state='active'`;
371
+ if (args.entity) {
372
+ where += ` AND entity=?`;
373
+ params.push(args.entity);
374
+ }
375
+ const excludeTemplateIds = args.excludeTemplateIds ?? [];
376
+ if (excludeTemplateIds.length > 0) {
377
+ const placeholders = excludeTemplateIds.map(() => "?").join(", ");
378
+ where += ` AND template_id NOT IN (${placeholders})`;
379
+ params.push(...excludeTemplateIds);
380
+ }
381
+ const q = `SELECT template_id FROM live_templates WHERE ${where} ORDER BY last_seen_at_ms ASC, template_id ASC LIMIT ?;`;
382
+ params.push(Math.max(1, Math.floor(args.limit)));
383
+ const rows = this.db.query(q).all(...params) as any[];
384
+ return rows.map((row) => String(row.template_id));
385
+ }
386
+
387
+ private retireLiveTemplateForCap(stream: string, templateId: string, nowMs: number): void {
388
+ this.stmts.retireLiveTemplateForCap.run(nowMs, stream, templateId);
389
+ }
390
+
391
+ private retireLiveTemplatesForCap(stream: string, templateIds: string[], nowMs: number): void {
392
+ for (const templateId of templateIds) {
393
+ this.stmts.retireLiveTemplateForCap.run(nowMs, stream, templateId);
394
+ }
395
+ }
396
+
397
+ listActiveLiveTemplateEntitiesByIds(stream: string, templateIds: string[]): string[] {
398
+ if (templateIds.length === 0) return [];
399
+ const entities = new Set<string>();
400
+ const chunkSize = 200;
401
+ for (let i = 0; i < templateIds.length; i += chunkSize) {
402
+ const chunk = templateIds.slice(i, i + chunkSize);
403
+ const placeholders = chunk.map(() => "?").join(",");
404
+ const rows = this.db
405
+ .query(
406
+ `SELECT DISTINCT entity
407
+ FROM live_templates
408
+ WHERE stream=? AND state='active' AND template_id IN (${placeholders});`
409
+ )
410
+ .all(stream, ...chunk) as any[];
411
+ for (const row of rows) {
412
+ const entity = String(row?.entity ?? "").trim();
413
+ if (entity !== "") entities.add(entity);
414
+ }
415
+ }
416
+ return Array.from(entities);
417
+ }
418
+
419
+ private evictToCapsInTransaction(args: {
420
+ stream: string;
421
+ protectedIds: Set<string>;
422
+ maxActiveTemplatesPerStream: number;
423
+ maxActiveTemplatesPerEntity: number;
424
+ nowMs: number;
425
+ }): Array<{ templateId: string; reason: "cap_exceeded"; cap: number }> {
426
+ const evicted: Array<{ templateId: string; reason: "cap_exceeded"; cap: number }> = [];
427
+ const entities = this.listActiveLiveTemplateEntityCounts(args.stream);
428
+ for (const row of entities) {
429
+ if (row.count <= args.maxActiveTemplatesPerEntity) continue;
430
+ const ids = this.pickLruTemplatesForCap({
431
+ stream: args.stream,
432
+ entity: row.entity,
433
+ protectedIds: args.protectedIds,
434
+ count: row.count - args.maxActiveTemplatesPerEntity,
435
+ });
436
+ this.retireLiveTemplatesForCap(args.stream, ids, args.nowMs);
437
+ for (const id of ids) evicted.push({ templateId: id, reason: "cap_exceeded", cap: args.maxActiveTemplatesPerEntity });
438
+ }
439
+
440
+ const streamCount = this.countActiveLiveTemplates(args.stream);
441
+ if (streamCount > args.maxActiveTemplatesPerStream) {
442
+ const ids = this.pickLruTemplatesForCap({
443
+ stream: args.stream,
444
+ protectedIds: args.protectedIds,
445
+ count: streamCount - args.maxActiveTemplatesPerStream,
446
+ });
447
+ this.retireLiveTemplatesForCap(args.stream, ids, args.nowMs);
448
+ for (const id of ids) evicted.push({ templateId: id, reason: "cap_exceeded", cap: args.maxActiveTemplatesPerStream });
449
+ }
450
+ return evicted;
451
+ }
452
+
453
+ private pickLruTemplatesForCap(args: { stream: string; entity?: string; protectedIds: Set<string>; count: number }): string[] {
454
+ if (args.count <= 0) return [];
455
+ const pick = (excludeProtected: boolean): string[] =>
456
+ this.listLiveTemplateLruIds({
457
+ stream: args.stream,
458
+ entity: args.entity,
459
+ excludeTemplateIds: excludeProtected ? Array.from(args.protectedIds) : [],
460
+ limit: args.count,
461
+ });
462
+ const ids = pick(true);
463
+ if (ids.length >= args.count) return ids;
464
+ const merged: string[] = [];
465
+ const seen = new Set<string>();
466
+ for (const id of [...ids, ...pick(false)]) {
467
+ if (seen.has(id)) continue;
468
+ seen.add(id);
469
+ merged.push(id);
470
+ if (merged.length >= args.count) break;
471
+ }
472
+ return merged;
473
+ }
474
+
475
+ private coerceLiveTemplateRow(row: any): LiveTemplateStoreRow {
476
+ return {
477
+ stream: String(row.stream),
478
+ template_id: String(row.template_id),
479
+ entity: String(row.entity),
480
+ fields_json: String(row.fields_json),
481
+ encodings_json: String(row.encodings_json),
482
+ state: String(row.state),
483
+ created_at_ms: toBigInt(row.created_at_ms),
484
+ last_seen_at_ms: toBigInt(row.last_seen_at_ms),
485
+ inactivity_ttl_ms: toBigInt(row.inactivity_ttl_ms),
486
+ active_from_source_offset: toBigInt(row.active_from_source_offset),
487
+ retired_at_ms: row.retired_at_ms == null ? null : toBigInt(row.retired_at_ms),
488
+ retired_reason: row.retired_reason == null ? null : String(row.retired_reason),
489
+ };
490
+ }
491
+ }