@frockbot/applet-sdk 0.7.179 → 0.7.180

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frockbot/applet-sdk",
3
- "version": "0.7.179",
3
+ "version": "0.7.180",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Authoring SDK for FrockBot Applets: schema-first Durable Object server, TanStack DB client, component kit, linter, and the build pipeline.",
@@ -28,11 +28,42 @@ import { convertV4MiniflareOptions, Miniflare } from "miniflare";
28
28
  import ts from "typescript";
29
29
 
30
30
  import type { AppletDiagnostic } from "../lint/index.js";
31
- import { bootedWithin, withOneMoreBoot } from "./boot.js";
32
31
  import { stableModulePaths } from "./module-paths.js";
33
- import { APPLET_COMPATIBILITY_DATE } from "./runtime.js";
34
32
  import { SDK_PLUGIN_TYPES } from "./paths.js";
35
33
 
34
+ /** Pinned with the SDK: the runtime a Plugin build is checked against. */
35
+ const PLUGIN_COMPATIBILITY_DATE = "2026-08-27";
36
+
37
+ /**
38
+ * A workerd boot takes well under a second; this is room for a loaded
39
+ * machine. Without it a runtime that never reported ready would hang the
40
+ * build container's request rather than answer.
41
+ */
42
+ const BOOT_DEADLINE_MS = 30_000;
43
+
44
+ /**
45
+ * Tearing a runtime down takes milliseconds, but Miniflare's dispose first
46
+ * waits out the startup, which can be the very thing that hung.
47
+ */
48
+ const DISPOSE_DEADLINE_MS = 10_000;
49
+
50
+ /** `work`, or an error naming what did not happen in time. */
51
+ async function within<T>(
52
+ work: Promise<T>,
53
+ ms: number,
54
+ what: string,
55
+ ): Promise<T> {
56
+ let timer: ReturnType<typeof setTimeout> | undefined;
57
+ const deadline = new Promise<never>((_, reject) => {
58
+ timer = setTimeout(() => reject(new Error(`${what} within ${ms}ms`)), ms);
59
+ });
60
+ try {
61
+ return await Promise.race([work, deadline]);
62
+ } finally {
63
+ clearTimeout(timer);
64
+ }
65
+ }
66
+
36
67
  export type PluginBuildStage =
37
68
  "descriptor" | "typecheck" | "bundle" | "describe";
38
69
 
@@ -347,17 +378,11 @@ export default {
347
378
  `;
348
379
 
349
380
  /**
350
- * Ask the built module what it exports, by running it. The boot is bounded
351
- * and tried once more (`boot.ts`), so a build answers rather than hanging on
352
- * a runtime that never came up.
381
+ * Ask the built module what it exports, by running it. Both the boot and the
382
+ * teardown are bounded, so a build answers rather than hanging on a runtime
383
+ * that never came up.
353
384
  */
354
- export function describePlugin(
355
- moduleCode: string,
356
- ): Promise<PluginDescriptionV1> {
357
- return withOneMoreBoot(() => describeInWorkerd(moduleCode));
358
- }
359
-
360
- async function describeInWorkerd(
385
+ export async function describePlugin(
361
386
  moduleCode: string,
362
387
  ): Promise<PluginDescriptionV1> {
363
388
  const miniflare = new Miniflare(
@@ -367,7 +392,7 @@ async function describeInWorkerd(
367
392
  { type: "ESModule", path: "/plugin.js", contents: moduleCode },
368
393
  ],
369
394
  modulesRoot: "/",
370
- compatibilityDate: APPLET_COMPATIBILITY_DATE,
395
+ compatibilityDate: PLUGIN_COMPATIBILITY_DATE,
371
396
  // Import-time code runs with no way out: every fetch is answered here.
372
397
  outboundService: async () =>
373
398
  new Response("the build describes a Plugin without a network", {
@@ -377,10 +402,12 @@ async function describeInWorkerd(
377
402
  port: 0,
378
403
  }),
379
404
  );
380
- let started = false;
381
405
  try {
382
- const url = await bootedWithin(miniflare.ready);
383
- started = true;
406
+ const url = await within(
407
+ miniflare.ready,
408
+ BOOT_DEADLINE_MS,
409
+ "The Workers runtime did not start",
410
+ );
384
411
  const response = (await miniflare.dispatchFetch(
385
412
  new URL(`/describe?${randomUUID()}`, url).toString(),
386
413
  )) as unknown as Response;
@@ -392,9 +419,15 @@ async function describeInWorkerd(
392
419
  }
393
420
  return validateDescription(body.description);
394
421
  } finally {
395
- // A runtime that never started is let go of rather than waited on.
396
- if (started) await miniflare.dispose();
397
- else void miniflare.dispose().catch(() => {});
422
+ // Awaited on every path, a boot that missed its deadline included: this
423
+ // is what kills workerd. Miniflare's fallback is a process exit hook,
424
+ // `bun test` runs none, and a dispose still pending when the host exits
425
+ // leaves workerd running.
426
+ await within(
427
+ miniflare.dispose(),
428
+ DISPOSE_DEADLINE_MS,
429
+ "The Workers runtime did not stop",
430
+ );
398
431
  }
399
432
  }
400
433
 
package/src/build/boot.ts DELETED
@@ -1,75 +0,0 @@
1
- // A workerd boot, bounded.
2
- //
3
- // Each build spawns its own workerd through Miniflare, and a spawn
4
- // occasionally never comes up (a bun+workerd spawn race, roughly one boot in
5
- // fifty): it either never reports ready, or it fails outright and leaves the
6
- // runtime's stdio socket with nothing to connect to. A build that awaited
7
- // `ready` unbounded would hang to the test's timeout, or hang the build
8
- // container's request. So a boot is given a deadline, a boot that misses it
9
- // is let go of rather than waited on, both shapes are named
10
- // `RuntimeDidNotStart`, and the caller tries once more before answering with
11
- // that as its diagnostic.
12
-
13
- /** How long a workerd boot is given before the build gives up on it. */
14
- export const BOOT_DEADLINE_MS = 30_000;
15
-
16
- /** A workerd that never came up; the boot, not the code, failed. */
17
- export class RuntimeDidNotStart extends Error {}
18
-
19
- /**
20
- * The same race seen from the other side: the spawn fails outright rather
21
- * than hanging, and the runtime's stdio socket is never there to connect to.
22
- * That is a boot that did not happen, not a fault in the code being built.
23
- */
24
- function spawnFailed(error: unknown): boolean {
25
- const { code, syscall } = (error ?? {}) as {
26
- code?: unknown;
27
- syscall?: unknown;
28
- };
29
- return (
30
- (syscall === "connect" || syscall === "spawn") &&
31
- (code === "ENOENT" || code === "ECONNREFUSED" || code === "EAGAIN")
32
- );
33
- }
34
-
35
- /**
36
- * `ready`, or a `RuntimeDidNotStart` when the runtime never came up — either
37
- * because `ready` did not settle in time or because the spawn failed.
38
- */
39
- export async function bootedWithin<T>(ready: Promise<T>): Promise<T> {
40
- let timer: ReturnType<typeof setTimeout> | undefined;
41
- const deadline = new Promise<never>((_, reject) => {
42
- timer = setTimeout(
43
- () =>
44
- reject(
45
- new RuntimeDidNotStart(
46
- `The Workers runtime did not start within ${BOOT_DEADLINE_MS}ms`,
47
- ),
48
- ),
49
- BOOT_DEADLINE_MS,
50
- );
51
- });
52
- try {
53
- return await Promise.race([ready, deadline]);
54
- } catch (error) {
55
- if (!spawnFailed(error)) throw error;
56
- throw new RuntimeDidNotStart(
57
- `The Workers runtime could not be spawned: ${
58
- error instanceof Error ? error.message : String(error)
59
- }`,
60
- { cause: error },
61
- );
62
- } finally {
63
- clearTimeout(timer);
64
- }
65
- }
66
-
67
- /** Runs `boot` again, once, when the runtime never came up the first time. */
68
- export async function withOneMoreBoot<T>(boot: () => Promise<T>): Promise<T> {
69
- try {
70
- return await boot();
71
- } catch (error) {
72
- if (!(error instanceof RuntimeDidNotStart)) throw error;
73
- return await boot();
74
- }
75
- }
@@ -1,132 +0,0 @@
1
- /**
2
- * The built Applet, running for real, in a Node process.
3
- *
4
- * Miniflare gives the built `dist/server.js` the one thing no fake can: a
5
- * SQLite-backed Durable Object with hibernating WebSockets, which is exactly
6
- * what the loader gives it in production. The build uses it to ask the
7
- * mounted class what
8
- * tools it declares rather than guessing from the source.
9
- */
10
-
11
- import { convertV4MiniflareOptions, Miniflare } from "miniflare";
12
-
13
- import { bootedWithin } from "./boot.js";
14
-
15
- /** Pinned with the SDK: the runtime a Plugin build is checked against. */
16
- export const APPLET_COMPATIBILITY_DATE = "2026-08-27";
17
-
18
- /**
19
- * The dev worker. It exists only to route: the DO class is the Applet's own,
20
- * and everything else here is the two seams the kernel provides in production
21
- * — a viewer token on the socket, and a `CAPABILITIES` binding.
22
- */
23
- const DEV_WORKER = `
24
- export { Applet } from "./server.js";
25
-
26
- export default {
27
- async fetch(request, env) {
28
- const url = new URL(request.url);
29
- const stub = env.APPLET.get(env.APPLET.idFromName(env.APPLET_ID));
30
-
31
- if (url.pathname === "/socket") {
32
- if (url.searchParams.get("token") !== env.APPLET_TOKEN) {
33
- return new Response("Forbidden", { status: 403 });
34
- }
35
- url.searchParams.set("viewer", url.searchParams.get("viewer") ?? "dev-viewer");
36
- return stub.fetch(new Request(url, request));
37
- }
38
-
39
- if (url.pathname === "/health") {
40
- return Response.json(await stub.health());
41
- }
42
-
43
- if (url.pathname === "/describe") {
44
- return Response.json(await stub.describe());
45
- }
46
-
47
- if (url.pathname === "/tool" && request.method === "POST") {
48
- const body = await request.json();
49
- try {
50
- return Response.json({ ok: true, result: await stub.invokeTool(body.name, body.input) });
51
- } catch (error) {
52
- return Response.json({ ok: false, error: String(error && error.message || error) });
53
- }
54
- }
55
-
56
- if (url.pathname === "/" || url.pathname === "/index.html") {
57
- return new Response(env.APPLET_UI, {
58
- headers: { "content-type": "text/html; charset=utf-8" },
59
- });
60
- }
61
- return new Response("Not found", { status: 404 });
62
- },
63
- };
64
- `;
65
-
66
- export interface AppletRuntimeOptions {
67
- /** Contents of `dist/server.js`: one ESM file importing only cloudflare:workers. */
68
- serverCode: string;
69
- /** Contents of `dist/ui.html`; omitted when only `health()` is wanted. */
70
- html?: string;
71
- appletId: string;
72
- /** The dev viewer token the socket demands. */
73
- token: string;
74
- /** 0 picks a free port. */
75
- port?: number;
76
- }
77
-
78
- export interface AppletRuntime {
79
- url: URL;
80
- fetch(path: string, init?: RequestInit): Promise<Response>;
81
- dispose(): Promise<void>;
82
- }
83
-
84
- export async function startAppletRuntime(
85
- options: AppletRuntimeOptions,
86
- ): Promise<AppletRuntime> {
87
- // Miniflare 5's own option shape is the wrangler config (`workers[].config`).
88
- // `convertV4MiniflareOptions` is the supported way to keep the flat v4 shape,
89
- // which is the one the Workers docs and the rest of this repo speak.
90
- const miniflare = new Miniflare(
91
- convertV4MiniflareOptions({
92
- modules: [
93
- { type: "ESModule", path: "/index.mjs", contents: DEV_WORKER },
94
- { type: "ESModule", path: "/server.js", contents: options.serverCode },
95
- ],
96
- modulesRoot: "/",
97
- compatibilityDate: APPLET_COMPATIBILITY_DATE,
98
- compatibilityFlags: ["nodejs_compat"],
99
- durableObjects: { APPLET: { className: "Applet", useSQLite: true } },
100
- serviceBindings: {
101
- // The lease-backed proxy is a later slice; models are unavailable.
102
- CAPABILITIES: async () =>
103
- Response.json({ status: "unavailable", reason: "dev" }),
104
- },
105
- bindings: {
106
- APPLET_ID: options.appletId,
107
- APPLET_TOKEN: options.token,
108
- APPLET_UI: options.html ?? "",
109
- },
110
- host: "127.0.0.1",
111
- port: options.port ?? 0,
112
- }),
113
- );
114
-
115
- let url: URL;
116
- try {
117
- url = await bootedWithin(miniflare.ready);
118
- } catch (error) {
119
- // A runtime that never started is let go of rather than waited on.
120
- void miniflare.dispose().catch(() => {});
121
- throw error;
122
- }
123
- return {
124
- url,
125
- fetch: (path, init) =>
126
- miniflare.dispatchFetch(
127
- new URL(path, url).toString(),
128
- init as never,
129
- ) as unknown as Promise<Response>,
130
- dispose: () => miniflare.dispose(),
131
- };
132
- }