@tokamakdev/plugin 0.1.0-beta.81 → 0.1.0-beta.83
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 +67 -0
- package/package.json +2 -2
- package/src/entrypoint.js +12 -0
- package/src/events.ts +35 -0
- package/src/registry.ts +40 -0
- package/src/testing.ts +22 -0
- package/src/worker.ts +0 -17
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# @tokamakdev/plugin
|
|
2
|
+
|
|
3
|
+
The shared base of tokamak plugins: the page's native transport, Worker events,
|
|
4
|
+
and test fakes.
|
|
5
|
+
|
|
6
|
+
| Entry | Contents |
|
|
7
|
+
| --- | --- |
|
|
8
|
+
| `@tokamakdev/plugin` | `FrontendPlugin`, the base class of a plugin's page API |
|
|
9
|
+
| `@tokamakdev/plugin/events` | `defineEvent`, which defines a plugin's Worker events |
|
|
10
|
+
| `@tokamakdev/plugin/testing` | A fake native transport, and `dispatch`, which runs event listeners as the runtime does |
|
|
11
|
+
|
|
12
|
+
A plugin lists `@tokamakdev/plugin` as a peer dependency, so an app has one
|
|
13
|
+
copy of it, shared with `@tokamakdev/tok`, which depends on it.
|
|
14
|
+
|
|
15
|
+
## Worker events
|
|
16
|
+
|
|
17
|
+
A plugin's native code delivers events to the app's Worker, whose listeners
|
|
18
|
+
register in `src/tokamak.ts` and run even when no page is loaded, such as when
|
|
19
|
+
the system starts the app in the background.
|
|
20
|
+
|
|
21
|
+
The plugin defines each event in its own `/events` entry, which apps import:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// events/index.ts, exported as "./events" in package.json
|
|
25
|
+
import { defineEvent } from "@tokamakdev/plugin/events";
|
|
26
|
+
|
|
27
|
+
/** Receives each alarm as it rings. A returned number snoozes it for that many minutes. */
|
|
28
|
+
export const onRing = defineEvent<{ id: string }, number>("alarms.ring");
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`defineEvent<T, Reply>(name)` returns the function that registers listeners,
|
|
32
|
+
typed with the event's data `T` and the `Reply` its listeners may return. The
|
|
33
|
+
name is the plugin's ID, a `.`, and the event's name. The event's reply is the
|
|
34
|
+
first value, in registration order, that a listener returns other than
|
|
35
|
+
`undefined`.
|
|
36
|
+
|
|
37
|
+
The native side emits the event through the host the plugin is created with,
|
|
38
|
+
which prefixes the plugin's ID, so listeners of `alarms.ring` receive
|
|
39
|
+
`host.emit("ring", …)`. Events are JSON:
|
|
40
|
+
|
|
41
|
+
```swift
|
|
42
|
+
host.emit("ring", event: ["id": alarm.id], timeout: 10) { result in
|
|
43
|
+
// `result` is the reply, parsed from JSON, or nil when no listener replies.
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```kotlin
|
|
48
|
+
// Blocks, so call it off the main thread.
|
|
49
|
+
val reply = host.emit("ring", JSONObject().put("id", alarm.id).toString(), 10_000)
|
|
50
|
+
// `reply` is the reply's JSON, "null" when no listener replies.
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`timeout` includes starting the runtime when the app is not running. `emit`
|
|
54
|
+
fails when the Worker cannot run, or when a listener has not returned by the
|
|
55
|
+
timeout; it is not retried. The runtime starts no Worker for an event without
|
|
56
|
+
listeners, so emit each event the plugin defines whether or not the app
|
|
57
|
+
listens. Plugin IDs contain no `.`.
|
|
58
|
+
|
|
59
|
+
Test listeners with `dispatch` from `@tokamakdev/plugin/testing`, which runs
|
|
60
|
+
them as the runtime does and returns what they threw:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { dispatch } from "@tokamakdev/plugin/testing";
|
|
64
|
+
|
|
65
|
+
onRing(() => 5);
|
|
66
|
+
const { reply, reported } = await dispatch("alarms.ring", { id: "a1" }, env);
|
|
67
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokamakdev/plugin",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.83",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/mantty/tokamak.git",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"type": "module",
|
|
10
10
|
"exports": {
|
|
11
11
|
".": "./src/index.ts",
|
|
12
|
-
"./
|
|
12
|
+
"./events": "./src/events.ts",
|
|
13
13
|
"./testing": "./src/testing.ts"
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// The tokamak Vite plugin exports this class from the entry Worker in tokamak builds and
|
|
2
|
+
// development, where the runtime and `tok dev` call `dispatch` to deliver events.
|
|
3
|
+
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
4
|
+
|
|
5
|
+
import { dispatch } from "./registry.ts";
|
|
6
|
+
|
|
7
|
+
export class TokamakEvents extends WorkerEntrypoint {
|
|
8
|
+
/** Runs the listeners of the event `name` and returns `{ reply, listened }`. */
|
|
9
|
+
dispatch(name, event) {
|
|
10
|
+
return dispatch(name, event, this.env, this.ctx);
|
|
11
|
+
}
|
|
12
|
+
}
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { register, type RegisteredListener } from "./registry.js";
|
|
2
|
+
|
|
3
|
+
declare global {
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace -- `wrangler types` declares the Worker's bindings here.
|
|
5
|
+
namespace Cloudflare {
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type -- the app's `wrangler types` declares the members.
|
|
7
|
+
interface Env {}
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** The invocation a listener runs in. */
|
|
12
|
+
export interface EventContext {
|
|
13
|
+
/** Keeps the event running until `promise` settles or the event's deadline passes. */
|
|
14
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Receives an event of type `T` with the Worker's `env`, and may return a `Reply`. */
|
|
18
|
+
export type Listener<T, Reply = void> = (
|
|
19
|
+
event: T,
|
|
20
|
+
env: Cloudflare.Env,
|
|
21
|
+
ctx: EventContext,
|
|
22
|
+
) => Reply | undefined | Promise<Reply | undefined>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Defines the event `name` and returns the function that registers its listeners. The event's
|
|
26
|
+
* reply is the first value, in registration order, that a listener returns other than
|
|
27
|
+
* `undefined`.
|
|
28
|
+
*/
|
|
29
|
+
export function defineEvent<T, Reply = void>(
|
|
30
|
+
name: string,
|
|
31
|
+
): (listener: Listener<T, Reply>) => void {
|
|
32
|
+
return (listener) => {
|
|
33
|
+
register(name, listener as RegisteredListener);
|
|
34
|
+
};
|
|
35
|
+
}
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** A registered listener. */
|
|
2
|
+
export type RegisteredListener = (event: unknown, env: unknown, ctx: unknown) => unknown;
|
|
3
|
+
|
|
4
|
+
/** What a dispatch returns. */
|
|
5
|
+
export interface Dispatched {
|
|
6
|
+
/** The first value, in registration order, that a listener returned other than `undefined`. */
|
|
7
|
+
reply: unknown;
|
|
8
|
+
/** The names of the events that have listeners. */
|
|
9
|
+
listened: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Every listener, by event name, in registration order. */
|
|
13
|
+
const listeners = new Map<string, RegisteredListener[]>();
|
|
14
|
+
|
|
15
|
+
export function register(name: string, listener: RegisteredListener): void {
|
|
16
|
+
const registered = listeners.get(name);
|
|
17
|
+
if (registered) registered.push(listener);
|
|
18
|
+
else listeners.set(name, [listener]);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Runs the listeners of the event `name` and waits for them to settle. A listener that throws is
|
|
23
|
+
* reported with `reportError`, and the others still run.
|
|
24
|
+
*/
|
|
25
|
+
export async function dispatch(
|
|
26
|
+
name: string,
|
|
27
|
+
event: unknown,
|
|
28
|
+
env: unknown,
|
|
29
|
+
ctx: unknown,
|
|
30
|
+
): Promise<Dispatched> {
|
|
31
|
+
const results = await Promise.allSettled(
|
|
32
|
+
(listeners.get(name) ?? []).map(async (listener) => await listener(event, env, ctx)),
|
|
33
|
+
);
|
|
34
|
+
let reply: unknown;
|
|
35
|
+
for (const result of results) {
|
|
36
|
+
if (result.status === "rejected") reportError(result.reason);
|
|
37
|
+
else if (reply === undefined) reply = result.value;
|
|
38
|
+
}
|
|
39
|
+
return { reply, listened: [...listeners.keys()] };
|
|
40
|
+
}
|
package/src/testing.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { dispatch as dispatchEvent, type Dispatched } from "./registry.js";
|
|
2
|
+
|
|
1
3
|
/** A native transport that records the page's messages and replies as the native bridges do. */
|
|
2
4
|
export class FakeNativeTransport {
|
|
3
5
|
/** Every message the page posted, parsed. */
|
|
@@ -37,3 +39,23 @@ export function connectNative(): FakeNativeTransport {
|
|
|
37
39
|
export function disconnectNative(): void {
|
|
38
40
|
Reflect.deleteProperty(globalThis, "__tokamakNative");
|
|
39
41
|
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Runs the listeners of the event `name` as the runtime does, returning the reply, the events
|
|
45
|
+
* that have listeners, and what listeners threw, which the runtime reports with `reportError`.
|
|
46
|
+
*/
|
|
47
|
+
export async function dispatch(
|
|
48
|
+
name: string,
|
|
49
|
+
event: unknown,
|
|
50
|
+
env: unknown = {},
|
|
51
|
+
ctx: unknown = { waitUntil: () => undefined },
|
|
52
|
+
): Promise<Dispatched & { reported: unknown[] }> {
|
|
53
|
+
const reported: unknown[] = [];
|
|
54
|
+
const previous = Reflect.get(globalThis, "reportError") as unknown;
|
|
55
|
+
globalThis.reportError = (error: unknown) => reported.push(error);
|
|
56
|
+
try {
|
|
57
|
+
return { ...(await dispatchEvent(name, event, env, ctx)), reported };
|
|
58
|
+
} finally {
|
|
59
|
+
Reflect.set(globalThis, "reportError", previous);
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/worker.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
declare const process:
|
|
2
|
-
| { readonly env: { readonly NODE_ENV?: string; readonly TOKAMAK_RUNTIME?: string } }
|
|
3
|
-
| undefined;
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Whether the Worker serves the tokamak runtime's calls to endpoints under `/tokamak/`: in a
|
|
7
|
-
* tokamak app, where the runtime sets `TOKAMAK_RUNTIME`, and in development. Those endpoints
|
|
8
|
-
* refuse requests otherwise.
|
|
9
|
-
*/
|
|
10
|
-
export function acceptsRuntimeCalls(): boolean {
|
|
11
|
-
// One expression: Rollup 4.63.0 drops callers' checks when this is an early return followed
|
|
12
|
-
// by a return of `typeof process !== "undefined" && false`.
|
|
13
|
-
return (
|
|
14
|
-
typeof process !== "undefined" &&
|
|
15
|
-
(process.env.NODE_ENV === "development" || process.env.TOKAMAK_RUNTIME === "true")
|
|
16
|
-
);
|
|
17
|
-
}
|