@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 +1 -1
- package/plugin/index.d.ts +30 -4
- package/src/build/artifacts.ts +13 -2
- package/src/build/boot.ts +46 -0
- package/src/build/plugin.ts +6 -38
- package/src/build/runtime.ts +10 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/applet-sdk",
|
|
3
|
-
"version": "0.7.
|
|
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
|
-
/**
|
|
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
|
-
|
|
370
|
+
delivery: PluginTriggerDelivery,
|
|
353
371
|
ctx: PluginContext,
|
|
354
|
-
) =>
|
|
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?:
|
|
411
|
+
triggers?: PluginTriggers;
|
|
386
412
|
}
|
package/src/build/artifacts.ts
CHANGED
|
@@ -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
|
-
/**
|
|
164
|
-
|
|
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
|
+
}
|
package/src/build/plugin.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
326
|
+
export function describePlugin(
|
|
334
327
|
moduleCode: string,
|
|
335
328
|
): Promise<PluginDescriptionV1> {
|
|
336
|
-
|
|
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`);
|
package/src/build/runtime.ts
CHANGED
|
@@ -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
|
-
|
|
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) =>
|