@tokamakdev/tok 0.1.1-beta.107 → 0.1.1-beta.112
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 +100 -10
- package/dist/astro.d.mts +5 -0
- package/dist/astro.mjs +160 -0
- package/dist/client.d.mts +6 -0
- package/dist/client.mjs +100 -0
- package/dist/dev-socket.d.mts +3 -6
- package/dist/dev-socket.mjs +167 -96
- package/dist/dev-worker.d.mts +21 -0
- package/dist/dev-worker.mjs +88 -0
- package/dist/events.d.mts +3 -3
- package/dist/events.mjs +3 -13
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +1 -1
- package/dist/native-worker.d.mts +12 -0
- package/dist/native-worker.mjs +101 -0
- package/dist/native.d.mts +17 -4
- package/dist/native.mjs +99 -52
- package/dist/plugin-manifest.d.mts +31 -0
- package/dist/plugin-manifest.mjs +96 -0
- package/dist/rpc-contract.d.mts +9 -0
- package/dist/rpc-contract.mjs +168 -0
- package/dist/service-types.d.mts +9 -0
- package/dist/service-types.mjs +64 -0
- package/dist/vite.d.mts +6 -17
- package/dist/vite.mjs +30 -208
- package/dist/worker-contract.d.mts +25 -0
- package/dist/worker-contract.mjs +343 -0
- package/dist/worker-graph.d.mts +43 -0
- package/dist/worker-graph.mjs +332 -0
- package/dist/worker-types.d.mts +46 -0
- package/dist/worker-types.mjs +112 -0
- package/package.json +28 -17
package/dist/dev-socket.mjs
CHANGED
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { Readable } from "node:stream";
|
|
3
|
+
import { pipeline } from "node:stream/promises";
|
|
4
4
|
import { WebSocketServer } from "ws";
|
|
5
|
+
import { decodeMessage, encodeMessage, messages } from "@tokamakdev/plugin/native-protocol";
|
|
6
|
+
import { DevWorker } from "./dev-worker.mjs";
|
|
5
7
|
/** The header that carries the device's session token, which `tok dev`'s relay forwards. */
|
|
6
8
|
export const SESSION_HEADER = "x-tokamak-session";
|
|
7
9
|
/** How long a plugin call waits for the device to connect. */
|
|
8
10
|
const CONNECT_TIMEOUT = 10_000;
|
|
9
|
-
/** Calls `TokamakEvents` in the Worker its `TOKAMAK` service binding names, through the dev registry. */
|
|
10
|
-
const CALLER = `export default {
|
|
11
|
-
async fetch(request, env) {
|
|
12
|
-
const { name, event } = await request.json();
|
|
13
|
-
try {
|
|
14
|
-
const { reply } = await env.TOKAMAK.dispatch(name, event);
|
|
15
|
-
return Response.json({ reply: reply ?? null });
|
|
16
|
-
} catch (error) {
|
|
17
|
-
return new Response(error?.stack ?? String(error), { status: 500 });
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
};`;
|
|
21
11
|
/**
|
|
22
12
|
* Listens on a loopback port, with `token`, for the device's dev WebSocket and the development
|
|
23
13
|
* Worker's plugin calls. Delivers each event the device sends to `TokamakEvents` in the
|
|
@@ -32,6 +22,10 @@ export async function openDevSocket(workerName, token) {
|
|
|
32
22
|
response.writeHead(401).end();
|
|
33
23
|
return;
|
|
34
24
|
}
|
|
25
|
+
if (request.url?.startsWith("/_tokamak/")) {
|
|
26
|
+
void forwardWorker(worker, request, response);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
35
29
|
if (request.method === "POST" && (request.url === "/call" || request.url === "/subscribe")) {
|
|
36
30
|
void forward(device, request, response);
|
|
37
31
|
return;
|
|
@@ -44,6 +38,10 @@ export async function openDevSocket(workerName, token) {
|
|
|
44
38
|
socket.end("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
|
|
45
39
|
return;
|
|
46
40
|
}
|
|
41
|
+
if (request.url?.startsWith("/_tokamak/")) {
|
|
42
|
+
void upgradeWorker(worker, request, socket, head, sockets);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
47
45
|
sockets.handleUpgrade(request, socket, head, (websocket) => serve(websocket, worker, device));
|
|
48
46
|
});
|
|
49
47
|
await new Promise((resolve, reject) => {
|
|
@@ -61,70 +59,175 @@ export async function openDevSocket(workerName, token) {
|
|
|
61
59
|
},
|
|
62
60
|
};
|
|
63
61
|
}
|
|
62
|
+
async function forwardWorker(worker, request, response) {
|
|
63
|
+
const abort = new AbortController();
|
|
64
|
+
response.once("close", () => abort.abort());
|
|
65
|
+
try {
|
|
66
|
+
const result = await worker.fetch(request, abort.signal);
|
|
67
|
+
response.writeHead(result.status, [...result.headers].flat());
|
|
68
|
+
if (result.body)
|
|
69
|
+
await pipeline(Readable.fromWeb(result.body), response);
|
|
70
|
+
else
|
|
71
|
+
response.end();
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (response.headersSent)
|
|
75
|
+
response.destroy(error instanceof Error ? error : new Error(String(error)));
|
|
76
|
+
else
|
|
77
|
+
response.writeHead(502).end(String(error));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function upgradeWorker(worker, request, socket, head, sockets) {
|
|
81
|
+
const abort = new AbortController();
|
|
82
|
+
let pending;
|
|
83
|
+
const release = () => {
|
|
84
|
+
const pair = pending;
|
|
85
|
+
pending = undefined;
|
|
86
|
+
if (!pair)
|
|
87
|
+
return;
|
|
88
|
+
pair.accept();
|
|
89
|
+
pair.close(1000, "Caller disconnected");
|
|
90
|
+
};
|
|
91
|
+
socket.once("close", () => { abort.abort(); release(); });
|
|
92
|
+
socket.once("error", () => socket.destroy());
|
|
93
|
+
try {
|
|
94
|
+
const result = await worker.fetch(request, abort.signal);
|
|
95
|
+
const pair = result.webSocket;
|
|
96
|
+
if (!pair) {
|
|
97
|
+
socket.end(`HTTP/1.1 ${result.status} ${result.statusText}\r\nConnection: close\r\n\r\n`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
pending = pair;
|
|
101
|
+
if (socket.destroyed) {
|
|
102
|
+
release();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
sockets.handleUpgrade(request, socket, head, websocket => {
|
|
106
|
+
void worker.couple(websocket, pair).then(() => { pending = undefined; }, () => { release(); websocket.terminate(); });
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
release();
|
|
111
|
+
socket.end("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
64
114
|
function serve(websocket, worker, device) {
|
|
65
115
|
device.connect(websocket);
|
|
66
116
|
// The device opens another dev WebSocket after this one fails.
|
|
67
117
|
websocket.on("error", () => websocket.terminate());
|
|
68
118
|
websocket.on("message", (data) => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
119
|
+
let message;
|
|
120
|
+
try {
|
|
121
|
+
const bytes = Array.isArray(data) ? Buffer.concat(data) : new Uint8Array(data);
|
|
122
|
+
message = decodeMessage(bytes);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
websocket.close(1002, "Invalid native message");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const { header, payload } = message;
|
|
129
|
+
if (header.type === "result" && header.id !== undefined) {
|
|
130
|
+
device.receive(header.id, { payload, failed: header.failed === true, done: header.done === true });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (header.type === "pull" && header.id !== undefined) {
|
|
134
|
+
device.pull(header.id);
|
|
72
135
|
return;
|
|
73
136
|
}
|
|
74
|
-
if (
|
|
137
|
+
if (header.type !== "event" || header.id === undefined || !header.name)
|
|
75
138
|
return;
|
|
76
|
-
|
|
139
|
+
const send = (type, value) => {
|
|
140
|
+
if (websocket.readyState === websocket.OPEN)
|
|
141
|
+
websocket.send(encodeMessage({ type, id: header.id }, value));
|
|
142
|
+
};
|
|
143
|
+
worker.dispatch(header.name, payload).then((reply) => send("reply", reply), (error) => send("error", encoder.encode(String(error))));
|
|
77
144
|
});
|
|
78
145
|
}
|
|
146
|
+
const encoder = new TextEncoder();
|
|
79
147
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* subscription.
|
|
148
|
+
* Relays opaque endpoint payloads; closing the response cancels the native request. A call with a
|
|
149
|
+
* body sends its later messages to the device as the device pulls them.
|
|
83
150
|
*/
|
|
84
151
|
async function forward(device, request, response) {
|
|
152
|
+
const frames = messages(request);
|
|
85
153
|
let call;
|
|
86
154
|
try {
|
|
87
|
-
|
|
155
|
+
const first = await frames.next();
|
|
156
|
+
if (first.done)
|
|
157
|
+
throw new TypeError("Missing native request");
|
|
158
|
+
call = first.value;
|
|
159
|
+
if (!call.header.plugin || !call.header.method)
|
|
160
|
+
throw new TypeError("Missing native endpoint");
|
|
88
161
|
}
|
|
89
162
|
catch {
|
|
90
163
|
response.writeHead(400).end();
|
|
91
164
|
return;
|
|
92
165
|
}
|
|
93
|
-
response.writeHead(200, { "content-type": "application/
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
response.
|
|
99
|
-
const cancel = device.send(
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
166
|
+
response.writeHead(200, { "content-type": "application/octet-stream" });
|
|
167
|
+
const subscribe = request.url === "/subscribe";
|
|
168
|
+
const type = subscribe ? "subscribe" : call.header.type === "callWithBody" ? "callWithBody" : "call";
|
|
169
|
+
if (subscribe)
|
|
170
|
+
response.flushHeaders();
|
|
171
|
+
const write = subscribe ? subscriptionWriter(response, call.header.overflow, () => { cancel(); }) : undefined;
|
|
172
|
+
const cancel = device.send(type, call, ({ payload, failed, done }) => {
|
|
173
|
+
const frame = encodeMessage({ type: "result", failed, done }, payload);
|
|
174
|
+
if (write)
|
|
175
|
+
write(frame, done);
|
|
176
|
+
else
|
|
177
|
+
response.end(frame);
|
|
178
|
+
}, async () => {
|
|
179
|
+
const next = await frames.next().catch(() => ({ done: true, value: undefined }));
|
|
180
|
+
return next.done ? failure("DataCloneError", "The call's body ended early") : chunkResult(next.value);
|
|
103
181
|
});
|
|
104
182
|
response.once("close", () => {
|
|
105
183
|
if (!response.writableEnded)
|
|
106
184
|
cancel();
|
|
107
185
|
});
|
|
108
186
|
}
|
|
109
|
-
function
|
|
110
|
-
return
|
|
111
|
-
const chunks = [];
|
|
112
|
-
request.on("data", (chunk) => chunks.push(chunk));
|
|
113
|
-
request.once("end", () => resolve(Buffer.concat(chunks).toString()));
|
|
114
|
-
request.once("error", reject);
|
|
115
|
-
});
|
|
187
|
+
function chunkResult({ header, payload }) {
|
|
188
|
+
return { payload, failed: header.failed === true, done: header.done === true };
|
|
116
189
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
190
|
+
/** The most a subscription's response holds while the Worker is not reading it. */
|
|
191
|
+
const SUBSCRIPTION_BUFFER = 1024 * 1024;
|
|
192
|
+
/**
|
|
193
|
+
* Writes a subscription's frames to `response`, holding those it cannot take yet. Past
|
|
194
|
+
* `SUBSCRIPTION_BUFFER`, `overflow` drops the oldest held frame or ends the subscription with
|
|
195
|
+
* `fail`.
|
|
196
|
+
*/
|
|
197
|
+
function subscriptionWriter(response, overflow, fail) {
|
|
198
|
+
const held = [];
|
|
199
|
+
let bytes = 0;
|
|
200
|
+
let writable = true;
|
|
201
|
+
const flush = () => {
|
|
202
|
+
while (writable && held.length) {
|
|
203
|
+
const { frame, done } = held.shift();
|
|
204
|
+
bytes -= frame.byteLength;
|
|
205
|
+
if (done)
|
|
206
|
+
response.end(frame);
|
|
207
|
+
else
|
|
208
|
+
writable = response.write(frame);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
response.on("drain", () => {
|
|
212
|
+
writable = true;
|
|
213
|
+
flush();
|
|
214
|
+
});
|
|
215
|
+
return (frame, done) => {
|
|
216
|
+
held.push({ frame, done });
|
|
217
|
+
bytes += frame.byteLength;
|
|
218
|
+
while (bytes > SUBSCRIPTION_BUFFER && held.length > 1) {
|
|
219
|
+
if (overflow !== "dropOldest") {
|
|
220
|
+
response.destroy(new Error("Native subscription consumer is too slow"));
|
|
221
|
+
fail();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
bytes -= held.shift().frame.byteLength;
|
|
225
|
+
}
|
|
226
|
+
flush();
|
|
227
|
+
};
|
|
124
228
|
}
|
|
125
|
-
/** A failed result, which ends a subscription. */
|
|
126
229
|
function failure(name, message) {
|
|
127
|
-
return {
|
|
230
|
+
return { payload: encoder.encode(JSON.stringify({ name, message })), failed: true, done: true };
|
|
128
231
|
}
|
|
129
232
|
/** The device, through its latest dev WebSocket, and the plugin calls waiting for its results. */
|
|
130
233
|
class Device {
|
|
@@ -151,15 +254,24 @@ class Device {
|
|
|
151
254
|
const pending = this.#pending.get(id);
|
|
152
255
|
if (!pending)
|
|
153
256
|
return;
|
|
154
|
-
if (result.done || pending.type
|
|
257
|
+
if (result.done || pending.type !== "subscribe")
|
|
155
258
|
this.#pending.delete(id);
|
|
156
259
|
pending.receive(result);
|
|
157
260
|
}
|
|
261
|
+
/** Sends the device the next chunk of the body of the call `id`, which it pulled. */
|
|
262
|
+
pull(id) {
|
|
263
|
+
const pending = this.#pending.get(id);
|
|
264
|
+
void pending?.pull?.().then(({ payload, failed, done }) => {
|
|
265
|
+
if (this.#pending.get(id) === pending)
|
|
266
|
+
pending.socket.send(encodeMessage({ type: "chunk", id, failed, done }, payload));
|
|
267
|
+
});
|
|
268
|
+
}
|
|
158
269
|
/**
|
|
159
270
|
* Sends a call or subscription once the device is connected, waiting up to 10 seconds for it,
|
|
160
|
-
* and passes each of its results to `receive
|
|
271
|
+
* and passes each of its results to `receive`; `pull` reads the next chunk of a call's body.
|
|
272
|
+
* Returns the function that cancels the request.
|
|
161
273
|
*/
|
|
162
|
-
send(type, request, receive) {
|
|
274
|
+
send(type, request, receive, pull) {
|
|
163
275
|
const id = this.#nextId++;
|
|
164
276
|
let cancelled = false;
|
|
165
277
|
void this.#connection().then((socket) => {
|
|
@@ -169,8 +281,8 @@ class Device {
|
|
|
169
281
|
receive(failure("NetworkError", "The device is not connected"));
|
|
170
282
|
return;
|
|
171
283
|
}
|
|
172
|
-
this.#pending.set(id, { type, socket, receive });
|
|
173
|
-
socket.send(
|
|
284
|
+
this.#pending.set(id, { type, socket, receive, pull });
|
|
285
|
+
socket.send(encodeMessage({ ...request.header, type, id }, request.payload));
|
|
174
286
|
});
|
|
175
287
|
return () => {
|
|
176
288
|
cancelled = true;
|
|
@@ -178,7 +290,7 @@ class Device {
|
|
|
178
290
|
if (!pending)
|
|
179
291
|
return;
|
|
180
292
|
this.#pending.delete(id);
|
|
181
|
-
pending.socket.send(
|
|
293
|
+
pending.socket.send(encodeMessage({ type: "cancel", id }));
|
|
182
294
|
};
|
|
183
295
|
}
|
|
184
296
|
/** The open dev WebSocket, once the device connects, or undefined after 10 seconds without. */
|
|
@@ -198,44 +310,3 @@ class Device {
|
|
|
198
310
|
});
|
|
199
311
|
}
|
|
200
312
|
}
|
|
201
|
-
/** The development Worker's `TokamakEvents`, called through a Miniflare session started on first use. */
|
|
202
|
-
class DevWorker {
|
|
203
|
-
name;
|
|
204
|
-
#miniflare;
|
|
205
|
-
constructor(name) {
|
|
206
|
-
this.name = name;
|
|
207
|
-
}
|
|
208
|
-
async dispatch(name, event) {
|
|
209
|
-
this.#miniflare ??= this.#start().catch((error) => {
|
|
210
|
-
this.#miniflare = undefined;
|
|
211
|
-
throw error;
|
|
212
|
-
});
|
|
213
|
-
const miniflare = await this.#miniflare;
|
|
214
|
-
const response = await miniflare.dispatchFetch("http://tokamak/", {
|
|
215
|
-
method: "POST",
|
|
216
|
-
body: JSON.stringify({ name, event }),
|
|
217
|
-
});
|
|
218
|
-
if (!response.ok)
|
|
219
|
-
throw new Error(await response.text());
|
|
220
|
-
return (await response.json()).reply;
|
|
221
|
-
}
|
|
222
|
-
async dispose() {
|
|
223
|
-
await (await this.#miniflare?.catch(() => undefined))?.dispose();
|
|
224
|
-
}
|
|
225
|
-
/** A Miniflare session from the Miniflare that Cloudflare's plugin uses. */
|
|
226
|
-
async #start() {
|
|
227
|
-
if (!this.name)
|
|
228
|
-
throw new Error("the Wrangler configuration names no Worker to deliver events to");
|
|
229
|
-
const require = createRequire(import.meta.resolve("@cloudflare/vite-plugin"));
|
|
230
|
-
const module = (await import(pathToFileURL(require.resolve("miniflare")).href));
|
|
231
|
-
const miniflare = new module.Miniflare(module.convertV4MiniflareOptions({
|
|
232
|
-
modules: true,
|
|
233
|
-
script: CALLER,
|
|
234
|
-
compatibilityDate: "2025-01-01",
|
|
235
|
-
serviceBindings: { TOKAMAK: { name: this.name, entrypoint: "TokamakEvents" } },
|
|
236
|
-
unsafeDevRegistryPath: module.getDefaultDevRegistryPath(),
|
|
237
|
-
}));
|
|
238
|
-
await miniflare.ready;
|
|
239
|
-
return miniflare;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { IncomingMessage } from "node:http";
|
|
2
|
+
import type { WebSocket } from "ws";
|
|
3
|
+
export interface WorkerSocket {
|
|
4
|
+
accept(): void;
|
|
5
|
+
close(code: number, reason: string): void;
|
|
6
|
+
}
|
|
7
|
+
interface WorkerResponse extends Response {
|
|
8
|
+
webSocket?: WorkerSocket | null;
|
|
9
|
+
}
|
|
10
|
+
/** The native Vite worker, reached through Miniflare's development registry. */
|
|
11
|
+
export declare class DevWorker {
|
|
12
|
+
#private;
|
|
13
|
+
private readonly name;
|
|
14
|
+
constructor(name: string | undefined);
|
|
15
|
+
/** Delivers the event `name` with its encoded data, resolving to the encoded reply. */
|
|
16
|
+
dispatch(name: string, event: Uint8Array): Promise<Uint8Array>;
|
|
17
|
+
fetch(request: IncomingMessage, signal: AbortSignal): Promise<WorkerResponse>;
|
|
18
|
+
couple(socket: WebSocket, pair: WorkerSocket): Promise<void>;
|
|
19
|
+
dispose(): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { Readable } from "node:stream";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
const CALLER = `export default {
|
|
5
|
+
async fetch(request, env) {
|
|
6
|
+
if (new URL(request.url).pathname.startsWith("/_tokamak/")) return env.TOKAMAK.fetch(request);
|
|
7
|
+
const name = request.headers.get("x-tokamak-event");
|
|
8
|
+
try {
|
|
9
|
+
const { reply } = await env.EVENTS.dispatch(name, new Uint8Array(await request.arrayBuffer()));
|
|
10
|
+
return new Response(reply);
|
|
11
|
+
} catch (error) {
|
|
12
|
+
return new Response(error?.stack ?? String(error), { status: 500 });
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
};`;
|
|
16
|
+
/** The native Vite worker, reached through Miniflare's development registry. */
|
|
17
|
+
export class DevWorker {
|
|
18
|
+
name;
|
|
19
|
+
#session;
|
|
20
|
+
constructor(name) {
|
|
21
|
+
this.name = name;
|
|
22
|
+
}
|
|
23
|
+
/** Delivers the event `name` with its encoded data, resolving to the encoded reply. */
|
|
24
|
+
async dispatch(name, event) {
|
|
25
|
+
const { runtime } = await this.#connect();
|
|
26
|
+
const response = await runtime.dispatchFetch("http://tokamak/", {
|
|
27
|
+
method: "POST", headers: { "x-tokamak-event": name }, body: event,
|
|
28
|
+
});
|
|
29
|
+
if (!response.ok)
|
|
30
|
+
throw new Error(await response.text());
|
|
31
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
32
|
+
}
|
|
33
|
+
async fetch(request, signal) {
|
|
34
|
+
const { runtime } = await this.#connect();
|
|
35
|
+
const headers = new Headers();
|
|
36
|
+
for (const [name, value] of Object.entries(request.headers)) {
|
|
37
|
+
if (value !== undefined)
|
|
38
|
+
headers.set(name, [value].flat().join(", "));
|
|
39
|
+
}
|
|
40
|
+
const host = headers.get("x-forwarded-host") ?? headers.get("host");
|
|
41
|
+
const protocol = headers.get("x-forwarded-proto") ?? "http";
|
|
42
|
+
const url = new URL(request.url ?? "/", `${protocol}://${host}`);
|
|
43
|
+
for (const name of ["x-tokamak-session", "x-tokamak-dev-socket", "x-forwarded-host", "x-forwarded-proto"])
|
|
44
|
+
headers.delete(name);
|
|
45
|
+
return runtime.dispatchFetch(url.href, {
|
|
46
|
+
method: request.method, headers, signal,
|
|
47
|
+
body: request.method === "GET" || request.method === "HEAD" ? undefined : Readable.toWeb(request),
|
|
48
|
+
duplex: "half",
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
async couple(socket, pair) {
|
|
52
|
+
const { module } = await this.#connect();
|
|
53
|
+
await module.coupleWebSocket(socket, pair);
|
|
54
|
+
}
|
|
55
|
+
async dispose() {
|
|
56
|
+
await (await this.#session?.catch(() => undefined))?.runtime.dispose();
|
|
57
|
+
}
|
|
58
|
+
#connect() {
|
|
59
|
+
return this.#session ??= this.#start().catch((error) => {
|
|
60
|
+
this.#session = undefined;
|
|
61
|
+
throw error;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async #start() {
|
|
65
|
+
if (!this.name)
|
|
66
|
+
throw new Error("the Wrangler configuration names no native Worker");
|
|
67
|
+
const require = createRequire(import.meta.resolve("@cloudflare/vite-plugin"));
|
|
68
|
+
const module = (await import(pathToFileURL(require.resolve("miniflare")).href));
|
|
69
|
+
const runtime = new module.Miniflare(module.convertV4MiniflareOptions({
|
|
70
|
+
modules: true,
|
|
71
|
+
script: CALLER,
|
|
72
|
+
compatibilityDate: "2025-01-01",
|
|
73
|
+
serviceBindings: {
|
|
74
|
+
TOKAMAK: { name: this.name },
|
|
75
|
+
EVENTS: { name: this.name, entrypoint: "TokamakEvents" },
|
|
76
|
+
},
|
|
77
|
+
unsafeDevRegistryPath: module.getDefaultDevRegistryPath(),
|
|
78
|
+
}));
|
|
79
|
+
try {
|
|
80
|
+
await runtime.ready;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
await runtime.dispose();
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
return { runtime, module };
|
|
87
|
+
}
|
|
88
|
+
}
|
package/dist/events.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type LifecycleStage, type TokamakContext } from "@tokamakdev/plugin";
|
|
1
2
|
/** Runs once per runtime start, including when the system starts the app in the background. */
|
|
2
3
|
export declare const onStart: (listener: import("@tokamakdev/plugin/events").Listener<{
|
|
3
4
|
readonly foreground: boolean;
|
|
@@ -6,11 +7,10 @@ export declare const onStart: (listener: import("@tokamakdev/plugin/events").Lis
|
|
|
6
7
|
export declare const onResume: (listener: import("@tokamakdev/plugin/events").Listener<Readonly<Record<string, never>>, void>) => void;
|
|
7
8
|
/** Runs each time the app moves out of the foreground. */
|
|
8
9
|
export declare const onSuspend: (listener: import("@tokamakdev/plugin/events").Listener<Readonly<Record<string, never>>, void>) => void;
|
|
9
|
-
|
|
10
|
-
export type LifecycleStage = "foreground" | "background";
|
|
10
|
+
export type { LifecycleStage } from "@tokamakdev/plugin";
|
|
11
11
|
/**
|
|
12
12
|
* The app's stage now, which can differ from the last event delivered: an `onSuspend` listener
|
|
13
13
|
* that runs late reads `"foreground"` once the user has returned. Rejects with
|
|
14
14
|
* `NotSupportedError` outside a tokamak Worker.
|
|
15
15
|
*/
|
|
16
|
-
export declare function getLifecycleStage(): Promise<LifecycleStage>;
|
|
16
|
+
export declare function getLifecycleStage(ctx: TokamakContext): Promise<LifecycleStage>;
|
package/dist/events.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { lifecycleStage } from "@tokamakdev/plugin";
|
|
2
2
|
import { defineEvent } from "@tokamakdev/plugin/events";
|
|
3
3
|
/** Runs once per runtime start, including when the system starts the app in the background. */
|
|
4
4
|
export const onStart = defineEvent("start");
|
|
@@ -6,21 +6,11 @@ export const onStart = defineEvent("start");
|
|
|
6
6
|
export const onResume = defineEvent("resume");
|
|
7
7
|
/** Runs each time the app moves out of the foreground. */
|
|
8
8
|
export const onSuspend = defineEvent("suspend");
|
|
9
|
-
/** The functions the runtime answers itself, as the reserved plugin `tokamak`. */
|
|
10
|
-
class Tokamak extends Plugin {
|
|
11
|
-
constructor() {
|
|
12
|
-
super("tokamak");
|
|
13
|
-
}
|
|
14
|
-
lifecycleStage() {
|
|
15
|
-
return this.call("lifecycleStage");
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
const tokamak = new Tokamak();
|
|
19
9
|
/**
|
|
20
10
|
* The app's stage now, which can differ from the last event delivered: an `onSuspend` listener
|
|
21
11
|
* that runs late reads `"foreground"` once the user has returned. Rejects with
|
|
22
12
|
* `NotSupportedError` outside a tokamak Worker.
|
|
23
13
|
*/
|
|
24
|
-
export function getLifecycleStage() {
|
|
25
|
-
return
|
|
14
|
+
export function getLifecycleStage(ctx) {
|
|
15
|
+
return lifecycleStage(ctx);
|
|
26
16
|
}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { TokamakEntrypoint } from "@tokamakdev/plugin/worker";
|
|
2
|
+
export type { TokamakContext, TokamakEvent, WebView } from "@tokamakdev/plugin/worker";
|
|
1
3
|
/** A setting value. */
|
|
2
4
|
export type Value = string | number | boolean;
|
|
3
5
|
/** One platform's settings: the shared keys and the settings its platform pack declares. */
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {};
|
|
1
|
+
export { TokamakEntrypoint } from "@tokamakdev/plugin/worker";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Plugin } from "vite";
|
|
2
|
+
import type { Config } from "./index.mjs";
|
|
3
|
+
import { type WorkerGraph } from "./worker-graph.mjs";
|
|
4
|
+
export type TokamakOptions = Config & {
|
|
5
|
+
/** Native worker entrypoint, relative to the Vite root; defaults to src/tokamak.ts or .js. */
|
|
6
|
+
module?: string;
|
|
7
|
+
};
|
|
8
|
+
/** The native worker and its development relay. */
|
|
9
|
+
export declare function createNativeWorker(options: TokamakOptions, output: string, prepare?: (root: string, graph: WorkerGraph) => void | Promise<void>): {
|
|
10
|
+
plugin: Plugin;
|
|
11
|
+
graph(): WorkerGraph;
|
|
12
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { X509Certificate } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { normalizePath } from "vite";
|
|
5
|
+
import { capabilitiesFor } from "@tokamakdev/plugin/capabilities";
|
|
6
|
+
import { openDevSocket } from "./dev-socket.mjs";
|
|
7
|
+
import { findModule } from "./worker-contract.mjs";
|
|
8
|
+
import { createWorkerGraph } from "./worker-graph.mjs";
|
|
9
|
+
const CAPABILITIES = "/_tokamak/capabilities.js";
|
|
10
|
+
const MIDDLEWARE_MODE_ERROR = "Vite is running in middleware mode, so tokamak cannot find the development server's address; tok dev needs Vite's own dev server";
|
|
11
|
+
/** The native worker and its development relay. */
|
|
12
|
+
export function createNativeWorker(options, output, prepare) {
|
|
13
|
+
const { module, ...config } = options;
|
|
14
|
+
const token = process.env.TOKAMAK_SESSION_TOKEN;
|
|
15
|
+
const platform = process.env.TOKAMAK_PLATFORM;
|
|
16
|
+
let graph;
|
|
17
|
+
const plugin = {
|
|
18
|
+
name: "tokamak",
|
|
19
|
+
enforce: "pre",
|
|
20
|
+
async config(vite, environment) {
|
|
21
|
+
const root = normalizePath(path.resolve(vite.root ?? process.cwd()));
|
|
22
|
+
graph = createWorkerGraph(root, findModule(root, module), path.join(output, "worker"));
|
|
23
|
+
graph.plugin.config.call(this, vite, environment);
|
|
24
|
+
await prepare?.(root, graph);
|
|
25
|
+
writeJson(output, "config.json", { root, config });
|
|
26
|
+
},
|
|
27
|
+
configEnvironment: {
|
|
28
|
+
order: "post",
|
|
29
|
+
handler(name, config) {
|
|
30
|
+
return graph.plugin.configEnvironment.handler(name, config);
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
buildStart(options) {
|
|
34
|
+
return graph.plugin.buildStart.call(this, options);
|
|
35
|
+
},
|
|
36
|
+
hotUpdate(options) {
|
|
37
|
+
return graph.plugin.hotUpdate.call(this, options);
|
|
38
|
+
},
|
|
39
|
+
resolveId(...args) {
|
|
40
|
+
return graph.plugin.resolveId.call(this, ...args);
|
|
41
|
+
},
|
|
42
|
+
load(...args) {
|
|
43
|
+
return graph.plugin.load.call(this, ...args);
|
|
44
|
+
},
|
|
45
|
+
configureServer: {
|
|
46
|
+
order: "pre",
|
|
47
|
+
async handler(server) {
|
|
48
|
+
const devSocket = await serveNativeWorker(server, graph.names(), output, token);
|
|
49
|
+
if (devSocket && token)
|
|
50
|
+
graph.relay(devSocket.port, token, platform);
|
|
51
|
+
// A page loaded from this server, not the device, still gets the device's capabilities.
|
|
52
|
+
server.middlewares.use(CAPABILITIES, (_request, response) => {
|
|
53
|
+
const capabilities = capabilitiesFor(graph.availability(), platform);
|
|
54
|
+
response.setHeader("content-type", "text/javascript; charset=utf-8");
|
|
55
|
+
response.end(`export default ${JSON.stringify(capabilities)};\n`);
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
return { plugin, graph: () => graph };
|
|
61
|
+
}
|
|
62
|
+
async function serveNativeWorker(server, names, output, token) {
|
|
63
|
+
const httpServer = server.httpServer;
|
|
64
|
+
if (!httpServer) {
|
|
65
|
+
if (!fs.existsSync(path.join(output, "server.json"))) {
|
|
66
|
+
writeJson(output, "server.json", { error: MIDDLEWARE_MODE_ERROR });
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const socket = await openDevSocket(names.native, token);
|
|
71
|
+
httpServer.once("close", () => void socket.close());
|
|
72
|
+
httpServer.once("listening", () => {
|
|
73
|
+
const urls = server.resolvedUrls;
|
|
74
|
+
writeJson(output, "server.json", {
|
|
75
|
+
url: urls?.local[0] ?? urls?.network[0],
|
|
76
|
+
workerName: names.app,
|
|
77
|
+
certificates: endEntityCertificates(server.config.server.https),
|
|
78
|
+
socketPort: socket.port,
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
return socket;
|
|
82
|
+
}
|
|
83
|
+
function endEntityCertificates(https) {
|
|
84
|
+
const cert = typeof https?.cert === "string" ? readFileIfExists(https.cert) : https?.cert;
|
|
85
|
+
return cert && [cert].flat().map((chain) => new X509Certificate(chain).toString()).join("");
|
|
86
|
+
}
|
|
87
|
+
function readFileIfExists(file) {
|
|
88
|
+
try {
|
|
89
|
+
return fs.readFileSync(path.resolve(file));
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return file;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Write reports atomically while tok watches for them. */
|
|
96
|
+
function writeJson(output, name, value) {
|
|
97
|
+
const file = path.join(output, name);
|
|
98
|
+
fs.mkdirSync(output, { recursive: true });
|
|
99
|
+
fs.writeFileSync(`${file}.tmp`, `${JSON.stringify(value, null, 2)}\n`);
|
|
100
|
+
fs.renameSync(`${file}.tmp`, file);
|
|
101
|
+
}
|
package/dist/native.d.mts
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
|
+
import type { NativePlatform } from "@tokamakdev/plugin/capabilities";
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
-
* `
|
|
4
|
-
* `waitUntil` from `cloudflare:workers` does.
|
|
3
|
+
* The development invocation `ctx`'s native connection, which reaches the byte endpoints of a
|
|
4
|
+
* device on `platform` through the relay on `port`. Its subscriptions are `ctx`'s `waitUntil` work.
|
|
5
5
|
*/
|
|
6
|
-
export declare function
|
|
6
|
+
export declare function connectDevice(port: number, token: string, ctx: {
|
|
7
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
8
|
+
}, platform?: NativePlatform): {
|
|
9
|
+
platform: NativePlatform | undefined;
|
|
10
|
+
call(plugin: string, method: string, payload: Uint8Array, { body, signal }?: {
|
|
11
|
+
body?: ReadableStream<Uint8Array>;
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
}): Promise<Uint8Array<ArrayBufferLike>>;
|
|
14
|
+
listen(plugin: string, method: string, payload: Uint8Array, { next, error, overflow }: {
|
|
15
|
+
next(value: Uint8Array, done: boolean): void;
|
|
16
|
+
error(error: DOMException, done: boolean): void;
|
|
17
|
+
overflow?: string;
|
|
18
|
+
}): () => void;
|
|
19
|
+
};
|