@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,189 @@
1
+ import type { Pool } from "pg";
2
+ import type {
3
+ ExactIndexDetailSnapshot,
4
+ FullModeDetailsSnapshot,
5
+ FullModeDetailsSnapshotRequest,
6
+ FullModeDetailsStore,
7
+ FullModeLagSnapshotRequest,
8
+ IndexStorageSummary,
9
+ LexiconIndexStorageSummary,
10
+ SecondaryIndexStorageSummary,
11
+ } from "../store/full_mode_details_store";
12
+ import type { SchemaRegistryRow } from "../store/schema_profile_store";
13
+ import type { ManifestRow } from "../store/segment_manifest_store";
14
+ import { readU64LE } from "../util/endian";
15
+ import { getPostgresSearchCompanionPlan, listPostgresSearchSegmentCompanions } from "./companions";
16
+ import { loadPostgresLexiconIndexManifest } from "./lexicon_index";
17
+ import { loadPostgresRoutingIndexManifest } from "./routing_index";
18
+ import { loadPostgresSecondaryIndexManifest } from "./secondary_index";
19
+ import type { PgExecutor } from "./types";
20
+ import { toBigInt, toBytes } from "./rows";
21
+
22
+ export class PostgresFullModeDetailsStore implements FullModeDetailsStore {
23
+ constructor(private readonly pool: Pool) {}
24
+
25
+ async getFullModeDetailsSnapshot(request: FullModeDetailsSnapshotRequest): Promise<FullModeDetailsSnapshot> {
26
+ const stream = request.stream;
27
+ const [segmentCount, uploadedSegmentCount, manifest, schemaRow, routingIndex, secondaryIndex, lexiconIndex, companionPlan, companionRows] =
28
+ await Promise.all([
29
+ countSegments(this.pool, stream),
30
+ countUploadedSegments(this.pool, stream),
31
+ getManifestRow(this.pool, stream),
32
+ getSchemaRegistry(this.pool, stream),
33
+ loadPostgresRoutingIndexManifest(this.pool, stream),
34
+ loadPostgresSecondaryIndexManifest(this.pool, stream),
35
+ loadPostgresLexiconIndexManifest(this.pool, stream),
36
+ getPostgresSearchCompanionPlan(this.pool, stream),
37
+ listPostgresSearchSegmentCompanions(this.pool, stream),
38
+ ]);
39
+ const exactIndexes: ExactIndexDetailSnapshot[] = request.exactIndexNames.map((indexName) => ({
40
+ indexName,
41
+ state: secondaryIndex.secondaryIndexStates.find((state) => state.index_name === indexName) ?? null,
42
+ activeRuns: secondaryIndex.secondaryIndexRuns.filter((run) => run.index_name === indexName),
43
+ retiredRuns: secondaryIndex.retiredSecondaryIndexRuns.filter((run) => run.index_name === indexName),
44
+ }));
45
+ return {
46
+ segmentCount,
47
+ uploadedSegmentCount,
48
+ manifest,
49
+ schemaRow,
50
+ uploadedSegmentBytes: await sumBigInt(this.pool, `SELECT COALESCE(SUM(size_bytes), 0) AS total FROM segments WHERE stream = $1 AND uploaded_at_ms IS NOT NULL;`, [stream]),
51
+ pendingSealedSegmentBytes: await sumBigInt(this.pool, `SELECT COALESCE(SUM(size_bytes), 0) AS total FROM segments WHERE stream = $1 AND uploaded_at_ms IS NULL;`, [stream]),
52
+ routingIndexStorage: await storageSummary(this.pool, "index_runs", stream),
53
+ secondaryIndexStorage: await secondaryStorageSummary(this.pool, stream),
54
+ lexiconIndexStorage: await lexiconStorageSummary(this.pool, stream),
55
+ bundledCompanionStorage: await storageSummary(this.pool, "search_segment_companions", stream),
56
+ routingState: routingIndex.indexState,
57
+ routingRuns: routingIndex.indexRuns,
58
+ retiredRoutingRuns: routingIndex.retiredRuns,
59
+ exactIndexes,
60
+ routingLexiconState:
61
+ lexiconIndex.lexiconIndexStates.find((state) => state.source_kind === "routing_key" && state.source_name === "") ?? null,
62
+ routingLexiconRuns: lexiconIndex.lexiconIndexRuns.filter((run) => run.source_kind === "routing_key" && run.source_name === ""),
63
+ retiredRoutingLexiconRuns: lexiconIndex.retiredLexiconIndexRuns.filter(
64
+ (run) => run.source_kind === "routing_key" && run.source_name === ""
65
+ ),
66
+ companionPlan,
67
+ companionRows,
68
+ };
69
+ }
70
+
71
+ async getFullModeLagSnapshot(request: FullModeLagSnapshotRequest): Promise<Map<number, bigint>> {
72
+ const meta = await getSegmentMeta(this.pool, request.stream);
73
+ const out = new Map<number, bigint>();
74
+ if (!meta) return out;
75
+ const indexes = Array.from(new Set(request.segmentIndexes.filter((index) => Number.isInteger(index) && index >= 0))).sort((a, b) => a - b);
76
+ for (const index of indexes) {
77
+ if (index >= meta.segmentCount) continue;
78
+ const offset = index * 8;
79
+ if (offset + 8 > meta.segmentLastTs.byteLength) continue;
80
+ out.set(index, readU64LE(meta.segmentLastTs, offset) / 1_000_000n);
81
+ }
82
+ return out;
83
+ }
84
+ }
85
+
86
+ async function countSegments(executor: PgExecutor, stream: string): Promise<number> {
87
+ const res = await executor.query<{ count: string }>(`SELECT COUNT(*) AS count FROM segments WHERE stream = $1;`, [stream]);
88
+ return Number(res.rows[0]?.count ?? 0);
89
+ }
90
+
91
+ async function countUploadedSegments(executor: PgExecutor, stream: string): Promise<number> {
92
+ const res = await executor.query<{ max_idx: number | string | null }>(
93
+ `SELECT MAX(segment_index) AS max_idx FROM segments WHERE stream = $1 AND r2_etag IS NOT NULL;`,
94
+ [stream]
95
+ );
96
+ const maxIdx = res.rows[0]?.max_idx == null ? -1 : Number(res.rows[0]!.max_idx);
97
+ return maxIdx >= 0 ? maxIdx + 1 : 0;
98
+ }
99
+
100
+ async function getManifestRow(executor: PgExecutor, stream: string): Promise<ManifestRow> {
101
+ const res = await executor.query(
102
+ `SELECT stream, generation, uploaded_generation, last_uploaded_at_ms, last_uploaded_etag, last_uploaded_size_bytes
103
+ FROM manifests WHERE stream = $1 LIMIT 1;`,
104
+ [stream]
105
+ );
106
+ if (!res.rows[0]) {
107
+ return { stream, generation: 0, uploaded_generation: 0, last_uploaded_at_ms: null, last_uploaded_etag: null, last_uploaded_size_bytes: null };
108
+ }
109
+ const row = res.rows[0];
110
+ return {
111
+ stream: String(row.stream),
112
+ generation: Number(row.generation),
113
+ uploaded_generation: Number(row.uploaded_generation),
114
+ last_uploaded_at_ms: row.last_uploaded_at_ms == null ? null : toBigInt(row.last_uploaded_at_ms),
115
+ last_uploaded_etag: row.last_uploaded_etag == null ? null : String(row.last_uploaded_etag),
116
+ last_uploaded_size_bytes: row.last_uploaded_size_bytes == null ? null : toBigInt(row.last_uploaded_size_bytes),
117
+ };
118
+ }
119
+
120
+ async function getSchemaRegistry(executor: PgExecutor, stream: string): Promise<SchemaRegistryRow | null> {
121
+ const res = await executor.query(`SELECT stream, schema_json, updated_at_ms, uploaded_size_bytes FROM schemas WHERE stream = $1 LIMIT 1;`, [stream]);
122
+ const row = res.rows[0];
123
+ if (!row) return null;
124
+ return {
125
+ stream: String(row.stream),
126
+ registry_json: String(row.schema_json),
127
+ updated_at_ms: toBigInt(row.updated_at_ms),
128
+ uploaded_size_bytes: toBigInt(row.uploaded_size_bytes ?? 0),
129
+ };
130
+ }
131
+
132
+ async function sumBigInt(executor: PgExecutor, sql: string, params: unknown[]): Promise<bigint> {
133
+ const res = await executor.query<{ total: string | number | bigint | null }>(sql, params);
134
+ return toBigInt(res.rows[0]?.total ?? 0);
135
+ }
136
+
137
+ async function storageSummary(executor: PgExecutor, tableName: "index_runs" | "search_segment_companions", stream: string): Promise<IndexStorageSummary> {
138
+ const res = await executor.query<{ cnt: string; total: string | number | bigint | null }>(
139
+ `SELECT COUNT(*) AS cnt, COALESCE(SUM(size_bytes), 0) AS total FROM ${tableName} WHERE stream = $1;`,
140
+ [stream]
141
+ );
142
+ return { object_count: Number(res.rows[0]?.cnt ?? 0), bytes: toBigInt(res.rows[0]?.total ?? 0) };
143
+ }
144
+
145
+ async function secondaryStorageSummary(executor: PgExecutor, stream: string): Promise<SecondaryIndexStorageSummary[]> {
146
+ const res = await executor.query(
147
+ `SELECT index_name, COUNT(*) AS cnt, COALESCE(SUM(size_bytes), 0) AS total
148
+ FROM secondary_index_runs
149
+ WHERE stream = $1
150
+ GROUP BY index_name
151
+ ORDER BY index_name ASC;`,
152
+ [stream]
153
+ );
154
+ return res.rows.map((row) => ({
155
+ index_name: String(row.index_name),
156
+ object_count: Number(row.cnt ?? 0),
157
+ bytes: toBigInt(row.total ?? 0),
158
+ }));
159
+ }
160
+
161
+ async function lexiconStorageSummary(executor: PgExecutor, stream: string): Promise<LexiconIndexStorageSummary[]> {
162
+ const res = await executor.query(
163
+ `SELECT source_kind, source_name, COUNT(*) AS cnt, COALESCE(SUM(size_bytes), 0) AS total
164
+ FROM lexicon_index_runs
165
+ WHERE stream = $1
166
+ GROUP BY source_kind, source_name
167
+ ORDER BY source_kind ASC, source_name ASC;`,
168
+ [stream]
169
+ );
170
+ return res.rows.map((row) => ({
171
+ source_kind: String(row.source_kind),
172
+ source_name: String(row.source_name),
173
+ object_count: Number(row.cnt ?? 0),
174
+ bytes: toBigInt(row.total ?? 0),
175
+ }));
176
+ }
177
+
178
+ async function getSegmentMeta(executor: PgExecutor, stream: string): Promise<{ segmentCount: number; segmentLastTs: Uint8Array } | null> {
179
+ const res = await executor.query(
180
+ `SELECT segment_count, segment_last_ts
181
+ FROM stream_segment_meta
182
+ WHERE stream = $1
183
+ LIMIT 1;`,
184
+ [stream]
185
+ );
186
+ const row = res.rows[0];
187
+ if (!row) return null;
188
+ return { segmentCount: Number(row.segment_count), segmentLastTs: toBytes(row.segment_last_ts) };
189
+ }
@@ -0,0 +1,260 @@
1
+ import type { LexiconIndexStore } from "../store/index_store";
2
+ import type { LexiconIndexRunRow, LexiconIndexStateRow } from "../store/rows";
3
+ import type { WalReadRow } from "../store/wal_store";
4
+ import type { PgExecutor } from "./types";
5
+ import { pgInt, toBigInt, PostgresIndexSharedStore } from "./rows";
6
+
7
+ export class PostgresLexiconIndexStore extends PostgresIndexSharedStore implements LexiconIndexStore {
8
+ constructor(
9
+ pool: ConstructorParameters<typeof PostgresIndexSharedStore>[0],
10
+ nowMs: () => bigint,
11
+ private readonly readWal: (stream: string, startOffset: bigint, endOffset: bigint, routingKey?: Uint8Array) => AsyncIterable<WalReadRow>
12
+ ) {
13
+ super(pool, nowMs);
14
+ }
15
+
16
+ readWalRange(stream: string, startOffset: bigint, endOffset: bigint, routingKey?: Uint8Array): AsyncIterable<WalReadRow> {
17
+ return this.readWal(stream, startOffset, endOffset, routingKey);
18
+ }
19
+
20
+ async getLexiconIndexState(stream: string, sourceKind: string, sourceName: string): Promise<LexiconIndexStateRow | null> {
21
+ return getPostgresLexiconIndexState(this.pool, stream, sourceKind, sourceName);
22
+ }
23
+
24
+ async upsertLexiconIndexState(stream: string, sourceKind: string, sourceName: string, indexedThrough: number): Promise<void> {
25
+ await upsertPostgresLexiconIndexState(this.pool, this.nowMs(), stream, sourceKind, sourceName, indexedThrough);
26
+ }
27
+
28
+ async updateLexiconIndexedThrough(stream: string, sourceKind: string, sourceName: string, indexedThrough: number): Promise<void> {
29
+ await this.pool.query(
30
+ `UPDATE lexicon_index_state
31
+ SET indexed_through = $1, updated_at_ms = $2
32
+ WHERE stream = $3 AND source_kind = $4 AND source_name = $5;`,
33
+ [indexedThrough, pgInt(this.nowMs()), stream, sourceKind, sourceName]
34
+ );
35
+ }
36
+
37
+ async listLexiconIndexRuns(stream: string, sourceKind: string, sourceName: string): Promise<LexiconIndexRunRow[]> {
38
+ return listPostgresLexiconIndexRuns(this.pool, stream, sourceKind, sourceName, false);
39
+ }
40
+
41
+ async listLexiconIndexRunsAll(stream: string, sourceKind: string, sourceName: string): Promise<LexiconIndexRunRow[]> {
42
+ return listPostgresLexiconIndexRuns(this.pool, stream, sourceKind, sourceName, true);
43
+ }
44
+
45
+ async listRetiredLexiconIndexRuns(stream: string, sourceKind: string, sourceName: string): Promise<LexiconIndexRunRow[]> {
46
+ return listPostgresRetiredLexiconIndexRuns(this.pool, stream, sourceKind, sourceName);
47
+ }
48
+
49
+ async insertLexiconIndexRun(row: Omit<LexiconIndexRunRow, "retired_gen" | "retired_at_ms">): Promise<void> {
50
+ await insertPostgresLexiconIndexRun(this.pool, row);
51
+ }
52
+
53
+ async retireLexiconIndexRuns(runIds: string[], retiredGen: number, retiredAtMs: bigint): Promise<void> {
54
+ await retirePostgresLexiconIndexRuns(this.pool, runIds, retiredGen, retiredAtMs);
55
+ }
56
+
57
+ async deleteLexiconIndexRuns(runIds: string[]): Promise<void> {
58
+ if (runIds.length === 0) return;
59
+ await this.pool.query(`DELETE FROM lexicon_index_runs WHERE run_id = ANY($1::text[]);`, [runIds]);
60
+ }
61
+
62
+ async deleteLexiconIndexSource(stream: string, sourceKind: string, sourceName: string): Promise<void> {
63
+ const client = await this.pool.connect();
64
+ try {
65
+ await client.query("BEGIN");
66
+ await client.query(`DELETE FROM lexicon_index_runs WHERE stream = $1 AND source_kind = $2 AND source_name = $3;`, [
67
+ stream,
68
+ sourceKind,
69
+ sourceName,
70
+ ]);
71
+ await client.query(`DELETE FROM lexicon_index_state WHERE stream = $1 AND source_kind = $2 AND source_name = $3;`, [
72
+ stream,
73
+ sourceKind,
74
+ sourceName,
75
+ ]);
76
+ await client.query("COMMIT");
77
+ } catch (error) {
78
+ await client.query("ROLLBACK").catch(() => {});
79
+ throw error;
80
+ } finally {
81
+ client.release();
82
+ }
83
+ }
84
+ }
85
+ export async function insertPostgresLexiconIndexRun(
86
+ executor: PgExecutor,
87
+ row: Omit<LexiconIndexRunRow, "retired_gen" | "retired_at_ms">,
88
+ opts: { idempotent?: boolean } = {}
89
+ ): Promise<void> {
90
+ const conflictSql = opts.idempotent
91
+ ? `ON CONFLICT(run_id) DO UPDATE SET
92
+ stream = excluded.stream,
93
+ source_kind = excluded.source_kind,
94
+ source_name = excluded.source_name,
95
+ level = excluded.level,
96
+ start_segment = excluded.start_segment,
97
+ end_segment = excluded.end_segment,
98
+ object_key = excluded.object_key,
99
+ size_bytes = excluded.size_bytes,
100
+ record_count = excluded.record_count,
101
+ retired_gen = NULL,
102
+ retired_at_ms = NULL`
103
+ : "";
104
+ await executor.query(
105
+ `INSERT INTO lexicon_index_runs(
106
+ run_id, stream, source_kind, source_name, level, start_segment, end_segment, object_key, size_bytes, record_count
107
+ )
108
+ VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
109
+ ${conflictSql};`,
110
+ [
111
+ row.run_id,
112
+ row.stream,
113
+ row.source_kind,
114
+ row.source_name,
115
+ row.level,
116
+ row.start_segment,
117
+ row.end_segment,
118
+ row.object_key,
119
+ row.size_bytes,
120
+ row.record_count,
121
+ ]
122
+ );
123
+ }
124
+ export async function upsertPostgresLexiconIndexState(
125
+ executor: PgExecutor,
126
+ nowMs: bigint,
127
+ stream: string,
128
+ sourceKind: string,
129
+ sourceName: string,
130
+ indexedThrough: number
131
+ ): Promise<void> {
132
+ await executor.query(
133
+ `INSERT INTO lexicon_index_state(stream, source_kind, source_name, indexed_through, updated_at_ms)
134
+ VALUES($1, $2, $3, $4, $5)
135
+ ON CONFLICT(stream, source_kind, source_name) DO UPDATE SET
136
+ indexed_through = excluded.indexed_through,
137
+ updated_at_ms = excluded.updated_at_ms;`,
138
+ [stream, sourceKind, sourceName, indexedThrough, pgInt(nowMs)]
139
+ );
140
+ }
141
+
142
+ export async function retirePostgresLexiconIndexRuns(
143
+ executor: PgExecutor,
144
+ runIds: string[],
145
+ retiredGen: number,
146
+ retiredAtMs: bigint
147
+ ): Promise<void> {
148
+ if (runIds.length === 0) return;
149
+ await executor.query(`UPDATE lexicon_index_runs SET retired_gen = $1, retired_at_ms = $2 WHERE run_id = ANY($3::text[]);`, [
150
+ retiredGen,
151
+ pgInt(retiredAtMs),
152
+ runIds,
153
+ ]);
154
+ }
155
+
156
+ export async function loadPostgresLexiconIndexManifest(
157
+ executor: PgExecutor,
158
+ stream: string
159
+ ): Promise<{
160
+ lexiconIndexStates: LexiconIndexStateRow[];
161
+ lexiconIndexRuns: LexiconIndexRunRow[];
162
+ retiredLexiconIndexRuns: LexiconIndexRunRow[];
163
+ }> {
164
+ const lexiconIndexStates = await listPostgresLexiconIndexStates(executor, stream);
165
+ const lexiconIndexRuns = (
166
+ await Promise.all(
167
+ lexiconIndexStates.map((state) => listPostgresLexiconIndexRuns(executor, stream, state.source_kind, state.source_name, false))
168
+ )
169
+ ).flat();
170
+ const retiredLexiconIndexRuns = (
171
+ await Promise.all(
172
+ lexiconIndexStates.map((state) => listPostgresRetiredLexiconIndexRuns(executor, stream, state.source_kind, state.source_name))
173
+ )
174
+ ).flat();
175
+ return { lexiconIndexStates, lexiconIndexRuns, retiredLexiconIndexRuns };
176
+ }
177
+
178
+ async function getPostgresLexiconIndexState(
179
+ executor: PgExecutor,
180
+ stream: string,
181
+ sourceKind: string,
182
+ sourceName: string
183
+ ): Promise<LexiconIndexStateRow | null> {
184
+ const res = await executor.query(
185
+ `SELECT * FROM lexicon_index_state
186
+ WHERE stream = $1 AND source_kind = $2 AND source_name = $3
187
+ LIMIT 1;`,
188
+ [stream, sourceKind, sourceName]
189
+ );
190
+ return res.rows[0] ? coerceLexiconIndexStateRow(res.rows[0]) : null;
191
+ }
192
+
193
+ async function listPostgresLexiconIndexStates(executor: PgExecutor, stream: string): Promise<LexiconIndexStateRow[]> {
194
+ const res = await executor.query(
195
+ `SELECT * FROM lexicon_index_state
196
+ WHERE stream = $1
197
+ ORDER BY source_kind ASC, source_name ASC;`,
198
+ [stream]
199
+ );
200
+ return res.rows.map(coerceLexiconIndexStateRow);
201
+ }
202
+
203
+ async function listPostgresLexiconIndexRuns(
204
+ executor: PgExecutor,
205
+ stream: string,
206
+ sourceKind: string,
207
+ sourceName: string,
208
+ includeRetired: boolean
209
+ ): Promise<LexiconIndexRunRow[]> {
210
+ const retiredClause = includeRetired ? "" : " AND retired_gen IS NULL";
211
+ const res = await executor.query(
212
+ `SELECT * FROM lexicon_index_runs
213
+ WHERE stream = $1 AND source_kind = $2 AND source_name = $3${retiredClause}
214
+ ORDER BY level ASC, start_segment ASC, end_segment ASC;`,
215
+ [stream, sourceKind, sourceName]
216
+ );
217
+ return res.rows.map(coerceLexiconIndexRunRow);
218
+ }
219
+
220
+ async function listPostgresRetiredLexiconIndexRuns(
221
+ executor: PgExecutor,
222
+ stream: string,
223
+ sourceKind: string,
224
+ sourceName: string
225
+ ): Promise<LexiconIndexRunRow[]> {
226
+ const res = await executor.query(
227
+ `SELECT * FROM lexicon_index_runs
228
+ WHERE stream = $1 AND source_kind = $2 AND source_name = $3 AND retired_gen IS NOT NULL
229
+ ORDER BY retired_gen ASC, retired_at_ms ASC, level ASC, start_segment ASC;`,
230
+ [stream, sourceKind, sourceName]
231
+ );
232
+ return res.rows.map(coerceLexiconIndexRunRow);
233
+ }
234
+
235
+ function coerceLexiconIndexStateRow(row: any): LexiconIndexStateRow {
236
+ return {
237
+ stream: String(row.stream),
238
+ source_kind: String(row.source_kind),
239
+ source_name: String(row.source_name),
240
+ indexed_through: Number(row.indexed_through),
241
+ updated_at_ms: toBigInt(row.updated_at_ms),
242
+ };
243
+ }
244
+
245
+ function coerceLexiconIndexRunRow(row: any): LexiconIndexRunRow {
246
+ return {
247
+ run_id: String(row.run_id),
248
+ stream: String(row.stream),
249
+ source_kind: String(row.source_kind),
250
+ source_name: String(row.source_name),
251
+ level: Number(row.level),
252
+ start_segment: Number(row.start_segment),
253
+ end_segment: Number(row.end_segment),
254
+ object_key: String(row.object_key),
255
+ size_bytes: Number(row.size_bytes),
256
+ record_count: Number(row.record_count),
257
+ retired_gen: row.retired_gen == null ? null : Number(row.retired_gen),
258
+ retired_at_ms: row.retired_at_ms == null ? null : toBigInt(row.retired_at_ms),
259
+ };
260
+ }
@@ -0,0 +1,189 @@
1
+ import type { Pool } from "pg";
2
+ import type { RoutingIndexStore } from "../store/index_store";
3
+ import type { IndexRunRow, IndexStateRow } from "../store/rows";
4
+ import type { PgExecutor } from "./types";
5
+ import { pgInt, PostgresIndexSharedStore, toBigInt, toBytes } from "./rows";
6
+
7
+ export class PostgresRoutingIndexStore extends PostgresIndexSharedStore implements RoutingIndexStore {
8
+ async getIndexState(stream: string): Promise<IndexStateRow | null> {
9
+ return getPostgresIndexState(this.pool, stream);
10
+ }
11
+
12
+ async upsertIndexState(stream: string, indexSecret: Uint8Array, indexedThrough: number): Promise<void> {
13
+ await upsertPostgresIndexState(this.pool, this.nowMs(), stream, indexSecret, indexedThrough);
14
+ }
15
+
16
+ async updateIndexedThrough(stream: string, indexedThrough: number): Promise<void> {
17
+ await this.pool.query(`UPDATE index_state SET indexed_through = $1, updated_at_ms = $2 WHERE stream = $3;`, [
18
+ indexedThrough,
19
+ pgInt(this.nowMs()),
20
+ stream,
21
+ ]);
22
+ }
23
+
24
+ async listIndexRuns(stream: string): Promise<IndexRunRow[]> {
25
+ return listPostgresIndexRuns(this.pool, stream, false);
26
+ }
27
+
28
+ async listIndexRunsAll(stream: string): Promise<IndexRunRow[]> {
29
+ return listPostgresIndexRuns(this.pool, stream, true);
30
+ }
31
+
32
+ async listRetiredIndexRuns(stream: string): Promise<IndexRunRow[]> {
33
+ const res = await this.pool.query(
34
+ `SELECT * FROM index_runs
35
+ WHERE stream = $1 AND retired_gen IS NOT NULL
36
+ ORDER BY retired_gen ASC, retired_at_ms ASC, level ASC, start_segment ASC;`,
37
+ [stream]
38
+ );
39
+ return res.rows.map(coerceIndexRunRow);
40
+ }
41
+
42
+ async insertIndexRun(row: Omit<IndexRunRow, "retired_gen" | "retired_at_ms">): Promise<void> {
43
+ await insertPostgresIndexRun(this.pool, row);
44
+ }
45
+
46
+ async retireIndexRuns(runIds: string[], retiredGen: number, retiredAtMs: bigint): Promise<void> {
47
+ await retirePostgresIndexRuns(this.pool, runIds, retiredGen, retiredAtMs);
48
+ }
49
+
50
+ async deleteIndexRuns(runIds: string[]): Promise<void> {
51
+ if (runIds.length === 0) return;
52
+ await this.pool.query(`DELETE FROM index_runs WHERE run_id = ANY($1::text[]);`, [runIds]);
53
+ }
54
+
55
+ async deleteIndex(stream: string): Promise<void> {
56
+ const client = await this.pool.connect();
57
+ try {
58
+ await client.query("BEGIN");
59
+ await client.query(`DELETE FROM index_runs WHERE stream = $1;`, [stream]);
60
+ await client.query(`DELETE FROM index_state WHERE stream = $1;`, [stream]);
61
+ await client.query("COMMIT");
62
+ } catch (error) {
63
+ await client.query("ROLLBACK").catch(() => {});
64
+ throw error;
65
+ } finally {
66
+ client.release();
67
+ }
68
+ }
69
+ }
70
+
71
+ export async function insertPostgresIndexRun(
72
+ executor: PgExecutor,
73
+ row: Omit<IndexRunRow, "retired_gen" | "retired_at_ms">,
74
+ opts: { idempotent?: boolean } = {}
75
+ ): Promise<void> {
76
+ const conflictSql = opts.idempotent
77
+ ? `ON CONFLICT(run_id) DO UPDATE SET
78
+ stream = excluded.stream,
79
+ level = excluded.level,
80
+ start_segment = excluded.start_segment,
81
+ end_segment = excluded.end_segment,
82
+ object_key = excluded.object_key,
83
+ size_bytes = excluded.size_bytes,
84
+ filter_len = excluded.filter_len,
85
+ record_count = excluded.record_count,
86
+ retired_gen = NULL,
87
+ retired_at_ms = NULL`
88
+ : "";
89
+ await executor.query(
90
+ `INSERT INTO index_runs(run_id, stream, level, start_segment, end_segment, object_key, size_bytes, filter_len, record_count)
91
+ VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9)
92
+ ${conflictSql};`,
93
+ [row.run_id, row.stream, row.level, row.start_segment, row.end_segment, row.object_key, row.size_bytes, row.filter_len, row.record_count]
94
+ );
95
+ }
96
+
97
+ export async function upsertPostgresIndexState(
98
+ executor: PgExecutor,
99
+ nowMs: bigint,
100
+ stream: string,
101
+ indexSecret: Uint8Array,
102
+ indexedThrough: number
103
+ ): Promise<void> {
104
+ await executor.query(
105
+ `INSERT INTO index_state(stream, index_secret, indexed_through, updated_at_ms)
106
+ VALUES($1, $2, $3, $4)
107
+ ON CONFLICT(stream) DO UPDATE SET
108
+ index_secret = excluded.index_secret,
109
+ indexed_through = excluded.indexed_through,
110
+ updated_at_ms = excluded.updated_at_ms;`,
111
+ [stream, Buffer.from(indexSecret), indexedThrough, pgInt(nowMs)]
112
+ );
113
+ }
114
+
115
+ export async function retirePostgresIndexRuns(
116
+ executor: PgExecutor,
117
+ runIds: string[],
118
+ retiredGen: number,
119
+ retiredAtMs: bigint
120
+ ): Promise<void> {
121
+ if (runIds.length === 0) return;
122
+ await executor.query(`UPDATE index_runs SET retired_gen = $1, retired_at_ms = $2 WHERE run_id = ANY($3::text[]);`, [
123
+ retiredGen,
124
+ pgInt(retiredAtMs),
125
+ runIds,
126
+ ]);
127
+ }
128
+
129
+ export async function loadPostgresRoutingIndexManifest(
130
+ executor: PgExecutor,
131
+ stream: string
132
+ ): Promise<{ indexState: IndexStateRow | null; indexRuns: IndexRunRow[]; retiredRuns: IndexRunRow[] }> {
133
+ return {
134
+ indexState: await getPostgresIndexState(executor, stream),
135
+ indexRuns: await listPostgresIndexRuns(executor, stream, false),
136
+ retiredRuns: await listPostgresRetiredIndexRuns(executor, stream),
137
+ };
138
+ }
139
+
140
+ async function getPostgresIndexState(executor: PgExecutor, stream: string): Promise<IndexStateRow | null> {
141
+ const res = await executor.query(`SELECT * FROM index_state WHERE stream = $1 LIMIT 1;`, [stream]);
142
+ return res.rows[0] ? coerceIndexStateRow(res.rows[0]) : null;
143
+ }
144
+
145
+ async function listPostgresIndexRuns(executor: PgExecutor, stream: string, includeRetired: boolean): Promise<IndexRunRow[]> {
146
+ const retiredClause = includeRetired ? "" : " AND retired_gen IS NULL";
147
+ const res = await executor.query(
148
+ `SELECT * FROM index_runs
149
+ WHERE stream = $1${retiredClause}
150
+ ORDER BY level ASC, start_segment ASC, end_segment ASC;`,
151
+ [stream]
152
+ );
153
+ return res.rows.map(coerceIndexRunRow);
154
+ }
155
+
156
+ async function listPostgresRetiredIndexRuns(executor: PgExecutor, stream: string): Promise<IndexRunRow[]> {
157
+ const res = await executor.query(
158
+ `SELECT * FROM index_runs
159
+ WHERE stream = $1 AND retired_gen IS NOT NULL
160
+ ORDER BY retired_gen ASC, retired_at_ms ASC, level ASC, start_segment ASC;`,
161
+ [stream]
162
+ );
163
+ return res.rows.map(coerceIndexRunRow);
164
+ }
165
+
166
+ function coerceIndexStateRow(row: any): IndexStateRow {
167
+ return {
168
+ stream: String(row.stream),
169
+ index_secret: toBytes(row.index_secret),
170
+ indexed_through: Number(row.indexed_through),
171
+ updated_at_ms: toBigInt(row.updated_at_ms),
172
+ };
173
+ }
174
+
175
+ function coerceIndexRunRow(row: any): IndexRunRow {
176
+ return {
177
+ run_id: String(row.run_id),
178
+ stream: String(row.stream),
179
+ level: Number(row.level),
180
+ start_segment: Number(row.start_segment),
181
+ end_segment: Number(row.end_segment),
182
+ object_key: String(row.object_key),
183
+ size_bytes: Number(row.size_bytes),
184
+ filter_len: Number(row.filter_len),
185
+ record_count: Number(row.record_count),
186
+ retired_gen: row.retired_gen == null ? null : Number(row.retired_gen),
187
+ retired_at_ms: row.retired_at_ms == null ? null : toBigInt(row.retired_at_ms),
188
+ };
189
+ }