@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,3144 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { createHash } from "node:crypto";
3
+ import type { Config } from "./config";
4
+ import { IngestQueue, type ProducerInfo, type AppendRow, type AppendResult } from "./ingest";
5
+ import type { ObjectStore } from "./objectstore/interface";
6
+ import type { StreamReader, ReadBatch, ReaderError, SearchHit, SearchResultBatch } from "./reader";
7
+ import { StreamNotifier } from "./notifier";
8
+ import { encodeOffset, parseOffsetResult, offsetToSeqOrNeg1, canonicalizeOffset, type ParsedOffset } from "./offset";
9
+ import { parseDurationMsResult } from "./util/duration";
10
+ import { Metrics } from "./metrics";
11
+ import { parseTimestampMsResult } from "./util/time";
12
+ import { cleanupTempSegments } from "./util/cleanup";
13
+ import { MetricsEmitter } from "./metrics_emitter";
14
+ import {
15
+ SchemaRegistryStore,
16
+ parseSchemaUpdateResult,
17
+ type SchemaRegistry,
18
+ type SchemaRegistryMutationError,
19
+ type SchemaRegistryReadError,
20
+ } from "./schema/registry";
21
+ import { decodeJsonPayloadWithRegistryResult } from "./schema/read_json";
22
+ import { resolvePointerResult } from "./util/json_pointer";
23
+ import { ExpirySweeper } from "./expiry_sweeper";
24
+ import type { StatsCollector } from "./stats";
25
+ import type { TouchProcessorStore } from "./store/touch_store";
26
+ import type { ObjectStoreAccountingStore, StorageStatsStore } from "./store/stats_accounting_store";
27
+ import { FullModeDetailsBuilder, type LocalStorageUsage } from "./details/full_mode_details";
28
+ import type { FullModeDetailsStore } from "./store/full_mode_details_store";
29
+ import { BackpressureGate } from "./backpressure";
30
+ import { MemoryPressureMonitor } from "./memory";
31
+ import { RuntimeMemorySampler } from "./runtime_memory_sampler";
32
+ import { TouchProcessorManager } from "./touch/manager";
33
+ import type { SegmentDiskCache } from "./segment/cache";
34
+ import { ConcurrencyGate } from "./concurrency_gate";
35
+ import {
36
+ buildProcessMemoryBreakdown,
37
+ type RuntimeHighWaterMark,
38
+ type RuntimeMemoryHighWaterSnapshot,
39
+ type RuntimeMemorySubsystemSnapshot,
40
+ type RuntimeMemorySnapshot,
41
+ } from "./runtime_memory";
42
+ import type { SegmenterController } from "./segment/segmenter_workers";
43
+ import type { UploaderController } from "./uploader";
44
+ import type { StreamIndexLookup } from "./index/indexer";
45
+ import { ForegroundActivityTracker } from "./foreground_activity";
46
+ import { Result } from "better-result";
47
+ import { parseReadFilterResult } from "./read_filter";
48
+ import { parseSearchRequestBodyResult, parseSearchRequestQueryResult } from "./search/query";
49
+ import { parseAggregateRequestBodyResult } from "./search/aggregate";
50
+ import {
51
+ StreamProfileStore,
52
+ parseProfileUpdateResult,
53
+ resolveCorrelationCapability,
54
+ resolveOtlpTracesCapability,
55
+ resolveJsonIngestCapability,
56
+ resolveTouchCapability,
57
+ type PreparedJsonRecord,
58
+ type StreamTouchRoute,
59
+ } from "./profiles";
60
+ import { encodeOtlpTraceExportResponse } from "./profiles/otelTraces/otlp";
61
+ import {
62
+ buildObserveSummary,
63
+ buildTimeSearchClauses,
64
+ buildTraceDetails,
65
+ choosePrimaryEvent,
66
+ compactEvlogRecord,
67
+ compactTimelineItem,
68
+ compactTraceSpanRecord,
69
+ combineSearchClauses,
70
+ parseObserveRequestResult,
71
+ quoteSearchValue,
72
+ sortTimeline,
73
+ summarizeSearchCoverage,
74
+ summarizeSearchQueryCoverage,
75
+ type ObserveSearchQueryCoverage,
76
+ } from "./observe/request";
77
+ import { buildRequestObservabilityPairingDescriptor } from "./observe/pairing";
78
+ import { dsError } from "./util/ds_error.ts";
79
+ import type { SchemaPublicationStore } from "./store/schema_publication";
80
+ import { requireWalControlPlaneStore, type WalControlPlaneStore } from "./store/capabilities";
81
+ import type { StreamRow } from "./store/rows";
82
+
83
+ export type AppDebugStore = {
84
+ close?: () => void | Promise<void>;
85
+ };
86
+
87
+ function withNosniff(headers: HeadersInit = {}): HeadersInit {
88
+ return {
89
+ "x-content-type-options": "nosniff",
90
+ ...headers,
91
+ };
92
+ }
93
+
94
+ function json(status: number, body: any, headers: HeadersInit = {}): Response {
95
+ return new Response(JSON.stringify(body), {
96
+ status,
97
+ headers: {
98
+ "content-type": "application/json; charset=utf-8",
99
+ "cache-control": "no-store",
100
+ ...withNosniff(headers),
101
+ },
102
+ });
103
+ }
104
+
105
+ const OVERLOAD_RETRY_AFTER_SECONDS = "1";
106
+ const UNAVAILABLE_RETRY_AFTER_SECONDS = "5";
107
+ const APPEND_REQUEST_TIMEOUT_MS = 3_000;
108
+ const HTTP_RESOLVER_TIMEOUT_MS = 5_000;
109
+ const SEARCH_REQUEST_TIMEOUT_MS = 3_000;
110
+ const TIMEOUT_SENTINEL = Symbol("request-timeout");
111
+ const DEFAULT_TOUCH_JOURNAL_FILTER_BYTES = 4 * (1 << 22);
112
+
113
+ type TimeoutSentinel = typeof TIMEOUT_SENTINEL;
114
+
115
+ function retryAfterHeaders(seconds: string, headers: HeadersInit = {}): HeadersInit {
116
+ return {
117
+ "retry-after": seconds,
118
+ ...headers,
119
+ };
120
+ }
121
+
122
+ function clampSearchRequestTimeoutMs(timeoutMs: number | null): number {
123
+ return timeoutMs == null ? SEARCH_REQUEST_TIMEOUT_MS : Math.min(timeoutMs, SEARCH_REQUEST_TIMEOUT_MS);
124
+ }
125
+
126
+ async function awaitWithCooperativeTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T | TimeoutSentinel> {
127
+ let timer: ReturnType<typeof setTimeout> | null = null;
128
+ try {
129
+ return await Promise.race([
130
+ promise,
131
+ new Promise<TimeoutSentinel>((resolve) => {
132
+ timer = setTimeout(() => resolve(TIMEOUT_SENTINEL), timeoutMs);
133
+ }),
134
+ ]);
135
+ } finally {
136
+ if (timer != null) clearTimeout(timer);
137
+ }
138
+ }
139
+
140
+ function isAbortLikeError(error: unknown): boolean {
141
+ return typeof error === "object" && error != null && "name" in error && (error as { name?: unknown }).name === "AbortError";
142
+ }
143
+
144
+ function searchResponseHeaders(search: SearchResultBatch): HeadersInit {
145
+ return {
146
+ "search-timed-out": search.timedOut ? "true" : "false",
147
+ "search-timeout-ms": String(search.timeoutMs ?? SEARCH_REQUEST_TIMEOUT_MS),
148
+ "search-took-ms": String(search.tookMs),
149
+ "search-total-relation": search.total.relation,
150
+ "search-coverage-complete": search.coverage.complete ? "true" : "false",
151
+ "search-indexed-segments": String(search.coverage.indexedSegments),
152
+ "search-indexed-segment-time-ms": String(search.coverage.indexedSegmentTimeMs),
153
+ "search-fts-section-get-ms": String(search.coverage.ftsSectionGetMs),
154
+ "search-fts-decode-ms": String(search.coverage.ftsDecodeMs),
155
+ "search-fts-clause-estimate-ms": String(search.coverage.ftsClauseEstimateMs),
156
+ "search-scanned-segments": String(search.coverage.scannedSegments),
157
+ "search-scanned-segment-time-ms": String(search.coverage.scannedSegmentTimeMs),
158
+ "search-scanned-tail-docs": String(search.coverage.scannedTailDocs),
159
+ "search-scanned-tail-time-ms": String(search.coverage.scannedTailTimeMs),
160
+ "search-exact-candidate-time-ms": String(search.coverage.exactCandidateTimeMs),
161
+ "search-candidate-doc-ids": String(search.coverage.candidateDocIds),
162
+ "search-decoded-records": String(search.coverage.decodedRecords),
163
+ "search-json-parse-time-ms": String(search.coverage.jsonParseTimeMs),
164
+ "search-segment-payload-bytes-fetched": String(search.coverage.segmentPayloadBytesFetched),
165
+ "search-sort-time-ms": String(search.coverage.sortTimeMs),
166
+ "search-peak-hits-held": String(search.coverage.peakHitsHeld),
167
+ "search-index-families-used": search.coverage.indexFamiliesUsed.join(","),
168
+ };
169
+ }
170
+
171
+ function internalError(message = "internal server error"): Response {
172
+ return json(500, { error: { code: "internal", message } });
173
+ }
174
+
175
+ function badRequest(msg: string): Response {
176
+ return json(400, { error: { code: "bad_request", message: msg } });
177
+ }
178
+
179
+ function unsupportedMediaType(msg: string): Response {
180
+ return json(415, { error: { code: "unsupported_media_type", message: msg } });
181
+ }
182
+
183
+ function notFound(msg = "not_found"): Response {
184
+ return json(404, { error: { code: "not_found", message: msg } });
185
+ }
186
+
187
+ function readerErrorResponse(err: ReaderError): Response {
188
+ if (err.kind === "not_found") return notFound();
189
+ if (err.kind === "gone") return notFound("stream expired");
190
+ if (err.kind === "internal") return internalError();
191
+ return badRequest(err.message);
192
+ }
193
+
194
+ function schemaMutationErrorResponse(err: SchemaRegistryMutationError): Response {
195
+ if (err.kind === "version_mismatch") return conflict(err.message);
196
+ return badRequest(err.message);
197
+ }
198
+
199
+ function schemaReadErrorResponse(_err: SchemaRegistryReadError): Response {
200
+ return internalError();
201
+ }
202
+
203
+ function conflict(msg: string, headers: HeadersInit = {}): Response {
204
+ return json(409, { error: { code: "conflict", message: msg } }, headers);
205
+ }
206
+
207
+ function tooLarge(msg: string): Response {
208
+ return json(413, { error: { code: "payload_too_large", message: msg } });
209
+ }
210
+
211
+ function unavailable(msg = "server shutting down"): Response {
212
+ return json(503, { error: { code: "unavailable", message: msg } }, retryAfterHeaders(UNAVAILABLE_RETRY_AFTER_SECONDS));
213
+ }
214
+
215
+ function overloaded(msg = "ingest queue full", code = "overloaded"): Response {
216
+ return json(429, { error: { code, message: msg } }, retryAfterHeaders(OVERLOAD_RETRY_AFTER_SECONDS));
217
+ }
218
+
219
+ function requestTimeout(msg = "request timed out"): Response {
220
+ return json(408, { error: { code: "request_timeout", message: msg } });
221
+ }
222
+
223
+ function appendTimeout(): Response {
224
+ return json(408, {
225
+ error: {
226
+ code: "append_timeout",
227
+ message: "append timed out; append outcome is unknown, check Stream-Next-Offset before retrying",
228
+ },
229
+ });
230
+ }
231
+
232
+ async function cancelRequestBody(req: Request): Promise<void> {
233
+ const body = req.body;
234
+ if (!body) return;
235
+ try {
236
+ await body.cancel("request rejected");
237
+ return;
238
+ } catch {
239
+ // ignore and try a reader-based cancel below
240
+ }
241
+ try {
242
+ const reader = body.getReader();
243
+ await reader.cancel("request rejected");
244
+ } catch {
245
+ // ignore
246
+ }
247
+ }
248
+
249
+ function normalizeContentType(value: string | null): string | null {
250
+ if (!value) return null;
251
+ const base = value.split(";")[0]?.trim().toLowerCase();
252
+ return base ? base : null;
253
+ }
254
+
255
+ function isRecord(value: unknown): value is Record<string, unknown> {
256
+ return !!value && typeof value === "object" && !Array.isArray(value);
257
+ }
258
+
259
+ function isJsonContentType(value: string | null): boolean {
260
+ return normalizeContentType(value) === "application/json";
261
+ }
262
+
263
+ function isTextContentType(value: string | null): boolean {
264
+ const norm = normalizeContentType(value);
265
+ return norm === "application/json" || (norm != null && norm.startsWith("text/"));
266
+ }
267
+
268
+ function parseStreamClosedHeader(value: string | null): boolean {
269
+ return value != null && value.trim().toLowerCase() === "true";
270
+ }
271
+
272
+ function parseStreamSeqHeader(value: string | null): Result<string | null, { message: string }> {
273
+ if (value == null) return Result.ok(null);
274
+ const v = value.trim();
275
+ if (v.length === 0) return Result.err({ message: "invalid Stream-Seq" });
276
+ return Result.ok(v);
277
+ }
278
+
279
+ function parseStreamTtlSeconds(value: string): Result<number, { message: string }> {
280
+ const s = value.trim();
281
+ if (/^(0|[1-9][0-9]*)$/.test(s)) return Result.ok(Number(s));
282
+ if (/^(0|[1-9][0-9]*)(ms|s|m|h|d)$/.test(s)) {
283
+ const msRes = parseDurationMsResult(s);
284
+ if (Result.isError(msRes)) return Result.err({ message: msRes.error.message });
285
+ const ms = msRes.value;
286
+ if (ms % 1000 !== 0) return Result.err({ message: "invalid Stream-TTL" });
287
+ return Result.ok(Math.floor(ms / 1000));
288
+ }
289
+ return Result.err({ message: "invalid Stream-TTL" });
290
+ }
291
+
292
+ function parseNonNegativeInt(value: string): number | null {
293
+ if (!/^[0-9]+$/.test(value)) return null;
294
+ const n = Number(value);
295
+ if (!Number.isFinite(n)) return null;
296
+ return n;
297
+ }
298
+
299
+ function splitSseLines(data: string): string[] {
300
+ if (data === "") return [""];
301
+ return data.split(/\r\n|\r|\n/);
302
+ }
303
+
304
+ function encodeSseEvent(eventType: string, data: string): string {
305
+ const lines = splitSseLines(data);
306
+ let out = `event: ${eventType}\n`;
307
+ for (const line of lines) {
308
+ out += `data:${line}\n`;
309
+ }
310
+ out += `\n`;
311
+ return out;
312
+ }
313
+
314
+ const INTERNAL_METRICS_STREAM = "__stream_metrics__";
315
+
316
+ function computeCursor(nowMs: number, provided: string | null): string {
317
+ let cursor = Math.floor(nowMs / 1000);
318
+ if (provided && /^[0-9]+$/.test(provided)) {
319
+ const n = Number(provided);
320
+ if (Number.isFinite(n) && n >= cursor) cursor = n + 1;
321
+ }
322
+ return String(cursor);
323
+ }
324
+
325
+ function concatPayloads(parts: Uint8Array[]): Buffer {
326
+ return Buffer.concat(parts.map((part) => Buffer.from(part.buffer, part.byteOffset, part.byteLength)));
327
+ }
328
+
329
+ function bodyBufferFromBytes(bytes: Uint8Array): ArrayBuffer {
330
+ const buffer = bytes.buffer;
331
+ if (bytes.byteOffset === 0 && bytes.byteLength === buffer.byteLength) {
332
+ return buffer as ArrayBuffer;
333
+ }
334
+ return buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
335
+ }
336
+
337
+ const JSON_TEXT_DECODER = new TextDecoder();
338
+ const JSON_TEXT_ENCODER = new TextEncoder();
339
+
340
+ function keyBytesFromString(s: string | null): Uint8Array | null {
341
+ if (s == null) return null;
342
+ return JSON_TEXT_ENCODER.encode(s);
343
+ }
344
+
345
+ function extractRoutingKey(reg: SchemaRegistry, value: any): Result<Uint8Array | null, { message: string }> {
346
+ if (!reg.routingKey) return Result.ok(null);
347
+ const { jsonPointer, required } = reg.routingKey;
348
+ const resolvedRes = resolvePointerResult(value, jsonPointer);
349
+ if (Result.isError(resolvedRes)) return Result.err({ message: resolvedRes.error.message });
350
+ const resolved = resolvedRes.value;
351
+ if (!resolved.exists) {
352
+ if (required) return Result.err({ message: "routing key missing" });
353
+ return Result.ok(null);
354
+ }
355
+ if (typeof resolved.value !== "string") return Result.err({ message: "routing key must be string" });
356
+ return Result.ok(keyBytesFromString(resolved.value));
357
+ }
358
+
359
+ function timestampToIsoString(value: bigint | null): string | null {
360
+ return value == null ? null : new Date(Number(value)).toISOString();
361
+ }
362
+
363
+ function weakEtag(namespace: string, body: string): string {
364
+ const hash = createHash("sha1").update(body).digest("hex");
365
+ return `W/"${namespace}:${hash}"`;
366
+ }
367
+
368
+
369
+ export type App<TDebugStore extends AppDebugStore = AppDebugStore> = {
370
+ fetch: (req: Request) => Promise<Response>;
371
+ close: () => Promise<void>;
372
+ ready: Promise<void>;
373
+ deps: {
374
+ config: Config;
375
+ db?: TDebugStore;
376
+ os?: ObjectStore;
377
+ ingest: IngestQueue;
378
+ notifier: StreamNotifier;
379
+ reader: StreamReader;
380
+ segmenter?: SegmenterController;
381
+ uploader?: UploaderController;
382
+ indexer?: StreamIndexLookup;
383
+ metrics: Metrics;
384
+ registry: SchemaRegistryStore;
385
+ profiles: StreamProfileStore;
386
+ touch?: TouchProcessorManager;
387
+ stats?: StatsCollector;
388
+ storageStats?: StorageStatsStore;
389
+ objectStoreAccounting?: ObjectStoreAccountingStore;
390
+ backpressure?: BackpressureGate;
391
+ memory?: MemoryPressureMonitor;
392
+ concurrency?: {
393
+ ingest: ConcurrencyGate;
394
+ read: ConcurrencyGate;
395
+ search: ConcurrencyGate;
396
+ asyncIndex: ConcurrencyGate;
397
+ };
398
+ memorySampler?: RuntimeMemorySampler;
399
+ };
400
+ };
401
+
402
+ export type CreateAppRuntimeArgs = {
403
+ config: Config;
404
+ controlStore: WalControlPlaneStore;
405
+ ingest: IngestQueue;
406
+ notifier: StreamNotifier;
407
+ registry: SchemaRegistryStore;
408
+ profiles: StreamProfileStore;
409
+ touch?: TouchProcessorManager;
410
+ stats?: StatsCollector;
411
+ backpressure?: BackpressureGate;
412
+ memory: MemoryPressureMonitor;
413
+ asyncIndexGate: ConcurrencyGate;
414
+ foregroundActivity: ForegroundActivityTracker;
415
+ metrics: Metrics;
416
+ memorySampler?: RuntimeMemorySampler;
417
+ };
418
+
419
+ type FullModeRuntimeDeps = {
420
+ store: ObjectStore;
421
+ segmenter: SegmenterController;
422
+ uploader: UploaderController;
423
+ segmentDiskCache?: SegmentDiskCache;
424
+ sizeReconciler?: {
425
+ start(): void;
426
+ stop(): void;
427
+ };
428
+ manifestPublication?: {
429
+ publishDeletedStreamManifest(stream: string): Promise<void>;
430
+ };
431
+ getLocalStorageUsage?: (stream: string) => {
432
+ segment_cache_bytes: number;
433
+ routing_index_cache_bytes: number;
434
+ exact_index_cache_bytes: number;
435
+ lexicon_index_cache_bytes: number;
436
+ companion_cache_bytes: number;
437
+ };
438
+ };
439
+
440
+ type AppRuntimeDeps = {
441
+ reader: StreamReader;
442
+ indexer?: StreamIndexLookup;
443
+ schemaPublication?: SchemaPublicationStore;
444
+ fullMode?: FullModeRuntimeDeps;
445
+ getRuntimeMemorySnapshot?: () => RuntimeMemorySubsystemSnapshot;
446
+ start(): void;
447
+ };
448
+
449
+ export type CreateAppCoreOptions<TDebugStore extends AppDebugStore = AppDebugStore> = {
450
+ store: WalControlPlaneStore;
451
+ debugStore?: TDebugStore;
452
+ touchStore?: TouchProcessorStore;
453
+ touchUseWorkers?: boolean;
454
+ storageStatsStore?: StorageStatsStore;
455
+ objectStoreAccountingStore?: ObjectStoreAccountingStore;
456
+ detailsStore?: FullModeDetailsStore;
457
+ detailsStorageBackend?: "sqlite" | "postgres";
458
+ lifecycleHooks?: {
459
+ beforeRuntimeCreate?(): void;
460
+ afterInternalMetricsProfileEnsured?(): void;
461
+ getInitialBackpressureBytes?(): number;
462
+ };
463
+ stats?: StatsCollector;
464
+ createRuntime(args: CreateAppRuntimeArgs): AppRuntimeDeps;
465
+ };
466
+
467
+ function validateRuntimeCapabilityBundle(
468
+ controlStore: WalControlPlaneStore,
469
+ touchStore: TouchProcessorStore | undefined,
470
+ storageStatsStore: StorageStatsStore | undefined,
471
+ objectStoreAccountingStore: ObjectStoreAccountingStore | undefined,
472
+ detailsStore: FullModeDetailsStore | undefined,
473
+ runtime: AppRuntimeDeps
474
+ ): void {
475
+ const caps = controlStore.capabilities;
476
+ if (caps.indexes && !runtime.indexer) throw dsError("index capability requires an index runtime");
477
+ if (caps.schemaPublication && !runtime.schemaPublication) throw dsError("schema publication capability requires a schema publication runtime");
478
+ if (caps.manifests && !runtime.fullMode?.manifestPublication) throw dsError("manifest capability requires a full-mode manifest runtime");
479
+ if (caps.storageStats && !storageStatsStore) throw dsError("storage stats capability requires a storage stats store");
480
+ if (caps.objectStoreAccounting && !objectStoreAccountingStore) throw dsError("object-store accounting capability requires an accounting store");
481
+ if ((caps.storageStats || caps.objectStoreAccounting || caps.indexes) && !detailsStore) {
482
+ throw dsError("details/index capabilities require a full-mode details store");
483
+ }
484
+ if (caps.touch && !touchStore) throw dsError("touch capability requires a touch metadata store");
485
+ if (caps.internalMetrics && !caps.builtinProfiles) throw dsError("internal metrics capability requires built-in profile support");
486
+ }
487
+
488
+ function reduceConcurrencyLimit(limit: number): number {
489
+ return Math.max(1, Math.ceil(Math.max(1, limit) / 2));
490
+ }
491
+
492
+ function gateSnapshot(configuredLimit: number, gate: ConcurrencyGate) {
493
+ return {
494
+ configured_limit: configuredLimit,
495
+ current_limit: gate.getLimit(),
496
+ active: gate.getActive(),
497
+ queued: gate.getQueued(),
498
+ };
499
+ }
500
+
501
+ function unsupportedCapability(name: string): Response {
502
+ return json(501, { error: { code: "unsupported_capability", message: `${name} capability is not available` } });
503
+ }
504
+
505
+ export function createAppCore<TDebugStore extends AppDebugStore = AppDebugStore>(
506
+ cfg: Config,
507
+ opts: CreateAppCoreOptions<TDebugStore>
508
+ ): App<TDebugStore> {
509
+ mkdirSync(cfg.rootDir, { recursive: true });
510
+ cleanupTempSegments(cfg.rootDir);
511
+
512
+ const controlStore = requireWalControlPlaneStore(opts.store);
513
+ const debugStore = opts.debugStore;
514
+ const touchStore = opts.touchStore;
515
+ const storageStatsStore = opts.storageStatsStore;
516
+ const objectStoreAccountingStore = opts.objectStoreAccountingStore;
517
+ const detailsStore = opts.detailsStore;
518
+ opts.lifecycleHooks?.beforeRuntimeCreate?.();
519
+ const stats = opts.stats;
520
+ const metrics = new Metrics();
521
+ const backpressure =
522
+ cfg.localBacklogMaxBytes > 0
523
+ ? new BackpressureGate(cfg.localBacklogMaxBytes, opts.lifecycleHooks?.getInitialBackpressureBytes?.() ?? 0)
524
+ : undefined;
525
+ const memorySampler =
526
+ cfg.memorySamplerPath != null
527
+ ? new RuntimeMemorySampler(cfg.memorySamplerPath, {
528
+ intervalMs: cfg.memorySamplerIntervalMs,
529
+ scope: "main",
530
+ })
531
+ : undefined;
532
+ const ingestGate = new ConcurrencyGate(cfg.ingestConcurrency);
533
+ const readGate = new ConcurrencyGate(cfg.readConcurrency);
534
+ const searchGate = new ConcurrencyGate(cfg.searchConcurrency);
535
+ const asyncIndexGate = new ConcurrencyGate(cfg.asyncIndexConcurrency);
536
+ const foregroundActivity = new ForegroundActivityTracker();
537
+ const memory = new MemoryPressureMonitor(cfg.memoryLimitBytes, {
538
+ onSample: (rss, overLimit) => {
539
+ metrics.record("process.rss.bytes", rss, "bytes");
540
+ if (overLimit) metrics.record("process.rss.over_limit", 1, "count");
541
+ searchGate.setLimit(overLimit ? reduceConcurrencyLimit(cfg.searchConcurrency) : cfg.searchConcurrency);
542
+ asyncIndexGate.setLimit(overLimit ? reduceConcurrencyLimit(cfg.asyncIndexConcurrency) : cfg.asyncIndexConcurrency);
543
+ },
544
+ heapSnapshotPath: cfg.heapSnapshotPath ?? undefined,
545
+ });
546
+ let httpAppendGcBytesSinceLast = 0;
547
+ let httpAppendGcLastMs = 0;
548
+ const maybeCollectAfterHttpAppend = (bodyBytes: number): void => {
549
+ if (cfg.memoryLimitBytes <= 0 || bodyBytes <= 0) return;
550
+ const limit = cfg.memoryLimitBytes;
551
+ httpAppendGcBytesSinceLast += Math.max(0, Math.floor(bodyBytes));
552
+ const usage = process.memoryUsage();
553
+ const smallMemoryPreset = limit <= 1024 * 1024 * 1024;
554
+ const byteCadence = smallMemoryPreset ? 8 * 1024 * 1024 : 64 * 1024 * 1024;
555
+ const abovePressureBand =
556
+ usage.rss > limit * 0.55 ||
557
+ usage.external > limit * 0.2 ||
558
+ usage.arrayBuffers > limit * 0.15;
559
+ if (!abovePressureBand && httpAppendGcBytesSinceLast < byteCadence) return;
560
+ const now = Date.now();
561
+ if (now - httpAppendGcLastMs < 1_000) return;
562
+ const gc = (globalThis as { Bun?: { gc?: (force?: boolean) => void } }).Bun?.gc;
563
+ if (typeof gc !== "function") return;
564
+ httpAppendGcLastMs = now;
565
+ httpAppendGcBytesSinceLast = 0;
566
+ try {
567
+ gc(true);
568
+ } catch {
569
+ try {
570
+ gc();
571
+ } catch {
572
+ return;
573
+ }
574
+ }
575
+ };
576
+ const appendResponseHeaders = (headers: HeadersInit = {}): HeadersInit => {
577
+ if (cfg.memoryLimitBytes > 0 && cfg.memoryLimitBytes <= 1024 * 1024 * 1024) {
578
+ return withNosniff({
579
+ ...headers,
580
+ connection: "close",
581
+ });
582
+ }
583
+ return withNosniff(headers);
584
+ };
585
+ const ingest = new IngestQueue(cfg, controlStore, stats, backpressure, metrics);
586
+ const notifier = new StreamNotifier();
587
+ const registry = new SchemaRegistryStore(controlStore);
588
+ const profiles = new StreamProfileStore(controlStore, { touchEnabled: !!touchStore });
589
+ const touch =
590
+ touchStore && controlStore.capabilities.touch
591
+ ? new TouchProcessorManager(cfg, touchStore, ingest, notifier, profiles, backpressure, { useWorkers: opts.touchUseWorkers })
592
+ : undefined;
593
+ const runtime = opts.createRuntime({
594
+ config: cfg,
595
+ controlStore,
596
+ ingest,
597
+ notifier,
598
+ registry,
599
+ profiles,
600
+ touch,
601
+ stats,
602
+ backpressure,
603
+ memory,
604
+ asyncIndexGate,
605
+ foregroundActivity,
606
+ metrics,
607
+ memorySampler,
608
+ });
609
+ validateRuntimeCapabilityBundle(controlStore, touchStore, storageStatsStore, objectStoreAccountingStore, detailsStore, runtime);
610
+ const {
611
+ reader,
612
+ indexer,
613
+ schemaPublication,
614
+ fullMode,
615
+ getRuntimeMemorySnapshot,
616
+ } = runtime;
617
+ const getLocalStorageUsage = fullMode?.getLocalStorageUsage;
618
+ const detailsBuilder =
619
+ detailsStore
620
+ ? new FullModeDetailsBuilder({
621
+ detailsStore,
622
+ storageBackend: opts.detailsStorageBackend,
623
+ storageStatsStore,
624
+ objectStoreAccountingStore,
625
+ getLocalStorageUsage: getLocalStorageUsage as ((stream: string) => Partial<LocalStorageUsage>) | undefined,
626
+ })
627
+ : undefined;
628
+ memorySampler?.start();
629
+ memory.start();
630
+ ingest.start();
631
+ const runtimeHighWater: RuntimeMemoryHighWaterSnapshot = {
632
+ process: {},
633
+ process_breakdown: {},
634
+ sqlite: {},
635
+ runtime_bytes: {},
636
+ runtime_totals: {},
637
+ };
638
+
639
+ const observeHighWaterValue = (target: Record<string, RuntimeHighWaterMark>, key: string, value: number, at: string): void => {
640
+ const next = Math.max(0, Math.floor(value));
641
+ const existing = target[key];
642
+ if (!existing || next > existing.value) {
643
+ target[key] = {
644
+ value: next,
645
+ at,
646
+ };
647
+ }
648
+ };
649
+
650
+ const buildRuntimeBytes = (runtimeMemory: RuntimeMemorySnapshot): Record<string, Record<string, number>> => {
651
+ const groups: Record<string, Record<string, number>> = {};
652
+ for (const [kind, values] of Object.entries(runtimeMemory.subsystems)) {
653
+ if (kind === "counts") continue;
654
+ groups[kind] = values;
655
+ }
656
+ return groups;
657
+ };
658
+
659
+ const buildTopStreamContributors = async (limit = 5) => {
660
+ const safeLimit = Math.max(1, Math.min(limit, 20));
661
+ const localStorageRows: Array<{
662
+ stream: string;
663
+ bytes: number;
664
+ wal_retained_bytes: number;
665
+ segment_cache_bytes: number;
666
+ index_cache_bytes: number;
667
+ }> = [];
668
+ const pendingWalRows: Array<{ stream: string; pending_wal_bytes: number; pending_rows: number }> = [];
669
+ let offset = 0;
670
+ const pageSize = 1000;
671
+ for (;;) {
672
+ const rows = await controlStore.listStreams(pageSize, offset);
673
+ if (rows.length === 0) break;
674
+ for (const row of rows) {
675
+ if (controlStore.isDeleted(row)) continue;
676
+ const usage = getLocalStorageUsage?.(row.stream) ?? { segment_cache_bytes: 0 };
677
+ const walRetainedBytes = Number(row.pending_bytes);
678
+ const segmentCacheBytes = Math.max(0, Math.floor(Number((usage as Record<string, number>).segment_cache_bytes ?? 0)));
679
+ const indexCacheBytes = Math.max(
680
+ 0,
681
+ Math.floor(
682
+ Number((usage as Record<string, number>).routing_index_cache_bytes ?? 0) +
683
+ Number((usage as Record<string, number>).exact_index_cache_bytes ?? 0) +
684
+ Number((usage as Record<string, number>).lexicon_index_cache_bytes ?? 0) +
685
+ Number((usage as Record<string, number>).companion_cache_bytes ?? 0)
686
+ )
687
+ );
688
+ localStorageRows.push({
689
+ stream: row.stream,
690
+ bytes: Math.max(0, walRetainedBytes + segmentCacheBytes + indexCacheBytes),
691
+ wal_retained_bytes: Math.max(0, walRetainedBytes),
692
+ segment_cache_bytes: segmentCacheBytes,
693
+ index_cache_bytes: indexCacheBytes,
694
+ });
695
+ pendingWalRows.push({
696
+ stream: row.stream,
697
+ pending_wal_bytes: Math.max(0, walRetainedBytes),
698
+ pending_rows: Math.max(0, Number(row.pending_rows)),
699
+ });
700
+ }
701
+ if (rows.length < pageSize) break;
702
+ offset += rows.length;
703
+ }
704
+ localStorageRows.sort((a, b) => b.bytes - a.bytes || a.stream.localeCompare(b.stream));
705
+ pendingWalRows.sort((a, b) => b.pending_wal_bytes - a.pending_wal_bytes || a.stream.localeCompare(b.stream));
706
+ return {
707
+ local_storage_bytes: localStorageRows.slice(0, safeLimit),
708
+ pending_wal_bytes: pendingWalRows.slice(0, safeLimit),
709
+ touch_journal_filter_bytes: touch?.getTopStreams(safeLimit) ?? [],
710
+ notifier_waiters: notifier.getTopStreams(safeLimit),
711
+ };
712
+ };
713
+
714
+ const buildRuntimeMemorySnapshot = (): RuntimeMemorySnapshot => {
715
+ const processUsage = process.memoryUsage();
716
+ const subsystemSnapshot = getRuntimeMemorySnapshot?.() ?? {
717
+ subsystems: {
718
+ heap_estimates: {},
719
+ mapped_files: {},
720
+ disk_caches: {},
721
+ configured_budgets: {},
722
+ pipeline_buffers: {},
723
+ sqlite_runtime: {},
724
+ counts: {},
725
+ },
726
+ totals: {
727
+ heap_estimate_bytes: 0,
728
+ mapped_file_bytes: 0,
729
+ disk_cache_bytes: 0,
730
+ configured_budget_bytes: 0,
731
+ pipeline_buffer_bytes: 0,
732
+ sqlite_runtime_bytes: 0,
733
+ },
734
+ };
735
+ const sqliteRuntimeBytes = subsystemSnapshot.subsystems.sqlite_runtime ?? {};
736
+ const runtimeCounts = subsystemSnapshot.subsystems.counts ?? {};
737
+ const snapshot: RuntimeMemorySnapshot = {
738
+ process: {
739
+ rss_bytes: processUsage.rss,
740
+ heap_total_bytes: processUsage.heapTotal,
741
+ heap_used_bytes: processUsage.heapUsed,
742
+ external_bytes: processUsage.external,
743
+ array_buffers_bytes: processUsage.arrayBuffers,
744
+ },
745
+ process_breakdown: buildProcessMemoryBreakdown({
746
+ process: {
747
+ rss_bytes: processUsage.rss,
748
+ heap_total_bytes: processUsage.heapTotal,
749
+ heap_used_bytes: processUsage.heapUsed,
750
+ external_bytes: processUsage.external,
751
+ array_buffers_bytes: processUsage.arrayBuffers,
752
+ },
753
+ mappedFileBytes: subsystemSnapshot.totals.mapped_file_bytes,
754
+ sqliteRuntimeBytes: Number(sqliteRuntimeBytes["sqlite_memory_used_bytes"] ?? 0),
755
+ }),
756
+ sqlite: {
757
+ available: Number(runtimeCounts["sqlite_open_connections"] ?? 0) > 0 || Number(sqliteRuntimeBytes["sqlite_memory_used_bytes"] ?? 0) > 0,
758
+ source:
759
+ Number(runtimeCounts["sqlite_open_connections"] ?? 0) > 0 || Number(sqliteRuntimeBytes["sqlite_memory_used_bytes"] ?? 0) > 0
760
+ ? "sqlite3_status64"
761
+ : "unavailable",
762
+ memory_used_bytes: Math.max(0, Math.floor(Number(sqliteRuntimeBytes["sqlite_memory_used_bytes"] ?? 0))),
763
+ memory_highwater_bytes: Math.max(
764
+ 0,
765
+ Math.floor(Number(sqliteRuntimeBytes["sqlite_memory_highwater_bytes"] ?? 0))
766
+ ),
767
+ pagecache_used_slots: Math.max(0, Math.floor(Number(runtimeCounts["sqlite_pagecache_used_slots"] ?? 0))),
768
+ pagecache_used_slots_highwater: Math.max(
769
+ 0,
770
+ Math.floor(Number(runtimeCounts["sqlite_pagecache_used_slots_highwater"] ?? 0))
771
+ ),
772
+ pagecache_overflow_bytes: Math.max(
773
+ 0,
774
+ Math.floor(Number(sqliteRuntimeBytes["sqlite_pagecache_overflow_bytes"] ?? 0))
775
+ ),
776
+ pagecache_overflow_highwater_bytes: Math.max(
777
+ 0,
778
+ Math.floor(Number(sqliteRuntimeBytes["sqlite_pagecache_overflow_highwater_bytes"] ?? 0))
779
+ ),
780
+ malloc_count: Math.max(0, Math.floor(Number(runtimeCounts["sqlite_malloc_count"] ?? 0))),
781
+ malloc_count_highwater: Math.max(
782
+ 0,
783
+ Math.floor(Number(runtimeCounts["sqlite_malloc_count_highwater"] ?? 0))
784
+ ),
785
+ open_connections: Math.max(0, Math.floor(Number(runtimeCounts["sqlite_open_connections"] ?? 0))),
786
+ prepared_statements: Math.max(
787
+ 0,
788
+ Math.floor(Number(runtimeCounts["sqlite_prepared_statements"] ?? 0))
789
+ ),
790
+ },
791
+ gc: memory.getGcStats(),
792
+ subsystems: subsystemSnapshot.subsystems,
793
+ totals: subsystemSnapshot.totals,
794
+ };
795
+ const ts = new Date().toISOString();
796
+ for (const [key, value] of Object.entries(snapshot.process)) observeHighWaterValue(runtimeHighWater.process, key, value, ts);
797
+ for (const [key, value] of Object.entries(snapshot.process_breakdown)) {
798
+ if (typeof value === "number") observeHighWaterValue(runtimeHighWater.process_breakdown, key, value, ts);
799
+ }
800
+ for (const [key, value] of Object.entries(snapshot.sqlite)) {
801
+ if (typeof value === "number") observeHighWaterValue(runtimeHighWater.sqlite, key, value, ts);
802
+ }
803
+ for (const [kind, values] of Object.entries(buildRuntimeBytes(snapshot))) {
804
+ const bucket = (runtimeHighWater.runtime_bytes[kind] ??= {});
805
+ for (const [key, value] of Object.entries(values)) observeHighWaterValue(bucket, key, value, ts);
806
+ }
807
+ for (const [key, value] of Object.entries(snapshot.totals)) observeHighWaterValue(runtimeHighWater.runtime_totals, key, value, ts);
808
+ if (snapshot.sqlite.memory_used_bytes > 0) observeHighWaterValue(runtimeHighWater.sqlite, "memory_used_bytes", snapshot.sqlite.memory_used_bytes, ts);
809
+ if (snapshot.sqlite.pagecache_overflow_bytes > 0)
810
+ observeHighWaterValue(runtimeHighWater.sqlite, "pagecache_overflow_bytes", snapshot.sqlite.pagecache_overflow_bytes, ts);
811
+ if (snapshot.sqlite.pagecache_used_slots > 0)
812
+ observeHighWaterValue(runtimeHighWater.sqlite, "pagecache_used_slots", snapshot.sqlite.pagecache_used_slots, ts);
813
+ if (snapshot.sqlite.malloc_count > 0) observeHighWaterValue(runtimeHighWater.sqlite, "malloc_count", snapshot.sqlite.malloc_count, ts);
814
+ return snapshot;
815
+ };
816
+
817
+ const buildLeakCandidateCounters = (): Record<string, number> => {
818
+ const runtimeMemory = buildRuntimeMemorySnapshot();
819
+ const runtimeCounts = runtimeMemory.subsystems.counts ?? {};
820
+ const countValue = (name: string): number => {
821
+ const raw = Number(runtimeCounts[name] ?? 0);
822
+ if (!Number.isFinite(raw)) return 0;
823
+ return Math.max(0, Math.floor(raw));
824
+ };
825
+ const touchMemory = touch?.getMemoryStats() ?? {
826
+ journals: 0,
827
+ journalsCreatedTotal: 0,
828
+ journalFilterBytesTotal: 0,
829
+ fineLagCoarseOnlyStreams: 0,
830
+ touchModeStreams: 0,
831
+ fineTokenBucketStreams: 0,
832
+ hotFineStreams: 0,
833
+ lagSourceOffsetStreams: 0,
834
+ restrictedTemplateBucketStreams: 0,
835
+ runtimeTotalsStreams: 0,
836
+ zeroRowBacklogStreakStreams: 0,
837
+ templateLastSeenEntries: 0,
838
+ templateDirtyLastSeenEntries: 0,
839
+ templateRateStateStreams: 0,
840
+ liveMetricsCounterStreams: 0,
841
+ };
842
+ const notifierMemory = notifier.getMemoryStats();
843
+ const metricsMemory = metrics.getMemoryStats();
844
+ return {
845
+ "tieredstore.mem.leak_candidate.segment_cache.pinned_entries": countValue("segment_pinned_files"),
846
+ "tieredstore.mem.leak_candidate.lexicon_file_cache.pinned_entries": countValue("lexicon_pinned_files"),
847
+ "tieredstore.mem.leak_candidate.companion_file_cache.pinned_entries": countValue("companion_pinned_files"),
848
+ "tieredstore.mem.leak_candidate.routing_run_disk_cache.pinned_entries": countValue("routing_run_disk_cache_pinned_entries"),
849
+ "tieredstore.mem.leak_candidate.exact_run_disk_cache.pinned_entries": countValue("exact_run_disk_cache_pinned_entries"),
850
+ "tieredstore.mem.leak_candidate.touch.journals.active_count": touchMemory.journals,
851
+ "tieredstore.mem.leak_candidate.touch.journals.created_total": touchMemory.journalsCreatedTotal,
852
+ "tieredstore.mem.leak_candidate.touch.journals.filter_bytes_total": touchMemory.journalFilterBytesTotal,
853
+ "tieredstore.mem.leak_candidate.touch.journal.default_filter_bytes": DEFAULT_TOUCH_JOURNAL_FILTER_BYTES,
854
+ "tieredstore.mem.leak_candidate.touch.maps.fine_lag_coarse_only_streams": touchMemory.fineLagCoarseOnlyStreams,
855
+ "tieredstore.mem.leak_candidate.touch.maps.touch_mode_streams": touchMemory.touchModeStreams,
856
+ "tieredstore.mem.leak_candidate.touch.maps.fine_token_bucket_streams": touchMemory.fineTokenBucketStreams,
857
+ "tieredstore.mem.leak_candidate.touch.maps.hot_fine_streams": touchMemory.hotFineStreams,
858
+ "tieredstore.mem.leak_candidate.touch.maps.lag_source_offset_streams": touchMemory.lagSourceOffsetStreams,
859
+ "tieredstore.mem.leak_candidate.touch.maps.restricted_template_bucket_streams": touchMemory.restrictedTemplateBucketStreams,
860
+ "tieredstore.mem.leak_candidate.touch.maps.runtime_totals_streams": touchMemory.runtimeTotalsStreams,
861
+ "tieredstore.mem.leak_candidate.touch.maps.zero_row_backlog_streams": touchMemory.zeroRowBacklogStreakStreams,
862
+ "tieredstore.mem.leak_candidate.live_template.last_seen_entries": touchMemory.templateLastSeenEntries,
863
+ "tieredstore.mem.leak_candidate.live_template.dirty_last_seen_entries": touchMemory.templateDirtyLastSeenEntries,
864
+ "tieredstore.mem.leak_candidate.live_template.rate_state_streams": touchMemory.templateRateStateStreams,
865
+ "tieredstore.mem.leak_candidate.live_metrics.counter_streams": touchMemory.liveMetricsCounterStreams,
866
+ "tieredstore.mem.leak_candidate.notifier.latest_seq_streams": notifierMemory.latestSeqStreams,
867
+ "tieredstore.mem.leak_candidate.notifier.details_version_streams": notifierMemory.detailsVersionStreams,
868
+ "tieredstore.mem.leak_candidate.metrics.series": metricsMemory.seriesCount,
869
+ "tieredstore.mem.leak_candidate.secondary_index.stream_idle_ticks_streams": countValue("secondary_index_stream_idle_ticks"),
870
+ "tieredstore.mem.leak_candidate.mock_r2.in_memory_bytes": countValue("mock_r2_in_memory_bytes"),
871
+ "tieredstore.mem.leak_candidate.mock_r2.object_count": countValue("mock_r2_object_count"),
872
+ };
873
+ };
874
+
875
+ const buildServerMem = async () => {
876
+ const runtimeMemory = buildRuntimeMemorySnapshot();
877
+ return {
878
+ ts: new Date().toISOString(),
879
+ process: runtimeMemory.process,
880
+ process_breakdown: runtimeMemory.process_breakdown,
881
+ sqlite: runtimeMemory.sqlite,
882
+ gc: runtimeMemory.gc,
883
+ high_water: runtimeHighWater,
884
+ counters: buildLeakCandidateCounters(),
885
+ runtime_counts: runtimeMemory.subsystems.counts,
886
+ runtime_bytes: buildRuntimeBytes(runtimeMemory),
887
+ runtime_totals: runtimeMemory.totals,
888
+ top_streams: await buildTopStreamContributors(),
889
+ };
890
+ };
891
+ memorySampler?.setSubsystemProvider(() => buildRuntimeMemorySnapshot().subsystems);
892
+ const buildServerDetails = async () => {
893
+ const runtimeMemory = buildRuntimeMemorySnapshot();
894
+ return {
895
+ auto_tune: {
896
+ enabled: cfg.autoTunePresetMb != null,
897
+ requested_memory_mb: cfg.autoTuneRequestedMemoryMb,
898
+ preset_mb: cfg.autoTunePresetMb,
899
+ effective_memory_limit_mb: cfg.autoTuneEffectiveMemoryLimitMb,
900
+ },
901
+ configured_limits: {
902
+ caches: {
903
+ sqlite_cache_bytes: cfg.sqliteCacheBytes,
904
+ worker_sqlite_cache_bytes: cfg.workerSqliteCacheBytes,
905
+ index_run_memory_cache_bytes: cfg.indexRunMemoryCacheBytes,
906
+ index_run_disk_cache_bytes: cfg.indexRunCacheMaxBytes,
907
+ lexicon_index_cache_bytes: cfg.lexiconIndexCacheMaxBytes,
908
+ segment_cache_bytes: cfg.segmentCacheMaxBytes,
909
+ companion_toc_cache_bytes: cfg.searchCompanionTocCacheBytes,
910
+ companion_section_cache_bytes: cfg.searchCompanionSectionCacheBytes,
911
+ companion_file_cache_bytes: cfg.searchCompanionFileCacheMaxBytes,
912
+ },
913
+ concurrency: {
914
+ ingest: cfg.ingestConcurrency,
915
+ read: cfg.readConcurrency,
916
+ search: cfg.searchConcurrency,
917
+ async_index: cfg.asyncIndexConcurrency,
918
+ upload: cfg.uploadConcurrency,
919
+ index_build: cfg.indexBuildConcurrency,
920
+ index_compact: cfg.indexCompactionConcurrency,
921
+ },
922
+ ingest: {
923
+ max_batch_requests: cfg.ingestMaxBatchRequests,
924
+ max_batch_bytes: cfg.ingestMaxBatchBytes,
925
+ max_queue_requests: cfg.ingestMaxQueueRequests,
926
+ max_queue_bytes: cfg.ingestMaxQueueBytes,
927
+ busy_timeout_ms: cfg.ingestBusyTimeoutMs,
928
+ local_backlog_max_bytes: cfg.localBacklogMaxBytes,
929
+ },
930
+ search: {
931
+ companion_batch_segments: cfg.searchCompanionBuildBatchSegments,
932
+ companion_yield_blocks: cfg.searchCompanionYieldBlocks,
933
+ wal_overlay_quiet_period_ms: cfg.searchWalOverlayQuietPeriodMs,
934
+ wal_overlay_max_bytes: cfg.searchWalOverlayMaxBytes,
935
+ },
936
+ segmenting: {
937
+ segment_max_bytes: cfg.segmentMaxBytes,
938
+ segment_target_rows: cfg.segmentTargetRows,
939
+ segmenter_workers: cfg.segmenterWorkers,
940
+ },
941
+ timeouts: {
942
+ append_request_timeout_ms: APPEND_REQUEST_TIMEOUT_MS,
943
+ search_request_timeout_ms: SEARCH_REQUEST_TIMEOUT_MS,
944
+ resolver_timeout_ms: HTTP_RESOLVER_TIMEOUT_MS,
945
+ object_store_timeout_ms: cfg.objectStoreTimeoutMs,
946
+ },
947
+ memory: {
948
+ pressure_limit_bytes: cfg.memoryLimitBytes,
949
+ },
950
+ },
951
+ runtime: {
952
+ memory: {
953
+ pressure_active: memory.isOverLimit(),
954
+ pressure_limit_bytes: memory.getLimitBytes(),
955
+ last_rss_bytes: memory.getLastRssBytes(),
956
+ max_rss_bytes: memory.getMaxRssBytes(),
957
+ process: runtimeMemory.process,
958
+ process_breakdown: runtimeMemory.process_breakdown,
959
+ sqlite: runtimeMemory.sqlite,
960
+ gc: runtimeMemory.gc,
961
+ subsystems: runtimeMemory.subsystems,
962
+ totals: runtimeMemory.totals,
963
+ high_water: runtimeHighWater,
964
+ },
965
+ ingest_queue: {
966
+ requests: ingest.getQueueStats().requests,
967
+ bytes: ingest.getQueueStats().bytes,
968
+ full: ingest.isQueueFull(),
969
+ },
970
+ local_backpressure: {
971
+ enabled: backpressure?.enabled() ?? false,
972
+ current_bytes: backpressure?.getCurrentBytes() ?? 0,
973
+ max_bytes: backpressure?.getMaxBytes() ?? 0,
974
+ over_limit: backpressure?.isOverLimit() ?? false,
975
+ },
976
+ uploads: {
977
+ pending_segments: fullMode?.uploader.countSegmentsWaiting() ?? 0,
978
+ },
979
+ concurrency: {
980
+ ingest: gateSnapshot(cfg.ingestConcurrency, ingestGate),
981
+ read: gateSnapshot(cfg.readConcurrency, readGate),
982
+ search: gateSnapshot(cfg.searchConcurrency, searchGate),
983
+ async_index: gateSnapshot(cfg.asyncIndexConcurrency, asyncIndexGate),
984
+ },
985
+ top_streams: await buildTopStreamContributors(),
986
+ },
987
+ };
988
+ };
989
+ const collectRuntimeMetrics = () => {
990
+ const queue = ingest.getQueueStats();
991
+ const emitGate = (name: string, configuredLimit: number, gate: ConcurrencyGate) => {
992
+ metrics.record("tieredstore.concurrency.limit", configuredLimit, "count", { gate: name, kind: "configured" });
993
+ metrics.record("tieredstore.concurrency.limit", gate.getLimit(), "count", { gate: name, kind: "effective" });
994
+ metrics.record("tieredstore.concurrency.active", gate.getActive(), "count", { gate: name });
995
+ metrics.record("tieredstore.concurrency.queued", gate.getQueued(), "count", { gate: name });
996
+ };
997
+ emitGate("ingest", cfg.ingestConcurrency, ingestGate);
998
+ emitGate("read", cfg.readConcurrency, readGate);
999
+ emitGate("search", cfg.searchConcurrency, searchGate);
1000
+ emitGate("async_index", cfg.asyncIndexConcurrency, asyncIndexGate);
1001
+ metrics.record("tieredstore.ingest.queue.capacity.requests", cfg.ingestMaxQueueRequests, "count");
1002
+ metrics.record("tieredstore.ingest.queue.capacity.bytes", cfg.ingestMaxQueueBytes, "bytes");
1003
+ metrics.record("tieredstore.upload.pending_segments", fullMode?.uploader.countSegmentsWaiting() ?? 0, "count");
1004
+ metrics.record("tieredstore.upload.concurrency.limit", cfg.uploadConcurrency, "count");
1005
+ if (cfg.memoryLimitBytes > 0) metrics.record("process.memory.limit.bytes", cfg.memoryLimitBytes, "bytes");
1006
+ const lastRss = memory.getLastRssBytes();
1007
+ if (lastRss > 0) metrics.record("process.rss.current.bytes", lastRss, "bytes");
1008
+ const maxRss = memory.snapshotMaxRssBytes();
1009
+ if (maxRss > 0) metrics.record("process.rss.max_interval.bytes", maxRss, "bytes");
1010
+ const runtimeMemory = buildRuntimeMemorySnapshot();
1011
+ metrics.record("process.heap.total.bytes", runtimeMemory.process.heap_total_bytes, "bytes");
1012
+ metrics.record("process.heap.used.bytes", runtimeMemory.process.heap_used_bytes, "bytes");
1013
+ metrics.record("process.external.bytes", runtimeMemory.process.external_bytes, "bytes");
1014
+ metrics.record("process.array_buffers.bytes", runtimeMemory.process.array_buffers_bytes, "bytes");
1015
+ if (runtimeMemory.process_breakdown.rss_anon_bytes != null) {
1016
+ metrics.record("process.memory.rss.anon.bytes", runtimeMemory.process_breakdown.rss_anon_bytes, "bytes");
1017
+ }
1018
+ if (runtimeMemory.process_breakdown.rss_file_bytes != null) {
1019
+ metrics.record("process.memory.rss.file.bytes", runtimeMemory.process_breakdown.rss_file_bytes, "bytes");
1020
+ }
1021
+ if (runtimeMemory.process_breakdown.rss_shmem_bytes != null) {
1022
+ metrics.record("process.memory.rss.shmem.bytes", runtimeMemory.process_breakdown.rss_shmem_bytes, "bytes");
1023
+ }
1024
+ if (runtimeMemory.process_breakdown.unattributed_anon_bytes != null) {
1025
+ metrics.record("process.memory.unattributed_anon.bytes", runtimeMemory.process_breakdown.unattributed_anon_bytes, "bytes");
1026
+ }
1027
+ metrics.record("process.memory.js_managed.bytes", runtimeMemory.process_breakdown.js_managed_bytes, "bytes");
1028
+ metrics.record(
1029
+ "process.memory.js_external_non_array_buffers.bytes",
1030
+ runtimeMemory.process_breakdown.js_external_non_array_buffers_bytes,
1031
+ "bytes"
1032
+ );
1033
+ metrics.record("process.memory.unattributed.bytes", runtimeMemory.process_breakdown.unattributed_rss_bytes, "bytes");
1034
+ metrics.record("tieredstore.sqlite.memory.used.bytes", runtimeMemory.sqlite.memory_used_bytes, "bytes");
1035
+ metrics.record("tieredstore.sqlite.memory.high_water.bytes", runtimeMemory.sqlite.memory_highwater_bytes, "bytes");
1036
+ metrics.record("tieredstore.sqlite.pagecache.used", runtimeMemory.sqlite.pagecache_used_slots, "count");
1037
+ metrics.record("tieredstore.sqlite.pagecache.high_water", runtimeMemory.sqlite.pagecache_used_slots_highwater, "count");
1038
+ metrics.record("tieredstore.sqlite.pagecache.overflow.bytes", runtimeMemory.sqlite.pagecache_overflow_bytes, "bytes");
1039
+ metrics.record(
1040
+ "tieredstore.sqlite.pagecache.overflow.high_water.bytes",
1041
+ runtimeMemory.sqlite.pagecache_overflow_highwater_bytes,
1042
+ "bytes"
1043
+ );
1044
+ metrics.record("tieredstore.sqlite.malloc.count", runtimeMemory.sqlite.malloc_count, "count");
1045
+ metrics.record("tieredstore.sqlite.malloc.high_water.count", runtimeMemory.sqlite.malloc_count_highwater, "count");
1046
+ metrics.record("tieredstore.sqlite.open_connections", runtimeMemory.sqlite.open_connections, "count");
1047
+ metrics.record("tieredstore.sqlite.prepared_statements", runtimeMemory.sqlite.prepared_statements, "count");
1048
+ for (const [kind, values] of Object.entries(runtimeMemory.subsystems)) {
1049
+ if (kind === "counts") continue;
1050
+ for (const [subsystem, value] of Object.entries(values)) {
1051
+ metrics.record("tieredstore.memory.subsystem.bytes", value, "bytes", { kind, subsystem });
1052
+ }
1053
+ }
1054
+ for (const [subsystem, value] of Object.entries(runtimeMemory.subsystems.counts)) {
1055
+ metrics.record("tieredstore.memory.subsystem.count", value, "count", { subsystem });
1056
+ }
1057
+ metrics.record("tieredstore.memory.tracked.bytes", runtimeMemory.totals.heap_estimate_bytes, "bytes", { kind: "heap_estimate" });
1058
+ metrics.record("tieredstore.memory.tracked.bytes", runtimeMemory.totals.mapped_file_bytes, "bytes", { kind: "mapped_file" });
1059
+ metrics.record("tieredstore.memory.tracked.bytes", runtimeMemory.totals.disk_cache_bytes, "bytes", { kind: "disk_cache" });
1060
+ metrics.record("tieredstore.memory.tracked.bytes", runtimeMemory.totals.configured_budget_bytes, "bytes", { kind: "configured_budget" });
1061
+ metrics.record("tieredstore.memory.tracked.bytes", runtimeMemory.totals.pipeline_buffer_bytes, "bytes", { kind: "pipeline_buffer" });
1062
+ metrics.record("tieredstore.memory.tracked.bytes", runtimeMemory.totals.sqlite_runtime_bytes, "bytes", { kind: "sqlite_runtime" });
1063
+ const memLeakCounters = buildLeakCandidateCounters();
1064
+ for (const [metricName, value] of Object.entries(memLeakCounters)) {
1065
+ const unit = metricName.endsWith("_bytes") ? "bytes" : "count";
1066
+ metrics.record(metricName, value, unit);
1067
+ }
1068
+ metrics.record("process.gc.forced.count", runtimeMemory.gc.forced_gc_count, "count");
1069
+ metrics.record("process.gc.reclaimed.bytes", runtimeMemory.gc.forced_gc_reclaimed_bytes_total, "bytes", { kind: "total" });
1070
+ if (runtimeMemory.gc.last_forced_gc_reclaimed_bytes != null) {
1071
+ metrics.record("process.gc.reclaimed.bytes", runtimeMemory.gc.last_forced_gc_reclaimed_bytes, "bytes", { kind: "last" });
1072
+ }
1073
+ if (runtimeMemory.gc.last_forced_gc_at_ms != null) {
1074
+ metrics.record("process.gc.last_forced_at_ms", runtimeMemory.gc.last_forced_gc_at_ms, "count");
1075
+ }
1076
+ metrics.record("process.heap.snapshot.count", runtimeMemory.gc.heap_snapshots_written, "count");
1077
+ if (runtimeMemory.gc.last_heap_snapshot_at_ms != null) {
1078
+ metrics.record("process.heap.snapshot.last_at_ms", runtimeMemory.gc.last_heap_snapshot_at_ms, "count");
1079
+ }
1080
+ for (const [metricName, entry] of Object.entries(runtimeHighWater.process)) {
1081
+ metrics.record("process.memory.high_water.bytes", entry.value, "bytes", { metric: metricName });
1082
+ }
1083
+ for (const [metricName, entry] of Object.entries(runtimeHighWater.process_breakdown)) {
1084
+ metrics.record("process.memory.high_water.bytes", entry.value, "bytes", { metric: metricName });
1085
+ }
1086
+ for (const [metricName, entry] of Object.entries(runtimeHighWater.runtime_totals)) {
1087
+ metrics.record("tieredstore.memory.high_water.bytes", entry.value, "bytes", { kind: "runtime_total", metric: metricName });
1088
+ }
1089
+ for (const [kind, entries] of Object.entries(runtimeHighWater.runtime_bytes)) {
1090
+ for (const [metricName, entry] of Object.entries(entries)) {
1091
+ metrics.record("tieredstore.memory.high_water.bytes", entry.value, "bytes", {
1092
+ kind: "runtime_subsystem",
1093
+ subsystem_kind: kind,
1094
+ metric: metricName,
1095
+ });
1096
+ }
1097
+ }
1098
+ for (const [metricName, entry] of Object.entries(runtimeHighWater.sqlite)) {
1099
+ const unit =
1100
+ metricName.includes("bytes") || metricName.includes("memory") || metricName.includes("overflow") ? "bytes" : "count";
1101
+ metrics.record("tieredstore.sqlite.high_water", entry.value, unit, { metric: metricName });
1102
+ }
1103
+ metrics.record("process.memory.pressure", memory.isOverLimit() ? 1 : 0, "count");
1104
+ if (backpressure) {
1105
+ metrics.record("tieredstore.backpressure.current.bytes", backpressure.getCurrentBytes(), "bytes");
1106
+ metrics.record("tieredstore.backpressure.limit.bytes", backpressure.getMaxBytes(), "bytes");
1107
+ metrics.record("tieredstore.backpressure.pressure", backpressure.isOverLimit() ? 1 : 0, "count");
1108
+ }
1109
+ if (cfg.autoTunePresetMb != null) {
1110
+ metrics.record("tieredstore.auto_tune.preset_mb", cfg.autoTunePresetMb, "count");
1111
+ }
1112
+ if (cfg.autoTuneEffectiveMemoryLimitMb != null) {
1113
+ metrics.record("tieredstore.auto_tune.effective_memory_limit_mb", cfg.autoTuneEffectiveMemoryLimitMb, "count");
1114
+ }
1115
+ };
1116
+ const metricsEmitter = new MetricsEmitter(metrics, ingest, cfg.metricsFlushIntervalMs, {
1117
+ onAppended: ({ lastOffset, stream }) => {
1118
+ notifier.notify(stream, lastOffset);
1119
+ notifier.notifyDetailsChanged(stream);
1120
+ },
1121
+ collectRuntimeMetrics,
1122
+ });
1123
+ const expirySweeper = new ExpirySweeper(cfg, controlStore);
1124
+ let closing = false;
1125
+
1126
+ const startupPromise = (async () => {
1127
+ let metricsProfileRes: Awaited<ReturnType<StreamProfileStore["updateProfileResult"]>> | null = null;
1128
+ if (controlStore.capabilities.internalMetrics) {
1129
+ await controlStore.ensureStream(INTERNAL_METRICS_STREAM, { contentType: "application/json", profile: "metrics" });
1130
+ opts.lifecycleHooks?.afterInternalMetricsProfileEnsured?.();
1131
+ metricsProfileRes = await profiles.updateProfileResult(INTERNAL_METRICS_STREAM, { kind: "metrics" });
1132
+ if (Result.isError(metricsProfileRes)) {
1133
+ throw dsError(`failed to initialize ${INTERNAL_METRICS_STREAM} profile: ${metricsProfileRes.error.message}`);
1134
+ }
1135
+ }
1136
+ if (closing) return;
1137
+ runtime.start();
1138
+ if (controlStore.capabilities.internalMetrics) metricsEmitter.start();
1139
+ expirySweeper.start();
1140
+ touch?.start();
1141
+ fullMode?.sizeReconciler?.start();
1142
+ if (schemaPublication && metricsProfileRes && Result.isOk(metricsProfileRes) && metricsProfileRes.value.schemaRegistry) {
1143
+ void schemaPublication
1144
+ .publishProfileSchemaRegistry(INTERNAL_METRICS_STREAM, metricsProfileRes.value.schemaRegistry)
1145
+ .catch(() => {
1146
+ // background best-effort; next manifest publication will reconcile
1147
+ });
1148
+ }
1149
+ })();
1150
+ const ready = startupPromise.catch((err) => {
1151
+ throw err;
1152
+ });
1153
+ void ready.catch(() => {});
1154
+
1155
+ const getLiveStreamResult = async (stream: string): Promise<Result<StreamRow, { status: 404; message: string }>> => {
1156
+ const srow = await controlStore.getStream(stream);
1157
+ if (!srow || controlStore.isDeleted(srow)) return Result.err({ status: 404, message: "not_found" });
1158
+ if (srow.expires_at_ms != null && controlStore.nowMs() > srow.expires_at_ms) {
1159
+ return Result.err({ status: 404, message: "stream expired" });
1160
+ }
1161
+ return Result.ok(srow as StreamRow);
1162
+ };
1163
+
1164
+ const liveStreamOrResponse = async (stream: string): Promise<StreamRow | Response> => {
1165
+ const srowRes = await getLiveStreamResult(stream);
1166
+ if (Result.isOk(srowRes)) return srowRes.value;
1167
+ return srowRes.error.message === "stream expired" ? notFound("stream expired") : notFound();
1168
+ };
1169
+
1170
+ const buildJsonRows = async (
1171
+ stream: string,
1172
+ bodyBytes: Uint8Array,
1173
+ routingKeyHeader: string | null,
1174
+ allowEmptyArray: boolean
1175
+ ): Promise<Result<{ rows: AppendRow[] }, { status: 400 | 500; message: string }>> => {
1176
+ const regRes = await registry.getRegistryResult(stream);
1177
+ if (Result.isError(regRes)) {
1178
+ return Result.err({ status: 500, message: regRes.error.message });
1179
+ }
1180
+ const profileRes = await profiles.getProfileResult(stream);
1181
+ if (Result.isError(profileRes)) {
1182
+ return Result.err({ status: 500, message: profileRes.error.message });
1183
+ }
1184
+ const reg = regRes.value;
1185
+ const jsonIngest = resolveJsonIngestCapability(profileRes.value);
1186
+ const text = JSON_TEXT_DECODER.decode(bodyBytes);
1187
+ let arr: any;
1188
+ try {
1189
+ arr = JSON.parse(text);
1190
+ } catch {
1191
+ return Result.err({ status: 400, message: "invalid JSON" });
1192
+ }
1193
+ if (!Array.isArray(arr)) arr = [arr];
1194
+ if (arr.length === 0 && !allowEmptyArray) return Result.err({ status: 400, message: "empty JSON array" });
1195
+ if (reg.routingKey && routingKeyHeader) {
1196
+ return Result.err({ status: 400, message: "Stream-Key not allowed when routingKey is configured" });
1197
+ }
1198
+
1199
+ const validator = reg.currentVersion > 0 ? registry.getValidatorForVersion(reg, reg.currentVersion) : null;
1200
+ if (reg.currentVersion > 0 && !validator) {
1201
+ return Result.err({ status: 500, message: "schema validator missing" });
1202
+ }
1203
+
1204
+ const rows: AppendRow[] = [];
1205
+ for (const v of arr) {
1206
+ let value = v;
1207
+ let profileRoutingKey: Uint8Array | null = null;
1208
+ if (jsonIngest) {
1209
+ const preparedRes = jsonIngest.prepareRecordResult({ stream, profile: profileRes.value, value: v });
1210
+ if (Result.isError(preparedRes)) return Result.err({ status: 400, message: preparedRes.error.message });
1211
+ value = preparedRes.value.value;
1212
+ profileRoutingKey = keyBytesFromString(preparedRes.value.routingKey);
1213
+ }
1214
+ if (validator && !validator(value)) {
1215
+ const msg = validator.errors ? validator.errors.map((e) => e.message).join("; ") : "schema validation failed";
1216
+ return Result.err({ status: 400, message: msg });
1217
+ }
1218
+ const rkRes = reg.routingKey
1219
+ ? extractRoutingKey(reg, value)
1220
+ : Result.ok(routingKeyHeader != null ? keyBytesFromString(routingKeyHeader) : profileRoutingKey);
1221
+ if (Result.isError(rkRes)) return Result.err({ status: 400, message: rkRes.error.message });
1222
+ rows.push({
1223
+ routingKey: rkRes.value,
1224
+ contentType: "application/json",
1225
+ payload: JSON_TEXT_ENCODER.encode(JSON.stringify(value)),
1226
+ });
1227
+ }
1228
+ return Result.ok({ rows });
1229
+ };
1230
+
1231
+ const buildPreparedJsonRows = async (
1232
+ stream: string,
1233
+ records: PreparedJsonRecord[]
1234
+ ): Promise<Result<{ rows: AppendRow[] }, { status: 400 | 500; message: string }>> => {
1235
+ const regRes = await registry.getRegistryResult(stream);
1236
+ if (Result.isError(regRes)) return Result.err({ status: 500, message: regRes.error.message });
1237
+ const reg = regRes.value;
1238
+ const validator = reg.currentVersion > 0 ? registry.getValidatorForVersion(reg, reg.currentVersion) : null;
1239
+ if (reg.currentVersion > 0 && !validator) {
1240
+ return Result.err({ status: 500, message: "schema validator missing" });
1241
+ }
1242
+ const rows: AppendRow[] = [];
1243
+ for (const record of records) {
1244
+ if (validator && !validator(record.value)) {
1245
+ const msg = validator.errors ? validator.errors.map((e) => e.message).join("; ") : "schema validation failed";
1246
+ return Result.err({ status: 400, message: msg });
1247
+ }
1248
+ rows.push({
1249
+ routingKey: keyBytesFromString(record.routingKey),
1250
+ contentType: "application/json",
1251
+ payload: JSON_TEXT_ENCODER.encode(JSON.stringify(record.value)),
1252
+ });
1253
+ }
1254
+ return Result.ok({ rows });
1255
+ };
1256
+
1257
+ const buildAppendRowsResult = async (
1258
+ stream: string,
1259
+ bodyBytes: Uint8Array,
1260
+ contentType: string,
1261
+ routingKeyHeader: string | null,
1262
+ allowEmptyJsonArray: boolean
1263
+ ): Promise<Result<{ rows: AppendRow[] }, { status: 400 | 500; message: string }>> => {
1264
+ if (isJsonContentType(contentType)) {
1265
+ return buildJsonRows(stream, bodyBytes, routingKeyHeader, allowEmptyJsonArray);
1266
+ }
1267
+ const regRes = await registry.getRegistryResult(stream);
1268
+ if (Result.isError(regRes)) return Result.err({ status: 500, message: regRes.error.message });
1269
+ const reg = regRes.value;
1270
+ if (reg.currentVersion > 0) return Result.err({ status: 400, message: "stream requires JSON" });
1271
+ return Result.ok({
1272
+ rows: [
1273
+ {
1274
+ routingKey: keyBytesFromString(routingKeyHeader),
1275
+ contentType,
1276
+ payload: bodyBytes,
1277
+ },
1278
+ ],
1279
+ });
1280
+ };
1281
+
1282
+ const enqueueAppend = (args: {
1283
+ stream: string;
1284
+ baseAppendMs: bigint;
1285
+ rows: AppendRow[];
1286
+ contentType: string | null;
1287
+ close: boolean;
1288
+ streamSeq?: string | null;
1289
+ producer?: ProducerInfo | null;
1290
+ }) =>
1291
+ ingest.append({
1292
+ stream: args.stream,
1293
+ baseAppendMs: args.baseAppendMs,
1294
+ rows: args.rows,
1295
+ contentType: args.contentType,
1296
+ streamSeq: args.streamSeq,
1297
+ producer: args.producer,
1298
+ close: args.close,
1299
+ });
1300
+
1301
+ const awaitAppendWithTimeout = async (appendPromise: Promise<AppendResult>): Promise<AppendResult | Response> => {
1302
+ const appendResult = await awaitWithCooperativeTimeout(appendPromise, APPEND_REQUEST_TIMEOUT_MS);
1303
+ return appendResult === TIMEOUT_SENTINEL ? appendTimeout() : appendResult;
1304
+ };
1305
+
1306
+ const recordAppendOutcome = (args: {
1307
+ stream: string;
1308
+ lastOffset: bigint;
1309
+ appendedRows: number;
1310
+ metricsBytes: number;
1311
+ ingestedBytes: number;
1312
+ touched: boolean;
1313
+ closed: boolean;
1314
+ }): void => {
1315
+ if (args.appendedRows > 0) {
1316
+ metrics.recordAppend(args.metricsBytes, args.appendedRows);
1317
+ notifier.notify(args.stream, args.lastOffset);
1318
+ notifier.notifyDetailsChanged(args.stream);
1319
+ touch?.notify(args.stream);
1320
+ }
1321
+ if (stats) {
1322
+ if (args.touched) stats.recordStreamTouched(args.stream);
1323
+ if (args.appendedRows > 0) stats.recordIngested(args.ingestedBytes);
1324
+ }
1325
+ if (args.closed) {
1326
+ notifier.notifyDetailsChanged(args.stream);
1327
+ notifier.notifyClose(args.stream);
1328
+ }
1329
+ };
1330
+
1331
+ const decodeJsonRecords = async (
1332
+ stream: string,
1333
+ records: Array<{ offset: bigint; payload: Uint8Array }>
1334
+ ): Promise<Result<{ values: any[] }, { status: 400 | 500; message: string }>> => {
1335
+ const regRes = await registry.getRegistryResult(stream);
1336
+ if (Result.isError(regRes)) return Result.err({ status: 500, message: regRes.error.message });
1337
+ const values: any[] = [];
1338
+ for (const r of records) {
1339
+ const valueRes = decodeJsonPayloadWithRegistryResult(registry, regRes.value, r.offset, r.payload);
1340
+ if (Result.isError(valueRes)) return valueRes;
1341
+ values.push(valueRes.value);
1342
+ }
1343
+ return Result.ok({ values });
1344
+ };
1345
+
1346
+ const encodeStoredJsonArrayResult = async (
1347
+ stream: string,
1348
+ records: Array<{ payload: Uint8Array }>
1349
+ ): Promise<Result<Buffer | null, { status: 400 | 500; message: string }>> => {
1350
+ const regRes = await registry.getRegistryResult(stream);
1351
+ if (Result.isError(regRes)) return Result.err({ status: 500, message: regRes.error.message });
1352
+ if (regRes.value.currentVersion !== 0) return Result.ok(null);
1353
+ const parts: Buffer[] = [Buffer.from("[")];
1354
+ for (let i = 0; i < records.length; i++) {
1355
+ if (i > 0) parts.push(Buffer.from(","));
1356
+ const payload = records[i]!.payload;
1357
+ parts.push(Buffer.from(payload.buffer, payload.byteOffset, payload.byteLength));
1358
+ }
1359
+ parts.push(Buffer.from("]"));
1360
+ return Result.ok(Buffer.concat(parts));
1361
+ };
1362
+
1363
+ type DetailsSnapshot = { etag: string; body: string; version: bigint };
1364
+
1365
+ const buildDetailsSnapshotResult = async (
1366
+ stream: string,
1367
+ mode: "details" | "index_status"
1368
+ ): Promise<Result<DetailsSnapshot, { status: 404 | 500; message: string }>> => {
1369
+ for (let attempt = 0; attempt < 3; attempt++) {
1370
+ const beforeVersion = notifier.currentDetailsVersion(stream);
1371
+ const srowRes = await getLiveStreamResult(stream);
1372
+ if (Result.isError(srowRes)) return srowRes;
1373
+ const srow = srowRes.value;
1374
+
1375
+ const regRes = await registry.getRegistryResult(stream);
1376
+ if (Result.isError(regRes)) return Result.err({ status: 500, message: regRes.error.message });
1377
+ const profileRes = await profiles.getProfileResourceResult(stream, srow);
1378
+ if (Result.isError(profileRes)) return Result.err({ status: 500, message: profileRes.error.message });
1379
+
1380
+ if (!detailsBuilder) return Result.err({ status: 500, message: "full-mode details store is not available" });
1381
+ const payload = await detailsBuilder.buildPayload({
1382
+ stream,
1383
+ row: srow,
1384
+ registry: regRes.value,
1385
+ profileResource: profileRes.value,
1386
+ mode,
1387
+ });
1388
+ const body = JSON.stringify(payload);
1389
+ const afterVersion = notifier.currentDetailsVersion(stream);
1390
+ if (beforeVersion === afterVersion) {
1391
+ return Result.ok({
1392
+ etag: weakEtag(mode, body),
1393
+ body,
1394
+ version: afterVersion,
1395
+ });
1396
+ }
1397
+ }
1398
+
1399
+ return Result.err({ status: 500, message: "details changed too quickly" });
1400
+ };
1401
+
1402
+ const fetch = async (req: Request): Promise<Response> => {
1403
+ if (closing) {
1404
+ return unavailable();
1405
+ }
1406
+ const requestAbortController = new AbortController();
1407
+ const abortFromClient = () => requestAbortController.abort(req.signal.reason);
1408
+ let timedOut = false;
1409
+ if (req.signal.aborted) requestAbortController.abort(req.signal.reason);
1410
+ else req.signal.addEventListener("abort", abortFromClient, { once: true });
1411
+ try {
1412
+ const runWithGate = async <T>(gate: ConcurrencyGate, fn: () => Promise<T>): Promise<T> =>
1413
+ gate.run(fn, requestAbortController.signal);
1414
+ const runForeground = async <T>(fn: () => Promise<T>): Promise<T> => {
1415
+ const leaveForeground = foregroundActivity.enter();
1416
+ try {
1417
+ return await fn();
1418
+ } finally {
1419
+ leaveForeground();
1420
+ }
1421
+ };
1422
+ const runForegroundWithGate = async <T>(gate: ConcurrencyGate, fn: () => Promise<T>): Promise<T> =>
1423
+ runForeground(() => runWithGate(gate, fn));
1424
+ const requestPromise = (async (): Promise<Response> => {
1425
+ await ready;
1426
+ let url: URL;
1427
+ try {
1428
+ url = new URL(req.url, "http://localhost");
1429
+ } catch {
1430
+ return badRequest("invalid url");
1431
+ }
1432
+ const path = url.pathname;
1433
+
1434
+ const handleOtlpTracesIngest = async (stream: string, autoCreate: boolean): Promise<Response> => {
1435
+ if (req.method !== "POST") return badRequest("unsupported method");
1436
+ const contentType = req.headers.get("content-type");
1437
+ if (!contentType) return badRequest("missing content-type");
1438
+ const leaveAppendPhase = memorySampler?.enter("append", {
1439
+ route: "otlp_traces",
1440
+ stream,
1441
+ content_type: normalizeContentType(contentType) ?? contentType,
1442
+ });
1443
+ try {
1444
+ return await runWithGate(ingestGate, async () => {
1445
+ let srow = await controlStore.getStream(stream);
1446
+ if (!srow && autoCreate) {
1447
+ srow = await controlStore.ensureStream(stream, { contentType: "application/json" });
1448
+ const profileRes = await profiles.updateProfileResult(stream, { kind: "otel-traces" });
1449
+ if (Result.isError(profileRes)) return badRequest(profileRes.error.message);
1450
+ try {
1451
+ if (profileRes.value.schemaRegistry) {
1452
+ if (schemaPublication) await schemaPublication.publishProfileSchemaRegistry(stream, profileRes.value.schemaRegistry);
1453
+ }
1454
+ } catch {
1455
+ return json(500, { error: { code: "internal", message: "profile upload failed" } });
1456
+ }
1457
+ indexer?.enqueue(stream);
1458
+ notifier.notifyDetailsChanged(stream);
1459
+ srow = await controlStore.getStream(stream);
1460
+ }
1461
+ if (!srow || controlStore.isDeleted(srow)) return notFound();
1462
+ if (srow.expires_at_ms != null && controlStore.nowMs() > srow.expires_at_ms) return notFound("stream expired");
1463
+
1464
+ const profileRes = await profiles.getProfileResult(stream, srow);
1465
+ if (Result.isError(profileRes)) return internalError("invalid stream profile");
1466
+ const capability = resolveOtlpTracesCapability(profileRes.value);
1467
+ if (!capability) return badRequest("stream profile does not support OTLP traces");
1468
+
1469
+ const ab = await req.arrayBuffer();
1470
+ if (ab.byteLength > cfg.appendMaxBodyBytes) return tooLarge(`body too large (max ${cfg.appendMaxBodyBytes})`);
1471
+ const bodyBytes = new Uint8Array(ab);
1472
+ const decodedRes = capability.decodeExportRequestResult({
1473
+ stream,
1474
+ profile: profileRes.value,
1475
+ contentType,
1476
+ contentEncoding: req.headers.get("content-encoding"),
1477
+ body: bodyBytes,
1478
+ maxDecodedBytes: cfg.appendMaxBodyBytes,
1479
+ });
1480
+ if (Result.isError(decodedRes)) {
1481
+ if (decodedRes.error.status === 415) return unsupportedMediaType(decodedRes.error.message);
1482
+ if (decodedRes.error.status === 413) return tooLarge(decodedRes.error.message);
1483
+ return badRequest(decodedRes.error.message);
1484
+ }
1485
+
1486
+ const rowsRes = await buildPreparedJsonRows(stream, decodedRes.value.records);
1487
+ if (Result.isError(rowsRes)) {
1488
+ if (rowsRes.error.status === 500) return internalError(rowsRes.error.message);
1489
+ return badRequest(rowsRes.error.message);
1490
+ }
1491
+ const rows = rowsRes.value.rows;
1492
+ let appendHeaders: Record<string, string> = {};
1493
+ if (rows.length > 0) {
1494
+ const appendResOrResponse = await awaitAppendWithTimeout(enqueueAppend({
1495
+ stream,
1496
+ baseAppendMs: controlStore.nowMs(),
1497
+ rows,
1498
+ contentType: "application/json",
1499
+ close: false,
1500
+ }));
1501
+ if (appendResOrResponse instanceof Response) return appendResOrResponse;
1502
+ const appendRes = appendResOrResponse;
1503
+ if (Result.isError(appendRes)) {
1504
+ if (appendRes.error.kind === "overloaded") return overloaded();
1505
+ if (appendRes.error.kind === "gone") return notFound("stream expired");
1506
+ if (appendRes.error.kind === "not_found") return notFound();
1507
+ if (appendRes.error.kind === "content_type_mismatch") return conflict("content-type mismatch");
1508
+ return json(500, { error: { code: "internal", message: "append failed" } });
1509
+ }
1510
+ const appendBytes = rows.reduce((acc, row) => acc + row.payload.byteLength, 0);
1511
+ recordAppendOutcome({
1512
+ stream,
1513
+ lastOffset: appendRes.value.lastOffset,
1514
+ appendedRows: appendRes.value.appendedRows,
1515
+ metricsBytes: appendBytes,
1516
+ ingestedBytes: bodyBytes.byteLength,
1517
+ touched: true,
1518
+ closed: appendRes.value.closed,
1519
+ });
1520
+ appendHeaders = {
1521
+ "stream-next-offset": encodeOffset(srow.epoch, appendRes.value.lastOffset),
1522
+ };
1523
+ }
1524
+
1525
+ const encoded = encodeOtlpTraceExportResponse(decodedRes.value);
1526
+ const responseBody = encoded.body instanceof Uint8Array ? bodyBufferFromBytes(encoded.body) : encoded.body;
1527
+ return new Response(responseBody, {
1528
+ status: 200,
1529
+ headers: withNosniff({
1530
+ "content-type": encoded.contentType,
1531
+ "cache-control": "no-store",
1532
+ ...appendHeaders,
1533
+ }),
1534
+ });
1535
+ });
1536
+ } finally {
1537
+ leaveAppendPhase?.();
1538
+ }
1539
+ };
1540
+
1541
+ const handleObserveRequest = async (): Promise<Response> => {
1542
+ if (req.method !== "POST") return badRequest("unsupported method");
1543
+ let body: unknown;
1544
+ try {
1545
+ body = await req.json();
1546
+ } catch {
1547
+ return badRequest("observe request must be valid JSON");
1548
+ }
1549
+ const requestRes = parseObserveRequestResult(body);
1550
+ if (Result.isError(requestRes)) return badRequest(requestRes.error.message);
1551
+ const observeReq = requestRes.value;
1552
+
1553
+ const loadCorrelationCapability = async (
1554
+ stream: string,
1555
+ role: "events" | "traces"
1556
+ ): Promise<ReturnType<typeof resolveCorrelationCapability> | Response> => {
1557
+ const srow = await controlStore.getStream(stream);
1558
+ if (!srow || controlStore.isDeleted(srow)) return notFound();
1559
+ if (srow.expires_at_ms != null && controlStore.nowMs() > srow.expires_at_ms) return notFound("stream expired");
1560
+ const profileRes = await profiles.getProfileResult(stream, srow);
1561
+ if (Result.isError(profileRes)) return internalError("invalid stream profile");
1562
+ if (role === "events" && profileRes.value.kind !== "evlog") {
1563
+ return badRequest(`streams.events must reference an evlog stream; ${stream} has profile ${profileRes.value.kind}`);
1564
+ }
1565
+ if (role === "traces" && profileRes.value.kind !== "otel-traces") {
1566
+ return badRequest(`streams.traces must reference an otel-traces stream; ${stream} has profile ${profileRes.value.kind}`);
1567
+ }
1568
+ const capability = resolveCorrelationCapability(profileRes.value);
1569
+ if (!capability) return badRequest(`stream ${stream} profile does not support observability correlation`);
1570
+ const regRes = await registry.getRegistryResult(stream);
1571
+ if (Result.isError(regRes)) return internalError(regRes.error.message);
1572
+ if (!regRes.value.search) return badRequest(`stream ${stream} does not have search configured`);
1573
+ return capability;
1574
+ };
1575
+
1576
+ const eventCorrelation =
1577
+ observeReq.include.events && observeReq.streams.events ? await loadCorrelationCapability(observeReq.streams.events, "events") : null;
1578
+ if (eventCorrelation instanceof Response) return eventCorrelation;
1579
+ const traceCorrelation =
1580
+ observeReq.include.trace && observeReq.streams.traces ? await loadCorrelationCapability(observeReq.streams.traces, "traces") : null;
1581
+ if (traceCorrelation instanceof Response) return traceCorrelation;
1582
+
1583
+ const runPagedSearch = async (
1584
+ stream: string,
1585
+ q: string,
1586
+ limit: number,
1587
+ sort: string[]
1588
+ ): Promise<{ hits: SearchHit[]; batches: SearchResultBatch[]; limitReached: boolean; query: ObserveSearchQueryCoverage } | Response> => {
1589
+ const regRes = await registry.getRegistryResult(stream);
1590
+ if (Result.isError(regRes)) return internalError(regRes.error.message);
1591
+ const hits: SearchHit[] = [];
1592
+ const batches: SearchResultBatch[] = [];
1593
+ const seenOffsets = new Set<string>();
1594
+ let searchAfter: unknown[] | null = null;
1595
+ let limitReached = false;
1596
+ while (hits.length < limit) {
1597
+ const size = Math.min(500, limit - hits.length);
1598
+ const requestBody: Record<string, unknown> = {
1599
+ q,
1600
+ size,
1601
+ sort,
1602
+ timeout_ms: SEARCH_REQUEST_TIMEOUT_MS,
1603
+ };
1604
+ if (searchAfter) requestBody.search_after = searchAfter;
1605
+ const parsedRes = parseSearchRequestBodyResult(regRes.value, requestBody);
1606
+ if (Result.isError(parsedRes)) return badRequest(parsedRes.error.message);
1607
+ const request = {
1608
+ ...parsedRes.value,
1609
+ timeoutMs: clampSearchRequestTimeoutMs(parsedRes.value.timeoutMs),
1610
+ };
1611
+ const searchRes = await runForegroundWithGate(searchGate, () => reader.searchResult({ stream, request }));
1612
+ if (Result.isError(searchRes)) return readerErrorResponse(searchRes.error);
1613
+ batches.push(searchRes.value);
1614
+ for (const hit of searchRes.value.hits) {
1615
+ if (seenOffsets.has(hit.offset)) continue;
1616
+ seenOffsets.add(hit.offset);
1617
+ hits.push(hit);
1618
+ if (hits.length >= limit) break;
1619
+ }
1620
+ if (!searchRes.value.nextSearchAfter || searchRes.value.hits.length === 0) break;
1621
+ searchAfter = searchRes.value.nextSearchAfter;
1622
+ if (hits.length >= limit) {
1623
+ limitReached = true;
1624
+ break;
1625
+ }
1626
+ }
1627
+ return { hits, batches, limitReached, query: summarizeSearchQueryCoverage(q, batches, hits, limitReached) };
1628
+ };
1629
+
1630
+ const timeClauses = buildTimeSearchClauses(observeReq.time);
1631
+ const lookupClause = (field: "req" | "trace" | "span", value: string) => `${field}:${quoteSearchValue(value)}`;
1632
+ const eventSort = ["timestamp:desc", "offset:desc"];
1633
+ const traceSort = ["timestamp:asc", "spanId:asc"];
1634
+ let eventHits: SearchHit[] = [];
1635
+ let eventBatches: SearchResultBatch[] = [];
1636
+ const eventQueries: ObserveSearchQueryCoverage[] = [];
1637
+ let eventLimitReached = false;
1638
+ let traceHits: SearchHit[] = [];
1639
+ let traceBatches: SearchResultBatch[] = [];
1640
+ const traceQueries: ObserveSearchQueryCoverage[] = [];
1641
+ let traceLimitReached = false;
1642
+ const candidateTraceIds = new Set<string>();
1643
+ const addTraceIdsFromHits = (hits: SearchHit[]) => {
1644
+ for (const hit of hits) {
1645
+ if (!isRecord(hit.source)) continue;
1646
+ const traceId = typeof hit.source.traceId === "string" ? hit.source.traceId : null;
1647
+ if (traceId) candidateTraceIds.add(traceId);
1648
+ }
1649
+ };
1650
+ const appendSearch = (
1651
+ target: "events" | "traces",
1652
+ result: { hits: SearchHit[]; batches: SearchResultBatch[]; limitReached: boolean; query: ObserveSearchQueryCoverage }
1653
+ ) => {
1654
+ const stream = result.batches[0]?.stream ?? "";
1655
+ if (target === "events") {
1656
+ const seen = new Set(eventHits.map((hit) => `${(hit as SearchHit & { stream?: string }).stream ?? ""}\0${hit.offset}`));
1657
+ for (const hit of result.hits) {
1658
+ const key = `${stream}\0${hit.offset}`;
1659
+ if (seen.has(key)) continue;
1660
+ seen.add(key);
1661
+ eventHits.push({ ...hit, stream } as SearchHit);
1662
+ }
1663
+ eventBatches.push(...result.batches);
1664
+ eventQueries.push(result.query);
1665
+ eventLimitReached = eventLimitReached || result.limitReached || eventHits.length >= observeReq.limits.events && !!result.batches.at(-1)?.nextSearchAfter;
1666
+ } else {
1667
+ const seen = new Set(traceHits.map((hit) => `${(hit as SearchHit & { stream?: string }).stream ?? ""}\0${hit.offset}`));
1668
+ for (const hit of result.hits) {
1669
+ const key = `${stream}\0${hit.offset}`;
1670
+ if (seen.has(key)) continue;
1671
+ seen.add(key);
1672
+ traceHits.push({ ...hit, stream } as SearchHit);
1673
+ }
1674
+ traceBatches.push(...result.batches);
1675
+ traceQueries.push(result.query);
1676
+ traceLimitReached = traceLimitReached || result.limitReached || traceHits.length >= observeReq.limits.spans && !!result.batches.at(-1)?.nextSearchAfter;
1677
+ }
1678
+ };
1679
+
1680
+ const searchEvents = async (field: "req" | "trace" | "span", value: string): Promise<Response | null> => {
1681
+ if (!observeReq.include.events || !observeReq.streams.events) return null;
1682
+ if (eventHits.length >= observeReq.limits.events) return null;
1683
+ const q = combineSearchClauses(lookupClause(field, value), ...timeClauses);
1684
+ const result = await runPagedSearch(observeReq.streams.events, q, observeReq.limits.events - eventHits.length, eventSort);
1685
+ if (result instanceof Response) return result;
1686
+ appendSearch("events", result);
1687
+ addTraceIdsFromHits(result.hits);
1688
+ return null;
1689
+ };
1690
+
1691
+ const searchTraces = async (field: "req" | "trace" | "span", value: string): Promise<Response | null> => {
1692
+ if (!observeReq.include.trace || !observeReq.streams.traces) return null;
1693
+ if (traceHits.length >= observeReq.limits.spans) return null;
1694
+ const q = combineSearchClauses(lookupClause(field, value), ...timeClauses);
1695
+ const result = await runPagedSearch(observeReq.streams.traces, q, observeReq.limits.spans - traceHits.length, traceSort);
1696
+ if (result instanceof Response) return result;
1697
+ appendSearch("traces", result);
1698
+ addTraceIdsFromHits(result.hits);
1699
+ return null;
1700
+ };
1701
+
1702
+ if (observeReq.lookup.requestId) {
1703
+ const eventResponse = await searchEvents("req", observeReq.lookup.requestId);
1704
+ if (eventResponse) return eventResponse;
1705
+ if (candidateTraceIds.size > 0) {
1706
+ for (const traceId of candidateTraceIds) {
1707
+ const traceResponse = await searchTraces("trace", traceId);
1708
+ if (traceResponse) return traceResponse;
1709
+ }
1710
+ } else {
1711
+ const traceResponse = await searchTraces("req", observeReq.lookup.requestId);
1712
+ if (traceResponse) return traceResponse;
1713
+ }
1714
+ } else if (observeReq.lookup.traceId) {
1715
+ candidateTraceIds.add(observeReq.lookup.traceId);
1716
+ const traceResponse = await searchTraces("trace", observeReq.lookup.traceId);
1717
+ if (traceResponse) return traceResponse;
1718
+ const eventResponse = await searchEvents("trace", observeReq.lookup.traceId);
1719
+ if (eventResponse) return eventResponse;
1720
+ } else if (observeReq.lookup.spanId) {
1721
+ const traceResponse = await searchTraces("span", observeReq.lookup.spanId);
1722
+ if (traceResponse) return traceResponse;
1723
+ if (candidateTraceIds.size > 0) {
1724
+ for (const traceId of Array.from(candidateTraceIds)) {
1725
+ const fullTraceResponse = await searchTraces("trace", traceId);
1726
+ if (fullTraceResponse) return fullTraceResponse;
1727
+ const eventResponse = await searchEvents("trace", traceId);
1728
+ if (eventResponse) return eventResponse;
1729
+ }
1730
+ } else {
1731
+ const eventResponse = await searchEvents("span", observeReq.lookup.spanId);
1732
+ if (eventResponse) return eventResponse;
1733
+ }
1734
+ }
1735
+
1736
+ const eventCoverage = summarizeSearchCoverage(eventBatches, eventHits, eventLimitReached, eventQueries);
1737
+ const traceCoverage = summarizeSearchCoverage(traceBatches, traceHits, traceLimitReached, traceQueries);
1738
+ const trace = buildTraceDetails(
1739
+ traceHits.map((hit) => hit.source),
1740
+ { spanLimitReached: traceCoverage.limit_reached, coverageComplete: traceCoverage.complete }
1741
+ );
1742
+ const primaryEventHit = choosePrimaryEvent(eventHits, trace.traceId ?? observeReq.lookup.traceId);
1743
+ const primaryEvent = primaryEventHit && isRecord(primaryEventHit.source) ? primaryEventHit.source : null;
1744
+ const timeline: unknown[] = [];
1745
+ if (observeReq.include.timeline) {
1746
+ const items: any[] = [];
1747
+ if (eventCorrelation && observeReq.streams.events) {
1748
+ for (const hit of eventHits) {
1749
+ items.push(...eventCorrelation.toTimelineItems({ stream: observeReq.streams.events, offset: hit.offset, record: hit.source }));
1750
+ }
1751
+ }
1752
+ if (traceCorrelation && observeReq.streams.traces) {
1753
+ for (const hit of traceHits) {
1754
+ items.push(...traceCorrelation.toTimelineItems({ stream: observeReq.streams.traces, offset: hit.offset, record: hit.source }));
1755
+ }
1756
+ }
1757
+ timeline.push(...sortTimeline(items));
1758
+ }
1759
+ const responsePrimaryEvent = observeReq.include.raw ? primaryEvent : compactEvlogRecord(primaryEvent);
1760
+ const responseEventMatches = eventHits.map((hit) => ({
1761
+ offset: hit.offset,
1762
+ source: observeReq.include.raw ? hit.source : compactEvlogRecord(hit.source),
1763
+ }));
1764
+ const responseTraceSpans = observeReq.include.raw ? trace.spans : trace.spans.map((span) => compactTraceSpanRecord(span));
1765
+ const responseTimeline = observeReq.include.raw ? timeline : timeline.map((item) => compactTimelineItem(item));
1766
+
1767
+ const warnings: string[] = [];
1768
+ if (observeReq.include.trace && traceHits.length === 0) warnings.push("no trace spans found");
1769
+ if (observeReq.include.events && eventHits.length === 0) warnings.push("no evlog events found");
1770
+ if (eventCoverage.limit_reached) warnings.push("event limit reached");
1771
+ if (traceCoverage.limit_reached) warnings.push("span limit reached");
1772
+ if (!eventCoverage.complete && eventCoverage.searched) warnings.push("event search coverage incomplete");
1773
+ if (!traceCoverage.complete && traceCoverage.searched) warnings.push("trace search coverage incomplete");
1774
+ if (trace.missingParents.length > 0) warnings.push("trace has missing parent spans");
1775
+
1776
+ return json(200, {
1777
+ lookup: {
1778
+ requestId:
1779
+ observeReq.lookup.requestId ??
1780
+ (primaryEvent && typeof primaryEvent.requestId === "string" ? primaryEvent.requestId : null) ??
1781
+ null,
1782
+ traceId: observeReq.lookup.traceId ?? trace.traceId,
1783
+ spanId: observeReq.lookup.spanId,
1784
+ },
1785
+ summary: buildObserveSummary({ lookup: observeReq.lookup, primaryEvent, trace }),
1786
+ evlog: observeReq.include.events
1787
+ ? {
1788
+ stream: observeReq.streams.events ?? null,
1789
+ primary: responsePrimaryEvent,
1790
+ matches: responseEventMatches,
1791
+ }
1792
+ : null,
1793
+ trace: observeReq.include.trace
1794
+ ? {
1795
+ stream: observeReq.streams.traces ?? null,
1796
+ traceId: trace.traceId,
1797
+ rootSpanId: trace.rootSpanId,
1798
+ spans: responseTraceSpans,
1799
+ tree: trace.tree,
1800
+ serviceMap: trace.serviceMap,
1801
+ criticalPath: trace.criticalPath,
1802
+ errors: trace.errors,
1803
+ partial: trace.partial,
1804
+ missingParents: trace.missingParents,
1805
+ duplicateSpans: trace.duplicateSpans,
1806
+ }
1807
+ : null,
1808
+ timeline: responseTimeline,
1809
+ coverage: {
1810
+ events: eventCoverage,
1811
+ traces: traceCoverage,
1812
+ warnings,
1813
+ },
1814
+ });
1815
+ };
1816
+
1817
+ if (path === "/health") {
1818
+ return json(200, { ok: true });
1819
+ }
1820
+ if (path === "/metrics") {
1821
+ return json(200, metrics.snapshot());
1822
+ }
1823
+ if (req.method === "GET" && path === "/v1/server/_details") {
1824
+ return json(200, await buildServerDetails());
1825
+ }
1826
+ if (req.method === "GET" && path === "/v1/server/_mem") {
1827
+ return json(200, await buildServerMem());
1828
+ }
1829
+ if (path === "/v1/traces") {
1830
+ if (!controlStore.capabilities.builtinProfiles) return unsupportedCapability("built-in profile");
1831
+ const stream = cfg.otlpTracesStream;
1832
+ if (!stream) return badRequest("DS_OTLP_TRACES_STREAM is not configured");
1833
+ return handleOtlpTracesIngest(stream, cfg.otlpAutoCreate);
1834
+ }
1835
+ if (path === "/v1/observe/request") {
1836
+ if (!controlStore.capabilities.indexes) return unsupportedCapability("observe");
1837
+ return handleObserveRequest();
1838
+ }
1839
+
1840
+ // /v1/streams
1841
+ if (req.method === "GET" && path === "/v1/streams") {
1842
+ const limit = Number(url.searchParams.get("limit") ?? "100");
1843
+ const offset = Number(url.searchParams.get("offset") ?? "0");
1844
+ const rows = await controlStore.listStreams(Math.max(0, Math.min(limit, 1000)), Math.max(0, offset));
1845
+ const out = [];
1846
+ for (const r of rows) {
1847
+ const profileRes = await profiles.getProfileResult(r.stream, r);
1848
+ if (Result.isError(profileRes)) return internalError("invalid stream profile");
1849
+ const profile = profileRes.value;
1850
+ const observability = buildRequestObservabilityPairingDescriptor(r.stream, profile);
1851
+ out.push({
1852
+ name: r.stream,
1853
+ created_at: new Date(Number(r.created_at_ms)).toISOString(),
1854
+ expires_at: r.expires_at_ms == null ? null : new Date(Number(r.expires_at_ms)).toISOString(),
1855
+ epoch: r.epoch,
1856
+ next_offset: r.next_offset.toString(),
1857
+ sealed_through: r.sealed_through.toString(),
1858
+ uploaded_through: r.uploaded_through.toString(),
1859
+ profile: profile.kind,
1860
+ ...(observability ? { observability } : {}),
1861
+ });
1862
+ }
1863
+ return json(200, out);
1864
+ }
1865
+
1866
+ // /v1/stream/:name[/_schema|/_profile|/_details|/_index_status] (accept encoded or raw slashes in name)
1867
+ const streamPrefix = "/v1/stream/";
1868
+ if (path.startsWith(streamPrefix)) {
1869
+ const rawRest = path.slice(streamPrefix.length);
1870
+ const rest = rawRest.replace(/\/+$/, "");
1871
+ if (rest.length === 0) return badRequest("missing stream name");
1872
+ const segments = rest.split("/");
1873
+ let isSchema = false;
1874
+ let isProfile = false;
1875
+ let isSearch = false;
1876
+ let isAggregate = false;
1877
+ let isDetails = false;
1878
+ let isIndexStatus = false;
1879
+ let isRoutingKeys = false;
1880
+ let isOtlpTraces = false;
1881
+ let pathKeyParam: string | null = null;
1882
+ let touchMode: StreamTouchRoute | null = null;
1883
+ if (
1884
+ segments.length >= 3 &&
1885
+ segments[segments.length - 3] === "_otlp" &&
1886
+ segments[segments.length - 2] === "v1" &&
1887
+ segments[segments.length - 1] === "traces"
1888
+ ) {
1889
+ isOtlpTraces = true;
1890
+ segments.splice(segments.length - 3, 3);
1891
+ } else if (segments[segments.length - 1] === "_schema") {
1892
+ isSchema = true;
1893
+ segments.pop();
1894
+ } else if (segments[segments.length - 1] === "_profile") {
1895
+ isProfile = true;
1896
+ segments.pop();
1897
+ } else if (segments[segments.length - 1] === "_search") {
1898
+ isSearch = true;
1899
+ segments.pop();
1900
+ } else if (segments[segments.length - 1] === "_aggregate") {
1901
+ isAggregate = true;
1902
+ segments.pop();
1903
+ } else if (segments[segments.length - 1] === "_details") {
1904
+ isDetails = true;
1905
+ segments.pop();
1906
+ } else if (segments[segments.length - 1] === "_index_status") {
1907
+ isIndexStatus = true;
1908
+ segments.pop();
1909
+ } else if (segments[segments.length - 1] === "_routing_keys") {
1910
+ isRoutingKeys = true;
1911
+ segments.pop();
1912
+ } else if (
1913
+ segments.length >= 3 &&
1914
+ segments[segments.length - 3] === "touch" &&
1915
+ segments[segments.length - 2] === "templates" &&
1916
+ segments[segments.length - 1] === "activate"
1917
+ ) {
1918
+ touchMode = { kind: "templates_activate" };
1919
+ segments.splice(segments.length - 3, 3);
1920
+ } else if (segments.length >= 2 && segments[segments.length - 2] === "touch" && segments[segments.length - 1] === "meta") {
1921
+ touchMode = { kind: "meta" };
1922
+ segments.splice(segments.length - 2, 2);
1923
+ } else if (segments.length >= 2 && segments[segments.length - 2] === "touch" && segments[segments.length - 1] === "wait") {
1924
+ touchMode = { kind: "wait" };
1925
+ segments.splice(segments.length - 2, 2);
1926
+ } else if (segments.length >= 2 && segments[segments.length - 2] === "pk") {
1927
+ pathKeyParam = decodeURIComponent(segments[segments.length - 1]);
1928
+ segments.splice(segments.length - 2, 2);
1929
+ }
1930
+ const streamPart = segments.join("/");
1931
+ if (streamPart.length === 0) return badRequest("missing stream name");
1932
+ const stream = decodeURIComponent(streamPart);
1933
+
1934
+ if (isOtlpTraces) {
1935
+ if (!controlStore.capabilities.builtinProfiles) return unsupportedCapability("built-in profile");
1936
+ return handleOtlpTracesIngest(stream, false);
1937
+ }
1938
+
1939
+ if (isSchema) {
1940
+ const srow = await controlStore.getStream(stream);
1941
+ if (!srow || controlStore.isDeleted(srow)) return notFound();
1942
+ if (srow.expires_at_ms != null && controlStore.nowMs() > srow.expires_at_ms) return notFound("stream expired");
1943
+
1944
+ if (req.method === "GET") {
1945
+ const regRes = await registry.getRegistryResult(stream);
1946
+ if (Result.isError(regRes)) return schemaReadErrorResponse(regRes.error);
1947
+ return json(200, regRes.value);
1948
+ }
1949
+ if (req.method === "POST") {
1950
+ let body: unknown;
1951
+ try {
1952
+ body = await req.json();
1953
+ } catch {
1954
+ return badRequest("schema update must be valid JSON");
1955
+ }
1956
+ const updateRes = parseSchemaUpdateResult(body);
1957
+ if (Result.isError(updateRes)) return badRequest(updateRes.error.message);
1958
+ const update = updateRes.value;
1959
+ if (update.search !== undefined && !controlStore.capabilities.indexes) {
1960
+ return unsupportedCapability("index");
1961
+ }
1962
+ if (update.schema === undefined && update.routingKey !== undefined && update.search === undefined) {
1963
+ const regRes = await registry.updateRoutingKeyResult(stream, update.routingKey ?? null);
1964
+ if (Result.isError(regRes)) return schemaMutationErrorResponse(regRes.error);
1965
+ try {
1966
+ if (schemaPublication) await schemaPublication.uploadSchemaRegistry(stream, regRes.value);
1967
+ } catch {
1968
+ return json(500, { error: { code: "internal", message: "schema upload failed" } });
1969
+ }
1970
+ indexer?.enqueue(stream);
1971
+ notifier.notifyDetailsChanged(stream);
1972
+ return json(200, regRes.value);
1973
+ }
1974
+ if (update.schema === undefined && update.search !== undefined && update.routingKey === undefined) {
1975
+ const regRes = await registry.updateSearchResult(stream, update.search ?? null);
1976
+ if (Result.isError(regRes)) return schemaMutationErrorResponse(regRes.error);
1977
+ try {
1978
+ if (schemaPublication) await schemaPublication.uploadSchemaRegistry(stream, regRes.value);
1979
+ } catch {
1980
+ return json(500, { error: { code: "internal", message: "schema upload failed" } });
1981
+ }
1982
+ indexer?.enqueue(stream);
1983
+ notifier.notifyDetailsChanged(stream);
1984
+ return json(200, regRes.value);
1985
+ }
1986
+ const regRes = await registry.updateRegistryResult(stream, {
1987
+ schema: update.schema,
1988
+ lens: update.lens,
1989
+ routingKey: update.routingKey ?? undefined,
1990
+ search: update.search,
1991
+ });
1992
+ if (Result.isError(regRes)) return schemaMutationErrorResponse(regRes.error);
1993
+ try {
1994
+ if (schemaPublication) await schemaPublication.uploadSchemaRegistry(stream, regRes.value);
1995
+ } catch {
1996
+ return json(500, { error: { code: "internal", message: "schema upload failed" } });
1997
+ }
1998
+ indexer?.enqueue(stream);
1999
+ notifier.notifyDetailsChanged(stream);
2000
+ return json(200, regRes.value);
2001
+ }
2002
+ return badRequest("unsupported method");
2003
+ }
2004
+
2005
+ if (isProfile) {
2006
+ const srowOrResponse = await liveStreamOrResponse(stream);
2007
+ if (srowOrResponse instanceof Response) return srowOrResponse;
2008
+ const srow = srowOrResponse;
2009
+
2010
+ if (req.method === "GET") {
2011
+ const profileRes = await profiles.getProfileResourceResult(stream, srow);
2012
+ if (Result.isError(profileRes)) return internalError("invalid stream profile");
2013
+ return json(200, profileRes.value);
2014
+ }
2015
+
2016
+ if (req.method === "POST") {
2017
+ let body: any;
2018
+ try {
2019
+ body = await req.json();
2020
+ } catch {
2021
+ return badRequest("profile update must be valid JSON");
2022
+ }
2023
+ const nextProfileRes = parseProfileUpdateResult(body);
2024
+ if (Result.isError(nextProfileRes)) return badRequest(nextProfileRes.error.message);
2025
+ if (nextProfileRes.value.kind !== "generic" && !controlStore.capabilities.builtinProfiles) {
2026
+ return unsupportedCapability("built-in profile");
2027
+ }
2028
+ const profileRes = await profiles.updateProfileResult(stream, nextProfileRes.value);
2029
+ if (Result.isError(profileRes)) return badRequest(profileRes.error.message);
2030
+ try {
2031
+ if (profileRes.value.schemaRegistry) {
2032
+ if (schemaPublication) await schemaPublication.publishProfileSchemaRegistry(stream, profileRes.value.schemaRegistry);
2033
+ }
2034
+ } catch {
2035
+ return json(500, { error: { code: "internal", message: "profile upload failed" } });
2036
+ }
2037
+ indexer?.enqueue(stream);
2038
+ notifier.notifyDetailsChanged(stream);
2039
+ return json(200, profileRes.value.resource);
2040
+ }
2041
+
2042
+ return badRequest("unsupported method");
2043
+ }
2044
+
2045
+ if (isDetails || isIndexStatus) {
2046
+ if (req.method !== "GET") return badRequest("unsupported method");
2047
+ if (isDetails && !controlStore.capabilities.storageStats) return unsupportedCapability("storage stats");
2048
+ if (isDetails && !controlStore.capabilities.objectStoreAccounting) return unsupportedCapability("object-store accounting");
2049
+ if (isIndexStatus && !controlStore.capabilities.indexes) return unsupportedCapability("index");
2050
+ const liveParam = url.searchParams.get("live") ?? "";
2051
+ let longPoll = false;
2052
+ if (liveParam === "" || liveParam === "false" || liveParam === "0") longPoll = false;
2053
+ else if (liveParam === "long-poll" || liveParam === "true" || liveParam === "1") longPoll = true;
2054
+ else return badRequest("invalid live mode");
2055
+
2056
+ const timeout = url.searchParams.get("timeout") ?? url.searchParams.get("timeout_ms");
2057
+ let timeoutMs: number | null = null;
2058
+ if (timeout) {
2059
+ if (/^[0-9]+$/.test(timeout)) {
2060
+ timeoutMs = Number(timeout);
2061
+ } else {
2062
+ const timeoutRes = parseDurationMsResult(timeout);
2063
+ if (Result.isError(timeoutRes)) return badRequest("invalid timeout");
2064
+ timeoutMs = timeoutRes.value;
2065
+ }
2066
+ }
2067
+
2068
+ const loadSnapshot = async (): Promise<Response | DetailsSnapshot> => {
2069
+ const snapshotRes = await buildDetailsSnapshotResult(stream, isIndexStatus ? "index_status" : "details");
2070
+ if (Result.isError(snapshotRes)) {
2071
+ if (snapshotRes.error.status === 404) {
2072
+ return snapshotRes.error.message === "stream expired" ? notFound("stream expired") : notFound();
2073
+ }
2074
+ return internalError(snapshotRes.error.message);
2075
+ }
2076
+ return snapshotRes.value;
2077
+ };
2078
+
2079
+ let snapshotOrResponse = await loadSnapshot();
2080
+ if (snapshotOrResponse instanceof Response) return snapshotOrResponse;
2081
+ let snapshot = snapshotOrResponse;
2082
+ const ifNoneMatch = req.headers.get("if-none-match");
2083
+
2084
+ if (!longPoll) {
2085
+ if (ifNoneMatch && ifNoneMatch === snapshot.etag) {
2086
+ return new Response(null, {
2087
+ status: 304,
2088
+ headers: withNosniff({ "cache-control": "no-store", etag: snapshot.etag }),
2089
+ });
2090
+ }
2091
+ return new Response(snapshot.body, {
2092
+ status: 200,
2093
+ headers: withNosniff({
2094
+ "content-type": "application/json; charset=utf-8",
2095
+ "cache-control": "no-store",
2096
+ etag: snapshot.etag,
2097
+ }),
2098
+ });
2099
+ }
2100
+
2101
+ if (!ifNoneMatch || ifNoneMatch !== snapshot.etag) {
2102
+ return new Response(snapshot.body, {
2103
+ status: 200,
2104
+ headers: withNosniff({
2105
+ "content-type": "application/json; charset=utf-8",
2106
+ "cache-control": "no-store",
2107
+ etag: snapshot.etag,
2108
+ }),
2109
+ });
2110
+ }
2111
+
2112
+ const deadline = Date.now() + (timeoutMs ?? 3000);
2113
+ while (ifNoneMatch === snapshot.etag) {
2114
+ const remaining = deadline - Date.now();
2115
+ if (remaining <= 0) {
2116
+ return new Response(null, {
2117
+ status: 304,
2118
+ headers: withNosniff({ "cache-control": "no-store", etag: snapshot.etag }),
2119
+ });
2120
+ }
2121
+ await notifier.waitForDetailsChange(stream, snapshot.version, remaining, req.signal);
2122
+ if (req.signal.aborted) return new Response(null, { status: 204 });
2123
+ snapshotOrResponse = await loadSnapshot();
2124
+ if (snapshotOrResponse instanceof Response) return snapshotOrResponse;
2125
+ snapshot = snapshotOrResponse;
2126
+ }
2127
+
2128
+ return new Response(snapshot.body, {
2129
+ status: 200,
2130
+ headers: withNosniff({
2131
+ "content-type": "application/json; charset=utf-8",
2132
+ "cache-control": "no-store",
2133
+ etag: snapshot.etag,
2134
+ }),
2135
+ });
2136
+ }
2137
+
2138
+ if (isRoutingKeys) {
2139
+ if (!controlStore.capabilities.indexes) return unsupportedCapability("routing key index");
2140
+ const srowOrResponse = await liveStreamOrResponse(stream);
2141
+ if (srowOrResponse instanceof Response) return srowOrResponse;
2142
+ const srow = srowOrResponse;
2143
+ if (req.method !== "GET") return badRequest("unsupported method");
2144
+ const regRes = await registry.getRegistryResult(stream);
2145
+ if (Result.isError(regRes)) return internalError();
2146
+ if (regRes.value.routingKey == null) return badRequest("routing key not configured");
2147
+ const limitRaw = url.searchParams.get("limit");
2148
+ const limit = limitRaw == null ? 100 : Number(limitRaw);
2149
+ if (!Number.isFinite(limit) || limit <= 0 || !Number.isInteger(limit) || limit > 500) return badRequest("invalid limit");
2150
+ const after = url.searchParams.get("after");
2151
+ const listRes = indexer?.listRoutingKeysResult
2152
+ ? await runForeground(() => indexer.listRoutingKeysResult!(stream, after, limit))
2153
+ : Result.err({ kind: "invalid_lexicon_index", message: "routing key lexicon unavailable" });
2154
+ if (Result.isError(listRes)) return internalError(listRes.error.message);
2155
+ return json(200, {
2156
+ stream,
2157
+ source: {
2158
+ kind: "routing_key",
2159
+ name: "",
2160
+ },
2161
+ took_ms: listRes.value.tookMs,
2162
+ coverage: {
2163
+ complete: listRes.value.coverage.complete,
2164
+ indexed_segments: listRes.value.coverage.indexedSegments,
2165
+ scanned_uploaded_segments: listRes.value.coverage.scannedUploadedSegments,
2166
+ scanned_local_segments: listRes.value.coverage.scannedLocalSegments,
2167
+ scanned_wal_rows: listRes.value.coverage.scannedWalRows,
2168
+ possible_missing_uploaded_segments: listRes.value.coverage.possibleMissingUploadedSegments,
2169
+ possible_missing_local_segments: listRes.value.coverage.possibleMissingLocalSegments,
2170
+ },
2171
+ timing: {
2172
+ lexicon_run_get_ms: listRes.value.timing.lexiconRunGetMs,
2173
+ lexicon_decode_ms: listRes.value.timing.lexiconDecodeMs,
2174
+ lexicon_enumerate_ms: listRes.value.timing.lexiconEnumerateMs,
2175
+ lexicon_merge_ms: listRes.value.timing.lexiconMergeMs,
2176
+ fallback_scan_ms: listRes.value.timing.fallbackScanMs,
2177
+ fallback_segment_get_ms: listRes.value.timing.fallbackSegmentGetMs,
2178
+ fallback_wal_scan_ms: listRes.value.timing.fallbackWalScanMs,
2179
+ lexicon_runs_loaded: listRes.value.timing.lexiconRunsLoaded,
2180
+ },
2181
+ keys: listRes.value.keys,
2182
+ next_after: listRes.value.nextAfter,
2183
+ });
2184
+ }
2185
+
2186
+ if (isSearch) {
2187
+ if (!controlStore.capabilities.indexes) return unsupportedCapability("search");
2188
+ const srowOrResponse = await liveStreamOrResponse(stream);
2189
+ if (srowOrResponse instanceof Response) return srowOrResponse;
2190
+ const srow = srowOrResponse;
2191
+
2192
+ const regRes = await registry.getRegistryResult(stream);
2193
+ if (Result.isError(regRes)) return internalError();
2194
+
2195
+ const respondSearch = async (requestBody: unknown, fromQuery: boolean): Promise<Response> => {
2196
+ const requestRes = fromQuery
2197
+ ? parseSearchRequestQueryResult(regRes.value, url.searchParams)
2198
+ : parseSearchRequestBodyResult(regRes.value, requestBody);
2199
+ if (Result.isError(requestRes)) return badRequest(requestRes.error.message);
2200
+ const request = {
2201
+ ...requestRes.value,
2202
+ timeoutMs: clampSearchRequestTimeoutMs(requestRes.value.timeoutMs),
2203
+ };
2204
+ const searchRes = await runForegroundWithGate(searchGate, () => reader.searchResult({ stream, request }));
2205
+ if (Result.isError(searchRes)) return readerErrorResponse(searchRes.error);
2206
+ const status = searchRes.value.timedOut ? 408 : 200;
2207
+ return json(status, {
2208
+ stream,
2209
+ snapshot_end_offset: searchRes.value.snapshotEndOffset,
2210
+ took_ms: searchRes.value.tookMs,
2211
+ timed_out: searchRes.value.timedOut,
2212
+ timeout_ms: searchRes.value.timeoutMs,
2213
+ coverage: {
2214
+ mode: searchRes.value.coverage.mode,
2215
+ complete: searchRes.value.coverage.complete,
2216
+ stream_head_offset: searchRes.value.coverage.streamHeadOffset,
2217
+ visible_through_offset: searchRes.value.coverage.visibleThroughOffset,
2218
+ visible_through_primary_timestamp_max: searchRes.value.coverage.visibleThroughPrimaryTimestampMax,
2219
+ oldest_omitted_append_at: searchRes.value.coverage.oldestOmittedAppendAt,
2220
+ possible_missing_events_upper_bound: searchRes.value.coverage.possibleMissingEventsUpperBound,
2221
+ possible_missing_uploaded_segments: searchRes.value.coverage.possibleMissingUploadedSegments,
2222
+ possible_missing_sealed_rows: searchRes.value.coverage.possibleMissingSealedRows,
2223
+ possible_missing_wal_rows: searchRes.value.coverage.possibleMissingWalRows,
2224
+ indexed_segments: searchRes.value.coverage.indexedSegments,
2225
+ indexed_segment_time_ms: searchRes.value.coverage.indexedSegmentTimeMs,
2226
+ fts_section_get_ms: searchRes.value.coverage.ftsSectionGetMs,
2227
+ fts_decode_ms: searchRes.value.coverage.ftsDecodeMs,
2228
+ fts_clause_estimate_ms: searchRes.value.coverage.ftsClauseEstimateMs,
2229
+ scanned_segments: searchRes.value.coverage.scannedSegments,
2230
+ scanned_segment_time_ms: searchRes.value.coverage.scannedSegmentTimeMs,
2231
+ scanned_tail_docs: searchRes.value.coverage.scannedTailDocs,
2232
+ scanned_tail_time_ms: searchRes.value.coverage.scannedTailTimeMs,
2233
+ exact_candidate_time_ms: searchRes.value.coverage.exactCandidateTimeMs,
2234
+ candidate_doc_ids: searchRes.value.coverage.candidateDocIds,
2235
+ decoded_records: searchRes.value.coverage.decodedRecords,
2236
+ json_parse_time_ms: searchRes.value.coverage.jsonParseTimeMs,
2237
+ segment_payload_bytes_fetched: searchRes.value.coverage.segmentPayloadBytesFetched,
2238
+ sort_time_ms: searchRes.value.coverage.sortTimeMs,
2239
+ peak_hits_held: searchRes.value.coverage.peakHitsHeld,
2240
+ index_families_used: searchRes.value.coverage.indexFamiliesUsed,
2241
+ },
2242
+ total: searchRes.value.total,
2243
+ hits: searchRes.value.hits,
2244
+ next_search_after: searchRes.value.nextSearchAfter,
2245
+ }, searchResponseHeaders(searchRes.value));
2246
+ };
2247
+
2248
+ if (req.method === "GET") {
2249
+ return respondSearch(null, true);
2250
+ }
2251
+
2252
+ if (req.method === "POST") {
2253
+ let body: unknown;
2254
+ try {
2255
+ body = await req.json();
2256
+ } catch {
2257
+ return badRequest("search request must be valid JSON");
2258
+ }
2259
+ return respondSearch(body, false);
2260
+ }
2261
+
2262
+ return badRequest("unsupported method");
2263
+ }
2264
+
2265
+ if (isAggregate) {
2266
+ if (!controlStore.capabilities.indexes) return unsupportedCapability("aggregate");
2267
+ const srowOrResponse = await liveStreamOrResponse(stream);
2268
+ if (srowOrResponse instanceof Response) return srowOrResponse;
2269
+ const srow = srowOrResponse;
2270
+ if (req.method !== "POST") return badRequest("unsupported method");
2271
+
2272
+ const regRes = await registry.getRegistryResult(stream);
2273
+ if (Result.isError(regRes)) return internalError();
2274
+
2275
+ let body: unknown;
2276
+ try {
2277
+ body = await req.json();
2278
+ } catch {
2279
+ return badRequest("aggregate request must be valid JSON");
2280
+ }
2281
+
2282
+ const requestRes = parseAggregateRequestBodyResult(regRes.value, body);
2283
+ if (Result.isError(requestRes)) return badRequest(requestRes.error.message);
2284
+ const aggregateRes = await runForegroundWithGate(searchGate, () => reader.aggregateResult({ stream, request: requestRes.value }));
2285
+ if (Result.isError(aggregateRes)) return readerErrorResponse(aggregateRes.error);
2286
+ return json(200, {
2287
+ stream,
2288
+ rollup: aggregateRes.value.rollup,
2289
+ from: aggregateRes.value.from,
2290
+ to: aggregateRes.value.to,
2291
+ interval: aggregateRes.value.interval,
2292
+ coverage: {
2293
+ mode: aggregateRes.value.coverage.mode,
2294
+ complete: aggregateRes.value.coverage.complete,
2295
+ stream_head_offset: aggregateRes.value.coverage.streamHeadOffset,
2296
+ visible_through_offset: aggregateRes.value.coverage.visibleThroughOffset,
2297
+ visible_through_primary_timestamp_max: aggregateRes.value.coverage.visibleThroughPrimaryTimestampMax,
2298
+ oldest_omitted_append_at: aggregateRes.value.coverage.oldestOmittedAppendAt,
2299
+ possible_missing_events_upper_bound: aggregateRes.value.coverage.possibleMissingEventsUpperBound,
2300
+ possible_missing_uploaded_segments: aggregateRes.value.coverage.possibleMissingUploadedSegments,
2301
+ possible_missing_sealed_rows: aggregateRes.value.coverage.possibleMissingSealedRows,
2302
+ possible_missing_wal_rows: aggregateRes.value.coverage.possibleMissingWalRows,
2303
+ used_rollups: aggregateRes.value.coverage.usedRollups,
2304
+ indexed_segments: aggregateRes.value.coverage.indexedSegments,
2305
+ scanned_segments: aggregateRes.value.coverage.scannedSegments,
2306
+ scanned_tail_docs: aggregateRes.value.coverage.scannedTailDocs,
2307
+ index_families_used: aggregateRes.value.coverage.indexFamiliesUsed,
2308
+ },
2309
+ buckets: aggregateRes.value.buckets,
2310
+ });
2311
+ }
2312
+
2313
+ if (touchMode) {
2314
+ if (!controlStore.capabilities.touch || !touch || !touchStore) return unsupportedCapability("touch");
2315
+ const srowOrResponse = await liveStreamOrResponse(stream);
2316
+ if (srowOrResponse instanceof Response) return srowOrResponse;
2317
+ const srow = srowOrResponse;
2318
+
2319
+ const profileRes = await profiles.getProfileResult(stream, srow);
2320
+ if (Result.isError(profileRes)) return internalError("invalid stream profile");
2321
+ const touchCapability = resolveTouchCapability(profileRes.value);
2322
+ if (!touchCapability?.handleRoute) return notFound("touch not enabled");
2323
+ return touchCapability.handleRoute({
2324
+ route: touchMode,
2325
+ req,
2326
+ stream,
2327
+ streamRow: srow,
2328
+ profile: profileRes.value,
2329
+ db: touchStore,
2330
+ touchManager: touch,
2331
+ respond: { json, badRequest, internalError, notFound },
2332
+ });
2333
+ }
2334
+
2335
+ // Stream lifecycle.
2336
+ if (req.method === "PUT") {
2337
+ const streamClosed = parseStreamClosedHeader(req.headers.get("stream-closed"));
2338
+ const ttlHeader = req.headers.get("stream-ttl");
2339
+ const expiresHeader = req.headers.get("stream-expires-at");
2340
+ if (ttlHeader && expiresHeader) return badRequest("only one of Stream-TTL or Stream-Expires-At is allowed");
2341
+
2342
+ let ttlSeconds: number | null = null;
2343
+ let expiresAtMs: bigint | null = null;
2344
+ if (ttlHeader) {
2345
+ const ttlRes = parseStreamTtlSeconds(ttlHeader);
2346
+ if (Result.isError(ttlRes)) return badRequest(ttlRes.error.message);
2347
+ ttlSeconds = ttlRes.value;
2348
+ expiresAtMs = controlStore.nowMs() + BigInt(ttlSeconds) * 1000n;
2349
+ } else if (expiresHeader) {
2350
+ const expiresRes = parseTimestampMsResult(expiresHeader);
2351
+ if (Result.isError(expiresRes)) return badRequest(expiresRes.error.message);
2352
+ expiresAtMs = expiresRes.value;
2353
+ }
2354
+
2355
+ const contentType = normalizeContentType(req.headers.get("content-type")) ?? "application/octet-stream";
2356
+ const routingKeyHeader = req.headers.get("stream-key");
2357
+ const leaveAppendPhase = memorySampler?.enter("append", {
2358
+ route: "put",
2359
+ stream,
2360
+ content_type: contentType,
2361
+ });
2362
+ try {
2363
+ return await runWithGate(ingestGate, async () => {
2364
+ const ab = await req.arrayBuffer();
2365
+ if (ab.byteLength > cfg.appendMaxBodyBytes) return tooLarge(`body too large (max ${cfg.appendMaxBodyBytes})`);
2366
+ const bodyBytes = new Uint8Array(ab);
2367
+
2368
+ let srow = await controlStore.getStream(stream);
2369
+ if (srow && controlStore.isDeleted(srow)) {
2370
+ await controlStore.hardDeleteStream(stream);
2371
+ srow = null;
2372
+ }
2373
+ if (srow && srow.expires_at_ms != null && controlStore.nowMs() > srow.expires_at_ms) {
2374
+ await controlStore.hardDeleteStream(stream);
2375
+ srow = null;
2376
+ }
2377
+
2378
+ if (srow) {
2379
+ const existingClosed = srow.closed !== 0;
2380
+ const existingContentType = normalizeContentType(srow.content_type) ?? srow.content_type;
2381
+ const ttlMatch =
2382
+ ttlSeconds != null
2383
+ ? srow.ttl_seconds != null && srow.ttl_seconds === ttlSeconds
2384
+ : expiresAtMs != null
2385
+ ? srow.ttl_seconds == null && srow.expires_at_ms != null && srow.expires_at_ms === expiresAtMs
2386
+ : srow.ttl_seconds == null && srow.expires_at_ms == null;
2387
+ if (existingContentType !== contentType || existingClosed !== streamClosed || !ttlMatch) {
2388
+ return conflict("stream config mismatch");
2389
+ }
2390
+
2391
+ const tailOffset = encodeOffset(srow.epoch, srow.next_offset - 1n);
2392
+ const headers: Record<string, string> = {
2393
+ "content-type": existingContentType,
2394
+ "stream-next-offset": tailOffset,
2395
+ };
2396
+ if (existingClosed) headers["stream-closed"] = "true";
2397
+ if (srow.expires_at_ms != null) headers["stream-expires-at"] = new Date(Number(srow.expires_at_ms)).toISOString();
2398
+ return new Response(null, { status: 200, headers: appendResponseHeaders(headers) });
2399
+ }
2400
+
2401
+ await controlStore.ensureStream(stream, { contentType, expiresAtMs, ttlSeconds, closed: false });
2402
+ notifier.notifyDetailsChanged(stream);
2403
+ let lastOffset = -1n;
2404
+ let appendedRows = 0;
2405
+ let closedNow = false;
2406
+
2407
+ if (bodyBytes.byteLength > 0) {
2408
+ const rowsRes = await buildAppendRowsResult(stream, bodyBytes, contentType, routingKeyHeader, true);
2409
+ if (Result.isError(rowsRes)) {
2410
+ if (rowsRes.error.status === 500) return internalError();
2411
+ return badRequest(rowsRes.error.message);
2412
+ }
2413
+ const rows = rowsRes.value.rows;
2414
+ appendedRows = rows.length;
2415
+ if (rows.length > 0 || streamClosed) {
2416
+ const appendResOrResponse = await awaitAppendWithTimeout(enqueueAppend({
2417
+ stream,
2418
+ baseAppendMs: controlStore.nowMs(),
2419
+ rows,
2420
+ contentType,
2421
+ close: streamClosed,
2422
+ }));
2423
+ if (appendResOrResponse instanceof Response) return appendResOrResponse;
2424
+ const appendRes = appendResOrResponse;
2425
+ if (Result.isError(appendRes)) {
2426
+ if (appendRes.error.kind === "overloaded") return overloaded();
2427
+ return json(500, { error: { code: "internal", message: "append failed" } });
2428
+ }
2429
+ lastOffset = appendRes.value.lastOffset;
2430
+ closedNow = appendRes.value.closed;
2431
+ }
2432
+ } else if (streamClosed) {
2433
+ const appendResOrResponse = await awaitAppendWithTimeout(enqueueAppend({
2434
+ stream,
2435
+ baseAppendMs: controlStore.nowMs(),
2436
+ rows: [],
2437
+ contentType,
2438
+ close: true,
2439
+ }));
2440
+ if (appendResOrResponse instanceof Response) return appendResOrResponse;
2441
+ const appendRes = appendResOrResponse;
2442
+ if (Result.isError(appendRes)) {
2443
+ if (appendRes.error.kind === "overloaded") return overloaded();
2444
+ return json(500, { error: { code: "internal", message: "close failed" } });
2445
+ }
2446
+ lastOffset = appendRes.value.lastOffset;
2447
+ closedNow = appendRes.value.closed;
2448
+ }
2449
+
2450
+ recordAppendOutcome({
2451
+ stream,
2452
+ lastOffset,
2453
+ appendedRows,
2454
+ metricsBytes: bodyBytes.byteLength,
2455
+ ingestedBytes: bodyBytes.byteLength,
2456
+ touched: bodyBytes.byteLength > 0 || streamClosed,
2457
+ closed: closedNow,
2458
+ });
2459
+
2460
+ const createdRow = (await controlStore.getStream(stream))!;
2461
+ const tailOffset = encodeOffset(createdRow.epoch, createdRow.next_offset - 1n);
2462
+ const headers: Record<string, string> = {
2463
+ "content-type": contentType,
2464
+ "stream-next-offset": appendedRows > 0 || streamClosed ? encodeOffset(createdRow.epoch, lastOffset) : tailOffset,
2465
+ location: req.url,
2466
+ };
2467
+ if (streamClosed || closedNow) headers["stream-closed"] = "true";
2468
+ if (createdRow.expires_at_ms != null) headers["stream-expires-at"] = new Date(Number(createdRow.expires_at_ms)).toISOString();
2469
+ return new Response(null, { status: 201, headers: appendResponseHeaders(headers) });
2470
+ });
2471
+ } finally {
2472
+ leaveAppendPhase?.();
2473
+ }
2474
+ }
2475
+
2476
+ if (req.method === "DELETE") {
2477
+ const deleted = await controlStore.deleteStream(stream);
2478
+ if (!deleted) return notFound();
2479
+ notifier.notifyDetailsChanged(stream);
2480
+ notifier.notifyClose(stream);
2481
+ await fullMode?.manifestPublication?.publishDeletedStreamManifest(stream);
2482
+ return new Response(null, { status: 204, headers: withNosniff() });
2483
+ }
2484
+
2485
+ if (req.method === "HEAD") {
2486
+ const srowOrResponse = await liveStreamOrResponse(stream);
2487
+ if (srowOrResponse instanceof Response) return srowOrResponse;
2488
+ const srow = srowOrResponse;
2489
+ const tailOffset = encodeOffset(srow.epoch, srow.next_offset - 1n);
2490
+ const headers: Record<string, string> = {
2491
+ "content-type": normalizeContentType(srow.content_type) ?? srow.content_type,
2492
+ "stream-next-offset": tailOffset,
2493
+ "stream-end-offset": tailOffset,
2494
+ "cache-control": "no-store",
2495
+ };
2496
+ if (srow.closed !== 0) headers["stream-closed"] = "true";
2497
+ if (srow.ttl_seconds != null && srow.expires_at_ms != null) {
2498
+ const remainingMs = Number(srow.expires_at_ms - controlStore.nowMs());
2499
+ const remaining = Math.max(0, Math.ceil(remainingMs / 1000));
2500
+ headers["stream-ttl"] = String(remaining);
2501
+ }
2502
+ if (srow.expires_at_ms != null) headers["stream-expires-at"] = new Date(Number(srow.expires_at_ms)).toISOString();
2503
+ return new Response(null, { status: 200, headers: withNosniff(headers) });
2504
+ }
2505
+
2506
+ if (req.method === "POST") {
2507
+ const srowOrResponse = await liveStreamOrResponse(stream);
2508
+ if (srowOrResponse instanceof Response) return srowOrResponse;
2509
+ const srow = srowOrResponse;
2510
+
2511
+ const streamClosed = parseStreamClosedHeader(req.headers.get("stream-closed"));
2512
+ const streamContentType = normalizeContentType(srow.content_type) ?? srow.content_type;
2513
+
2514
+ const producerId = req.headers.get("producer-id");
2515
+ const producerEpochHeader = req.headers.get("producer-epoch");
2516
+ const producerSeqHeader = req.headers.get("producer-seq");
2517
+ let producer: ProducerInfo | null = null;
2518
+ if (producerId != null || producerEpochHeader != null || producerSeqHeader != null) {
2519
+ if (!producerId || producerId.trim() === "") return badRequest("invalid Producer-Id");
2520
+ if (!producerEpochHeader || !producerSeqHeader) return badRequest("missing producer headers");
2521
+ const epoch = parseNonNegativeInt(producerEpochHeader);
2522
+ const seq = parseNonNegativeInt(producerSeqHeader);
2523
+ if (epoch == null || seq == null) return badRequest("invalid producer headers");
2524
+ producer = { id: producerId, epoch, seq };
2525
+ }
2526
+
2527
+ let streamSeq: string | null = null;
2528
+ const streamSeqRes = parseStreamSeqHeader(req.headers.get("stream-seq"));
2529
+ if (Result.isError(streamSeqRes)) return badRequest(streamSeqRes.error.message);
2530
+ streamSeq = streamSeqRes.value;
2531
+
2532
+ const tsHdr = req.headers.get("stream-timestamp");
2533
+ let baseAppendMs = controlStore.nowMs();
2534
+ if (tsHdr) {
2535
+ const tsRes = parseTimestampMsResult(tsHdr);
2536
+ if (Result.isError(tsRes)) return badRequest(tsRes.error.message);
2537
+ baseAppendMs = tsRes.value;
2538
+ }
2539
+
2540
+ const leaveAppendPhase = memorySampler?.enter("append", {
2541
+ route: "post",
2542
+ stream,
2543
+ stream_content_type: streamContentType,
2544
+ });
2545
+ let appendBodyBytesForGc = 0;
2546
+ try {
2547
+ const response = await runWithGate(ingestGate, async () => {
2548
+ const ab = await req.arrayBuffer();
2549
+ if (ab.byteLength > cfg.appendMaxBodyBytes) return tooLarge(`body too large (max ${cfg.appendMaxBodyBytes})`);
2550
+ const bodyBytes = new Uint8Array(ab);
2551
+ appendBodyBytesForGc = bodyBytes.byteLength;
2552
+
2553
+ const isCloseOnly = streamClosed && bodyBytes.byteLength === 0;
2554
+ if (bodyBytes.byteLength === 0 && !streamClosed) return badRequest("empty body");
2555
+
2556
+ let reqContentType = normalizeContentType(req.headers.get("content-type"));
2557
+ if (!isCloseOnly && !reqContentType) return badRequest("missing content-type");
2558
+
2559
+ const routingKeyHeader = req.headers.get("stream-key");
2560
+ let rows: AppendRow[] = [];
2561
+ if (!isCloseOnly) {
2562
+ const rowsRes = await buildAppendRowsResult(stream, bodyBytes, reqContentType!, routingKeyHeader, false);
2563
+ if (Result.isError(rowsRes)) {
2564
+ if (rowsRes.error.status === 500) return internalError();
2565
+ return badRequest(rowsRes.error.message);
2566
+ }
2567
+ rows = rowsRes.value.rows;
2568
+ }
2569
+
2570
+ const appendResOrResponse = await awaitAppendWithTimeout(enqueueAppend({
2571
+ stream,
2572
+ baseAppendMs,
2573
+ rows,
2574
+ contentType: reqContentType ?? streamContentType,
2575
+ streamSeq,
2576
+ producer,
2577
+ close: streamClosed,
2578
+ }));
2579
+ if (appendResOrResponse instanceof Response) return appendResOrResponse;
2580
+ const appendRes = appendResOrResponse;
2581
+
2582
+ if (Result.isError(appendRes)) {
2583
+ const err = appendRes.error;
2584
+ if (err.kind === "overloaded") return overloaded();
2585
+ if (err.kind === "gone") return notFound("stream expired");
2586
+ if (err.kind === "not_found") return notFound();
2587
+ if (err.kind === "content_type_mismatch") return conflict("content-type mismatch");
2588
+ if (err.kind === "stream_seq") {
2589
+ return conflict("sequence mismatch", {
2590
+ "stream-expected-seq": err.expected,
2591
+ "stream-received-seq": err.received,
2592
+ });
2593
+ }
2594
+ if (err.kind === "closed") {
2595
+ const headers: Record<string, string> = {
2596
+ "stream-next-offset": encodeOffset(srow.epoch, err.lastOffset),
2597
+ "stream-closed": "true",
2598
+ };
2599
+ return new Response(null, { status: 409, headers: appendResponseHeaders(headers) });
2600
+ }
2601
+ if (err.kind === "producer_stale_epoch") {
2602
+ return new Response(null, {
2603
+ status: 403,
2604
+ headers: appendResponseHeaders({ "producer-epoch": String(err.producerEpoch) }),
2605
+ });
2606
+ }
2607
+ if (err.kind === "producer_gap") {
2608
+ return new Response(null, {
2609
+ status: 409,
2610
+ headers: appendResponseHeaders({
2611
+ "producer-expected-seq": String(err.expected),
2612
+ "producer-received-seq": String(err.received),
2613
+ }),
2614
+ });
2615
+ }
2616
+ if (err.kind === "producer_epoch_seq") return badRequest("invalid producer sequence");
2617
+ return json(500, { error: { code: "internal", message: "append failed" } });
2618
+ }
2619
+ const res = appendRes.value;
2620
+
2621
+ const appendBytes = rows.reduce((acc, r) => acc + r.payload.byteLength, 0);
2622
+ recordAppendOutcome({
2623
+ stream,
2624
+ lastOffset: res.lastOffset,
2625
+ appendedRows: res.appendedRows,
2626
+ metricsBytes: appendBytes,
2627
+ ingestedBytes: bodyBytes.byteLength,
2628
+ touched: true,
2629
+ closed: res.closed,
2630
+ });
2631
+
2632
+ const headers: Record<string, string> = {
2633
+ "stream-next-offset": encodeOffset(srow.epoch, res.lastOffset),
2634
+ };
2635
+ if (res.closed) headers["stream-closed"] = "true";
2636
+ if (producer && res.producer) {
2637
+ headers["producer-epoch"] = String(res.producer.epoch);
2638
+ headers["producer-seq"] = String(res.producer.seq);
2639
+ }
2640
+
2641
+ const status = producer && res.appendedRows > 0 ? 200 : 204;
2642
+ return new Response(null, { status, headers: appendResponseHeaders(headers) });
2643
+ });
2644
+ maybeCollectAfterHttpAppend(appendBodyBytesForGc);
2645
+ return response;
2646
+ } finally {
2647
+ leaveAppendPhase?.();
2648
+ }
2649
+ }
2650
+
2651
+ if (req.method === "GET") {
2652
+ const srowOrResponse = await liveStreamOrResponse(stream);
2653
+ if (srowOrResponse instanceof Response) return srowOrResponse;
2654
+ const srow = srowOrResponse;
2655
+
2656
+ const streamContentType = normalizeContentType(srow.content_type) ?? srow.content_type;
2657
+ const isJsonStream = streamContentType === "application/json";
2658
+
2659
+ const fmtParam = url.searchParams.get("format");
2660
+ let format: "raw" | "json" = isJsonStream ? "json" : "raw";
2661
+ if (fmtParam) {
2662
+ if (fmtParam !== "raw" && fmtParam !== "json") return badRequest("invalid format");
2663
+ format = fmtParam as "raw" | "json";
2664
+ }
2665
+ if (format === "json" && !isJsonStream) return badRequest("invalid format");
2666
+
2667
+ const pathKey = pathKeyParam ?? null;
2668
+ const key = pathKey ?? url.searchParams.get("key");
2669
+ const rawFilter = url.searchParams.get("filter");
2670
+ let filterInput: string | null = null;
2671
+ let filter = null;
2672
+ if (rawFilter != null) {
2673
+ if (!isJsonStream) return badRequest("filter requires application/json stream content-type");
2674
+ filterInput = rawFilter.trim();
2675
+ const regRes = await registry.getRegistryResult(stream);
2676
+ if (Result.isError(regRes)) return internalError();
2677
+ const filterRes = parseReadFilterResult(regRes.value, filterInput);
2678
+ if (Result.isError(filterRes)) return badRequest(filterRes.error.message);
2679
+ filter = filterRes.value;
2680
+ }
2681
+
2682
+ const liveParam = url.searchParams.get("live") ?? "";
2683
+ const cursorParam = url.searchParams.get("cursor");
2684
+ let mode: "catchup" | "long-poll" | "sse";
2685
+ if (liveParam === "" || liveParam === "false" || liveParam === "0") mode = "catchup";
2686
+ else if (liveParam === "long-poll" || liveParam === "true" || liveParam === "1") mode = "long-poll";
2687
+ else if (liveParam === "sse") mode = "sse";
2688
+ else return badRequest("invalid live mode");
2689
+ if (filter && mode === "sse") return badRequest("filter does not support live=sse");
2690
+
2691
+ const timeout = url.searchParams.get("timeout") ?? url.searchParams.get("timeout_ms");
2692
+ let timeoutMs: number | null = null;
2693
+ if (timeout) {
2694
+ if (/^[0-9]+$/.test(timeout)) {
2695
+ timeoutMs = Number(timeout);
2696
+ } else {
2697
+ const timeoutRes = parseDurationMsResult(timeout);
2698
+ if (Result.isError(timeoutRes)) return badRequest("invalid timeout");
2699
+ timeoutMs = timeoutRes.value;
2700
+ }
2701
+ }
2702
+
2703
+ const hasOffsetParam = url.searchParams.has("offset");
2704
+ let offset = url.searchParams.get("offset");
2705
+ if (hasOffsetParam && (!offset || offset.trim() === "")) return badRequest("missing offset");
2706
+ const sinceParam = url.searchParams.get("since");
2707
+ if (!offset && sinceParam) {
2708
+ const sinceRes = parseTimestampMsResult(sinceParam);
2709
+ if (Result.isError(sinceRes)) return badRequest(sinceRes.error.message);
2710
+ const seekRes = await reader.seekOffsetByTimestampResult(stream, sinceRes.value, key ?? null);
2711
+ if (Result.isError(seekRes)) return readerErrorResponse(seekRes.error);
2712
+ offset = seekRes.value;
2713
+ }
2714
+
2715
+ if (!offset) {
2716
+ if (mode === "catchup") offset = "-1";
2717
+ else return badRequest("missing offset");
2718
+ }
2719
+
2720
+ let parsedOffset: ParsedOffset | null = null;
2721
+ if (offset !== "now") {
2722
+ const offsetRes = parseOffsetResult(offset);
2723
+ if (Result.isError(offsetRes)) return badRequest(offsetRes.error.message);
2724
+ parsedOffset = offsetRes.value;
2725
+ }
2726
+
2727
+ const ifNoneMatch = req.headers.get("if-none-match");
2728
+
2729
+ const sendBatch = async (batch: ReadBatch, cacheControl: string | null, includeEtag: boolean): Promise<Response> => {
2730
+ const upToDate = batch.nextOffsetSeq === batch.endOffsetSeq;
2731
+ const closedAtTail = srow.closed !== 0 && upToDate;
2732
+ const etag = includeEtag
2733
+ ? `W/\"slice:${canonicalizeOffset(offset!)}:${batch.nextOffset}:key=${key ?? ""}:fmt=${format}:filter=${filterInput ? encodeURIComponent(filterInput) : ""}\"`
2734
+ : null;
2735
+ const baseHeaders: Record<string, string> = {
2736
+ "stream-next-offset": batch.nextOffset,
2737
+ "stream-end-offset": batch.endOffset,
2738
+ "cross-origin-resource-policy": "cross-origin",
2739
+ };
2740
+ if (upToDate) baseHeaders["stream-up-to-date"] = "true";
2741
+ if (closedAtTail) baseHeaders["stream-closed"] = "true";
2742
+ if (cacheControl) baseHeaders["cache-control"] = cacheControl;
2743
+ if (etag) baseHeaders["etag"] = etag;
2744
+ if (srow.expires_at_ms != null) baseHeaders["stream-expires-at"] = new Date(Number(srow.expires_at_ms)).toISOString();
2745
+ if (batch.filterScanLimitReached) {
2746
+ baseHeaders["stream-filter-scan-limit-reached"] = "true";
2747
+ baseHeaders["stream-filter-scan-limit-bytes"] = String(batch.filterScanLimitBytes ?? 0);
2748
+ baseHeaders["stream-filter-scanned-bytes"] = String(batch.filterScannedBytes ?? 0);
2749
+ }
2750
+
2751
+ if (etag && ifNoneMatch && ifNoneMatch === etag) {
2752
+ return new Response(null, { status: 304, headers: withNosniff(baseHeaders) });
2753
+ }
2754
+
2755
+ if (format === "json") {
2756
+ const encodedRes = await encodeStoredJsonArrayResult(stream, batch.records);
2757
+ if (Result.isError(encodedRes)) {
2758
+ if (encodedRes.error.status === 500) return internalError();
2759
+ return badRequest(encodedRes.error.message);
2760
+ }
2761
+ if (encodedRes.value) {
2762
+ metrics.recordRead(encodedRes.value.byteLength, batch.records.length);
2763
+ const headers: Record<string, string> = {
2764
+ "content-type": "application/json",
2765
+ ...baseHeaders,
2766
+ };
2767
+ return new Response(bodyBufferFromBytes(encodedRes.value), { status: 200, headers: withNosniff(headers) });
2768
+ }
2769
+
2770
+ const decoded = await decodeJsonRecords(stream, batch.records);
2771
+ if (Result.isError(decoded)) {
2772
+ if (decoded.error.status === 500) return internalError();
2773
+ return badRequest(decoded.error.message);
2774
+ }
2775
+ const body = JSON.stringify(decoded.value.values);
2776
+ metrics.recordRead(body.length, decoded.value.values.length);
2777
+ const headers: Record<string, string> = {
2778
+ "content-type": "application/json",
2779
+ ...baseHeaders,
2780
+ };
2781
+ return new Response(body, { status: 200, headers: withNosniff(headers) });
2782
+ }
2783
+
2784
+ const outBytes = concatPayloads(batch.records.map((r) => r.payload));
2785
+ metrics.recordRead(outBytes.byteLength, batch.records.length);
2786
+ const headers: Record<string, string> = {
2787
+ "content-type": streamContentType,
2788
+ ...baseHeaders,
2789
+ };
2790
+ return new Response(bodyBufferFromBytes(outBytes), { status: 200, headers: withNosniff(headers) });
2791
+ };
2792
+
2793
+ if (mode === "sse") {
2794
+ const baseCursor = srow.closed !== 0 ? null : computeCursor(Date.now(), cursorParam);
2795
+ const dataEncoding = isTextContentType(streamContentType) ? "text" : "base64";
2796
+ const startOffsetSeq = offset === "now" ? srow.next_offset - 1n : offsetToSeqOrNeg1(parsedOffset!);
2797
+ const startOffset = offset === "now" ? encodeOffset(srow.epoch, startOffsetSeq) : canonicalizeOffset(offset);
2798
+
2799
+ const encoder = new TextEncoder();
2800
+ let aborted = false;
2801
+ const abortController = new AbortController();
2802
+ const streamBody = new ReadableStream({
2803
+ start(controller) {
2804
+ (async () => {
2805
+ const fail = (message: string): void => {
2806
+ if (aborted) return;
2807
+ aborted = true;
2808
+ abortController.abort();
2809
+ controller.error(new Error(message));
2810
+ };
2811
+ let currentOffset = startOffset;
2812
+ let currentSeq = startOffsetSeq;
2813
+ let first = true;
2814
+ while (!aborted) {
2815
+ let batch: ReadBatch;
2816
+ if (offset === "now" && first) {
2817
+ batch = {
2818
+ stream,
2819
+ format,
2820
+ key: key ?? null,
2821
+ requestOffset: startOffset,
2822
+ endOffset: startOffset,
2823
+ nextOffset: startOffset,
2824
+ endOffsetSeq: currentSeq,
2825
+ nextOffsetSeq: currentSeq,
2826
+ records: [],
2827
+ };
2828
+ } else {
2829
+ const batchRes = await runForegroundWithGate(readGate, () =>
2830
+ reader.readResult({ stream, offset: currentOffset, key: key ?? null, format, filter })
2831
+ );
2832
+ if (Result.isError(batchRes)) {
2833
+ fail(batchRes.error.message);
2834
+ return;
2835
+ }
2836
+ batch = batchRes.value;
2837
+ }
2838
+ first = false;
2839
+
2840
+ let ssePayload = "";
2841
+
2842
+ if (batch.records.length > 0) {
2843
+ let dataPayload = "";
2844
+ if (format === "json") {
2845
+ const encodedRes = await encodeStoredJsonArrayResult(stream, batch.records);
2846
+ if (Result.isError(encodedRes)) {
2847
+ fail(encodedRes.error.message);
2848
+ return;
2849
+ }
2850
+ if (encodedRes.value) {
2851
+ dataPayload = new TextDecoder().decode(encodedRes.value);
2852
+ } else {
2853
+ const decoded = await decodeJsonRecords(stream, batch.records);
2854
+ if (Result.isError(decoded)) {
2855
+ fail(decoded.error.message);
2856
+ return;
2857
+ }
2858
+ dataPayload = JSON.stringify(decoded.value.values);
2859
+ }
2860
+ } else {
2861
+ const outBytes = concatPayloads(batch.records.map((r) => r.payload));
2862
+ dataPayload =
2863
+ dataEncoding === "base64"
2864
+ ? Buffer.from(outBytes).toString("base64")
2865
+ : new TextDecoder().decode(outBytes);
2866
+ }
2867
+ ssePayload += encodeSseEvent("data", dataPayload);
2868
+ }
2869
+
2870
+ const upToDate = batch.nextOffsetSeq === batch.endOffsetSeq;
2871
+ const latest = await controlStore.getStream(stream);
2872
+ const closedNow = !!latest && latest.closed !== 0 && upToDate;
2873
+
2874
+ const control: Record<string, any> = { streamNextOffset: batch.nextOffset };
2875
+ if (upToDate) control.upToDate = true;
2876
+ if (closedNow) control.streamClosed = true;
2877
+ if (!closedNow && baseCursor) control.streamCursor = baseCursor;
2878
+ ssePayload += encodeSseEvent("control", JSON.stringify(control));
2879
+ controller.enqueue(encoder.encode(ssePayload));
2880
+
2881
+ if (closedNow) break;
2882
+ currentOffset = batch.nextOffset;
2883
+ currentSeq = batch.nextOffsetSeq;
2884
+ if (!upToDate) continue;
2885
+
2886
+ const sseWaitMs = timeoutMs == null ? 30_000 : timeoutMs;
2887
+ await notifier.waitFor(stream, currentSeq, sseWaitMs, abortController.signal);
2888
+ }
2889
+ if (!aborted) controller.close();
2890
+ })().catch((err) => {
2891
+ if (!aborted) controller.error(err);
2892
+ });
2893
+ },
2894
+ cancel() {
2895
+ aborted = true;
2896
+ abortController.abort();
2897
+ },
2898
+ });
2899
+
2900
+ const headers: Record<string, string> = {
2901
+ "content-type": "text/event-stream",
2902
+ "cache-control": "no-cache",
2903
+ "cross-origin-resource-policy": "cross-origin",
2904
+ "stream-next-offset": startOffset,
2905
+ "stream-end-offset": encodeOffset(srow.epoch, srow.next_offset - 1n),
2906
+ };
2907
+ if (dataEncoding === "base64") headers["stream-sse-data-encoding"] = "base64";
2908
+ return new Response(streamBody, { status: 200, headers: withNosniff(headers) });
2909
+ }
2910
+
2911
+ const defaultLongPollTimeoutMs = 3000;
2912
+
2913
+ if (offset === "now") {
2914
+ const tailOffset = encodeOffset(srow.epoch, srow.next_offset - 1n);
2915
+ if (srow.closed !== 0) {
2916
+ if (mode === "long-poll") {
2917
+ const headers: Record<string, string> = {
2918
+ "stream-next-offset": tailOffset,
2919
+ "stream-end-offset": tailOffset,
2920
+ "stream-up-to-date": "true",
2921
+ "stream-closed": "true",
2922
+ "cache-control": "no-store",
2923
+ };
2924
+ if (srow.expires_at_ms != null) headers["stream-expires-at"] = new Date(Number(srow.expires_at_ms)).toISOString();
2925
+ return new Response(null, { status: 204, headers: withNosniff(headers) });
2926
+ }
2927
+ const headers: Record<string, string> = {
2928
+ "content-type": streamContentType,
2929
+ "stream-next-offset": tailOffset,
2930
+ "stream-end-offset": tailOffset,
2931
+ "stream-up-to-date": "true",
2932
+ "stream-closed": "true",
2933
+ "cache-control": "no-store",
2934
+ "cross-origin-resource-policy": "cross-origin",
2935
+ };
2936
+ if (srow.expires_at_ms != null) headers["stream-expires-at"] = new Date(Number(srow.expires_at_ms)).toISOString();
2937
+ const body = format === "json" ? "[]" : "";
2938
+ return new Response(body, { status: 200, headers: withNosniff(headers) });
2939
+ }
2940
+
2941
+ if (mode === "long-poll") {
2942
+ const deadline = Date.now() + (timeoutMs ?? defaultLongPollTimeoutMs);
2943
+ let currentOffset = tailOffset;
2944
+ while (true) {
2945
+ const batchRes = await runForegroundWithGate(readGate, () =>
2946
+ reader.readResult({ stream, offset: currentOffset, key: key ?? null, format, filter })
2947
+ );
2948
+ if (Result.isError(batchRes)) return readerErrorResponse(batchRes.error);
2949
+ const batch = batchRes.value;
2950
+ if (batch.records.length > 0 || batch.filterScanLimitReached) {
2951
+ const cursor = computeCursor(Date.now(), cursorParam);
2952
+ const resp = await sendBatch(batch, "no-store", false);
2953
+ const headers = new Headers(resp.headers);
2954
+ headers.set("stream-cursor", cursor);
2955
+ return new Response(resp.body, { status: resp.status, headers });
2956
+ }
2957
+ const latest = await controlStore.getStream(stream);
2958
+ if (latest && latest.closed !== 0 && batch.nextOffsetSeq === batch.endOffsetSeq) {
2959
+ const latestTail = encodeOffset(latest.epoch, latest.next_offset - 1n);
2960
+ const headers: Record<string, string> = {
2961
+ "stream-next-offset": latestTail,
2962
+ "stream-end-offset": latestTail,
2963
+ "stream-up-to-date": "true",
2964
+ "stream-closed": "true",
2965
+ "cache-control": "no-store",
2966
+ };
2967
+ if (latest.expires_at_ms != null) headers["stream-expires-at"] = new Date(Number(latest.expires_at_ms)).toISOString();
2968
+ return new Response(null, { status: 204, headers: withNosniff(headers) });
2969
+ }
2970
+ const remaining = deadline - Date.now();
2971
+ if (remaining <= 0) break;
2972
+ currentOffset = batch.nextOffset;
2973
+ await notifier.waitFor(stream, batch.endOffsetSeq, remaining, req.signal);
2974
+ if (req.signal.aborted) return new Response(null, { status: 204 });
2975
+ }
2976
+ const latest = await controlStore.getStream(stream);
2977
+ const latestTail = latest ? encodeOffset(latest.epoch, latest.next_offset - 1n) : tailOffset;
2978
+ const headers: Record<string, string> = {
2979
+ "stream-next-offset": latestTail,
2980
+ "stream-end-offset": latestTail,
2981
+ "stream-up-to-date": "true",
2982
+ "cache-control": "no-store",
2983
+ };
2984
+ if (latest && latest.closed !== 0) headers["stream-closed"] = "true";
2985
+ else headers["stream-cursor"] = computeCursor(Date.now(), cursorParam);
2986
+ if (latest && latest.expires_at_ms != null) headers["stream-expires-at"] = new Date(Number(latest.expires_at_ms)).toISOString();
2987
+ return new Response(null, { status: 204, headers: withNosniff(headers) });
2988
+ }
2989
+
2990
+ const headers: Record<string, string> = {
2991
+ "content-type": streamContentType,
2992
+ "stream-next-offset": tailOffset,
2993
+ "stream-end-offset": tailOffset,
2994
+ "stream-up-to-date": "true",
2995
+ "cache-control": "no-store",
2996
+ "cross-origin-resource-policy": "cross-origin",
2997
+ };
2998
+ const body = format === "json" ? "[]" : "";
2999
+ return new Response(body, { status: 200, headers: withNosniff(headers) });
3000
+ }
3001
+
3002
+ if (mode === "long-poll") {
3003
+ const deadline = Date.now() + (timeoutMs ?? defaultLongPollTimeoutMs);
3004
+ let currentOffset = offset;
3005
+ while (true) {
3006
+ const batchRes = await runForegroundWithGate(readGate, () =>
3007
+ reader.readResult({ stream, offset: currentOffset, key: key ?? null, format, filter })
3008
+ );
3009
+ if (Result.isError(batchRes)) return readerErrorResponse(batchRes.error);
3010
+ const batch = batchRes.value;
3011
+ if (batch.records.length > 0 || batch.filterScanLimitReached) {
3012
+ const cursor = computeCursor(Date.now(), cursorParam);
3013
+ const resp = await sendBatch(batch, "no-store", false);
3014
+ const headers = new Headers(resp.headers);
3015
+ headers.set("stream-cursor", cursor);
3016
+ return new Response(resp.body, { status: resp.status, headers });
3017
+ }
3018
+ const latest = await controlStore.getStream(stream);
3019
+ if (latest && latest.closed !== 0 && batch.nextOffsetSeq === batch.endOffsetSeq) {
3020
+ const latestTail = encodeOffset(latest.epoch, latest.next_offset - 1n);
3021
+ const headers: Record<string, string> = {
3022
+ "stream-next-offset": latestTail,
3023
+ "stream-end-offset": latestTail,
3024
+ "stream-up-to-date": "true",
3025
+ "stream-closed": "true",
3026
+ "cache-control": "no-store",
3027
+ };
3028
+ if (latest.expires_at_ms != null) headers["stream-expires-at"] = new Date(Number(latest.expires_at_ms)).toISOString();
3029
+ return new Response(null, { status: 204, headers: withNosniff(headers) });
3030
+ }
3031
+ const remaining = deadline - Date.now();
3032
+ if (remaining <= 0) break;
3033
+ currentOffset = batch.nextOffset;
3034
+ await notifier.waitFor(stream, batch.endOffsetSeq, remaining, req.signal);
3035
+ if (req.signal.aborted) return new Response(null, { status: 204 });
3036
+ }
3037
+ const latest = await controlStore.getStream(stream);
3038
+ const latestTail = latest ? encodeOffset(latest.epoch, latest.next_offset - 1n) : currentOffset;
3039
+ const headers: Record<string, string> = {
3040
+ "stream-next-offset": latestTail,
3041
+ "stream-end-offset": latestTail,
3042
+ "stream-up-to-date": "true",
3043
+ "cache-control": "no-store",
3044
+ };
3045
+ if (latest && latest.closed !== 0) headers["stream-closed"] = "true";
3046
+ else headers["stream-cursor"] = computeCursor(Date.now(), cursorParam);
3047
+ if (latest && latest.expires_at_ms != null) headers["stream-expires-at"] = new Date(Number(latest.expires_at_ms)).toISOString();
3048
+ return new Response(null, { status: 204, headers: withNosniff(headers) });
3049
+ }
3050
+
3051
+ const batchRes = await runForegroundWithGate(readGate, () =>
3052
+ reader.readResult({ stream, offset, key: key ?? null, format, filter })
3053
+ );
3054
+ if (Result.isError(batchRes)) return readerErrorResponse(batchRes.error);
3055
+ const batch = batchRes.value;
3056
+ const cacheControl = "immutable, max-age=31536000";
3057
+ return sendBatch(batch, cacheControl, true);
3058
+ }
3059
+
3060
+ return badRequest("unsupported method");
3061
+ }
3062
+
3063
+ return notFound();
3064
+ })();
3065
+ const resolved = await awaitWithCooperativeTimeout(requestPromise, HTTP_RESOLVER_TIMEOUT_MS);
3066
+ if (resolved === TIMEOUT_SENTINEL) {
3067
+ timedOut = true;
3068
+ requestAbortController.abort(new Error("request timed out"));
3069
+ void requestPromise.catch(() => {});
3070
+ await cancelRequestBody(req);
3071
+ return requestTimeout();
3072
+ }
3073
+ return resolved;
3074
+ } catch (e: any) {
3075
+ if (isAbortLikeError(e)) {
3076
+ if (timedOut) return requestTimeout();
3077
+ return new Response(null, { status: 204 });
3078
+ }
3079
+ const msg = String(e?.message ?? e);
3080
+ if (!closing && !msg.includes("Statement has finalized")) {
3081
+ // eslint-disable-next-line no-console
3082
+ console.error("request failed", e);
3083
+ }
3084
+ return internalError();
3085
+ } finally {
3086
+ req.signal.removeEventListener("abort", abortFromClient);
3087
+ }
3088
+ };
3089
+
3090
+ const close = async () => {
3091
+ closing = true;
3092
+ await ready.catch(() => {});
3093
+ // Await the worker-thread pools so their threads are fully gone before we
3094
+ // return. The host process (e.g. @prisma/dev) frees other native resources
3095
+ // -- PGlite's WebAssembly JIT pages -- right after this resolves; a worker
3096
+ // thread still tearing down at that moment races V8's process-global JIT
3097
+ // bookkeeping and can abort the process on Linux.
3098
+ await touch?.stop();
3099
+ await fullMode?.segmenter.stop(true);
3100
+ await fullMode?.uploader.stop(true);
3101
+ await indexer?.stop();
3102
+ await metricsEmitter.stop();
3103
+ await expirySweeper.stop();
3104
+ fullMode?.sizeReconciler?.stop();
3105
+ await ingest.stop();
3106
+ memorySampler?.stop();
3107
+ memory.stop();
3108
+ await controlStore.close();
3109
+ if (debugStore && debugStore !== (controlStore as unknown as AppDebugStore) && typeof debugStore.close === "function") await debugStore.close();
3110
+ };
3111
+
3112
+ return {
3113
+ fetch,
3114
+ close,
3115
+ ready,
3116
+ deps: {
3117
+ config: cfg,
3118
+ db: debugStore,
3119
+ os: fullMode?.store,
3120
+ ingest,
3121
+ notifier,
3122
+ reader,
3123
+ segmenter: fullMode?.segmenter,
3124
+ uploader: fullMode?.uploader,
3125
+ indexer,
3126
+ metrics,
3127
+ registry,
3128
+ profiles,
3129
+ touch,
3130
+ stats,
3131
+ storageStats: storageStatsStore,
3132
+ objectStoreAccounting: objectStoreAccountingStore,
3133
+ backpressure,
3134
+ memory,
3135
+ concurrency: {
3136
+ ingest: ingestGate,
3137
+ read: readGate,
3138
+ search: searchGate,
3139
+ asyncIndex: asyncIndexGate,
3140
+ },
3141
+ memorySampler,
3142
+ },
3143
+ };
3144
+ }