@frockbot/applet-sdk 0.7.38 → 0.7.39

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.38",
3
+ "version": "0.7.39",
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.",
package/plugin/index.d.ts CHANGED
@@ -347,11 +347,37 @@ export type PluginHooks = {
347
347
  [Event in PluginHookEvent]?: PluginHook<Event>;
348
348
  };
349
349
 
350
- /** A trigger handler: what it returns, when non-empty, is what the Bot reads. */
350
+ /**
351
+ * One delivery handed to a trigger: the posted body as it arrived, and the
352
+ * headers lower-cased with the door's own credential removed.
353
+ */
354
+ export interface PluginTriggerDelivery {
355
+ headers: Record<string, string>;
356
+ body: string;
357
+ }
358
+
359
+ /** A trigger's refusal: the Routine does not fire, and the receipt says why. */
360
+ export interface PluginTriggerDrop {
361
+ drop: true;
362
+ reason?: string;
363
+ }
364
+
365
+ /**
366
+ * A trigger handler: a non-empty string fires the Routine with that text.
367
+ * A `{ drop: true }` — or nothing at all — leaves it unfired.
368
+ */
351
369
  export type PluginTrigger = (
352
- event: { [key: string]: unknown },
370
+ delivery: PluginTriggerDelivery,
353
371
  ctx: PluginContext,
354
- ) => Promise<string | undefined | void> | string | undefined | void;
372
+ ) =>
373
+ | Promise<string | PluginTriggerDrop | undefined | void>
374
+ | string
375
+ | PluginTriggerDrop
376
+ | undefined
377
+ | void;
378
+
379
+ /** Every trigger the module exports, by the name `plugin.json` declares. */
380
+ export type PluginTriggers = Record<string, PluginTrigger>;
355
381
 
356
382
  /**
357
383
  * A tool call's answer. A string is handed to the Bot as it is; anything else
@@ -382,5 +408,5 @@ export interface PluginModule {
382
408
  hooks?: PluginHooks;
383
409
  /** Values other Plugins that `consume` a service of the same name receive. */
384
410
  services?: Record<string, unknown>;
385
- triggers?: Record<string, PluginTrigger>;
411
+ triggers?: PluginTriggers;
386
412
  }
@@ -29,6 +29,7 @@ import { build as esbuild, type Metafile } from "esbuild";
29
29
  import type { AppletDescriptionV1 } from "../server/applet.js";
30
30
  import { readDescriptor, type AppletBuildManifestV1 } from "./manifest.js";
31
31
  import { bundlerNodePaths, SDK_ENTRIES, SDK_ROOT } from "./paths.js";
32
+ import { withOneMoreBoot } from "./boot.js";
32
33
  import { startAppletRuntime } from "./runtime.js";
33
34
 
34
35
  export interface AppletArtifactsV1 {
@@ -160,8 +161,18 @@ function page(title: string, script: string): string {
160
161
  ].join("\n");
161
162
  }
162
163
 
163
- /** Ask the built module what it declares, by running it. */
164
- export async function readDescription(
164
+ /**
165
+ * Ask the built module what it declares, by running it. A boot that never
166
+ * reports ready is tried once more (`boot.ts`) before the build gives up.
167
+ */
168
+ export function readDescription(
169
+ serverCode: string,
170
+ appletId: string,
171
+ ): Promise<AppletDescriptionV1> {
172
+ return withOneMoreBoot(() => describeInRuntime(serverCode, appletId));
173
+ }
174
+
175
+ async function describeInRuntime(
165
176
  serverCode: string,
166
177
  appletId: string,
167
178
  ): Promise<AppletDescriptionV1> {
@@ -0,0 +1,46 @@
1
+ // A workerd boot, bounded.
2
+ //
3
+ // Each build spawns its own workerd through Miniflare, and a spawn
4
+ // occasionally never reports ready (a bun+workerd spawn race, roughly one
5
+ // boot in fifty). A build that awaited `ready` unbounded would hang to the
6
+ // test's timeout, or hang the build container's request. So a boot is given
7
+ // a deadline, a boot that misses it is let go of rather than waited on, and
8
+ // the caller tries once more before answering with the deadline as its
9
+ // diagnostic.
10
+
11
+ /** How long a workerd boot is given before the build gives up on it. */
12
+ export const BOOT_DEADLINE_MS = 30_000;
13
+
14
+ /** A workerd that never reported ready; the boot, not the code, failed. */
15
+ export class RuntimeDidNotStart extends Error {}
16
+
17
+ /** `ready`, or a `RuntimeDidNotStart` when it has not settled in time. */
18
+ export async function bootedWithin<T>(ready: Promise<T>): Promise<T> {
19
+ let timer: ReturnType<typeof setTimeout> | undefined;
20
+ const deadline = new Promise<never>((_, reject) => {
21
+ timer = setTimeout(
22
+ () =>
23
+ reject(
24
+ new RuntimeDidNotStart(
25
+ `The Workers runtime did not start within ${BOOT_DEADLINE_MS}ms`,
26
+ ),
27
+ ),
28
+ BOOT_DEADLINE_MS,
29
+ );
30
+ });
31
+ try {
32
+ return await Promise.race([ready, deadline]);
33
+ } finally {
34
+ clearTimeout(timer);
35
+ }
36
+ }
37
+
38
+ /** Runs `boot` again, once, when the runtime never came up the first time. */
39
+ export async function withOneMoreBoot<T>(boot: () => Promise<T>): Promise<T> {
40
+ try {
41
+ return await boot();
42
+ } catch (error) {
43
+ if (!(error instanceof RuntimeDidNotStart)) throw error;
44
+ return await boot();
45
+ }
46
+ }
@@ -29,6 +29,7 @@ import { convertV4MiniflareOptions, Miniflare } from "miniflare";
29
29
  import ts from "typescript";
30
30
 
31
31
  import type { AppletDiagnostic } from "../lint/index.js";
32
+ import { bootedWithin, withOneMoreBoot } from "./boot.js";
32
33
  import { APPLET_COMPATIBILITY_DATE } from "./runtime.js";
33
34
  import { SDK_PLUGIN_TYPES } from "./paths.js";
34
35
 
@@ -317,28 +318,15 @@ export default {
317
318
  };
318
319
  `;
319
320
 
320
- /** How long a workerd boot is given before the build gives up on it. */
321
- const BOOT_DEADLINE_MS = 30_000;
322
-
323
- /** A workerd that never reported ready; the boot, not the Plugin, failed. */
324
- class RuntimeDidNotStart extends Error {}
325
-
326
321
  /**
327
- * Ask the built module what it exports, by running it.
328
- *
329
- * Each build spawns its own workerd, and a spawn occasionally never reports
330
- * ready. A boot that misses the deadline is let go of and tried once more, so
331
- * a build answers rather than hanging on a runtime that never came up.
322
+ * Ask the built module what it exports, by running it. The boot is bounded
323
+ * and tried once more (`boot.ts`), so a build answers rather than hanging on
324
+ * a runtime that never came up.
332
325
  */
333
- export async function describePlugin(
326
+ export function describePlugin(
334
327
  moduleCode: string,
335
328
  ): Promise<PluginDescriptionV1> {
336
- try {
337
- return await describeInWorkerd(moduleCode);
338
- } catch (error) {
339
- if (!(error instanceof RuntimeDidNotStart)) throw error;
340
- return await describeInWorkerd(moduleCode);
341
- }
329
+ return withOneMoreBoot(() => describeInWorkerd(moduleCode));
342
330
  }
343
331
 
344
332
  async function describeInWorkerd(
@@ -382,26 +370,6 @@ async function describeInWorkerd(
382
370
  }
383
371
  }
384
372
 
385
- async function bootedWithin(ready: Promise<URL>): Promise<URL> {
386
- let timer: ReturnType<typeof setTimeout> | undefined;
387
- const deadline = new Promise<never>((_, reject) => {
388
- timer = setTimeout(
389
- () =>
390
- reject(
391
- new RuntimeDidNotStart(
392
- `The Workers runtime did not start within ${BOOT_DEADLINE_MS}ms`,
393
- ),
394
- ),
395
- BOOT_DEADLINE_MS,
396
- );
397
- });
398
- try {
399
- return await Promise.race([ready, deadline]);
400
- } finally {
401
- clearTimeout(timer);
402
- }
403
- }
404
-
405
373
  function validateDescription(input: PluginDescriptionV1): PluginDescriptionV1 {
406
374
  if (!Array.isArray(input.tools) || input.tools.length > MAX_TOOLS) {
407
375
  throw new Error(`The Plugin declares more than ${MAX_TOOLS} tools`);
@@ -10,6 +10,8 @@
10
10
 
11
11
  import { convertV4MiniflareOptions, Miniflare } from "miniflare";
12
12
 
13
+ import { bootedWithin } from "./boot.js";
14
+
13
15
  /** Pinned with the SDK: the runtime an Applet is checked against. */
14
16
  export const APPLET_COMPATIBILITY_DATE = "2026-08-27";
15
17
 
@@ -110,7 +112,14 @@ export async function startAppletRuntime(
110
112
  }),
111
113
  );
112
114
 
113
- const url = await miniflare.ready;
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
+ }
114
123
  return {
115
124
  url,
116
125
  fetch: (path, init) =>