@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.
- package/CODE_OF_CONDUCT.md +45 -0
- package/CONTRIBUTING.md +76 -0
- package/LICENSE +201 -0
- package/README.md +58 -0
- package/SECURITY.md +42 -0
- package/bin/prisma-streams-server +2 -0
- package/package.json +46 -0
- package/src/app.ts +583 -0
- package/src/app_core.ts +3144 -0
- package/src/app_local.ts +206 -0
- package/src/auth.ts +124 -0
- package/src/auto_tune.ts +69 -0
- package/src/backpressure.ts +66 -0
- package/src/bootstrap.ts +613 -0
- package/src/compute/demo_entry.ts +415 -0
- package/src/compute/demo_site.ts +1242 -0
- package/src/compute/entry.ts +19 -0
- package/src/compute/package_entry.ts +4 -0
- package/src/compute/virtual-modules.d.ts +15 -0
- package/src/compute/worker_module_url.ts +9 -0
- package/src/concurrency_gate.ts +108 -0
- package/src/config.ts +402 -0
- package/src/db/bootstrap_store.ts +9 -0
- package/src/db/db.ts +2424 -0
- package/src/db/schema.ts +925 -0
- package/src/db/sqlite_manifest_snapshot.ts +81 -0
- package/src/db/sqlite_touch_store.ts +491 -0
- package/src/db/sqlite_wal_store.ts +472 -0
- package/src/details/full_mode_details.ts +568 -0
- package/src/expiry_sweeper.ts +47 -0
- package/src/foreground_activity.ts +55 -0
- package/src/hist.ts +169 -0
- package/src/index/binary_fuse.ts +379 -0
- package/src/index/indexer.ts +947 -0
- package/src/index/lexicon_file_cache.ts +261 -0
- package/src/index/lexicon_format.ts +93 -0
- package/src/index/lexicon_indexer.ts +863 -0
- package/src/index/run_cache.ts +84 -0
- package/src/index/run_format.ts +213 -0
- package/src/index/schedule.ts +28 -0
- package/src/index/secondary_indexer.ts +901 -0
- package/src/index/secondary_schema.ts +105 -0
- package/src/ingest.ts +309 -0
- package/src/lens/lens.ts +501 -0
- package/src/manifest.ts +249 -0
- package/src/memory.ts +334 -0
- package/src/metrics.ts +147 -0
- package/src/metrics_emitter.ts +83 -0
- package/src/notifier.ts +180 -0
- package/src/objectstore/accounting.ts +151 -0
- package/src/objectstore/interface.ts +13 -0
- package/src/objectstore/mock_r2.ts +269 -0
- package/src/objectstore/null.ts +32 -0
- package/src/objectstore/r2.ts +318 -0
- package/src/observe/pairing.ts +61 -0
- package/src/observe/request.ts +772 -0
- package/src/offset.ts +70 -0
- package/src/postgres/bootstrap.ts +269 -0
- package/src/postgres/companions.ts +197 -0
- package/src/postgres/control_restore.ts +109 -0
- package/src/postgres/details.ts +189 -0
- package/src/postgres/lexicon_index.ts +260 -0
- package/src/postgres/routing_index.ts +189 -0
- package/src/postgres/rows.ts +132 -0
- package/src/postgres/schema.ts +355 -0
- package/src/postgres/secondary_index.ts +238 -0
- package/src/postgres/segments.ts +900 -0
- package/src/postgres/stats.ts +103 -0
- package/src/postgres/store.ts +947 -0
- package/src/postgres/touch.ts +591 -0
- package/src/postgres/types.ts +32 -0
- package/src/profiles/evlog/schema.ts +234 -0
- package/src/profiles/evlog.ts +473 -0
- package/src/profiles/generic.ts +51 -0
- package/src/profiles/index.ts +237 -0
- package/src/profiles/metrics/block_format.ts +109 -0
- package/src/profiles/metrics/normalize.ts +366 -0
- package/src/profiles/metrics/schema.ts +319 -0
- package/src/profiles/metrics.ts +83 -0
- package/src/profiles/otelTraces/normalize.ts +955 -0
- package/src/profiles/otelTraces/otlp.ts +1002 -0
- package/src/profiles/otelTraces/schema.ts +408 -0
- package/src/profiles/otelTraces.ts +390 -0
- package/src/profiles/profile.ts +284 -0
- package/src/profiles/stateProtocol/change_event_conformance.typecheck.ts +35 -0
- package/src/profiles/stateProtocol/changes.ts +24 -0
- package/src/profiles/stateProtocol/ingest.ts +115 -0
- package/src/profiles/stateProtocol/routes.ts +511 -0
- package/src/profiles/stateProtocol/types.ts +6 -0
- package/src/profiles/stateProtocol/validation.ts +51 -0
- package/src/profiles/stateProtocol.ts +107 -0
- package/src/read_filter.ts +468 -0
- package/src/reader.ts +2986 -0
- package/src/runtime/hash.ts +156 -0
- package/src/runtime/hash_vendor/LICENSE.hash-wasm +38 -0
- package/src/runtime/hash_vendor/NOTICE.md +8 -0
- package/src/runtime/hash_vendor/xxhash3.umd.min.cjs +7 -0
- package/src/runtime/hash_vendor/xxhash32.umd.min.cjs +7 -0
- package/src/runtime/hash_vendor/xxhash64.umd.min.cjs +7 -0
- package/src/runtime/host_runtime.ts +5 -0
- package/src/runtime_memory.ts +200 -0
- package/src/runtime_memory_sampler.ts +237 -0
- package/src/schema/lens_schema.ts +290 -0
- package/src/schema/proof.ts +547 -0
- package/src/schema/read_json.ts +51 -0
- package/src/schema/registry.ts +966 -0
- package/src/search/agg_format.ts +638 -0
- package/src/search/aggregate.ts +409 -0
- package/src/search/binary/codec.ts +162 -0
- package/src/search/binary/docset.ts +67 -0
- package/src/search/binary/restart_strings.ts +181 -0
- package/src/search/binary/varint.ts +34 -0
- package/src/search/bitset.ts +19 -0
- package/src/search/col_format.ts +382 -0
- package/src/search/col_runtime.ts +59 -0
- package/src/search/column_encoding.ts +43 -0
- package/src/search/companion_file_cache.ts +319 -0
- package/src/search/companion_format.ts +327 -0
- package/src/search/companion_manager.ts +1305 -0
- package/src/search/companion_plan.ts +229 -0
- package/src/search/exact_format.ts +281 -0
- package/src/search/exact_runtime.ts +55 -0
- package/src/search/fts_format.ts +423 -0
- package/src/search/fts_runtime.ts +333 -0
- package/src/search/query.ts +875 -0
- package/src/search/schema.ts +245 -0
- package/src/segment/cache.ts +270 -0
- package/src/segment/cached_segment.ts +89 -0
- package/src/segment/format.ts +403 -0
- package/src/segment/segmenter.ts +412 -0
- package/src/segment/segmenter_worker.ts +72 -0
- package/src/segment/segmenter_workers.ts +130 -0
- package/src/server.ts +264 -0
- package/src/server_auto_tune.ts +158 -0
- package/src/sqlite/adapter.ts +335 -0
- package/src/sqlite/runtime_stats.ts +163 -0
- package/src/stats.ts +205 -0
- package/src/store/append.ts +50 -0
- package/src/store/bootstrap_restore_store.ts +71 -0
- package/src/store/capabilities.ts +86 -0
- package/src/store/full_mode_details_store.ts +71 -0
- package/src/store/index_store.ts +104 -0
- package/src/store/profile_touch_store.ts +1 -0
- package/src/store/rows.ts +144 -0
- package/src/store/schema_profile_store.ts +73 -0
- package/src/store/schema_publication.ts +6 -0
- package/src/store/segment_manifest_store.ts +129 -0
- package/src/store/segment_read_store.ts +22 -0
- package/src/store/stats_accounting_store.ts +83 -0
- package/src/store/touch_store.ts +98 -0
- package/src/store/wal_store.ts +21 -0
- package/src/stream_size_reconciler.ts +100 -0
- package/src/touch/canonical_change.ts +7 -0
- package/src/touch/live_keys.ts +158 -0
- package/src/touch/live_metrics.ts +841 -0
- package/src/touch/live_templates.ts +449 -0
- package/src/touch/manager.ts +1292 -0
- package/src/touch/process_batch.ts +576 -0
- package/src/touch/processor_worker.ts +85 -0
- package/src/touch/spec.ts +459 -0
- package/src/touch/touch_journal.ts +771 -0
- package/src/touch/touch_key_id.ts +20 -0
- package/src/touch/worker_pool.ts +191 -0
- package/src/touch/worker_protocol.ts +57 -0
- package/src/types/proper-lockfile.d.ts +1 -0
- package/src/uploader.ts +358 -0
- package/src/util/base32_crockford.ts +81 -0
- package/src/util/bloom256.ts +67 -0
- package/src/util/byte_lru.ts +73 -0
- package/src/util/cleanup.ts +22 -0
- package/src/util/crc32c.ts +29 -0
- package/src/util/ds_error.ts +15 -0
- package/src/util/duration.ts +17 -0
- package/src/util/endian.ts +53 -0
- package/src/util/json_pointer.ts +148 -0
- package/src/util/log.ts +25 -0
- package/src/util/lru.ts +53 -0
- package/src/util/retry.ts +35 -0
- package/src/util/siphash.ts +71 -0
- package/src/util/stream_paths.ts +50 -0
- package/src/util/time.ts +14 -0
- package/src/util/yield.ts +3 -0
- package/src/util/zstd.ts +24 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { bootstrapFromR2 } from "../bootstrap";
|
|
2
|
+
import { createApp, type App } from "../app";
|
|
3
|
+
import { loadConfig } from "../config";
|
|
4
|
+
import type { SqliteDurableStore } from "../db/db";
|
|
5
|
+
import type { AppendRow } from "../ingest";
|
|
6
|
+
import { MockR2Store } from "../objectstore/mock_r2";
|
|
7
|
+
import { R2ObjectStore } from "../objectstore/r2";
|
|
8
|
+
import { resolveJsonIngestCapability } from "../profiles";
|
|
9
|
+
import type { SchemaRegistry } from "../schema/registry";
|
|
10
|
+
import { dsError } from "../util/ds_error.ts";
|
|
11
|
+
import { resolvePointerResult } from "../util/json_pointer";
|
|
12
|
+
import { initConsoleLogging } from "../util/log";
|
|
13
|
+
import { ensureComputeArgv } from "./entry";
|
|
14
|
+
import { createComputeDemoSite, type PrebuiltStudioAssets } from "./demo_site";
|
|
15
|
+
import { applyAutoTune, AutoTuneApplyError, parseAutoTuneArg } from "../server_auto_tune";
|
|
16
|
+
import { parseAuthConfigResult, withAuth } from "../auth";
|
|
17
|
+
import { Result } from "better-result";
|
|
18
|
+
|
|
19
|
+
initConsoleLogging();
|
|
20
|
+
|
|
21
|
+
export type StreamsFetchTarget = {
|
|
22
|
+
appendGenerateBatch?: (stream: string, events: Array<Record<string, unknown>>) => Promise<void>;
|
|
23
|
+
beginGenerateJob?: (stream: string) => void;
|
|
24
|
+
endGenerateJob?: (stream: string) => void;
|
|
25
|
+
ensureGenerateStream?: (stream: string) => Promise<void>;
|
|
26
|
+
fetch(request: Request): Promise<Response>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const EXTERNAL_STREAMS_URL_ENVS = [
|
|
30
|
+
"COMPUTE_DEMO_STREAMS_SERVER_URL",
|
|
31
|
+
"STREAMS_SERVER_URL",
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
function fallbackStudioAssets(): PrebuiltStudioAssets {
|
|
35
|
+
const message =
|
|
36
|
+
"Studio assets were not bundled. Build this entrypoint with bun run build:compute-demo-bundle.";
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
appScript: `const root = document.getElementById("root"); if (root) root.innerHTML = "<pre style=\\"white-space:pre-wrap;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;padding:24px\\">${message}</pre>";`,
|
|
40
|
+
appStyles:
|
|
41
|
+
"html,body{margin:0;background:#08111b;color:#e9f3fb;font-family:ui-sans-serif,system-ui,sans-serif;}",
|
|
42
|
+
builtAssets: new Map(),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function loadStudioAssets(): Promise<PrebuiltStudioAssets> {
|
|
47
|
+
try {
|
|
48
|
+
return (await import("virtual:prebuilt-studio-assets")) as PrebuiltStudioAssets;
|
|
49
|
+
} catch {
|
|
50
|
+
return fallbackStudioAssets();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function loadIdleTimeoutSeconds(): number {
|
|
55
|
+
const raw = process.env.DS_HTTP_IDLE_TIMEOUT_SECONDS;
|
|
56
|
+
if (raw == null || raw.trim() === "") return 180;
|
|
57
|
+
const value = Number(raw);
|
|
58
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
59
|
+
console.error(`invalid DS_HTTP_IDLE_TIMEOUT_SECONDS: ${raw}`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeExternalStreamsServerUrl(value: string): string {
|
|
66
|
+
const trimmed = value.trim();
|
|
67
|
+
if (trimmed === "") {
|
|
68
|
+
throw dsError("external Streams server URL must not be empty");
|
|
69
|
+
}
|
|
70
|
+
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)
|
|
71
|
+
? trimmed
|
|
72
|
+
: `https://${trimmed}`;
|
|
73
|
+
return withScheme.endsWith("/") ? withScheme.slice(0, -1) : withScheme;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function resolveExternalStreamsServerUrl(
|
|
77
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
78
|
+
): string | null {
|
|
79
|
+
for (const name of EXTERNAL_STREAMS_URL_ENVS) {
|
|
80
|
+
const raw = env[name];
|
|
81
|
+
if (raw == null || raw.trim() === "") continue;
|
|
82
|
+
return normalizeExternalStreamsServerUrl(raw);
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function createExternalStreamsTarget(baseUrl: string): StreamsFetchTarget {
|
|
88
|
+
const normalizedBaseUrl = normalizeExternalStreamsServerUrl(baseUrl);
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
async fetch(request: Request): Promise<Response> {
|
|
92
|
+
const requestUrl = new URL(request.url);
|
|
93
|
+
const upstreamUrl = new URL(
|
|
94
|
+
`${requestUrl.pathname}${requestUrl.search}`,
|
|
95
|
+
`${normalizedBaseUrl}/`,
|
|
96
|
+
);
|
|
97
|
+
const headers = new Headers(request.headers);
|
|
98
|
+
headers.delete("host");
|
|
99
|
+
const body =
|
|
100
|
+
request.method === "GET" || request.method === "HEAD"
|
|
101
|
+
? undefined
|
|
102
|
+
: await request.arrayBuffer();
|
|
103
|
+
|
|
104
|
+
const response = await fetch(upstreamUrl, {
|
|
105
|
+
body,
|
|
106
|
+
headers,
|
|
107
|
+
method: request.method,
|
|
108
|
+
redirect: "manual",
|
|
109
|
+
signal: request.signal,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return new Response(response.body, {
|
|
113
|
+
headers: response.headers,
|
|
114
|
+
status: response.status,
|
|
115
|
+
statusText: response.statusText,
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function applyColocatedComputeDemoArgv(
|
|
122
|
+
argv: string[],
|
|
123
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
124
|
+
opts: { log?: (message: string) => void } = {},
|
|
125
|
+
): string[] {
|
|
126
|
+
const next = ensureComputeArgv(argv, env);
|
|
127
|
+
const args = next.slice(2);
|
|
128
|
+
const autoTune = parseAutoTuneArg(args);
|
|
129
|
+
if (autoTune.enabled) {
|
|
130
|
+
applyAutoTune(autoTune.valueMb, { env, log: opts.log });
|
|
131
|
+
}
|
|
132
|
+
return next;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const DIRECT_APPEND_TEXT_ENCODER = new TextEncoder();
|
|
136
|
+
|
|
137
|
+
function keyBytesFromString(value: string | null): Uint8Array | null {
|
|
138
|
+
return value == null ? null : DIRECT_APPEND_TEXT_ENCODER.encode(value);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function extractRoutingKey(reg: SchemaRegistry, value: unknown): Uint8Array | null {
|
|
142
|
+
if (!reg.routingKey) return null;
|
|
143
|
+
const resolvedRes = resolvePointerResult(value, reg.routingKey.jsonPointer);
|
|
144
|
+
if (Result.isError(resolvedRes)) {
|
|
145
|
+
throw dsError(resolvedRes.error.message);
|
|
146
|
+
}
|
|
147
|
+
const resolved = resolvedRes.value;
|
|
148
|
+
if (!resolved.exists) {
|
|
149
|
+
if (reg.routingKey.required) throw dsError("routing key missing");
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
if (typeof resolved.value !== "string") throw dsError("routing key must be string");
|
|
153
|
+
return keyBytesFromString(resolved.value);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function appendErrorMessage(kind: string): string {
|
|
157
|
+
if (kind === "not_found") return "stream not found";
|
|
158
|
+
if (kind === "gone") return "stream expired";
|
|
159
|
+
if (kind === "content_type_mismatch") return "content-type mismatch";
|
|
160
|
+
if (kind === "overloaded") return "ingest queue full";
|
|
161
|
+
if (kind === "closed") return "stream is closed";
|
|
162
|
+
return "append failed";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function createColocatedStreamsTarget(streamsApp: App<SqliteDurableStore>): StreamsFetchTarget {
|
|
166
|
+
let activeGenerateJobs = 0;
|
|
167
|
+
const generateStreamsToEnqueue = new Set<string>();
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
appendGenerateBatch: async (stream, events) => {
|
|
171
|
+
const { db, ingest, metrics, notifier, profiles, registry, stats, touch } = streamsApp.deps;
|
|
172
|
+
if (!db || !touch) throw dsError("colocated streams target requires SQLite touch runtime");
|
|
173
|
+
const streamRow = db.getStream(stream);
|
|
174
|
+
if (!streamRow || db.isDeleted(streamRow)) throw dsError("stream not found");
|
|
175
|
+
|
|
176
|
+
const regRes = await registry.getRegistryResult(stream);
|
|
177
|
+
if (Result.isError(regRes)) throw dsError(regRes.error.message);
|
|
178
|
+
const profileRes = await profiles.getProfileResult(stream, streamRow);
|
|
179
|
+
if (Result.isError(profileRes)) throw dsError(profileRes.error.message);
|
|
180
|
+
const jsonIngest = resolveJsonIngestCapability(profileRes.value);
|
|
181
|
+
const reg = regRes.value;
|
|
182
|
+
const validator = reg.currentVersion > 0 ? registry.getValidatorForVersion(reg, reg.currentVersion) : null;
|
|
183
|
+
if (reg.currentVersion > 0 && !validator) throw dsError("schema validator missing");
|
|
184
|
+
|
|
185
|
+
const rows: AppendRow[] = [];
|
|
186
|
+
let encodedBytes = 0;
|
|
187
|
+
for (const event of events) {
|
|
188
|
+
let value: unknown = event;
|
|
189
|
+
let profileRoutingKey: Uint8Array | null = null;
|
|
190
|
+
if (jsonIngest) {
|
|
191
|
+
const preparedRes = jsonIngest.prepareRecordResult({ stream, profile: profileRes.value, value: event });
|
|
192
|
+
if (Result.isError(preparedRes)) throw dsError(preparedRes.error.message);
|
|
193
|
+
value = preparedRes.value.value;
|
|
194
|
+
profileRoutingKey = keyBytesFromString(preparedRes.value.routingKey);
|
|
195
|
+
}
|
|
196
|
+
if (validator && !validator(value)) {
|
|
197
|
+
const message = validator.errors ? validator.errors.map((error) => error.message).join("; ") : "schema validation failed";
|
|
198
|
+
throw dsError(message);
|
|
199
|
+
}
|
|
200
|
+
const payload = DIRECT_APPEND_TEXT_ENCODER.encode(JSON.stringify(value));
|
|
201
|
+
encodedBytes += payload.byteLength;
|
|
202
|
+
rows.push({
|
|
203
|
+
routingKey: reg.routingKey ? extractRoutingKey(reg, value) : profileRoutingKey,
|
|
204
|
+
contentType: "application/json",
|
|
205
|
+
payload,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const appendRes = await ingest.append({
|
|
210
|
+
stream,
|
|
211
|
+
baseAppendMs: db.nowMs(),
|
|
212
|
+
rows,
|
|
213
|
+
contentType: "application/json",
|
|
214
|
+
});
|
|
215
|
+
if (Result.isError(appendRes)) {
|
|
216
|
+
throw dsError(appendErrorMessage(appendRes.error.kind));
|
|
217
|
+
}
|
|
218
|
+
if (appendRes.value.appendedRows > 0) {
|
|
219
|
+
metrics.recordAppend(encodedBytes, appendRes.value.appendedRows);
|
|
220
|
+
notifier.notify(stream, appendRes.value.lastOffset);
|
|
221
|
+
notifier.notifyDetailsChanged(stream);
|
|
222
|
+
touch.notify(stream);
|
|
223
|
+
stats?.recordStreamTouched(stream);
|
|
224
|
+
stats?.recordIngested(encodedBytes);
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
beginGenerateJob: (stream) => {
|
|
228
|
+
activeGenerateJobs += 1;
|
|
229
|
+
generateStreamsToEnqueue.add(stream);
|
|
230
|
+
if (activeGenerateJobs !== 1) return;
|
|
231
|
+
streamsApp.deps.indexer?.stop();
|
|
232
|
+
streamsApp.deps.segmenter?.stop(true);
|
|
233
|
+
},
|
|
234
|
+
endGenerateJob: (stream) => {
|
|
235
|
+
generateStreamsToEnqueue.add(stream);
|
|
236
|
+
activeGenerateJobs = Math.max(0, activeGenerateJobs - 1);
|
|
237
|
+
if (activeGenerateJobs !== 0) return;
|
|
238
|
+
streamsApp.deps.segmenter?.start();
|
|
239
|
+
streamsApp.deps.indexer?.start();
|
|
240
|
+
for (const pendingStream of generateStreamsToEnqueue) {
|
|
241
|
+
streamsApp.deps.indexer?.enqueue(pendingStream);
|
|
242
|
+
}
|
|
243
|
+
generateStreamsToEnqueue.clear();
|
|
244
|
+
},
|
|
245
|
+
fetch: (request) => streamsApp.fetch(request),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function main(): Promise<void> {
|
|
250
|
+
const authConfigResult = parseAuthConfigResult(process.argv.slice(2));
|
|
251
|
+
if (Result.isError(authConfigResult)) {
|
|
252
|
+
console.error(authConfigResult.error.message);
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
const authConfig = authConfigResult.value;
|
|
256
|
+
const studioAssets = await loadStudioAssets();
|
|
257
|
+
const externalStreamsServerUrl = resolveExternalStreamsServerUrl();
|
|
258
|
+
let streamsTarget: StreamsFetchTarget;
|
|
259
|
+
let closeStreamsTarget: (() => Promise<void>) | null = null;
|
|
260
|
+
let cfg: ReturnType<typeof loadConfig>;
|
|
261
|
+
|
|
262
|
+
if (externalStreamsServerUrl) {
|
|
263
|
+
cfg = loadConfig();
|
|
264
|
+
streamsTarget = createExternalStreamsTarget(externalStreamsServerUrl);
|
|
265
|
+
console.log(
|
|
266
|
+
`prisma-streams compute demo using external Streams server ${externalStreamsServerUrl}`,
|
|
267
|
+
);
|
|
268
|
+
} else {
|
|
269
|
+
try {
|
|
270
|
+
process.argv = applyColocatedComputeDemoArgv(process.argv);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
if (error instanceof AutoTuneApplyError) {
|
|
273
|
+
console.error(error.message);
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
throw error;
|
|
277
|
+
}
|
|
278
|
+
cfg = loadConfig();
|
|
279
|
+
const args = process.argv.slice(2);
|
|
280
|
+
|
|
281
|
+
const storeIdx = args.indexOf("--object-store");
|
|
282
|
+
const storeChoice = storeIdx >= 0 ? args[storeIdx + 1] : null;
|
|
283
|
+
if (!storeChoice || (storeChoice !== "r2" && storeChoice !== "local")) {
|
|
284
|
+
console.error("missing or invalid --object-store (expected: r2 | local)");
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
const bootstrapEnabled = args.includes("--bootstrap-from-r2");
|
|
288
|
+
|
|
289
|
+
let store;
|
|
290
|
+
if (storeChoice === "local") {
|
|
291
|
+
const memBytesRaw = process.env.DS_MOCK_R2_MAX_INMEM_BYTES;
|
|
292
|
+
const memMbRaw = process.env.DS_MOCK_R2_MAX_INMEM_MB;
|
|
293
|
+
const putDelayRaw = process.env.DS_MOCK_R2_PUT_DELAY_MS;
|
|
294
|
+
const getDelayRaw = process.env.DS_MOCK_R2_GET_DELAY_MS;
|
|
295
|
+
const headDelayRaw = process.env.DS_MOCK_R2_HEAD_DELAY_MS;
|
|
296
|
+
const listDelayRaw = process.env.DS_MOCK_R2_LIST_DELAY_MS;
|
|
297
|
+
const memBytes = memBytesRaw
|
|
298
|
+
? Number(memBytesRaw)
|
|
299
|
+
: memMbRaw
|
|
300
|
+
? Number(memMbRaw) * 1024 * 1024
|
|
301
|
+
: null;
|
|
302
|
+
const putDelayMs = putDelayRaw ? Number(putDelayRaw) : 0;
|
|
303
|
+
const getDelayMs = getDelayRaw ? Number(getDelayRaw) : 0;
|
|
304
|
+
const headDelayMs = headDelayRaw ? Number(headDelayRaw) : 0;
|
|
305
|
+
const listDelayMs = listDelayRaw ? Number(listDelayRaw) : 0;
|
|
306
|
+
if (memBytesRaw && !Number.isFinite(memBytes)) {
|
|
307
|
+
console.error(`invalid DS_MOCK_R2_MAX_INMEM_BYTES: ${memBytesRaw}`);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
if (memMbRaw && !Number.isFinite(Number(memMbRaw))) {
|
|
311
|
+
console.error(`invalid DS_MOCK_R2_MAX_INMEM_MB: ${memMbRaw}`);
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
for (const [name, value] of [
|
|
315
|
+
["DS_MOCK_R2_PUT_DELAY_MS", putDelayMs],
|
|
316
|
+
["DS_MOCK_R2_GET_DELAY_MS", getDelayMs],
|
|
317
|
+
["DS_MOCK_R2_HEAD_DELAY_MS", headDelayMs],
|
|
318
|
+
["DS_MOCK_R2_LIST_DELAY_MS", listDelayMs],
|
|
319
|
+
] as const) {
|
|
320
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
321
|
+
console.error(`invalid ${name}: ${process.env[name]}`);
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const spillDir = process.env.DS_MOCK_R2_SPILL_DIR;
|
|
326
|
+
store = new MockR2Store({
|
|
327
|
+
maxInMemoryBytes: memBytes ?? undefined,
|
|
328
|
+
spillDir,
|
|
329
|
+
faults: {
|
|
330
|
+
putDelayMs,
|
|
331
|
+
getDelayMs,
|
|
332
|
+
headDelayMs,
|
|
333
|
+
listDelayMs,
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
} else {
|
|
337
|
+
const bucket = process.env.DURABLE_STREAMS_R2_BUCKET;
|
|
338
|
+
const accountId = process.env.DURABLE_STREAMS_R2_ACCOUNT_ID;
|
|
339
|
+
const accessKeyId = process.env.DURABLE_STREAMS_R2_ACCESS_KEY_ID;
|
|
340
|
+
const secretAccessKey = process.env.DURABLE_STREAMS_R2_SECRET_ACCESS_KEY;
|
|
341
|
+
const endpoint = process.env.DURABLE_STREAMS_R2_ENDPOINT;
|
|
342
|
+
const region = process.env.DURABLE_STREAMS_R2_REGION;
|
|
343
|
+
if (!bucket || !accountId || !accessKeyId || !secretAccessKey) {
|
|
344
|
+
console.error(
|
|
345
|
+
"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",
|
|
346
|
+
);
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
store = new R2ObjectStore({
|
|
350
|
+
accessKeyId,
|
|
351
|
+
accountId,
|
|
352
|
+
bucket,
|
|
353
|
+
secretAccessKey,
|
|
354
|
+
endpoint,
|
|
355
|
+
region,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (bootstrapEnabled) {
|
|
360
|
+
await bootstrapFromR2(cfg, store, { clearLocal: true });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const streamsApp = createApp(cfg, store);
|
|
364
|
+
streamsTarget = createColocatedStreamsTarget(streamsApp);
|
|
365
|
+
closeStreamsTarget = () => streamsApp.close();
|
|
366
|
+
}
|
|
367
|
+
const demoSite = createComputeDemoSite({
|
|
368
|
+
studioAssets,
|
|
369
|
+
streamsApp: streamsTarget,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const server = Bun.serve({
|
|
373
|
+
fetch: withAuth(authConfig, (request) => demoSite.fetch(request)),
|
|
374
|
+
hostname: cfg.host,
|
|
375
|
+
idleTimeout: loadIdleTimeoutSeconds(),
|
|
376
|
+
port: cfg.port,
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
let shuttingDown = false;
|
|
380
|
+
const shutdown = async (signal: NodeJS.Signals): Promise<void> => {
|
|
381
|
+
if (shuttingDown) return;
|
|
382
|
+
shuttingDown = true;
|
|
383
|
+
console.log(`received ${signal}, shutting down prisma-streams compute demo`);
|
|
384
|
+
try {
|
|
385
|
+
server.stop(true);
|
|
386
|
+
} catch (error) {
|
|
387
|
+
console.error("failed to stop HTTP server cleanly", error);
|
|
388
|
+
}
|
|
389
|
+
try {
|
|
390
|
+
demoSite.close();
|
|
391
|
+
} catch (error) {
|
|
392
|
+
console.error("failed to close compute demo cleanly", error);
|
|
393
|
+
}
|
|
394
|
+
if (closeStreamsTarget) {
|
|
395
|
+
try {
|
|
396
|
+
await closeStreamsTarget();
|
|
397
|
+
} catch (error) {
|
|
398
|
+
console.error("failed to close streams application cleanly", error);
|
|
399
|
+
process.exitCode = 1;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
process.once("SIGINT", () => void shutdown("SIGINT"));
|
|
405
|
+
process.once("SIGTERM", () => void shutdown("SIGTERM"));
|
|
406
|
+
|
|
407
|
+
const listenTarget = cfg.host.includes(":")
|
|
408
|
+
? `[${cfg.host}]:${server.port}`
|
|
409
|
+
: `${cfg.host}:${server.port}`;
|
|
410
|
+
console.log(`prisma-streams compute demo listening on ${listenTarget}`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (import.meta.main) {
|
|
414
|
+
await main();
|
|
415
|
+
}
|