@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,568 @@
1
+ import { buildRequestObservabilityPairingDescriptor } from "../observe/pairing";
2
+ import { hashSecondaryIndexField } from "../index/secondary_schema";
3
+ import { buildDesiredSearchCompanionPlan, hashSearchCompanionPlan } from "../search/companion_plan";
4
+ import type { SchemaRegistry, SearchConfig } from "../schema/registry";
5
+ import type { StreamProfileResource, StreamProfileSpec } from "../profiles";
6
+ import type { SearchSegmentCompanionRow, StreamRow } from "../store/rows";
7
+ import type { FullModeDetailsSnapshot, FullModeDetailsStore } from "../store/full_mode_details_store";
8
+ import type { ObjectStoreAccountingStore, StorageStatsStore } from "../store/stats_accounting_store";
9
+ import { streamHash16Hex } from "../util/stream_paths";
10
+ import { dsError } from "../util/ds_error";
11
+
12
+ export type LocalStorageUsage = {
13
+ segment_cache_bytes: number;
14
+ routing_index_cache_bytes: number;
15
+ exact_index_cache_bytes: number;
16
+ lexicon_index_cache_bytes: number;
17
+ companion_cache_bytes: number;
18
+ };
19
+
20
+ export type FullModeDetailsBuilderOptions = {
21
+ detailsStore: FullModeDetailsStore;
22
+ storageBackend?: "sqlite" | "postgres";
23
+ storageStatsStore?: StorageStatsStore;
24
+ objectStoreAccountingStore?: ObjectStoreAccountingStore;
25
+ getLocalStorageUsage?: (stream: string) => Partial<LocalStorageUsage>;
26
+ };
27
+
28
+ export type FullModeDetailsMode = "details" | "index_status";
29
+
30
+ type ExactIndexStatus = {
31
+ name: string;
32
+ kind: string;
33
+ indexed_segment_count: number;
34
+ lag_segments: number;
35
+ lag_ms: string | null;
36
+ bytes_at_rest: string;
37
+ object_count: number;
38
+ active_run_count: number;
39
+ retired_run_count: number;
40
+ fully_indexed_uploaded_segments: boolean;
41
+ stale_configuration: boolean;
42
+ updated_at: string | null;
43
+ };
44
+
45
+ type SearchFamilyStatus = {
46
+ family: "exact" | "col" | "fts" | "agg" | "mblk";
47
+ fields: string[];
48
+ plan_generation: number;
49
+ covered_segment_count: number;
50
+ contiguous_covered_segment_count: number;
51
+ lag_segments: number;
52
+ lag_ms: string | null;
53
+ bytes_at_rest: string;
54
+ object_count: number;
55
+ stale_segment_count: number;
56
+ fully_indexed_uploaded_segments: boolean;
57
+ updated_at: string | null;
58
+ };
59
+
60
+ type ExactIndexCoverage = {
61
+ name: string;
62
+ kind: string;
63
+ configHash: string;
64
+ configMatches: boolean;
65
+ indexedSegmentCount: number;
66
+ };
67
+
68
+ type SearchFamilyCoverage = {
69
+ family: "exact" | "col" | "fts" | "agg" | "mblk";
70
+ fields: string[];
71
+ coveredSegmentCount: number;
72
+ contiguousCoveredCount: number;
73
+ familyBytes: bigint;
74
+ familyObjectCount: number;
75
+ };
76
+
77
+ type LagLookup = Map<number, bigint>;
78
+
79
+ type IndexStatusPayload = {
80
+ stream: string;
81
+ profile: string;
82
+ desired_index_plan_generation: number;
83
+ segments: {
84
+ total_count: number;
85
+ uploaded_count: number;
86
+ };
87
+ manifest: {
88
+ generation: number;
89
+ uploaded_generation: number;
90
+ last_uploaded_at: string | null;
91
+ last_uploaded_etag: string | null;
92
+ last_uploaded_size_bytes: string | null;
93
+ };
94
+ routing_key_index: {
95
+ configured: boolean;
96
+ indexed_segment_count: number;
97
+ lag_segments: number;
98
+ lag_ms: string | null;
99
+ bytes_at_rest: string;
100
+ object_count: number;
101
+ active_run_count: number;
102
+ retired_run_count: number;
103
+ fully_indexed_uploaded_segments: boolean;
104
+ updated_at: string | null;
105
+ };
106
+ routing_key_lexicon: {
107
+ configured: boolean;
108
+ indexed_segment_count: number;
109
+ lag_segments: number;
110
+ lag_ms: string | null;
111
+ bytes_at_rest: string;
112
+ object_count: number;
113
+ active_run_count: number;
114
+ retired_run_count: number;
115
+ fully_indexed_uploaded_segments: boolean;
116
+ updated_at: string | null;
117
+ };
118
+ exact_indexes: ExactIndexStatus[];
119
+ bundled_companions: {
120
+ object_count: number;
121
+ bytes_at_rest: string;
122
+ fully_indexed_uploaded_segments: boolean;
123
+ };
124
+ search_families: SearchFamilyStatus[];
125
+ };
126
+
127
+ type IndexStatusSnapshot = {
128
+ payload: IndexStatusPayload;
129
+ currentCompanionRows: SearchSegmentCompanionRow[];
130
+ };
131
+
132
+ function normalizeContentType(value: string | null): string | null {
133
+ if (!value) return null;
134
+ const base = value.split(";")[0]?.trim().toLowerCase();
135
+ return base ? base : null;
136
+ }
137
+
138
+ function timestampToIsoString(value: bigint | null): string | null {
139
+ return value == null ? null : new Date(Number(value)).toISOString();
140
+ }
141
+
142
+ function configuredExactIndexes(search: SearchConfig | undefined): Array<{ name: string; kind: string; configHash: string }> {
143
+ if (!search) return [];
144
+ return Object.entries(search.fields)
145
+ .filter(([, field]) => field.exact === true && field.kind !== "text")
146
+ .map(([name, field]) => ({
147
+ name,
148
+ kind: field.kind,
149
+ configHash: hashSecondaryIndexField({ name, config: field }),
150
+ }))
151
+ .sort((a, b) => a.name.localeCompare(b.name));
152
+ }
153
+
154
+ function configuredSearchFamilies(search: SearchConfig | undefined): Array<{ family: "exact" | "col" | "fts" | "agg" | "mblk"; fields: string[] }> {
155
+ if (!search) return [];
156
+ const out: Array<{ family: "exact" | "col" | "fts" | "agg" | "mblk"; fields: string[] }> = [];
157
+ const exactFields = Object.entries(search.fields)
158
+ .filter(([, field]) => field.exact === true && field.kind !== "text")
159
+ .map(([name]) => name)
160
+ .sort((a, b) => a.localeCompare(b));
161
+ if (exactFields.length > 0) out.push({ family: "exact", fields: exactFields });
162
+ const colFields = Object.entries(search.fields)
163
+ .filter(([, field]) => field.column === true)
164
+ .map(([name]) => name)
165
+ .sort((a, b) => a.localeCompare(b));
166
+ if (colFields.length > 0) out.push({ family: "col", fields: colFields });
167
+ const ftsFields = Object.entries(search.fields)
168
+ .filter(([, field]) => field.kind === "text" || (field.kind === "keyword" && field.prefix === true))
169
+ .map(([name]) => name)
170
+ .sort((a, b) => a.localeCompare(b));
171
+ if (ftsFields.length > 0) out.push({ family: "fts", fields: ftsFields });
172
+ const aggRollups = Object.keys(search.rollups ?? {}).sort((a, b) => a.localeCompare(b));
173
+ if (aggRollups.length > 0) out.push({ family: "agg", fields: aggRollups });
174
+ if (search.profile === "metrics") out.push({ family: "mblk", fields: ["metrics"] });
175
+ return out;
176
+ }
177
+
178
+ function parseCompanionSections(value: string): Set<string> {
179
+ try {
180
+ const parsed = JSON.parse(value);
181
+ return new Set(Array.isArray(parsed) ? parsed.filter((entry) => typeof entry === "string") : []);
182
+ } catch {
183
+ return new Set();
184
+ }
185
+ }
186
+
187
+ function parseCompanionSectionSizes(value: string): Record<string, number> {
188
+ try {
189
+ const parsed = JSON.parse(value);
190
+ if (!parsed || typeof parsed !== "object") return {};
191
+ const out: Record<string, number> = {};
192
+ for (const [key, raw] of Object.entries(parsed)) {
193
+ if (typeof raw === "number" && Number.isFinite(raw) && raw >= 0) out[key] = raw;
194
+ }
195
+ return out;
196
+ } catch {
197
+ return {};
198
+ }
199
+ }
200
+
201
+ function contiguousCoveredSegmentCount(rows: Array<{ segment_index: number; sections_json: string }>, family: string): number {
202
+ let expected = 0;
203
+ for (const row of rows) {
204
+ if (row.segment_index < expected) continue;
205
+ if (row.segment_index > expected) break;
206
+ if (!parseCompanionSections(row.sections_json).has(family)) break;
207
+ expected += 1;
208
+ }
209
+ return expected;
210
+ }
211
+
212
+ export class FullModeDetailsBuilder {
213
+ constructor(private readonly opts: FullModeDetailsBuilderOptions) {}
214
+
215
+ async buildPayload(args: {
216
+ stream: string;
217
+ row: StreamRow;
218
+ registry: SchemaRegistry;
219
+ profileResource: StreamProfileResource;
220
+ mode: FullModeDetailsMode;
221
+ }): Promise<Record<string, unknown>> {
222
+ const profileKind = args.profileResource.profile.kind;
223
+ const configuredExact = configuredExactIndexes(args.registry.search);
224
+ const snapshot = await this.opts.detailsStore.getFullModeDetailsSnapshot({
225
+ stream: args.stream,
226
+ exactIndexNames: configuredExact.map((entry) => entry.name),
227
+ });
228
+ const indexStatus = await this.buildIndexStatus(args.stream, args.row, args.registry, profileKind, snapshot, configuredExact);
229
+ if (args.mode === "index_status") return indexStatus.payload;
230
+ return {
231
+ stream: this.buildStreamSummary(args.stream, args.row, args.profileResource.profile, snapshot),
232
+ profile: args.profileResource,
233
+ schema: args.registry,
234
+ index_status: indexStatus.payload,
235
+ storage: await this.buildStorageBreakdown(args.stream, args.row, snapshot, indexStatus.currentCompanionRows, indexStatus.payload),
236
+ object_store_requests: await this.buildObjectStoreRequestSummary(args.stream),
237
+ };
238
+ }
239
+
240
+ private buildStreamSummary(stream: string, row: StreamRow, profile: StreamProfileSpec, snapshot: FullModeDetailsSnapshot) {
241
+ const observability = buildRequestObservabilityPairingDescriptor(stream, profile);
242
+ return {
243
+ name: stream,
244
+ content_type: normalizeContentType(row.content_type) ?? row.content_type,
245
+ profile: profile.kind,
246
+ ...(observability ? { observability } : {}),
247
+ created_at: timestampToIsoString(row.created_at_ms),
248
+ updated_at: timestampToIsoString(row.updated_at_ms),
249
+ expires_at: timestampToIsoString(row.expires_at_ms),
250
+ ttl_seconds: row.ttl_seconds,
251
+ stream_seq: row.stream_seq,
252
+ closed: row.closed !== 0,
253
+ epoch: row.epoch,
254
+ next_offset: row.next_offset.toString(),
255
+ sealed_through: row.sealed_through.toString(),
256
+ uploaded_through: row.uploaded_through.toString(),
257
+ segment_count: snapshot.segmentCount,
258
+ uploaded_segment_count: snapshot.uploadedSegmentCount,
259
+ pending_rows: row.pending_rows.toString(),
260
+ pending_bytes: row.pending_bytes.toString(),
261
+ total_size_bytes: row.logical_size_bytes.toString(),
262
+ wal_rows: row.wal_rows.toString(),
263
+ wal_bytes: row.wal_bytes.toString(),
264
+ last_append_at: timestampToIsoString(row.last_append_ms),
265
+ last_segment_cut_at: timestampToIsoString(row.last_segment_cut_ms),
266
+ };
267
+ }
268
+
269
+ private buildIndexLagMsFromLookup(lagLookup: LagLookup, headRow: StreamRow, coveredSegmentCount: number): string | null {
270
+ if (coveredSegmentCount <= 0) return null;
271
+ const coveredLastAppendMs = lagLookup.get(coveredSegmentCount - 1) ?? null;
272
+ if (coveredLastAppendMs == null) return null;
273
+ const lagMs = headRow.last_append_ms > coveredLastAppendMs ? headRow.last_append_ms - coveredLastAppendMs : 0n;
274
+ return lagMs.toString();
275
+ }
276
+
277
+ private async buildStorageBreakdown(
278
+ stream: string,
279
+ row: StreamRow,
280
+ snapshot: FullModeDetailsSnapshot,
281
+ currentCompanionRows: Array<{
282
+ sections_json: string;
283
+ section_sizes_json: string;
284
+ size_bytes: number;
285
+ }>,
286
+ indexStatus: IndexStatusPayload
287
+ ) {
288
+ const manifest = snapshot.manifest;
289
+ const schemaRow = snapshot.schemaRow;
290
+ const uploadedSegmentBytes = snapshot.uploadedSegmentBytes;
291
+ const pendingSealedSegmentBytes = snapshot.pendingSealedSegmentBytes;
292
+ const routingIndexStorage = snapshot.routingIndexStorage;
293
+ const routingLexiconStorage =
294
+ snapshot.lexiconIndexStorage
295
+ .find((entry) => entry.source_kind === "routing_key" && entry.source_name === "") ?? { object_count: 0, bytes: 0n };
296
+ const companionStorage = snapshot.bundledCompanionStorage;
297
+ const localStorageUsage: LocalStorageUsage = {
298
+ segment_cache_bytes: 0,
299
+ routing_index_cache_bytes: 0,
300
+ exact_index_cache_bytes: 0,
301
+ lexicon_index_cache_bytes: 0,
302
+ companion_cache_bytes: 0,
303
+ ...(this.opts.getLocalStorageUsage?.(stream) ?? {}),
304
+ };
305
+ if (!this.opts.storageStatsStore) throw dsError("storage stats store is not available");
306
+ const sharedBytes = BigInt((await this.opts.storageStatsStore.getWalDbSizeBytes()) + (await this.opts.storageStatsStore.getMetaDbSizeBytes()));
307
+ const sharedDbStorage = {
308
+ shared_db_total_bytes: sharedBytes.toString(),
309
+ ...(this.opts.storageBackend === "postgres"
310
+ ? { postgres_shared_total_bytes: sharedBytes.toString() }
311
+ : { sqlite_shared_total_bytes: sharedBytes.toString() }),
312
+ };
313
+ const exactIndexBytes = indexStatus.exact_indexes.reduce((sum: bigint, entry) => sum + BigInt(entry.bytes_at_rest), 0n);
314
+ const familyBytes = new Map<string, bigint>();
315
+ for (const row of currentCompanionRows) {
316
+ const sizes = parseCompanionSectionSizes(row.section_sizes_json);
317
+ for (const [kind, size] of Object.entries(sizes)) {
318
+ familyBytes.set(kind, (familyBytes.get(kind) ?? 0n) + BigInt(size));
319
+ }
320
+ }
321
+ return {
322
+ object_storage: {
323
+ total_bytes: (
324
+ uploadedSegmentBytes +
325
+ routingIndexStorage.bytes +
326
+ routingLexiconStorage.bytes +
327
+ exactIndexBytes +
328
+ companionStorage.bytes +
329
+ (manifest.last_uploaded_size_bytes ?? 0n) +
330
+ (schemaRow?.uploaded_size_bytes ?? 0n)
331
+ ).toString(),
332
+ segments_bytes: uploadedSegmentBytes.toString(),
333
+ indexes_bytes: (routingIndexStorage.bytes + routingLexiconStorage.bytes + exactIndexBytes + companionStorage.bytes).toString(),
334
+ manifest_and_meta_bytes: ((manifest.last_uploaded_size_bytes ?? 0n) + (schemaRow?.uploaded_size_bytes ?? 0n)).toString(),
335
+ manifest_bytes: (manifest.last_uploaded_size_bytes ?? 0n).toString(),
336
+ schema_registry_bytes: (schemaRow?.uploaded_size_bytes ?? 0n).toString(),
337
+ segment_object_count: indexStatus.segments.uploaded_count,
338
+ routing_index_object_count: routingIndexStorage.object_count,
339
+ routing_lexicon_object_count: routingLexiconStorage.object_count,
340
+ exact_index_object_count: indexStatus.exact_indexes.reduce((sum: number, entry) => sum + entry.object_count, 0),
341
+ bundled_companion_object_count: companionStorage.object_count,
342
+ },
343
+ local_storage: {
344
+ total_bytes: (
345
+ row.wal_bytes +
346
+ pendingSealedSegmentBytes +
347
+ BigInt(localStorageUsage.segment_cache_bytes) +
348
+ BigInt(localStorageUsage.routing_index_cache_bytes) +
349
+ BigInt(localStorageUsage.exact_index_cache_bytes) +
350
+ BigInt(localStorageUsage.lexicon_index_cache_bytes) +
351
+ BigInt(localStorageUsage.companion_cache_bytes)
352
+ ).toString(),
353
+ wal_retained_bytes: row.wal_bytes.toString(),
354
+ pending_tail_bytes: row.pending_bytes.toString(),
355
+ pending_sealed_segment_bytes: pendingSealedSegmentBytes.toString(),
356
+ segment_cache_bytes: String(localStorageUsage.segment_cache_bytes),
357
+ routing_index_cache_bytes: String(localStorageUsage.routing_index_cache_bytes),
358
+ exact_index_cache_bytes: String(localStorageUsage.exact_index_cache_bytes),
359
+ lexicon_index_cache_bytes: String(localStorageUsage.lexicon_index_cache_bytes),
360
+ companion_cache_bytes: String(localStorageUsage.companion_cache_bytes),
361
+ ...sharedDbStorage,
362
+ },
363
+ companion_families: {
364
+ exact_bytes: String(familyBytes.get("exact") ?? 0n),
365
+ col_bytes: String(familyBytes.get("col") ?? 0n),
366
+ fts_bytes: String(familyBytes.get("fts") ?? 0n),
367
+ agg_bytes: String(familyBytes.get("agg") ?? 0n),
368
+ mblk_bytes: String(familyBytes.get("mblk") ?? 0n),
369
+ },
370
+ };
371
+ }
372
+
373
+ private async buildObjectStoreRequestSummary(stream: string) {
374
+ if (!this.opts.objectStoreAccountingStore) throw dsError("object-store accounting store is not available");
375
+ const summary = await this.opts.objectStoreAccountingStore.getObjectStoreRequestSummaryByHash(streamHash16Hex(stream));
376
+ return {
377
+ puts: summary.puts.toString(),
378
+ reads: summary.reads.toString(),
379
+ gets: summary.gets.toString(),
380
+ heads: summary.heads.toString(),
381
+ lists: summary.lists.toString(),
382
+ deletes: summary.deletes.toString(),
383
+ by_artifact: summary.by_artifact.map((entry) => ({
384
+ artifact: entry.artifact,
385
+ puts: entry.puts.toString(),
386
+ gets: entry.gets.toString(),
387
+ heads: entry.heads.toString(),
388
+ lists: entry.lists.toString(),
389
+ deletes: entry.deletes.toString(),
390
+ reads: entry.reads.toString(),
391
+ })),
392
+ };
393
+ }
394
+
395
+ private async buildIndexStatus(
396
+ stream: string,
397
+ row: StreamRow,
398
+ reg: SchemaRegistry,
399
+ profileKind: string,
400
+ snapshot: FullModeDetailsSnapshot,
401
+ configuredExact: Array<{ name: string; kind: string; configHash: string }>
402
+ ): Promise<IndexStatusSnapshot> {
403
+ const segmentCount = snapshot.segmentCount;
404
+ const uploadedSegmentCount = snapshot.uploadedSegmentCount;
405
+ const manifest = snapshot.manifest;
406
+
407
+ const routingState = snapshot.routingState;
408
+ const routingRuns = snapshot.routingRuns;
409
+ const retiredRoutingRuns = snapshot.retiredRoutingRuns;
410
+ const routingStorage = snapshot.routingIndexStorage;
411
+ const routingLexiconState = snapshot.routingLexiconState;
412
+ const routingLexiconRuns = snapshot.routingLexiconRuns;
413
+ const retiredRoutingLexiconRuns = snapshot.retiredRoutingLexiconRuns;
414
+ const routingLexiconStorage =
415
+ snapshot.lexiconIndexStorage
416
+ .find((entry) => entry.source_kind === "routing_key" && entry.source_name === "") ?? { object_count: 0, bytes: 0n };
417
+ const secondaryIndexStorage = new Map(snapshot.secondaryIndexStorage.map((entry) => [entry.index_name, entry]));
418
+ const exactSnapshots = new Map(snapshot.exactIndexes.map((entry) => [entry.indexName, entry]));
419
+
420
+ const exactCoverages: ExactIndexCoverage[] = [];
421
+ for (const { name, kind, configHash } of configuredExact) {
422
+ const exactSnapshot = exactSnapshots.get(name);
423
+ const state = exactSnapshot?.state ?? null;
424
+ const configMatches = state?.config_hash === configHash;
425
+ const indexedSegmentCount = configMatches ? (state?.indexed_through ?? 0) : 0;
426
+ exactCoverages.push({ name, kind, configHash, configMatches, indexedSegmentCount });
427
+ }
428
+
429
+ const desiredCompanionPlan = buildDesiredSearchCompanionPlan(reg);
430
+ const desiredCompanionHash = hashSearchCompanionPlan(desiredCompanionPlan);
431
+ const companionPlanRow = snapshot.companionPlan;
432
+ const desiredIndexPlanGeneration =
433
+ Object.values(desiredCompanionPlan.families).some(Boolean)
434
+ ? companionPlanRow
435
+ ? companionPlanRow.plan_hash === desiredCompanionHash
436
+ ? companionPlanRow.generation
437
+ : companionPlanRow.generation + 1
438
+ : 1
439
+ : 0;
440
+ const companionRows = snapshot.companionRows;
441
+ const currentCompanionRows = companionRows.filter((row) => row.plan_generation === desiredIndexPlanGeneration);
442
+ const currentCompanionBytes = currentCompanionRows.reduce((sum, entry) => sum + BigInt(entry.size_bytes), 0n);
443
+ const searchFamilyCoverages: SearchFamilyCoverage[] = [];
444
+ for (const { family, fields } of configuredSearchFamilies(reg.search)) {
445
+ const coveredSegmentCount = currentCompanionRows.filter((row) => parseCompanionSections(row.sections_json).has(family)).length;
446
+ const contiguousCoveredCount = contiguousCoveredSegmentCount(currentCompanionRows, family);
447
+ let familyBytes = 0n;
448
+ let familyObjectCount = 0;
449
+ for (const row of currentCompanionRows) {
450
+ const size = parseCompanionSectionSizes(row.section_sizes_json)[family];
451
+ if (size == null) continue;
452
+ familyBytes += BigInt(size);
453
+ familyObjectCount += 1;
454
+ }
455
+ searchFamilyCoverages.push({
456
+ family,
457
+ fields,
458
+ coveredSegmentCount,
459
+ contiguousCoveredCount,
460
+ familyBytes,
461
+ familyObjectCount,
462
+ });
463
+ }
464
+
465
+ const lagSegmentIndexes = new Set<number>();
466
+ const addLagPoint = (coveredSegmentCount: number): void => {
467
+ if (coveredSegmentCount > 0) lagSegmentIndexes.add(coveredSegmentCount - 1);
468
+ };
469
+ addLagPoint(routingState?.indexed_through ?? 0);
470
+ addLagPoint(routingLexiconState?.indexed_through ?? 0);
471
+ for (const coverage of exactCoverages) addLagPoint(coverage.indexedSegmentCount);
472
+ for (const coverage of searchFamilyCoverages) addLagPoint(coverage.contiguousCoveredCount);
473
+ const lagLookup = await this.opts.detailsStore.getFullModeLagSnapshot({
474
+ stream,
475
+ segmentIndexes: Array.from(lagSegmentIndexes),
476
+ });
477
+
478
+ const exactIndexes: ExactIndexStatus[] = [];
479
+ for (const { name, kind, configMatches, indexedSegmentCount } of exactCoverages) {
480
+ const exactSnapshot = exactSnapshots.get(name);
481
+ const state = exactSnapshot?.state ?? null;
482
+ const storage = secondaryIndexStorage.get(name);
483
+ exactIndexes.push({
484
+ name,
485
+ kind,
486
+ indexed_segment_count: indexedSegmentCount,
487
+ lag_segments: Math.max(0, uploadedSegmentCount - indexedSegmentCount),
488
+ lag_ms: this.buildIndexLagMsFromLookup(lagLookup, row, indexedSegmentCount),
489
+ bytes_at_rest: String(storage?.bytes ?? 0n),
490
+ object_count: storage?.object_count ?? 0,
491
+ active_run_count: exactSnapshot?.activeRuns.length ?? 0,
492
+ retired_run_count: exactSnapshot?.retiredRuns.length ?? 0,
493
+ fully_indexed_uploaded_segments: configMatches && indexedSegmentCount >= uploadedSegmentCount,
494
+ stale_configuration: !configMatches,
495
+ updated_at: timestampToIsoString(state?.updated_at_ms ?? null),
496
+ });
497
+ }
498
+
499
+ const searchFamilies: SearchFamilyStatus[] = [];
500
+ for (const { family, fields, coveredSegmentCount, contiguousCoveredCount, familyBytes, familyObjectCount } of searchFamilyCoverages) {
501
+ searchFamilies.push({
502
+ family,
503
+ fields,
504
+ plan_generation: desiredIndexPlanGeneration,
505
+ covered_segment_count: coveredSegmentCount,
506
+ contiguous_covered_segment_count: contiguousCoveredCount,
507
+ lag_segments: Math.max(0, uploadedSegmentCount - contiguousCoveredCount),
508
+ lag_ms: this.buildIndexLagMsFromLookup(lagLookup, row, contiguousCoveredCount),
509
+ bytes_at_rest: familyBytes.toString(),
510
+ object_count: familyObjectCount,
511
+ stale_segment_count: Math.max(0, uploadedSegmentCount - coveredSegmentCount),
512
+ fully_indexed_uploaded_segments: coveredSegmentCount >= uploadedSegmentCount,
513
+ updated_at: timestampToIsoString(companionPlanRow?.updated_at_ms ?? null),
514
+ });
515
+ }
516
+
517
+ return {
518
+ currentCompanionRows,
519
+ payload: {
520
+ stream,
521
+ profile: profileKind,
522
+ desired_index_plan_generation: desiredIndexPlanGeneration,
523
+ segments: {
524
+ total_count: segmentCount,
525
+ uploaded_count: uploadedSegmentCount,
526
+ },
527
+ manifest: {
528
+ generation: manifest.generation,
529
+ uploaded_generation: manifest.uploaded_generation,
530
+ last_uploaded_at: timestampToIsoString(manifest.last_uploaded_at_ms),
531
+ last_uploaded_etag: manifest.last_uploaded_etag,
532
+ last_uploaded_size_bytes: manifest.last_uploaded_size_bytes?.toString() ?? null,
533
+ },
534
+ routing_key_index: {
535
+ configured: reg.routingKey != null,
536
+ indexed_segment_count: routingState?.indexed_through ?? 0,
537
+ lag_segments: Math.max(0, uploadedSegmentCount - (routingState?.indexed_through ?? 0)),
538
+ lag_ms: this.buildIndexLagMsFromLookup(lagLookup, row, routingState?.indexed_through ?? 0),
539
+ bytes_at_rest: routingStorage.bytes.toString(),
540
+ object_count: routingStorage.object_count,
541
+ active_run_count: routingRuns.length,
542
+ retired_run_count: retiredRoutingRuns.length,
543
+ fully_indexed_uploaded_segments: reg.routingKey == null ? true : (routingState?.indexed_through ?? 0) >= uploadedSegmentCount,
544
+ updated_at: timestampToIsoString(routingState?.updated_at_ms ?? null),
545
+ },
546
+ routing_key_lexicon: {
547
+ configured: reg.routingKey != null,
548
+ indexed_segment_count: routingLexiconState?.indexed_through ?? 0,
549
+ lag_segments: Math.max(0, uploadedSegmentCount - (routingLexiconState?.indexed_through ?? 0)),
550
+ lag_ms: this.buildIndexLagMsFromLookup(lagLookup, row, routingLexiconState?.indexed_through ?? 0),
551
+ bytes_at_rest: routingLexiconStorage.bytes.toString(),
552
+ object_count: routingLexiconStorage.object_count,
553
+ active_run_count: routingLexiconRuns.length,
554
+ retired_run_count: retiredRoutingLexiconRuns.length,
555
+ fully_indexed_uploaded_segments: reg.routingKey == null ? true : (routingLexiconState?.indexed_through ?? 0) >= uploadedSegmentCount,
556
+ updated_at: timestampToIsoString(routingLexiconState?.updated_at_ms ?? null),
557
+ },
558
+ exact_indexes: exactIndexes,
559
+ bundled_companions: {
560
+ object_count: currentCompanionRows.length,
561
+ bytes_at_rest: currentCompanionBytes.toString(),
562
+ fully_indexed_uploaded_segments: currentCompanionRows.length >= uploadedSegmentCount,
563
+ },
564
+ search_families: searchFamilies,
565
+ },
566
+ };
567
+ }
568
+ }
@@ -0,0 +1,47 @@
1
+ import type { Config } from "./config";
2
+ import type { StreamStore } from "./store/capabilities";
3
+
4
+ export class ExpirySweeper {
5
+ private readonly cfg: Config;
6
+ private readonly store: StreamStore;
7
+ private timer: any | null = null;
8
+ private tickPromise: Promise<void> | null = null;
9
+
10
+ constructor(cfg: Config, store: StreamStore) {
11
+ this.cfg = cfg;
12
+ this.store = store;
13
+ }
14
+
15
+ start(): void {
16
+ if (this.timer || this.cfg.expirySweepIntervalMs <= 0) return;
17
+ this.timer = setInterval(() => {
18
+ void this.tick();
19
+ }, this.cfg.expirySweepIntervalMs);
20
+ }
21
+
22
+ async stop(): Promise<void> {
23
+ if (this.timer) clearInterval(this.timer);
24
+ this.timer = null;
25
+ await this.tickPromise;
26
+ }
27
+
28
+ private async tick(): Promise<void> {
29
+ if (this.tickPromise) return this.tickPromise;
30
+ this.tickPromise = this.runTick().finally(() => {
31
+ this.tickPromise = null;
32
+ });
33
+ return this.tickPromise;
34
+ }
35
+
36
+ private async runTick(): Promise<void> {
37
+ const expired = await this.store.listExpiredStreams(this.cfg.expirySweepBatchLimit);
38
+ if (expired.length === 0) return;
39
+ for (const stream of expired) {
40
+ try {
41
+ await this.store.deleteStream(stream);
42
+ } catch {
43
+ // ignore deletion errors
44
+ }
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,55 @@
1
+ import { yieldToEventLoop } from "./util/yield";
2
+
3
+ export class ForegroundActivityTracker {
4
+ private active = 0;
5
+ private lastActivityAt = 0;
6
+ private readonly idleResolvers = new Set<() => void>();
7
+
8
+ enter(): () => void {
9
+ this.lastActivityAt = Date.now();
10
+ this.active += 1;
11
+ let released = false;
12
+ return () => {
13
+ if (released) return;
14
+ released = true;
15
+ this.lastActivityAt = Date.now();
16
+ this.active = Math.max(0, this.active - 1);
17
+ if (this.active !== 0) return;
18
+ const resolvers = Array.from(this.idleResolvers);
19
+ this.idleResolvers.clear();
20
+ for (const resolve of resolvers) resolve();
21
+ };
22
+ }
23
+
24
+ isActive(): boolean {
25
+ return this.active > 0;
26
+ }
27
+
28
+ getActive(): number {
29
+ return this.active;
30
+ }
31
+
32
+ wasActiveWithin(windowMs: number): boolean {
33
+ return Date.now() - this.lastActivityAt <= windowMs;
34
+ }
35
+
36
+ async yieldBackgroundWork(maxPauseMs = 5): Promise<void> {
37
+ if (this.active === 0) {
38
+ await yieldToEventLoop();
39
+ return;
40
+ }
41
+ let idleResolve!: () => void;
42
+ const idlePromise = new Promise<void>((resolve) => {
43
+ idleResolve = resolve;
44
+ this.idleResolvers.add(resolve);
45
+ });
46
+ try {
47
+ await Promise.race([
48
+ idlePromise,
49
+ new Promise<void>((resolve) => setTimeout(resolve, maxPauseMs)),
50
+ ]);
51
+ } finally {
52
+ this.idleResolvers.delete(idleResolve);
53
+ }
54
+ }
55
+ }