1tube 0.1.43 → 0.1.45
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/package.json
CHANGED
package/scripts/npm-cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
|
-
import { existsSync } from "node:fs";
|
|
4
|
+
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
5
5
|
import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
|
6
6
|
import process from "node:process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -55,8 +55,27 @@ const userSuppliedConfig = denoPassthrough.some((a) =>
|
|
|
55
55
|
a.startsWith("--config=") || a.startsWith("-c=")
|
|
56
56
|
);
|
|
57
57
|
|
|
58
|
+
// Managed lockfile
|
|
59
|
+
// ----------------
|
|
60
|
+
// A lockfile is what makes a big functions project boot fast: without one
|
|
61
|
+
// (`--no-lock`) Deno re-solves the entire npm/jsr version graph from
|
|
62
|
+
// scratch on every start, and a 100-function project pays that as a
|
|
63
|
+
// multi-second floor every boot. People reach for `--no-lock` to dodge
|
|
64
|
+
// "the lock went stale when I bumped a dep" pain — but in dev Deno's
|
|
65
|
+
// lockfile is ADDITIVE and non-frozen: a changed dependency is appended
|
|
66
|
+
// without error. So the right default is a lockfile we own, not none.
|
|
67
|
+
//
|
|
68
|
+
// We point `--lock` at `.1tube/deno.lock` — a gateway-owned scratch path
|
|
69
|
+
// (already gitignored alongside `.1tube/logs.db`) so it never adds git
|
|
70
|
+
// churn and never collides with a project's own committed `deno.lock`.
|
|
71
|
+
// We never pass `--frozen`, so dep changes self-heal. `1TUBE_REFRESH_LOCK=1`
|
|
72
|
+
// drops the managed lock for a clean rebuild. Any explicit `--lock`,
|
|
73
|
+
// `--no-lock`, or `--frozen` the user passes wins — we don't second-guess.
|
|
74
|
+
const managedLock = resolveManagedLock(denoPassthrough);
|
|
75
|
+
|
|
58
76
|
const denoArgs = ["run", "--quiet", "-A"];
|
|
59
77
|
if (!userSuppliedConfig) denoArgs.push("--config", denoConfig);
|
|
78
|
+
if (managedLock) denoArgs.push("--lock", managedLock);
|
|
60
79
|
denoArgs.push(...denoPassthrough, cliEntrypoint, ...forwardedTubeArgs);
|
|
61
80
|
|
|
62
81
|
// Deno 2.9 enables a 24h "minimum dependency age" by default, which makes
|
|
@@ -78,6 +97,9 @@ if (
|
|
|
78
97
|
) {
|
|
79
98
|
childEnv.NPM_CONFIG_MIN_RELEASE_AGE = "0";
|
|
80
99
|
}
|
|
100
|
+
// Let the gateway print which lockfile is in effect (it can't read its own
|
|
101
|
+
// process's `--lock` flag back out of Deno).
|
|
102
|
+
if (managedLock) childEnv.ONETUBE_LOCK = managedLock;
|
|
81
103
|
|
|
82
104
|
// Resolve the real deno binary up front. Node ≥18.20 refuses to spawn a
|
|
83
105
|
// `.cmd`/`.bat` shim without a shell, but spawning a real `.exe` (or a
|
|
@@ -142,6 +164,60 @@ child.on("exit", (code, signal) => {
|
|
|
142
164
|
process.exit(code ?? 1);
|
|
143
165
|
});
|
|
144
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Decide whether to inject a 1tube-managed lockfile, and where.
|
|
169
|
+
*
|
|
170
|
+
* Returns the absolute lock path to pass to `--lock`, or null when the
|
|
171
|
+
* caller already made an explicit locking choice (`--lock`, `--no-lock`,
|
|
172
|
+
* or `--frozen`) — in which case we stay out of the way entirely.
|
|
173
|
+
*
|
|
174
|
+
* The managed lock lives at `<cwd>/.1tube/deno.lock`: a scratch path that
|
|
175
|
+
* 1tube already owns and gitignores, so enabling it adds no git churn and
|
|
176
|
+
* never clashes with a project's own committed `deno.lock`.
|
|
177
|
+
*
|
|
178
|
+
* @param {string[]} passthrough Raw deno flags being forwarded to `deno run`.
|
|
179
|
+
* @returns {string | null}
|
|
180
|
+
*/
|
|
181
|
+
function resolveManagedLock(passthrough) {
|
|
182
|
+
const explicit = passthrough.some((a) =>
|
|
183
|
+
a === "--lock" || a.startsWith("--lock=") ||
|
|
184
|
+
a === "--no-lock" || a.startsWith("--no-lock=") ||
|
|
185
|
+
a === "--frozen" || a.startsWith("--frozen=")
|
|
186
|
+
);
|
|
187
|
+
if (explicit) return null;
|
|
188
|
+
|
|
189
|
+
const dir = join(process.cwd(), ".1tube");
|
|
190
|
+
const lockPath = join(dir, "deno.lock");
|
|
191
|
+
try {
|
|
192
|
+
mkdirSync(dir, { recursive: true });
|
|
193
|
+
} catch {
|
|
194
|
+
// If we can't create the scratch dir, fall back to no managed lock
|
|
195
|
+
// rather than crash the launcher.
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// `1TUBE_REFRESH_LOCK=1` forces a clean rebuild by dropping the managed
|
|
200
|
+
// lock; Deno then re-derives it from the current deps on this boot.
|
|
201
|
+
if (truthy(process.env.ONETUBE_REFRESH_LOCK ?? process.env["1TUBE_REFRESH_LOCK"])) {
|
|
202
|
+
try {
|
|
203
|
+
rmSync(lockPath, { force: true });
|
|
204
|
+
} catch {
|
|
205
|
+
// Best-effort.
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return lockPath;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Loose truthiness for env-var flags: "1", "true", "yes", "on" (any case).
|
|
213
|
+
*
|
|
214
|
+
* @param {string | undefined} v
|
|
215
|
+
* @returns {boolean}
|
|
216
|
+
*/
|
|
217
|
+
function truthy(v) {
|
|
218
|
+
return v !== undefined && /^(1|true|yes|on)$/i.test(v.trim());
|
|
219
|
+
}
|
|
220
|
+
|
|
145
221
|
/**
|
|
146
222
|
* Split a flag string into argv tokens, honoring single/double quotes so
|
|
147
223
|
* values with spaces (`--env-file="my env.env"`) survive. Good enough for
|
|
@@ -95,7 +95,10 @@ export interface DenoReloadDebouncerOptions {
|
|
|
95
95
|
* `debounceMs`. Unset = classic trailing-only behavior.
|
|
96
96
|
*/
|
|
97
97
|
leadingMs?: number;
|
|
98
|
-
|
|
98
|
+
// Awaited at the call site, so a synchronous callback is fine too (a test
|
|
99
|
+
// that just records the paths needn't be `async`, which would trip
|
|
100
|
+
// require-await). The real reloader flush is async.
|
|
101
|
+
flushFn: (paths: readonly string[]) => void | Promise<void>;
|
|
99
102
|
setTimer?: (cb: () => void, ms: number) => number;
|
|
100
103
|
clearTimer?: (id: number) => void;
|
|
101
104
|
}
|
|
@@ -398,7 +401,9 @@ export function createDenoHotReloader(
|
|
|
398
401
|
}`;
|
|
399
402
|
log(`[1tube] HMR ${reason}`);
|
|
400
403
|
try {
|
|
401
|
-
const summary = await opts.host.reload(affected, reason
|
|
404
|
+
const summary = await opts.host.reload(affected, reason, {
|
|
405
|
+
changedPaths: paths,
|
|
406
|
+
});
|
|
402
407
|
const parts: string[] = [];
|
|
403
408
|
if (summary.reloaded.length > 0) {
|
|
404
409
|
parts.push(`reloaded=${summary.reloaded.join(",")}`);
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Colocated worker-side entry point (dev) for the Deno backend.
|
|
3
|
+
*
|
|
4
|
+
* Where {@link ./worker-entry.ts} runs ONE function per Worker, this
|
|
5
|
+
* bootstrap hosts EVERY function in a single isolate. The point is
|
|
6
|
+
* dependency sharing: a 100-function project where most functions import
|
|
7
|
+
* the same heavy npm graph (`ai`, `@supabase/supabase-js`, `@google/genai`,
|
|
8
|
+
* `zod`, �) pays to compile + evaluate that graph exactly once here,
|
|
9
|
+
* instead of once per function across 100 isolates. On a real project that
|
|
10
|
+
* collapses warm-boot from ~per-function-sum/cores down to roughly the
|
|
11
|
+
* one-time cost of the union graph (measured ~6s vs ~18s).
|
|
12
|
+
*
|
|
13
|
+
* Trade-off: functions share globals and a crash takes the whole isolate
|
|
14
|
+
* down. That's why this mode is dev-only � production keeps isolate-per-
|
|
15
|
+
* function (or workerd) for real isolation.
|
|
16
|
+
*
|
|
17
|
+
* Lifecycle:
|
|
18
|
+
*
|
|
19
|
+
* host ? init : { functions: [{name, entryUrl, manifest}], � }
|
|
20
|
+
* Imported SERIALLY so each function's top-level
|
|
21
|
+
* `serve()` registration can be attributed to the
|
|
22
|
+
* right name (parallel imports would interleave the
|
|
23
|
+
* single global registry stub). The first import pays
|
|
24
|
+
* the shared-graph cost; the rest are cache hits, so
|
|
25
|
+
* serial is nearly as fast as parallel but correct.
|
|
26
|
+
* Streams `fn_ready` / `fn_error` per function, then
|
|
27
|
+
* `all_done`.
|
|
28
|
+
*
|
|
29
|
+
* host ? dispatch : { id, name, � } ? look up the captured handler by
|
|
30
|
+
* name and run it (per-request AsyncLocalStorage so
|
|
31
|
+
* concurrent invocations attribute console output).
|
|
32
|
+
*
|
|
33
|
+
* host ? reload : { functions: [...] } ? re-import each entry with a
|
|
34
|
+
* `?v=` cache-bust so only the edited module (and its
|
|
35
|
+
* changed local deps) re-evaluates; unchanged npm
|
|
36
|
+
* modules stay warm in the isolate. Updates the
|
|
37
|
+
* handler map and streams `fn_ready` / `fn_error`.
|
|
38
|
+
*
|
|
39
|
+
* host ? remove : { names: [...] } ? drop handlers for deleted fns.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/// <reference lib="deno.worker" />
|
|
43
|
+
|
|
44
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
45
|
+
import type { FunctionManifest } from "../../manifest.ts";
|
|
46
|
+
import type { AuthContext } from "../../registry.ts";
|
|
47
|
+
|
|
48
|
+
interface FnSpec {
|
|
49
|
+
name: string;
|
|
50
|
+
entryUrl: string;
|
|
51
|
+
manifest: FunctionManifest;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface InitMessage {
|
|
55
|
+
type: "init";
|
|
56
|
+
functions: FnSpec[];
|
|
57
|
+
captureConsole?: boolean;
|
|
58
|
+
/** Per-import hard cap (ms) so one function blocking at top-level can't
|
|
59
|
+
* stall the whole boot. */
|
|
60
|
+
importTimeoutMs?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface DispatchMessage {
|
|
64
|
+
type: "dispatch";
|
|
65
|
+
id: number;
|
|
66
|
+
name: string;
|
|
67
|
+
url: string;
|
|
68
|
+
method: string;
|
|
69
|
+
headers: Array<[string, string]>;
|
|
70
|
+
body: ReadableStream<Uint8Array> | null;
|
|
71
|
+
auth: AuthContext | null;
|
|
72
|
+
invocationId?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface ReloadMessage {
|
|
76
|
+
type: "reload";
|
|
77
|
+
functions: FnSpec[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface RemoveMessage {
|
|
81
|
+
type: "remove";
|
|
82
|
+
names: string[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type HostMessage =
|
|
86
|
+
| InitMessage
|
|
87
|
+
| DispatchMessage
|
|
88
|
+
| ReloadMessage
|
|
89
|
+
| RemoveMessage;
|
|
90
|
+
|
|
91
|
+
type Handler = (
|
|
92
|
+
req: Request,
|
|
93
|
+
auth?: AuthContext,
|
|
94
|
+
) => Response | Promise<Response>;
|
|
95
|
+
|
|
96
|
+
interface RegistryStub {
|
|
97
|
+
register(
|
|
98
|
+
handler: Handler,
|
|
99
|
+
opts: { public: boolean; timeoutMs?: number },
|
|
100
|
+
): void;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface CapturedHandler {
|
|
104
|
+
handler: Handler;
|
|
105
|
+
isPublic: boolean;
|
|
106
|
+
timeoutMs?: number;
|
|
107
|
+
manifest: FunctionManifest;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const handlers = new Map<string, CapturedHandler>();
|
|
111
|
+
|
|
112
|
+
// Set immediately before each import so the registry stub can attribute a
|
|
113
|
+
// `serve()` call (and any boot-time console output) to the right function.
|
|
114
|
+
let importing: { name: string; captured: CapturedHandler | null } | null = null;
|
|
115
|
+
|
|
116
|
+
const invocationContext = new AsyncLocalStorage<
|
|
117
|
+
{ invocationId?: string; name: string }
|
|
118
|
+
>();
|
|
119
|
+
|
|
120
|
+
let consoleCaptureOn = false;
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Console capture (mirrors worker-entry.ts, but tags every line with the
|
|
124
|
+
// owning function so the host can attribute output in a shared isolate).
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
type ConsoleLevel = "debug" | "log" | "info" | "warn" | "error";
|
|
128
|
+
const CONSOLE_LEVELS: ConsoleLevel[] = ["debug", "log", "info", "warn", "error"];
|
|
129
|
+
const MAX_CONSOLE_MESSAGE = 8 * 1024;
|
|
130
|
+
|
|
131
|
+
function formatConsoleArg(arg: unknown): string {
|
|
132
|
+
if (typeof arg === "string") return arg;
|
|
133
|
+
try {
|
|
134
|
+
return Deno.inspect(arg, {
|
|
135
|
+
depth: 4,
|
|
136
|
+
colors: false,
|
|
137
|
+
strAbbreviateSize: 4096,
|
|
138
|
+
});
|
|
139
|
+
} catch {
|
|
140
|
+
try {
|
|
141
|
+
return String(arg);
|
|
142
|
+
} catch {
|
|
143
|
+
return "[unprintable]";
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function currentFunctionName(): string {
|
|
149
|
+
return invocationContext.getStore()?.name ?? importing?.name ?? "<colocated>";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function installConsoleCapture(): void {
|
|
153
|
+
if (consoleCaptureOn) return;
|
|
154
|
+
consoleCaptureOn = true;
|
|
155
|
+
for (const level of CONSOLE_LEVELS) {
|
|
156
|
+
const original = console[level].bind(console);
|
|
157
|
+
console[level] = (...args: unknown[]) => {
|
|
158
|
+
original(...args);
|
|
159
|
+
try {
|
|
160
|
+
let message = args.map(formatConsoleArg).join(" ");
|
|
161
|
+
if (message.length > MAX_CONSOLE_MESSAGE) {
|
|
162
|
+
message = message.slice(0, MAX_CONSOLE_MESSAGE) + "�";
|
|
163
|
+
}
|
|
164
|
+
postMessage({
|
|
165
|
+
type: "console",
|
|
166
|
+
name: currentFunctionName(),
|
|
167
|
+
level,
|
|
168
|
+
message,
|
|
169
|
+
tsMs: Date.now(),
|
|
170
|
+
invocationId: invocationContext.getStore()?.invocationId ?? null,
|
|
171
|
+
});
|
|
172
|
+
} catch {
|
|
173
|
+
// postMessage can throw during teardown; dropping a captured line
|
|
174
|
+
// is acceptable, breaking user code is not.
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// Registry stub: function top-level `serve()` lands here. We attribute the
|
|
182
|
+
// registration to whichever function is currently importing.
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
const stub: RegistryStub = {
|
|
186
|
+
register(handler, opts) {
|
|
187
|
+
if (!importing) return; // serve() called outside an import window � ignore
|
|
188
|
+
if (importing.captured) {
|
|
189
|
+
console.warn(
|
|
190
|
+
`[1tube] "${importing.name}" called serve() twice; using the last handler`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
importing.captured = {
|
|
194
|
+
handler,
|
|
195
|
+
isPublic: opts.public,
|
|
196
|
+
timeoutMs: opts.timeoutMs,
|
|
197
|
+
manifest: { runtime: "deno" } as unknown as FunctionManifest,
|
|
198
|
+
};
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
(globalThis as { __edgeFunctionRegistry?: RegistryStub })
|
|
203
|
+
.__edgeFunctionRegistry = stub;
|
|
204
|
+
|
|
205
|
+
// Shared-module RPC is unnecessary in colocated mode: a shared module like
|
|
206
|
+
// `_shared/profile-cache.ts` is imported once in THIS isolate, so its side
|
|
207
|
+
// effects already run exactly once. Provide a no-op so any leftover stub
|
|
208
|
+
// reference doesn't throw.
|
|
209
|
+
(globalThis as { __1tube_call_shared?: unknown }).__1tube_call_shared =
|
|
210
|
+
async () => {
|
|
211
|
+
throw new Error(
|
|
212
|
+
"shared-module RPC is not used in colocated dev mode (module runs in-isolate)",
|
|
213
|
+
);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
self.addEventListener("unhandledrejection", (e) => {
|
|
217
|
+
e.preventDefault();
|
|
218
|
+
const err = (e as PromiseRejectionEvent).reason;
|
|
219
|
+
postMessage({
|
|
220
|
+
type: "unhandledrejection",
|
|
221
|
+
name: currentFunctionName(),
|
|
222
|
+
message: err instanceof Error ? err.message : String(err),
|
|
223
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
224
|
+
invocationId: invocationContext.getStore()?.invocationId ?? null,
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// Import + (re)registration
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
function importWithTimeout(url: string, timeoutMs: number): Promise<void> {
|
|
233
|
+
return Promise.race([
|
|
234
|
+
import(url).then(() => {}),
|
|
235
|
+
new Promise<void>((_, reject) =>
|
|
236
|
+
setTimeout(
|
|
237
|
+
() => reject(new Error(`import timed out after ${timeoutMs}ms`)),
|
|
238
|
+
timeoutMs,
|
|
239
|
+
)
|
|
240
|
+
),
|
|
241
|
+
]);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Import one function entry and capture its handler. `bust` appends a
|
|
246
|
+
* cache-busting query so reloads re-evaluate the edited module instead of
|
|
247
|
+
* reusing the isolate's cached copy.
|
|
248
|
+
*/
|
|
249
|
+
async function loadFunction(
|
|
250
|
+
fn: FnSpec,
|
|
251
|
+
importTimeoutMs: number,
|
|
252
|
+
bust: boolean,
|
|
253
|
+
): Promise<void> {
|
|
254
|
+
const t0 = performance.now();
|
|
255
|
+
importing = { name: fn.name, captured: null };
|
|
256
|
+
const url = bust
|
|
257
|
+
? `${fn.entryUrl}${fn.entryUrl.includes("?") ? "&" : "?"}v=${Date.now()}-${
|
|
258
|
+
Math.random().toString(36).slice(2, 8)
|
|
259
|
+
}`
|
|
260
|
+
: fn.entryUrl;
|
|
261
|
+
try {
|
|
262
|
+
await importWithTimeout(url, importTimeoutMs);
|
|
263
|
+
} catch (err) {
|
|
264
|
+
importing = null;
|
|
265
|
+
postMessage({
|
|
266
|
+
type: "fn_error",
|
|
267
|
+
name: fn.name,
|
|
268
|
+
ms: performance.now() - t0,
|
|
269
|
+
message: err instanceof Error ? err.message : String(err),
|
|
270
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
271
|
+
});
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const captured = importing.captured;
|
|
275
|
+
importing = null;
|
|
276
|
+
if (!captured) {
|
|
277
|
+
postMessage({
|
|
278
|
+
type: "fn_error",
|
|
279
|
+
name: fn.name,
|
|
280
|
+
ms: performance.now() - t0,
|
|
281
|
+
message: `Function "${fn.name}" loaded without calling serve()`,
|
|
282
|
+
});
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
captured.manifest = fn.manifest;
|
|
286
|
+
handlers.set(fn.name, captured);
|
|
287
|
+
postMessage({
|
|
288
|
+
type: "fn_ready",
|
|
289
|
+
name: fn.name,
|
|
290
|
+
ms: performance.now() - t0,
|
|
291
|
+
isPublic: captured.isPublic,
|
|
292
|
+
timeoutMs: captured.timeoutMs,
|
|
293
|
+
manifest: fn.manifest,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function runDispatch(msg: DispatchMessage): Promise<void> {
|
|
298
|
+
const captured = handlers.get(msg.name);
|
|
299
|
+
if (!captured) {
|
|
300
|
+
postMessage({
|
|
301
|
+
type: "response_error",
|
|
302
|
+
id: msg.id,
|
|
303
|
+
message: `Function "${msg.name}" is not loaded`,
|
|
304
|
+
});
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const headers = new Headers(msg.headers);
|
|
308
|
+
const init: RequestInit = { method: msg.method, headers };
|
|
309
|
+
if (msg.body !== null) {
|
|
310
|
+
init.body = msg.body;
|
|
311
|
+
(init as unknown as { duplex: "half" }).duplex = "half";
|
|
312
|
+
}
|
|
313
|
+
const req = new Request(msg.url, init);
|
|
314
|
+
let resp: Response;
|
|
315
|
+
try {
|
|
316
|
+
const run = () =>
|
|
317
|
+
Promise.resolve(
|
|
318
|
+
captured.isPublic
|
|
319
|
+
? captured.handler(req)
|
|
320
|
+
: captured.handler(req, msg.auth ?? undefined),
|
|
321
|
+
);
|
|
322
|
+
resp = await invocationContext.run(
|
|
323
|
+
{ invocationId: msg.invocationId, name: msg.name },
|
|
324
|
+
run,
|
|
325
|
+
);
|
|
326
|
+
} catch (err) {
|
|
327
|
+
postMessage({
|
|
328
|
+
type: "response_error",
|
|
329
|
+
id: msg.id,
|
|
330
|
+
message: err instanceof Error ? err.message : String(err),
|
|
331
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
332
|
+
});
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const respHeaders: Array<[string, string]> = [];
|
|
336
|
+
resp.headers.forEach((v, k) => respHeaders.push([k, v]));
|
|
337
|
+
const body = resp.body;
|
|
338
|
+
postMessage(
|
|
339
|
+
{
|
|
340
|
+
type: "response",
|
|
341
|
+
id: msg.id,
|
|
342
|
+
status: resp.status,
|
|
343
|
+
headers: respHeaders,
|
|
344
|
+
body,
|
|
345
|
+
},
|
|
346
|
+
body ? [body as unknown as Transferable] : [],
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
let importTimeoutMs = 30_000;
|
|
351
|
+
|
|
352
|
+
self.onmessage = (ev: MessageEvent<HostMessage>) => {
|
|
353
|
+
const msg = ev.data;
|
|
354
|
+
switch (msg.type) {
|
|
355
|
+
case "init": {
|
|
356
|
+
if (msg.captureConsole) installConsoleCapture();
|
|
357
|
+
importTimeoutMs = msg.importTimeoutMs ?? 30_000;
|
|
358
|
+
void (async () => {
|
|
359
|
+
// Serial: the first import warms the shared graph, the rest are
|
|
360
|
+
// cache hits. Serial keeps `serve()` attribution unambiguous.
|
|
361
|
+
for (const fn of msg.functions) {
|
|
362
|
+
await loadFunction(fn, importTimeoutMs, false);
|
|
363
|
+
}
|
|
364
|
+
postMessage({ type: "all_done" });
|
|
365
|
+
})();
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
case "dispatch": {
|
|
369
|
+
void runDispatch(msg);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
case "reload": {
|
|
373
|
+
void (async () => {
|
|
374
|
+
for (const fn of msg.functions) {
|
|
375
|
+
await loadFunction(fn, importTimeoutMs, true);
|
|
376
|
+
}
|
|
377
|
+
postMessage({ type: "reload_done" });
|
|
378
|
+
})();
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
case "remove": {
|
|
382
|
+
for (const name of msg.names) handlers.delete(name);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
};
|