@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,206 @@
1
+ import type { Config } from "./config";
2
+ import { createAppCore, type App } from "./app_core";
3
+ import type { ObjectStore } from "./objectstore/interface";
4
+ import { NullObjectStore } from "./objectstore/null";
5
+ import { StreamReader } from "./reader";
6
+ import type { StreamIndexLookup } from "./index/indexer";
7
+ import type { RoutingKeyLexiconListResult } from "./index/lexicon_indexer";
8
+ import type { StatsCollector } from "./stats";
9
+ import { readSqliteRuntimeMemoryStats } from "./sqlite/runtime_stats";
10
+ import { Result } from "better-result";
11
+ import { SqliteDurableStore } from "./db/db";
12
+ import type { WalControlPlaneStore } from "./store/capabilities";
13
+
14
+ const TEXT_DECODER = new TextDecoder();
15
+
16
+ class LocalIndexLookup implements StreamIndexLookup {
17
+ constructor(private readonly db: SqliteDurableStore) {}
18
+
19
+ start(): void {}
20
+
21
+ async stop(): Promise<void> {}
22
+
23
+ enqueue(_stream: string): void {}
24
+
25
+ async candidateSegmentsForRoutingKey(_stream: string, _keyBytes: Uint8Array): Promise<null> {
26
+ return null;
27
+ }
28
+
29
+ async candidateSegmentsForSecondaryIndex(_stream: string, _indexName: string, _keyBytes: Uint8Array): Promise<null> {
30
+ return null;
31
+ }
32
+
33
+ async getAggSegmentCompanion(_stream: string, _segmentIndex: number): Promise<null> {
34
+ return null;
35
+ }
36
+
37
+ async getColSegmentCompanion(_stream: string, _segmentIndex: number): Promise<null> {
38
+ return null;
39
+ }
40
+
41
+ async getExactSegmentCompanion(_stream: string, _segmentIndex: number): Promise<null> {
42
+ return null;
43
+ }
44
+
45
+ async getFtsSegmentCompanion(_stream: string, _segmentIndex: number): Promise<null> {
46
+ return null;
47
+ }
48
+
49
+ async getMetricsBlockSegmentCompanion(_stream: string, _segmentIndex: number): Promise<null> {
50
+ return null;
51
+ }
52
+
53
+ async listRoutingKeysResult(
54
+ stream: string,
55
+ after: string | null,
56
+ limit: number
57
+ ): Promise<Result<RoutingKeyLexiconListResult, { kind: string; message: string }>> {
58
+ const srow = this.db.getStream(stream);
59
+ if (!srow || this.db.isDeleted(srow)) {
60
+ return Result.err({ kind: "invalid_lexicon_index", message: "stream not found" });
61
+ }
62
+ const safeLimit = Math.max(1, Math.min(limit, 500));
63
+ const keys = new Set<string>();
64
+ let scannedWalRows = 0;
65
+ for (const rec of this.db.iterWalRange(stream, 0n, srow.next_offset - 1n)) {
66
+ scannedWalRows += 1;
67
+ const rawKey = rec.routing_key == null ? null : rec.routing_key instanceof Uint8Array ? rec.routing_key : new Uint8Array(rec.routing_key);
68
+ if (!rawKey || rawKey.byteLength === 0) continue;
69
+ keys.add(TEXT_DECODER.decode(rawKey));
70
+ }
71
+ const sorted = Array.from(keys).sort();
72
+ const filtered = after == null ? sorted : sorted.filter((key) => key > after);
73
+ const page = filtered.slice(0, safeLimit);
74
+ const nextAfter = filtered.length > safeLimit ? page[page.length - 1] ?? null : null;
75
+ return Result.ok({
76
+ keys: page,
77
+ nextAfter,
78
+ tookMs: 0,
79
+ coverage: {
80
+ complete: true,
81
+ indexedSegments: 0,
82
+ scannedUploadedSegments: 0,
83
+ scannedLocalSegments: 0,
84
+ scannedWalRows,
85
+ possibleMissingUploadedSegments: 0,
86
+ possibleMissingLocalSegments: 0,
87
+ },
88
+ timing: {
89
+ lexiconRunGetMs: 0,
90
+ lexiconDecodeMs: 0,
91
+ lexiconEnumerateMs: 0,
92
+ lexiconMergeMs: 0,
93
+ fallbackScanMs: 0,
94
+ fallbackSegmentGetMs: 0,
95
+ fallbackWalScanMs: 0,
96
+ lexiconRunsLoaded: 0,
97
+ },
98
+ });
99
+ }
100
+
101
+ getLocalStorageUsage(_stream: string) {
102
+ return {
103
+ routing_index_cache_bytes: 0,
104
+ exact_index_cache_bytes: 0,
105
+ companion_cache_bytes: 0,
106
+ lexicon_index_cache_bytes: 0,
107
+ };
108
+ }
109
+ }
110
+
111
+ export type CreateLocalAppOptions = {
112
+ stats?: StatsCollector;
113
+ };
114
+
115
+ function localControlStore(db: SqliteDurableStore): WalControlPlaneStore {
116
+ return new Proxy(db, {
117
+ get(target, prop, receiver) {
118
+ if (prop === "capabilities") {
119
+ return {
120
+ ...target.capabilities,
121
+ manifests: false,
122
+ schemaPublication: false,
123
+ internalMetrics: false,
124
+ };
125
+ }
126
+ const value = Reflect.get(target, prop, receiver);
127
+ return typeof value === "function" ? value.bind(target) : value;
128
+ },
129
+ }) as WalControlPlaneStore;
130
+ }
131
+
132
+ export function createLocalApp(cfg: Config, os?: ObjectStore, opts: CreateLocalAppOptions = {}): App<SqliteDurableStore> {
133
+ const db = new SqliteDurableStore(cfg.dbPath, { cacheBytes: cfg.sqliteCacheBytes });
134
+ const controlStore = localControlStore(db);
135
+ return createAppCore(cfg, {
136
+ debugStore: db,
137
+ touchStore: db.touch,
138
+ storageStatsStore: db,
139
+ objectStoreAccountingStore: db,
140
+ detailsStore: db,
141
+ lifecycleHooks: {
142
+ getInitialBackpressureBytes: () => db.sumPendingBytes() + db.sumPendingSegmentBytes(),
143
+ },
144
+ store: controlStore,
145
+ stats: opts.stats,
146
+ createRuntime: ({ config, registry, memorySampler, memory }) => {
147
+ const store = os ?? new NullObjectStore();
148
+ const indexer = new LocalIndexLookup(db);
149
+ const reader = new StreamReader(
150
+ config,
151
+ controlStore,
152
+ registry,
153
+ { segmentReads: db, objectStore: store, index: indexer },
154
+ memorySampler,
155
+ memory
156
+ );
157
+
158
+ return {
159
+ reader,
160
+ indexer,
161
+ getRuntimeMemorySnapshot: () => {
162
+ const sqliteRuntime = readSqliteRuntimeMemoryStats();
163
+ return {
164
+ subsystems: {
165
+ heap_estimates: {
166
+ ingest_queue_payload_bytes: 0,
167
+ },
168
+ mapped_files: {},
169
+ disk_caches: {},
170
+ configured_budgets: {
171
+ sqlite_cache_budget_bytes: config.sqliteCacheBytes,
172
+ worker_sqlite_cache_budget_bytes: config.workerSqliteCacheBytes,
173
+ },
174
+ pipeline_buffers: {},
175
+ sqlite_runtime: {
176
+ sqlite_memory_used_bytes: sqliteRuntime.memory_used_bytes,
177
+ sqlite_memory_highwater_bytes: sqliteRuntime.memory_highwater_bytes,
178
+ sqlite_pagecache_overflow_bytes: sqliteRuntime.pagecache_overflow_bytes,
179
+ sqlite_pagecache_overflow_highwater_bytes: sqliteRuntime.pagecache_overflow_highwater_bytes,
180
+ },
181
+ counts: {
182
+ ingest_queue_requests: 0,
183
+ pending_upload_segments: 0,
184
+ sqlite_pagecache_used_slots: sqliteRuntime.pagecache_used_slots,
185
+ sqlite_pagecache_used_slots_highwater: sqliteRuntime.pagecache_used_slots_highwater,
186
+ sqlite_malloc_count: sqliteRuntime.malloc_count,
187
+ sqlite_malloc_count_highwater: sqliteRuntime.malloc_count_highwater,
188
+ sqlite_open_connections: sqliteRuntime.open_connections,
189
+ sqlite_prepared_statements: sqliteRuntime.prepared_statements,
190
+ },
191
+ },
192
+ totals: {
193
+ heap_estimate_bytes: 0,
194
+ mapped_file_bytes: 0,
195
+ disk_cache_bytes: 0,
196
+ configured_budget_bytes: config.sqliteCacheBytes + config.workerSqliteCacheBytes,
197
+ pipeline_buffer_bytes: 0,
198
+ sqlite_runtime_bytes: sqliteRuntime.memory_used_bytes + sqliteRuntime.pagecache_overflow_bytes,
199
+ },
200
+ };
201
+ },
202
+ start: (): void => {},
203
+ };
204
+ },
205
+ });
206
+ }
package/src/auth.ts ADDED
@@ -0,0 +1,124 @@
1
+ import { timingSafeEqual } from "node:crypto";
2
+ import { Result } from "better-result";
3
+
4
+ export type AuthConfig =
5
+ | {
6
+ mode: "none";
7
+ }
8
+ | {
9
+ mode: "api-key";
10
+ apiKeyBytes: Buffer;
11
+ };
12
+
13
+ export type AuthConfigError = {
14
+ message: string;
15
+ };
16
+
17
+ const API_KEY_MIN_LENGTH = 10;
18
+
19
+ function hasFlagArg(args: string[], flag: string): boolean {
20
+ return args.includes(flag);
21
+ }
22
+
23
+ function valuesForOption(args: string[], option: string): string[] {
24
+ const values: string[] = [];
25
+ const prefix = `${option}=`;
26
+ for (let i = 0; i < args.length; i += 1) {
27
+ const arg = args[i];
28
+ if (arg === option) {
29
+ const value = args[i + 1];
30
+ if (value != null && !value.startsWith("--")) {
31
+ values.push(value);
32
+ i += 1;
33
+ } else {
34
+ values.push("");
35
+ }
36
+ continue;
37
+ }
38
+ if (arg.startsWith(prefix)) {
39
+ values.push(arg.slice(prefix.length));
40
+ }
41
+ }
42
+ return values;
43
+ }
44
+
45
+ function authConfigError(message: string): Result<AuthConfig, AuthConfigError> {
46
+ return Result.err({ message });
47
+ }
48
+
49
+ export function parseAuthConfigResult(args: string[], env: NodeJS.ProcessEnv = process.env): Result<AuthConfig, AuthConfigError> {
50
+ const noAuth = hasFlagArg(args, "--no-auth");
51
+ const authStrategyValues = valuesForOption(args, "--auth-strategy");
52
+ const hasAuthStrategy = authStrategyValues.length > 0;
53
+
54
+ if (noAuth && hasAuthStrategy) {
55
+ return authConfigError("invalid auth configuration: provide exactly one of --no-auth or --auth-strategy api-key");
56
+ }
57
+ if (!noAuth && !hasAuthStrategy) {
58
+ return authConfigError("missing auth configuration: expected --no-auth or --auth-strategy api-key");
59
+ }
60
+ if (noAuth) {
61
+ return Result.ok({ mode: "none" });
62
+ }
63
+ if (authStrategyValues.length !== 1 || authStrategyValues[0] !== "api-key") {
64
+ return authConfigError("invalid --auth-strategy (expected: api-key)");
65
+ }
66
+
67
+ const apiKey = env.API_KEY;
68
+ if (apiKey == null || apiKey.length < API_KEY_MIN_LENGTH) {
69
+ return authConfigError(`API_KEY must be present and contain at least ${API_KEY_MIN_LENGTH} characters`);
70
+ }
71
+
72
+ return Result.ok({
73
+ mode: "api-key",
74
+ apiKeyBytes: Buffer.from(apiKey, "utf8"),
75
+ });
76
+ }
77
+
78
+ function unauthorized(): Response {
79
+ return new Response(
80
+ JSON.stringify({
81
+ error: {
82
+ code: "unauthorized",
83
+ message: "unauthorized",
84
+ },
85
+ }),
86
+ {
87
+ status: 401,
88
+ headers: {
89
+ "content-type": "application/json; charset=utf-8",
90
+ "cache-control": "no-store",
91
+ "x-content-type-options": "nosniff",
92
+ "www-authenticate": "Bearer",
93
+ },
94
+ }
95
+ );
96
+ }
97
+
98
+ function parseBearerCredential(header: string | null): string | null {
99
+ if (header == null) return null;
100
+ const match = /^Bearer (.+)$/i.exec(header);
101
+ return match?.[1] ?? null;
102
+ }
103
+
104
+ function credentialsMatch(config: Extract<AuthConfig, { mode: "api-key" }>, credential: string): boolean {
105
+ const credentialBytes = Buffer.from(credential, "utf8");
106
+ const paddedCredential = Buffer.alloc(config.apiKeyBytes.length);
107
+ credentialBytes.copy(paddedCredential, 0, 0, Math.min(credentialBytes.length, paddedCredential.length));
108
+ const bytesMatch = timingSafeEqual(config.apiKeyBytes, paddedCredential);
109
+ return credentialBytes.length === config.apiKeyBytes.length && bytesMatch;
110
+ }
111
+
112
+ export function authenticateRequest(config: AuthConfig, request: Request): Response | null {
113
+ if (config.mode === "none") return null;
114
+ const credential = parseBearerCredential(request.headers.get("authorization"));
115
+ if (credential == null || !credentialsMatch(config, credential)) {
116
+ return unauthorized();
117
+ }
118
+ return null;
119
+ }
120
+
121
+ export function withAuth(config: AuthConfig, fetch: (request: Request) => Promise<Response>): (request: Request) => Promise<Response> {
122
+ if (config.mode === "none") return fetch;
123
+ return async (request: Request): Promise<Response> => authenticateRequest(config, request) ?? fetch(request);
124
+ }
@@ -0,0 +1,69 @@
1
+ export type AutoTuneConfig = {
2
+ segmentMaxMiB: number;
3
+ segmentTargetRows: number;
4
+ segmentCacheMb: number;
5
+ indexCheckMs: number;
6
+ sqliteCacheMb: number;
7
+ workerSqliteCacheMb: number;
8
+ indexMemMb: number;
9
+ lexiconIndexCacheMb: number;
10
+ searchCompanionTocCacheMb: number;
11
+ searchCompanionSectionCacheMb: number;
12
+ ingestBatchMb: number;
13
+ ingestQueueMb: number;
14
+ ingestConcurrency: number;
15
+ readConcurrency: number;
16
+ searchConcurrency: number;
17
+ asyncIndexConcurrency: number;
18
+ indexBuildConcurrency: number;
19
+ indexCompactConcurrency: number;
20
+ segmenterWorkers: number;
21
+ uploadConcurrency: number;
22
+ searchCompanionBatchSegments: number;
23
+ searchCompanionYieldBlocks: number;
24
+ };
25
+
26
+ export const AUTO_TUNE_PRESETS = [256, 512, 1024, 2048, 4096, 8192] as const;
27
+
28
+ export function memoryLimitForPreset(preset: number): number {
29
+ return preset === 256 ? 300 : preset;
30
+ }
31
+
32
+ export function tuneForPreset(p: number): AutoTuneConfig {
33
+ return {
34
+ // <=1 GiB hosts need smaller cut units because segment build/compression
35
+ // can transiently hold several encoded copies of the candidate rows.
36
+ segmentMaxMiB: p <= 1024 ? 8 : 16,
37
+ segmentTargetRows: p <= 1024 ? 50_000 : 100_000,
38
+ // The 1 GiB Compute host only has about 685 MiB of usable RSS after the
39
+ // platform clamp, so it cannot afford a persistent 256 MiB local segment
40
+ // cache on top of active ingest and background reads.
41
+ segmentCacheMb: p >= 2048 ? 256 : 0,
42
+ // Small hosts defer background sweeps so routing/exact backfill does not
43
+ // immediately start re-reading uploaded history during a large ingest burst.
44
+ indexCheckMs: p >= 2048 ? 1_000 : 3_600_000,
45
+ sqliteCacheMb: Math.max(8, Math.floor(p / 16)),
46
+ workerSqliteCacheMb: Math.max(8, Math.min(32, Math.floor(p / 128))),
47
+ indexMemMb: Math.max(4, Math.floor(p / 64)),
48
+ lexiconIndexCacheMb: p >= 8192 ? 256 : p >= 4096 ? 128 : p >= 2048 ? 64 : p >= 1024 ? 32 : p >= 512 ? 16 : 8,
49
+ searchCompanionTocCacheMb: p >= 8192 ? 4 : p >= 4096 ? 2 : 1,
50
+ searchCompanionSectionCacheMb: p >= 8192 ? 128 : p >= 4096 ? 64 : p >= 2048 ? 32 : p >= 1024 ? 16 : 8,
51
+ // Keep append working sets tighter on <=2 GiB presets because the request path
52
+ // still holds multiple copies of JSON batches while normalizing and queuing.
53
+ ingestBatchMb: p >= 8192 ? 64 : p >= 4096 ? 16 : p >= 2048 ? 8 : p >= 1024 ? 4 : 2,
54
+ ingestQueueMb: p >= 8192 ? 128 : p >= 4096 ? 64 : p >= 2048 ? 32 : p >= 1024 ? 16 : 8,
55
+ ingestConcurrency: p >= 8192 ? 8 : p >= 4096 ? 4 : p >= 1024 ? 2 : 1,
56
+ readConcurrency: p >= 8192 ? 16 : p >= 4096 ? 8 : p >= 1024 ? 4 : 2,
57
+ searchConcurrency: p >= 8192 ? 8 : p >= 4096 ? 4 : p >= 1024 ? 2 : 1,
58
+ asyncIndexConcurrency: p >= 8192 ? 4 : p >= 4096 ? 2 : 1,
59
+ // Keep <=2 GiB presets single-lane for background work. These hosts do not
60
+ // have enough headroom for append, segment cut, upload, and companion work
61
+ // to overlap aggressively under the GH Archive "all" workload.
62
+ indexBuildConcurrency: p >= 8192 ? 4 : p >= 4096 ? 2 : 1,
63
+ indexCompactConcurrency: p >= 8192 ? 4 : p >= 4096 ? 2 : 1,
64
+ segmenterWorkers: p >= 8192 ? 4 : p >= 4096 ? 2 : p >= 2048 ? 1 : 0,
65
+ uploadConcurrency: p >= 8192 ? 8 : p >= 4096 ? 4 : p >= 2048 ? 2 : 1,
66
+ searchCompanionBatchSegments: p >= 8192 ? 4 : p >= 4096 ? 2 : 1,
67
+ searchCompanionYieldBlocks: p >= 8192 ? 4 : p >= 4096 ? 2 : 1,
68
+ };
69
+ }
@@ -0,0 +1,66 @@
1
+ export class BackpressureGate {
2
+ private readonly maxBytes: number;
3
+ private currentBytes: number;
4
+ private reservedBytes: number;
5
+
6
+ constructor(maxBytes: number, initialBytes: number) {
7
+ this.maxBytes = maxBytes;
8
+ this.currentBytes = Math.max(0, initialBytes);
9
+ this.reservedBytes = 0;
10
+ }
11
+
12
+ enabled(): boolean {
13
+ return this.maxBytes > 0;
14
+ }
15
+
16
+ reserve(bytes: number): boolean {
17
+ if (this.maxBytes <= 0) return true;
18
+ if (bytes <= 0) return true;
19
+ if (this.currentBytes + this.reservedBytes + bytes > this.maxBytes) return false;
20
+ this.reservedBytes += bytes;
21
+ return true;
22
+ }
23
+
24
+ commit(bytes: number, reservedBytes: number = bytes): void {
25
+ if (this.maxBytes <= 0) return;
26
+ if (bytes <= 0) return;
27
+ if (reservedBytes > 0) this.reservedBytes = Math.max(0, this.reservedBytes - reservedBytes);
28
+ this.currentBytes += bytes;
29
+ }
30
+
31
+ release(bytes: number): void {
32
+ if (this.maxBytes <= 0) return;
33
+ if (bytes <= 0) return;
34
+ this.reservedBytes = Math.max(0, this.reservedBytes - bytes);
35
+ }
36
+
37
+ adjustOnSeal(payloadBytes: number, segmentBytes: number): void {
38
+ if (this.maxBytes <= 0) return;
39
+ const delta = segmentBytes - payloadBytes;
40
+ this.currentBytes = Math.max(0, this.currentBytes + delta);
41
+ }
42
+
43
+ adjustOnUpload(segmentBytes: number): void {
44
+ if (this.maxBytes <= 0) return;
45
+ this.currentBytes = Math.max(0, this.currentBytes - segmentBytes);
46
+ }
47
+
48
+ adjustOnWalTrim(payloadBytes: number): void {
49
+ if (this.maxBytes <= 0) return;
50
+ if (payloadBytes <= 0) return;
51
+ this.currentBytes = Math.max(0, this.currentBytes - payloadBytes);
52
+ }
53
+
54
+ getCurrentBytes(): number {
55
+ return this.currentBytes;
56
+ }
57
+
58
+ getMaxBytes(): number {
59
+ return this.maxBytes;
60
+ }
61
+
62
+ isOverLimit(): boolean {
63
+ if (this.maxBytes <= 0) return false;
64
+ return this.currentBytes + this.reservedBytes >= this.maxBytes;
65
+ }
66
+ }