@frockbot/applet-sdk 0.7.178 → 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/README.md +83 -99
- package/package.json +1 -1
- package/src/build/plugin.ts +53 -21
- package/src/build/boot.ts +0 -75
- package/src/build/runtime.ts +0 -132
package/README.md
CHANGED
|
@@ -1,113 +1,97 @@
|
|
|
1
1
|
# @frockbot/applet-sdk
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
kit on the theme tokens, a linter, and the build pipeline the cloud build
|
|
6
|
-
service runs.
|
|
7
|
-
|
|
8
|
-
An Applet is authored with the `applet_*` tools, built by `apps/applet-build`,
|
|
9
|
-
and mounted as a Durable Object facet from an immutable artifact. There is no
|
|
10
|
-
CLI: nothing outside the service builds an Applet, and no Computer is involved
|
|
11
|
-
at any point.
|
|
3
|
+
What a FrockBot Plugin is written against, and the build that turns a
|
|
4
|
+
Plugin's source into the module and manifest a publish stores (ADR 0026).
|
|
12
5
|
|
|
13
6
|
## Entry points
|
|
14
7
|
|
|
15
|
-
| Import | For
|
|
16
|
-
| ----------------------------------- |
|
|
17
|
-
| `@frockbot/applet-sdk/
|
|
18
|
-
| `@frockbot/applet-sdk/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
8
|
+
| Import | For |
|
|
9
|
+
| ----------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
10
|
+
| `@frockbot/applet-sdk/plugin` | types only: `PluginModule`, `PluginContext` and the rest — a Plugin's `plugin.ts` |
|
|
11
|
+
| `@frockbot/applet-sdk/build/plugin` | `runPluginBuildV1` — the four stages, for the build service and the seeded Plugins' script |
|
|
12
|
+
|
|
13
|
+
## A Plugin
|
|
14
|
+
|
|
15
|
+
A Plugin is a directory: a `plugin.json` descriptor, a `plugin.ts` module,
|
|
16
|
+
and any `.ts` files beside it that the module imports. `plugin.ts` has no
|
|
17
|
+
default export. It exports `tools` and `execute` by name, and may export
|
|
18
|
+
`hooks`, `services`, `triggers`, `views`, `cards` and `modelProviders`
|
|
19
|
+
(`PluginModule`). `tools` may be empty: a Plugin that only serves hooks
|
|
20
|
+
builds.
|
|
21
|
+
|
|
22
|
+
`@frockbot/applet-sdk/plugin` is declarations only, so it is imported with
|
|
23
|
+
`import type`; a value import of it fails the bundle stage.
|
|
24
|
+
`app/plugins/sdk-types.test.ts` pins its `PluginContext`, hook events, grants
|
|
25
|
+
and hook payloads to the kernel's own types, so a Plugin that type-checks
|
|
26
|
+
here sees the `ctx` the kernel builds.
|
|
27
|
+
|
|
28
|
+
A model provider (`PluginModelProvider`, ADR 0032) answers a normalized model
|
|
29
|
+
request with normalized stream events, and makes its one upstream call
|
|
30
|
+
through `ctx.modelTransport`. The deployment serves a provider only from the
|
|
31
|
+
artifact its own provider catalog names, so this is not a way for a
|
|
32
|
+
Bot-written Plugin to reach a provider.
|
|
33
|
+
|
|
34
|
+
`plugin/template/` is the scaffold a new Plugin starts as, with
|
|
35
|
+
`__PLUGIN_ID__` and `__PLUGIN_NAME__` for `plugin_create` to fill in.
|
|
36
|
+
`scripts/build-applets-assets.ts` carries it into the Worker as
|
|
37
|
+
`app/plugins/template.generated.ts`, and carries `plugin/index.d.ts` into the
|
|
38
|
+
Plugins Skill as `app/plugins/skills/plugins/references/types.md`.
|
|
25
39
|
|
|
26
40
|
## The build
|
|
27
41
|
|
|
28
|
-
`
|
|
29
|
-
directory
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
a
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
is
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
`
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
`
|
|
71
|
-
|
|
72
|
-
The Cloudflare programming model is not hidden: an Applet is a Durable Object
|
|
73
|
-
with SQLite and hibernating sockets. What the SDK does hide is every binding
|
|
74
|
-
name — an author sees `tables`, `tools`, and `this.db`.
|
|
75
|
-
|
|
76
|
-
## Wire protocol
|
|
77
|
-
|
|
78
|
-
JSON frames, at most 64 KB each, decoded by `src/protocol/` at both ends;
|
|
79
|
-
an unknown type, field, or table fails closed. Two versions are spoken on the
|
|
80
|
-
same server, told apart by the socket URL: a page built against v2 opens with
|
|
81
|
-
`v=2`, and a page built before it opens with nothing and is spoken to in v1.
|
|
82
|
-
|
|
83
|
-
| Direction | Frame | Carries |
|
|
84
|
-
| --------------- | ---------- | ------------------------------------------------------------------------------------------- |
|
|
85
|
-
| server → client | `hello` | contract, generationId, viewer, tables, revision, cursor — and in v2, the `snapshot` itself |
|
|
86
|
-
| client → server | `hello` | contract, optional `since` cursor for catch-up; in v2 only on a resume or when asked |
|
|
87
|
-
| server → client | `snapshot` | every row of every table, plus the cursor |
|
|
88
|
-
| server → client | `changes` | ordered row changes, optionally tagged with a client txn id |
|
|
89
|
-
| client → server | `mutate` | one client transaction: insert/update/delete |
|
|
90
|
-
| server → client | `ack` | the resulting rows for that txn |
|
|
91
|
-
| server → client | `reject` | why the txn was refused (the client rolls back) |
|
|
92
|
-
|
|
93
|
-
The host hands the page its credential in an `init` postMessage, and a fresh
|
|
94
|
-
credential later in a `refresh` of the same shape; the page reconnects in
|
|
95
|
-
place rather than being reloaded, and with its cursor on the URL that is the
|
|
96
|
-
`changes` path.
|
|
97
|
-
|
|
98
|
-
A v2 page's first render waits on one frame: the server's `hello` carries the
|
|
99
|
-
snapshot when the URL named no `since` cursor, and the page marks its
|
|
100
|
-
collections ready on it. A reconnect puts `since` on the URL, gets a plain
|
|
101
|
-
`hello`, and asks for `changes` as v1 does. A snapshot that would not fit the
|
|
102
|
-
frame is left out of the hello and the v1 exchange follows.
|
|
42
|
+
`runPluginBuildV1(directory, { mode, id })` is four named stages over one
|
|
43
|
+
directory. A stage that fails stops the run and names itself, with a list of
|
|
44
|
+
`{file, line, column, message, severity}` diagnostics. `check` stops after
|
|
45
|
+
the type checker; `build` goes on to the module and its manifest.
|
|
46
|
+
|
|
47
|
+
1. `descriptor`: `plugin.json` is a JSON object whose `id` matches
|
|
48
|
+
`/^[a-z][a-z0-9-]{0,63}$/` and, when the caller passes `id`, is that id.
|
|
49
|
+
The build reads nothing else from it. The app Worker decodes the full
|
|
50
|
+
descriptor and refuses a publish whose descriptor and manifest disagree
|
|
51
|
+
(`pluginManifestDisagreementV1` in `app/plugins/authoring.ts`).
|
|
52
|
+
2. `typecheck`: every `.ts` file in the directory, strict, against ES2022
|
|
53
|
+
and the DOM lib for `fetch`, `Request` and `Response`, with
|
|
54
|
+
`@frockbot/applet-sdk/plugin` resolved to `plugin/index.d.ts`. The
|
|
55
|
+
directory must hold a `plugin.ts`. Only errors fail the stage.
|
|
56
|
+
3. `bundle`: esbuild makes one unminified ESM module with every import
|
|
57
|
+
inlined. Nothing is external, so a specifier the bundler cannot inline
|
|
58
|
+
fails here, not at mount. `module-paths.ts` rewrites esbuild's module-path
|
|
59
|
+
comments relative to the Plugin's directory, so the same source builds to
|
|
60
|
+
the same bytes wherever it is built.
|
|
61
|
+
4. `describe`: the bundle runs in Miniflare beside a describing Worker, with
|
|
62
|
+
no bindings and no outbound network: every `fetch` is answered with a 403.
|
|
63
|
+
Import-time code runs inside workerd, never in the build's own process.
|
|
64
|
+
What the module exports is the manifest. `tools` must be an array and
|
|
65
|
+
`execute` a function; each tool needs a name matching
|
|
66
|
+
`/^[a-z][a-z0-9_]{0,63}$/` and a description; `hooks`, `triggers` and
|
|
67
|
+
`views` hold functions, `services` any values, each card a `render` and
|
|
68
|
+
each model provider a `stream`. A Plugin declares at most 64 tools, each
|
|
69
|
+
name once.
|
|
70
|
+
|
|
71
|
+
Each describe spawns its own workerd, and `boot.ts` bounds the boot. A
|
|
72
|
+
runtime that is not ready within `BOOT_DEADLINE_MS` (30 seconds), or whose
|
|
73
|
+
spawn fails outright, is a `RuntimeDidNotStart`. The build lets that runtime
|
|
74
|
+
go rather than waiting on it and boots once more; if the second boot does not
|
|
75
|
+
come up either, the stage fails with that as its diagnostic.
|
|
76
|
+
|
|
77
|
+
A build answers the module text and its manifest:
|
|
78
|
+
`{ contract: 1, tools, hooks, services, triggers, views, cards, modelProviders, hashes: { module } }`,
|
|
79
|
+
where `hashes.module` is the SHA-256 of the module.
|
|
80
|
+
|
|
81
|
+
Two callers run it. The build service in `apps/applet-build` runs `check`
|
|
82
|
+
for `plugin_check` and `build` for `plugin_publish`.
|
|
83
|
+
`scripts/build-seeded-plugins.ts` builds each Plugin under
|
|
84
|
+
`app/plugins/seeded/` into the Worker bundle through the same stages.
|
|
103
85
|
|
|
104
86
|
## Tests
|
|
105
87
|
|
|
106
88
|
```sh
|
|
107
|
-
bun
|
|
89
|
+
bun run test
|
|
108
90
|
```
|
|
109
91
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
92
|
+
`test/plugin-build.test.ts` runs `runPluginBuildV1` over the real template,
|
|
93
|
+
which `test/plugin-scaffold.ts` fills in and writes to a temporary directory.
|
|
94
|
+
It covers each stage's failure and diagnostics, identical module bytes from
|
|
95
|
+
different and symlinked roots, and the manifest read off each kind of export.
|
|
96
|
+
Every build test boots workerd through Miniflare, so a run needs to bind a
|
|
97
|
+
local port. The root `bun test` runs this file too.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/applet-sdk",
|
|
3
|
-
"version": "0.7.
|
|
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.",
|
package/src/build/plugin.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Plugin build (ADR 0026): four named stages over one directory
|
|
3
|
-
* the Applet build's type checker, bundler and Miniflare boot.
|
|
2
|
+
* The Plugin build (ADR 0026): four named stages over one directory.
|
|
4
3
|
*
|
|
5
4
|
* 1. `descriptor` — `plugin.json` parses and names the Plugin the caller
|
|
6
5
|
* asked for. Nothing more is decided here: the app Worker holds the full
|
|
@@ -29,11 +28,42 @@ import { convertV4MiniflareOptions, Miniflare } from "miniflare";
|
|
|
29
28
|
import ts from "typescript";
|
|
30
29
|
|
|
31
30
|
import type { AppletDiagnostic } from "../lint/index.js";
|
|
32
|
-
import { bootedWithin, withOneMoreBoot } from "./boot.js";
|
|
33
31
|
import { stableModulePaths } from "./module-paths.js";
|
|
34
|
-
import { APPLET_COMPATIBILITY_DATE } from "./runtime.js";
|
|
35
32
|
import { SDK_PLUGIN_TYPES } from "./paths.js";
|
|
36
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
|
+
|
|
37
67
|
export type PluginBuildStage =
|
|
38
68
|
"descriptor" | "typecheck" | "bundle" | "describe";
|
|
39
69
|
|
|
@@ -348,17 +378,11 @@ export default {
|
|
|
348
378
|
`;
|
|
349
379
|
|
|
350
380
|
/**
|
|
351
|
-
* Ask the built module what it exports, by running it.
|
|
352
|
-
*
|
|
353
|
-
*
|
|
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.
|
|
354
384
|
*/
|
|
355
|
-
export function describePlugin(
|
|
356
|
-
moduleCode: string,
|
|
357
|
-
): Promise<PluginDescriptionV1> {
|
|
358
|
-
return withOneMoreBoot(() => describeInWorkerd(moduleCode));
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
async function describeInWorkerd(
|
|
385
|
+
export async function describePlugin(
|
|
362
386
|
moduleCode: string,
|
|
363
387
|
): Promise<PluginDescriptionV1> {
|
|
364
388
|
const miniflare = new Miniflare(
|
|
@@ -368,7 +392,7 @@ async function describeInWorkerd(
|
|
|
368
392
|
{ type: "ESModule", path: "/plugin.js", contents: moduleCode },
|
|
369
393
|
],
|
|
370
394
|
modulesRoot: "/",
|
|
371
|
-
compatibilityDate:
|
|
395
|
+
compatibilityDate: PLUGIN_COMPATIBILITY_DATE,
|
|
372
396
|
// Import-time code runs with no way out: every fetch is answered here.
|
|
373
397
|
outboundService: async () =>
|
|
374
398
|
new Response("the build describes a Plugin without a network", {
|
|
@@ -378,10 +402,12 @@ async function describeInWorkerd(
|
|
|
378
402
|
port: 0,
|
|
379
403
|
}),
|
|
380
404
|
);
|
|
381
|
-
let started = false;
|
|
382
405
|
try {
|
|
383
|
-
const url = await
|
|
384
|
-
|
|
406
|
+
const url = await within(
|
|
407
|
+
miniflare.ready,
|
|
408
|
+
BOOT_DEADLINE_MS,
|
|
409
|
+
"The Workers runtime did not start",
|
|
410
|
+
);
|
|
385
411
|
const response = (await miniflare.dispatchFetch(
|
|
386
412
|
new URL(`/describe?${randomUUID()}`, url).toString(),
|
|
387
413
|
)) as unknown as Response;
|
|
@@ -393,9 +419,15 @@ async function describeInWorkerd(
|
|
|
393
419
|
}
|
|
394
420
|
return validateDescription(body.description);
|
|
395
421
|
} finally {
|
|
396
|
-
//
|
|
397
|
-
|
|
398
|
-
|
|
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
|
+
);
|
|
399
431
|
}
|
|
400
432
|
}
|
|
401
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
|
-
}
|
package/src/build/runtime.ts
DELETED
|
@@ -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
|
-
}
|