@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
package/src/server.ts ADDED
@@ -0,0 +1,264 @@
1
+ import { loadConfig } from "./config";
2
+ import { createApp, createPostgresApp, createPostgresFullApp, type App } from "./app";
3
+ import { StatsCollector, StatsReporter } from "./stats";
4
+ import { LatencyHistogramCollector, HistogramReporter } from "./hist";
5
+ import { MockR2Store } from "./objectstore/mock_r2";
6
+ import type { ObjectStore } from "./objectstore/interface";
7
+ import { R2ObjectStore } from "./objectstore/r2";
8
+ import { bootstrapFromR2 } from "./bootstrap";
9
+ import { bootstrapPostgresFromR2 } from "./postgres/bootstrap";
10
+ import { PostgresDurableStore } from "./postgres/store";
11
+ import { initConsoleLogging } from "./util/log";
12
+ import { applyAutoTune, AutoTuneApplyError, parseAutoTuneArg } from "./server_auto_tune";
13
+ import { parseAuthConfigResult, withAuth } from "./auth";
14
+ import { Result } from "better-result";
15
+
16
+ initConsoleLogging();
17
+
18
+ const args = process.argv.slice(2);
19
+ const authConfigResult = parseAuthConfigResult(args);
20
+ if (Result.isError(authConfigResult)) {
21
+ console.error(authConfigResult.error.message);
22
+ process.exit(1);
23
+ }
24
+ const authConfig = authConfigResult.value;
25
+
26
+ const autoTune = parseAutoTuneArg(args);
27
+ if (autoTune.enabled) {
28
+ try {
29
+ applyAutoTune(autoTune.valueMb);
30
+ } catch (error) {
31
+ if (error instanceof AutoTuneApplyError) {
32
+ console.error(error.message);
33
+ process.exit(1);
34
+ }
35
+ throw error;
36
+ }
37
+ }
38
+
39
+ const cfg = loadConfig();
40
+
41
+ const statsEnabled = args.includes("--stats");
42
+ const histEnabled = args.includes("--hist");
43
+ const bootstrapEnabled = args.includes("--bootstrap-from-r2");
44
+ const bpBudgetRaw = process.env.DS_BACKPRESSURE_BUDGET_MS;
45
+ const bpBudgetMs = bpBudgetRaw ? Number(bpBudgetRaw) : cfg.ingestFlushIntervalMs + 1;
46
+ if (bpBudgetRaw && !Number.isFinite(bpBudgetMs)) {
47
+ // eslint-disable-next-line no-console
48
+ console.error(`invalid DS_BACKPRESSURE_BUDGET_MS: ${bpBudgetRaw}`);
49
+ process.exit(1);
50
+ }
51
+ const stats = statsEnabled ? new StatsCollector({ backpressureBudgetMs: bpBudgetMs }) : undefined;
52
+ const hist = histEnabled ? new LatencyHistogramCollector() : undefined;
53
+
54
+ const storeIdx = args.indexOf("--object-store");
55
+ const storeChoice = storeIdx >= 0 ? args[storeIdx + 1] : null;
56
+
57
+ function requireObjectStoreChoice(): "r2" | "local" {
58
+ if (!storeChoice || (storeChoice !== "r2" && storeChoice !== "local")) {
59
+ // eslint-disable-next-line no-console
60
+ console.error("missing or invalid --object-store (expected: r2 | local)");
61
+ process.exit(1);
62
+ }
63
+ return storeChoice;
64
+ }
65
+
66
+ function createConfiguredObjectStore(choice: "r2" | "local"): ObjectStore {
67
+ if (choice === "local") {
68
+ const memBytesRaw = process.env.DS_MOCK_R2_MAX_INMEM_BYTES;
69
+ const memMbRaw = process.env.DS_MOCK_R2_MAX_INMEM_MB;
70
+ const putDelayRaw = process.env.DS_MOCK_R2_PUT_DELAY_MS;
71
+ const getDelayRaw = process.env.DS_MOCK_R2_GET_DELAY_MS;
72
+ const headDelayRaw = process.env.DS_MOCK_R2_HEAD_DELAY_MS;
73
+ const listDelayRaw = process.env.DS_MOCK_R2_LIST_DELAY_MS;
74
+ const memBytes = memBytesRaw ? Number(memBytesRaw) : memMbRaw ? Number(memMbRaw) * 1024 * 1024 : null;
75
+ const putDelayMs = putDelayRaw ? Number(putDelayRaw) : 0;
76
+ const getDelayMs = getDelayRaw ? Number(getDelayRaw) : 0;
77
+ const headDelayMs = headDelayRaw ? Number(headDelayRaw) : 0;
78
+ const listDelayMs = listDelayRaw ? Number(listDelayRaw) : 0;
79
+ if (memBytesRaw && !Number.isFinite(memBytes)) {
80
+ // eslint-disable-next-line no-console
81
+ console.error(`invalid DS_MOCK_R2_MAX_INMEM_BYTES: ${memBytesRaw}`);
82
+ process.exit(1);
83
+ }
84
+ if (memMbRaw && !Number.isFinite(Number(memMbRaw))) {
85
+ // eslint-disable-next-line no-console
86
+ console.error(`invalid DS_MOCK_R2_MAX_INMEM_MB: ${memMbRaw}`);
87
+ process.exit(1);
88
+ }
89
+ for (const [name, value] of [
90
+ ["DS_MOCK_R2_PUT_DELAY_MS", putDelayMs],
91
+ ["DS_MOCK_R2_GET_DELAY_MS", getDelayMs],
92
+ ["DS_MOCK_R2_HEAD_DELAY_MS", headDelayMs],
93
+ ["DS_MOCK_R2_LIST_DELAY_MS", listDelayMs],
94
+ ] as const) {
95
+ if (!Number.isFinite(value) || value < 0) {
96
+ // eslint-disable-next-line no-console
97
+ console.error(`invalid ${name}: ${process.env[name]}`);
98
+ process.exit(1);
99
+ }
100
+ }
101
+ return new MockR2Store({
102
+ maxInMemoryBytes: memBytes ?? undefined,
103
+ spillDir: process.env.DS_MOCK_R2_SPILL_DIR,
104
+ faults: {
105
+ putDelayMs,
106
+ getDelayMs,
107
+ headDelayMs,
108
+ listDelayMs,
109
+ },
110
+ });
111
+ }
112
+
113
+ const bucket = process.env.DURABLE_STREAMS_R2_BUCKET;
114
+ const accountId = process.env.DURABLE_STREAMS_R2_ACCOUNT_ID;
115
+ const accessKeyId = process.env.DURABLE_STREAMS_R2_ACCESS_KEY_ID;
116
+ const secretAccessKey = process.env.DURABLE_STREAMS_R2_SECRET_ACCESS_KEY;
117
+ const endpoint = process.env.DURABLE_STREAMS_R2_ENDPOINT;
118
+ const region = process.env.DURABLE_STREAMS_R2_REGION;
119
+ if (!bucket || !accountId || !accessKeyId || !secretAccessKey) {
120
+ // eslint-disable-next-line no-console
121
+ console.error("missing R2 env vars: DURABLE_STREAMS_R2_BUCKET, DURABLE_STREAMS_R2_ACCOUNT_ID, DURABLE_STREAMS_R2_ACCESS_KEY_ID, DURABLE_STREAMS_R2_SECRET_ACCESS_KEY");
122
+ process.exit(1);
123
+ }
124
+ return new R2ObjectStore({
125
+ bucket,
126
+ accountId,
127
+ accessKeyId,
128
+ secretAccessKey,
129
+ endpoint,
130
+ region,
131
+ });
132
+ }
133
+
134
+ let app: App;
135
+ if (cfg.storage === "postgres") {
136
+ if (cfg.postgresUrl == null) {
137
+ // loadConfig validates this; keep the local guard for future call-site changes.
138
+ // eslint-disable-next-line no-console
139
+ console.error("DS_POSTGRES_URL is required when DS_STORAGE=postgres");
140
+ process.exit(1);
141
+ }
142
+ if (cfg.postgresMode === "wal") {
143
+ if (storeIdx >= 0) {
144
+ // eslint-disable-next-line no-console
145
+ console.error("postgres WAL mode does not support --object-store");
146
+ process.exit(1);
147
+ }
148
+ if (bootstrapEnabled) {
149
+ // eslint-disable-next-line no-console
150
+ console.error("postgres WAL mode does not support --bootstrap-from-r2");
151
+ process.exit(1);
152
+ }
153
+ const postgresStore = await PostgresDurableStore.connect(cfg.postgresUrl);
154
+ app = createPostgresApp(cfg, postgresStore, { stats });
155
+ } else {
156
+ const objectStore = createConfiguredObjectStore(requireObjectStoreChoice());
157
+ if (bootstrapEnabled) {
158
+ await bootstrapPostgresFromR2(cfg, objectStore, cfg.postgresUrl, { clearLocal: true });
159
+ }
160
+ const postgresStore = await PostgresDurableStore.connectFull(cfg.postgresUrl);
161
+ app = createPostgresFullApp(cfg, postgresStore, objectStore, { stats });
162
+ }
163
+ } else {
164
+ const store = createConfiguredObjectStore(requireObjectStoreChoice());
165
+
166
+ if (bootstrapEnabled) {
167
+ await bootstrapFromR2(cfg, store, { clearLocal: true });
168
+ }
169
+
170
+ app = createApp(cfg, store, { stats });
171
+ }
172
+ const statsIntervalMs = process.env.DS_STATS_INTERVAL_MS ? Number(process.env.DS_STATS_INTERVAL_MS) : 60_000;
173
+ if (process.env.DS_STATS_INTERVAL_MS && !Number.isFinite(statsIntervalMs)) {
174
+ // eslint-disable-next-line no-console
175
+ console.error(`invalid DS_STATS_INTERVAL_MS: ${process.env.DS_STATS_INTERVAL_MS}`);
176
+ process.exit(1);
177
+ }
178
+ const statsReporter =
179
+ statsEnabled && stats && app.deps.storageStats && app.deps.uploader
180
+ ? new StatsReporter(stats, app.deps.storageStats, app.deps.uploader, app.deps.ingest, app.deps.backpressure, app.deps.memory, statsIntervalMs)
181
+ : null;
182
+ const histReporter = histEnabled && hist ? new HistogramReporter(hist, statsIntervalMs) : null;
183
+
184
+ const fetchWithHist = hist
185
+ ? async (req: Request): Promise<Response> => {
186
+ const start = Date.now();
187
+ const resp = await app.fetch(req);
188
+ const url = req.url;
189
+ let path: string | null = null;
190
+ if (url.startsWith("/")) {
191
+ path = url;
192
+ } else {
193
+ const schemeIdx = url.indexOf("://");
194
+ if (schemeIdx !== -1) {
195
+ const pathIdx = url.indexOf("/", schemeIdx + 3);
196
+ path = pathIdx === -1 ? "/" : url.slice(pathIdx);
197
+ }
198
+ }
199
+ if (path) {
200
+ const isStream = path.startsWith("/v1/stream/") || path.startsWith("/v1/streams");
201
+ if (isStream) {
202
+ const ms = Date.now() - start;
203
+ const method = req.method.toUpperCase();
204
+ if (method === "GET" || method === "HEAD") hist.recordRead(ms);
205
+ else if (method === "POST" || method === "PUT" || method === "DELETE") hist.recordWrite(ms);
206
+ }
207
+ }
208
+ return resp;
209
+ }
210
+ : app.fetch;
211
+ const fetchWithAuth = withAuth(authConfig, fetchWithHist);
212
+
213
+ const server = Bun.serve({
214
+ hostname: cfg.host,
215
+ port: cfg.port,
216
+ // Default Bun idleTimeout is 10s, which is too low for long-poll endpoints like /touch/wait.
217
+ // Bun expects seconds here.
218
+ idleTimeout: (() => {
219
+ const raw = process.env.DS_HTTP_IDLE_TIMEOUT_SECONDS;
220
+ if (raw == null || raw.trim() === "") return 180;
221
+ const n = Number(raw);
222
+ if (!Number.isFinite(n) || n <= 0) {
223
+ // eslint-disable-next-line no-console
224
+ console.error(`invalid DS_HTTP_IDLE_TIMEOUT_SECONDS: ${raw}`);
225
+ process.exit(1);
226
+ }
227
+ return n;
228
+ })(),
229
+ fetch: fetchWithAuth,
230
+ });
231
+
232
+ statsReporter?.start();
233
+ histReporter?.start();
234
+
235
+ let shuttingDown = false;
236
+ const shutdown = async (signal: NodeJS.Signals) => {
237
+ if (shuttingDown) return;
238
+ shuttingDown = true;
239
+ // eslint-disable-next-line no-console
240
+ console.log(`received ${signal}, shutting down prisma-streams server`);
241
+ statsReporter?.stop();
242
+ histReporter?.stop();
243
+ try {
244
+ server.stop(true);
245
+ } catch (err) {
246
+ // eslint-disable-next-line no-console
247
+ console.error("failed to stop HTTP server cleanly", err);
248
+ }
249
+ try {
250
+ await app.close();
251
+ } catch (err) {
252
+ // eslint-disable-next-line no-console
253
+ console.error("failed to close application cleanly", err);
254
+ process.exitCode = 1;
255
+ }
256
+ };
257
+
258
+ const listenTarget = cfg.host.includes(":") ? `[${cfg.host}]:${server.port}` : `${cfg.host}:${server.port}`;
259
+
260
+ process.once("SIGINT", () => shutdown("SIGINT"));
261
+ process.once("SIGTERM", () => shutdown("SIGTERM"));
262
+
263
+ // eslint-disable-next-line no-console
264
+ console.log(`prisma-streams server listening on ${listenTarget}`);
@@ -0,0 +1,158 @@
1
+ import { AUTO_TUNE_PRESETS, memoryLimitForPreset, tuneForPreset, type AutoTuneConfig } from "./auto_tune";
2
+
3
+ export class AutoTuneApplyError extends Error {
4
+ constructor(message: string) {
5
+ super(message);
6
+ this.name = "AutoTuneApplyError";
7
+ }
8
+ }
9
+
10
+ export function parseAutoTuneArg(args: string[]): { enabled: boolean; valueMb: number | null } {
11
+ let enabled = false;
12
+ let valueMb: number | null = null;
13
+ for (let i = 0; i < args.length; i++) {
14
+ const arg = args[i];
15
+ if (arg === "--auto-tune") {
16
+ enabled = true;
17
+ const next = args[i + 1];
18
+ if (next && !next.startsWith("--") && /^[0-9]+$/.test(next)) {
19
+ valueMb = Number(next);
20
+ }
21
+ } else if (arg.startsWith("--auto-tune=")) {
22
+ enabled = true;
23
+ const raw = arg.split("=", 2)[1] ?? "";
24
+ if (raw.trim() !== "") valueMb = Number(raw);
25
+ }
26
+ }
27
+ return { enabled, valueMb };
28
+ }
29
+
30
+ function formatPresetList<T>(presets: number[], selected: number, map: (preset: number) => T, fmt: (val: T) => string): string {
31
+ return presets
32
+ .map((preset) => {
33
+ const value = fmt(map(preset));
34
+ return preset === selected ? `[${value}]` : value;
35
+ })
36
+ .join(", ");
37
+ }
38
+
39
+ export function applyAutoTune(
40
+ overrideMb: number | null,
41
+ opts: {
42
+ env?: NodeJS.ProcessEnv;
43
+ log?: (message: string) => void;
44
+ } = {}
45
+ ): void {
46
+ const env = opts.env ?? process.env;
47
+ const log = opts.log ?? console.log;
48
+ const envMemRaw = env.DS_MEMORY_LIMIT_MB;
49
+ if (overrideMb != null) {
50
+ if (envMemRaw) {
51
+ throw new AutoTuneApplyError("--auto-tune with a value cannot be used with DS_MEMORY_LIMIT_MB");
52
+ }
53
+ } else if (!envMemRaw) {
54
+ throw new AutoTuneApplyError("--auto-tune requires DS_MEMORY_LIMIT_MB to be set (or pass a value)");
55
+ }
56
+ const memMb = overrideMb != null ? overrideMb : Number(envMemRaw);
57
+ if (!Number.isFinite(memMb) || memMb <= 0) {
58
+ const bad = overrideMb != null ? String(overrideMb) : String(envMemRaw);
59
+ throw new AutoTuneApplyError(`invalid DS_MEMORY_LIMIT_MB: ${bad}`);
60
+ }
61
+ if (env.DS_MEMORY_LIMIT_BYTES) {
62
+ throw new AutoTuneApplyError("--auto-tune does not allow DS_MEMORY_LIMIT_BYTES; use DS_MEMORY_LIMIT_MB");
63
+ }
64
+
65
+ const conflictVars = [
66
+ "DS_SEGMENT_MAX_BYTES",
67
+ "DS_SEGMENT_TARGET_ROWS",
68
+ "DS_SEGMENT_CACHE_MAX_BYTES",
69
+ "DS_INDEX_CHECK_MS",
70
+ "DS_SQLITE_CACHE_MB",
71
+ "DS_SQLITE_CACHE_BYTES",
72
+ "DS_WORKER_SQLITE_CACHE_MB",
73
+ "DS_WORKER_SQLITE_CACHE_BYTES",
74
+ "DS_INDEX_RUN_MEM_CACHE_BYTES",
75
+ "DS_LEXICON_INDEX_CACHE_MAX_BYTES",
76
+ "DS_INGEST_MAX_BATCH_BYTES",
77
+ "DS_INGEST_MAX_QUEUE_BYTES",
78
+ "DS_INGEST_CONCURRENCY",
79
+ "DS_READ_CONCURRENCY",
80
+ "DS_SEARCH_CONCURRENCY",
81
+ "DS_ASYNC_INDEX_CONCURRENCY",
82
+ "DS_INDEX_BUILD_CONCURRENCY",
83
+ "DS_INDEX_COMPACT_CONCURRENCY",
84
+ "DS_SEGMENTER_WORKERS",
85
+ "DS_UPLOAD_CONCURRENCY",
86
+ "DS_SEARCH_COMPANION_TOC_CACHE_BYTES",
87
+ "DS_SEARCH_COMPANION_SECTION_CACHE_BYTES",
88
+ "DS_SEARCH_COMPANION_BATCH_SEGMENTS",
89
+ "DS_SEARCH_COMPANION_YIELD_BLOCKS",
90
+ ];
91
+ const conflicts = conflictVars.filter((v) => env[v] != null);
92
+ if (conflicts.length > 0) {
93
+ throw new AutoTuneApplyError(`--auto-tune cannot be used with manual memory settings: ${conflicts.join(", ")}`);
94
+ }
95
+
96
+ const presets = [...AUTO_TUNE_PRESETS];
97
+ const preset = [...presets].reverse().find((v) => v <= memMb);
98
+ if (!preset) {
99
+ throw new AutoTuneApplyError(`DS_MEMORY_LIMIT_MB=${memMb} is below the minimum preset (256)`);
100
+ }
101
+ const tune: AutoTuneConfig = tuneForPreset(preset);
102
+
103
+ const memoryLimitMb = memoryLimitForPreset(preset);
104
+ env.DS_AUTO_TUNE_REQUESTED_MB = String(memMb);
105
+ env.DS_AUTO_TUNE_PRESET_MB = String(preset);
106
+ env.DS_AUTO_TUNE_EFFECTIVE_MEMORY_LIMIT_MB = String(memoryLimitMb);
107
+ env.DS_MEMORY_LIMIT_MB = String(memoryLimitMb);
108
+ env.DS_SEGMENT_MAX_BYTES = String(tune.segmentMaxMiB * 1024 * 1024);
109
+ env.DS_SEGMENT_TARGET_ROWS = String(tune.segmentTargetRows);
110
+ env.DS_SEGMENT_CACHE_MAX_BYTES = String(tune.segmentCacheMb * 1024 * 1024);
111
+ env.DS_INDEX_CHECK_MS = String(tune.indexCheckMs);
112
+ env.DS_SQLITE_CACHE_MB = String(tune.sqliteCacheMb);
113
+ env.DS_WORKER_SQLITE_CACHE_MB = String(tune.workerSqliteCacheMb);
114
+ env.DS_INDEX_RUN_MEM_CACHE_BYTES = String(tune.indexMemMb * 1024 * 1024);
115
+ env.DS_LEXICON_INDEX_CACHE_MAX_BYTES = String(tune.lexiconIndexCacheMb * 1024 * 1024);
116
+ env.DS_SEARCH_COMPANION_TOC_CACHE_BYTES = String(tune.searchCompanionTocCacheMb * 1024 * 1024);
117
+ env.DS_SEARCH_COMPANION_SECTION_CACHE_BYTES = String(tune.searchCompanionSectionCacheMb * 1024 * 1024);
118
+ env.DS_INGEST_MAX_BATCH_BYTES = String(tune.ingestBatchMb * 1024 * 1024);
119
+ env.DS_INGEST_MAX_QUEUE_BYTES = String(tune.ingestQueueMb * 1024 * 1024);
120
+ env.DS_INGEST_CONCURRENCY = String(tune.ingestConcurrency);
121
+ env.DS_READ_CONCURRENCY = String(tune.readConcurrency);
122
+ env.DS_SEARCH_CONCURRENCY = String(tune.searchConcurrency);
123
+ env.DS_ASYNC_INDEX_CONCURRENCY = String(tune.asyncIndexConcurrency);
124
+ env.DS_INDEX_BUILD_CONCURRENCY = String(tune.indexBuildConcurrency);
125
+ env.DS_INDEX_COMPACT_CONCURRENCY = String(tune.indexCompactConcurrency);
126
+ env.DS_SEGMENTER_WORKERS = String(tune.segmenterWorkers);
127
+ env.DS_UPLOAD_CONCURRENCY = String(tune.uploadConcurrency);
128
+ env.DS_SEARCH_COMPANION_BATCH_SEGMENTS = String(tune.searchCompanionBatchSegments);
129
+ env.DS_SEARCH_COMPANION_YIELD_BLOCKS = String(tune.searchCompanionYieldBlocks);
130
+
131
+ const presetLine = formatPresetList(presets, preset, (v) => v, (v) => String(v));
132
+ log(`Auto-tuning for memory preset ${presetLine}`);
133
+ log(
134
+ `DS_MEMORY_LIMIT_MB presets: ${formatPresetList(presets, preset, (p) => memoryLimitForPreset(p), (v) => String(v))}`
135
+ );
136
+ log(`DS_SEGMENT_MAX_MIB presets: ${formatPresetList(presets, preset, (p) => tuneForPreset(p).segmentMaxMiB, (v) => String(v))}`);
137
+ log(`DS_SEGMENT_TARGET_ROWS presets: ${formatPresetList(presets, preset, (p) => tuneForPreset(p).segmentTargetRows, (v) => String(v))}`);
138
+ log(`DS_SEGMENT_CACHE_MB presets: ${formatPresetList(presets, preset, (p) => tuneForPreset(p).segmentCacheMb, (v) => String(v))}`);
139
+ log(`DS_INDEX_CHECK_MS presets: ${formatPresetList(presets, preset, (p) => tuneForPreset(p).indexCheckMs, (v) => String(v))}`);
140
+ log(`DS_SQLITE_CACHE_MB presets: ${formatPresetList(presets, preset, (p) => tuneForPreset(p).sqliteCacheMb, (v) => String(v))}`);
141
+ log(`DS_WORKER_SQLITE_CACHE_MB presets: ${formatPresetList(presets, preset, (p) => tuneForPreset(p).workerSqliteCacheMb, (v) => String(v))}`);
142
+ log(`DS_INDEX_RUN_MEM_CACHE_MB presets: ${formatPresetList(presets, preset, (p) => tuneForPreset(p).indexMemMb, (v) => String(v))}`);
143
+ log(`DS_LEXICON_INDEX_CACHE_MB presets: ${formatPresetList(presets, preset, (p) => tuneForPreset(p).lexiconIndexCacheMb, (v) => String(v))}`);
144
+ log(`DS_SEARCH_COMPANION_TOC_CACHE_MB presets: ${formatPresetList(presets, preset, (p) => tuneForPreset(p).searchCompanionTocCacheMb, (v) => String(v))}`);
145
+ log(`DS_SEARCH_COMPANION_SECTION_CACHE_MB presets: ${formatPresetList(presets, preset, (p) => tuneForPreset(p).searchCompanionSectionCacheMb, (v) => String(v))}`);
146
+ log(`DS_INGEST_MAX_BATCH_MB presets: ${formatPresetList(presets, preset, (p) => tuneForPreset(p).ingestBatchMb, (v) => String(v))}`);
147
+ log(`DS_INGEST_MAX_QUEUE_MB presets: ${formatPresetList(presets, preset, (p) => tuneForPreset(p).ingestQueueMb, (v) => String(v))}`);
148
+ log(`DS_INGEST_CONCURRENCY presets: ${formatPresetList(presets, preset, (p) => tuneForPreset(p).ingestConcurrency, (v) => String(v))}`);
149
+ log(`DS_READ_CONCURRENCY presets: ${formatPresetList(presets, preset, (p) => tuneForPreset(p).readConcurrency, (v) => String(v))}`);
150
+ log(`DS_SEARCH_CONCURRENCY presets: ${formatPresetList(presets, preset, (p) => tuneForPreset(p).searchConcurrency, (v) => String(v))}`);
151
+ log(`DS_ASYNC_INDEX_CONCURRENCY presets: ${formatPresetList(presets, preset, (p) => tuneForPreset(p).asyncIndexConcurrency, (v) => String(v))}`);
152
+ log(`DS_INDEX_BUILD_CONCURRENCY presets: ${formatPresetList(presets, preset, (p) => tuneForPreset(p).indexBuildConcurrency, (v) => String(v))}`);
153
+ log(`DS_INDEX_COMPACT_CONCURRENCY presets: ${formatPresetList(presets, preset, (p) => tuneForPreset(p).indexCompactConcurrency, (v) => String(v))}`);
154
+ log(`DS_SEGMENTER_WORKERS presets: ${formatPresetList(presets, preset, (p) => tuneForPreset(p).segmenterWorkers, (v) => String(v))}`);
155
+ log(`DS_UPLOAD_CONCURRENCY presets: ${formatPresetList(presets, preset, (p) => tuneForPreset(p).uploadConcurrency, (v) => String(v))}`);
156
+ log(`DS_SEARCH_COMPANION_BATCH_SEGMENTS presets: ${formatPresetList(presets, preset, (p) => tuneForPreset(p).searchCompanionBatchSegments, (v) => String(v))}`);
157
+ log(`DS_SEARCH_COMPANION_YIELD_BLOCKS presets: ${formatPresetList(presets, preset, (p) => tuneForPreset(p).searchCompanionYieldBlocks, (v) => String(v))}`);
158
+ }