export-runtime 0.0.2 → 0.0.4
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/client.js +84 -1
- package/handler.js +200 -1
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -162,12 +162,28 @@ __DEVALUE_PARSE__
|
|
|
162
162
|
const ws = new WebSocket(__WS_URL__);
|
|
163
163
|
const pending = new Map();
|
|
164
164
|
let nextId = 1;
|
|
165
|
+
let keepaliveInterval = null;
|
|
165
166
|
|
|
166
167
|
const ready = new Promise((resolve, reject) => {
|
|
167
|
-
ws.onopen = () =>
|
|
168
|
+
ws.onopen = () => {
|
|
169
|
+
// Start keepalive ping every 30 seconds
|
|
170
|
+
keepaliveInterval = setInterval(() => {
|
|
171
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
172
|
+
ws.send(stringify({ type: "ping", id: 0 }));
|
|
173
|
+
}
|
|
174
|
+
}, 30000);
|
|
175
|
+
resolve(undefined);
|
|
176
|
+
};
|
|
168
177
|
ws.onerror = (e) => reject(e);
|
|
169
178
|
});
|
|
170
179
|
|
|
180
|
+
ws.onclose = () => {
|
|
181
|
+
if (keepaliveInterval) {
|
|
182
|
+
clearInterval(keepaliveInterval);
|
|
183
|
+
keepaliveInterval = null;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
171
187
|
const sendRequest = async (msg) => {
|
|
172
188
|
await ready;
|
|
173
189
|
const id = nextId++;
|
|
@@ -179,6 +195,10 @@ const sendRequest = async (msg) => {
|
|
|
179
195
|
|
|
180
196
|
ws.onmessage = (event) => {
|
|
181
197
|
const msg = parse(event.data);
|
|
198
|
+
|
|
199
|
+
// Ignore pong responses (keepalive)
|
|
200
|
+
if (msg.type === "pong") return;
|
|
201
|
+
|
|
182
202
|
const resolver = pending.get(msg.id);
|
|
183
203
|
if (!resolver) return;
|
|
184
204
|
|
|
@@ -201,6 +221,43 @@ ws.onmessage = (event) => {
|
|
|
201
221
|
}
|
|
202
222
|
};
|
|
203
223
|
resolver.resolve(iteratorProxy);
|
|
224
|
+
} else if (msg.valueType === "readablestream") {
|
|
225
|
+
// Create a ReadableStream proxy that pulls from server
|
|
226
|
+
const streamId = msg.streamId;
|
|
227
|
+
const stream = new ReadableStream({
|
|
228
|
+
async pull(controller) {
|
|
229
|
+
try {
|
|
230
|
+
const result = await sendRequest({ type: "stream-read", streamId });
|
|
231
|
+
if (result.done) {
|
|
232
|
+
controller.close();
|
|
233
|
+
} else {
|
|
234
|
+
controller.enqueue(result.value);
|
|
235
|
+
}
|
|
236
|
+
} catch (err) {
|
|
237
|
+
controller.error(err);
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
async cancel() {
|
|
241
|
+
await sendRequest({ type: "stream-cancel", streamId });
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
resolver.resolve(stream);
|
|
245
|
+
} else if (msg.valueType === "writablestream") {
|
|
246
|
+
// Create a WritableStream proxy that pushes to server
|
|
247
|
+
const writableId = msg.writableId;
|
|
248
|
+
const stream = new WritableStream({
|
|
249
|
+
async write(chunk) {
|
|
250
|
+
const data = chunk instanceof Uint8Array ? Array.from(chunk) : chunk;
|
|
251
|
+
await sendRequest({ type: "writable-write", writableId, chunk: data });
|
|
252
|
+
},
|
|
253
|
+
async close() {
|
|
254
|
+
await sendRequest({ type: "writable-close", writableId });
|
|
255
|
+
},
|
|
256
|
+
async abort(reason) {
|
|
257
|
+
await sendRequest({ type: "writable-abort", writableId });
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
resolver.resolve(stream);
|
|
204
261
|
} else {
|
|
205
262
|
resolver.resolve(msg.value);
|
|
206
263
|
}
|
|
@@ -208,6 +265,11 @@ ws.onmessage = (event) => {
|
|
|
208
265
|
} else if (msg.type === "iterate-result") {
|
|
209
266
|
resolver.resolve({ value: msg.value, done: msg.done });
|
|
210
267
|
pending.delete(msg.id);
|
|
268
|
+
} else if (msg.type === "stream-result") {
|
|
269
|
+
// Convert array back to Uint8Array if it was serialized
|
|
270
|
+
const value = Array.isArray(msg.value) ? new Uint8Array(msg.value) : msg.value;
|
|
271
|
+
resolver.resolve({ value, done: msg.done });
|
|
272
|
+
pending.delete(msg.id);
|
|
211
273
|
}
|
|
212
274
|
};
|
|
213
275
|
|
|
@@ -249,5 +311,26 @@ const createProxy = (path = []) => new Proxy(function(){}, {
|
|
|
249
311
|
}
|
|
250
312
|
});
|
|
251
313
|
|
|
314
|
+
// Helper to create a client-side WritableStream that can be passed to server functions
|
|
315
|
+
export const createUploadStream = async () => {
|
|
316
|
+
const result = await sendRequest({ type: "writable-create" });
|
|
317
|
+
const writableId = result.writableId;
|
|
318
|
+
|
|
319
|
+
const stream = new WritableStream({
|
|
320
|
+
async write(chunk) {
|
|
321
|
+
const data = chunk instanceof Uint8Array ? Array.from(chunk) : chunk;
|
|
322
|
+
await sendRequest({ type: "writable-write", writableId, chunk: data });
|
|
323
|
+
},
|
|
324
|
+
async close() {
|
|
325
|
+
return sendRequest({ type: "writable-close", writableId });
|
|
326
|
+
},
|
|
327
|
+
async abort(reason) {
|
|
328
|
+
await sendRequest({ type: "writable-abort", writableId });
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
return { stream, writableId };
|
|
333
|
+
};
|
|
334
|
+
|
|
252
335
|
__NAMED_EXPORTS__
|
|
253
336
|
`;
|
package/handler.js
CHANGED
|
@@ -13,15 +13,93 @@ const getByPath = (obj, path) => {
|
|
|
13
13
|
const isAsyncIterable = (value) =>
|
|
14
14
|
value != null && typeof value[Symbol.asyncIterator] === "function";
|
|
15
15
|
|
|
16
|
+
const isReadableStream = (value) =>
|
|
17
|
+
value != null && typeof value.getReader === "function" && typeof value.pipeTo === "function";
|
|
18
|
+
|
|
16
19
|
const isClass = (fn) =>
|
|
17
20
|
typeof fn === "function" && /^class\s/.test(Function.prototype.toString.call(fn));
|
|
18
21
|
|
|
22
|
+
// Generate TypeScript type definitions from exports
|
|
23
|
+
const generateTypeDefinitions = (exports, exportKeys) => {
|
|
24
|
+
const lines = [
|
|
25
|
+
"// Auto-generated type definitions",
|
|
26
|
+
"// All functions are async over the network",
|
|
27
|
+
"",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const generateType = (value, name, indent = "") => {
|
|
31
|
+
if (isClass(value)) {
|
|
32
|
+
// Extract class method names
|
|
33
|
+
const proto = value.prototype;
|
|
34
|
+
const methodNames = Object.getOwnPropertyNames(proto).filter(
|
|
35
|
+
(n) => n !== "constructor" && typeof proto[n] === "function"
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
lines.push(`${indent}export declare class ${name} {`);
|
|
39
|
+
lines.push(`${indent} constructor(...args: any[]);`);
|
|
40
|
+
for (const method of methodNames) {
|
|
41
|
+
lines.push(`${indent} ${method}(...args: any[]): Promise<any>;`);
|
|
42
|
+
}
|
|
43
|
+
lines.push(`${indent} [Symbol.dispose](): Promise<void>;`);
|
|
44
|
+
lines.push(`${indent} "[release]"(): Promise<void>;`);
|
|
45
|
+
lines.push(`${indent}}`);
|
|
46
|
+
} else if (typeof value === "function") {
|
|
47
|
+
// Check if it's an async generator
|
|
48
|
+
const fnStr = Function.prototype.toString.call(value);
|
|
49
|
+
if (fnStr.startsWith("async function*") || fnStr.includes("async *")) {
|
|
50
|
+
lines.push(
|
|
51
|
+
`${indent}export declare function ${name}(...args: any[]): Promise<AsyncIterable<any>>;`
|
|
52
|
+
);
|
|
53
|
+
} else if (fnStr.includes("ReadableStream")) {
|
|
54
|
+
lines.push(
|
|
55
|
+
`${indent}export declare function ${name}(...args: any[]): Promise<ReadableStream<any>>;`
|
|
56
|
+
);
|
|
57
|
+
} else {
|
|
58
|
+
lines.push(
|
|
59
|
+
`${indent}export declare function ${name}(...args: any[]): Promise<any>;`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
} else if (typeof value === "object" && value !== null) {
|
|
63
|
+
// Nested object with methods
|
|
64
|
+
const keys = Object.keys(value);
|
|
65
|
+
lines.push(`${indent}export declare const ${name}: {`);
|
|
66
|
+
for (const key of keys) {
|
|
67
|
+
const v = value[key];
|
|
68
|
+
if (typeof v === "function") {
|
|
69
|
+
lines.push(`${indent} ${key}(...args: any[]): Promise<any>;`);
|
|
70
|
+
} else {
|
|
71
|
+
lines.push(`${indent} ${key}: any;`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
lines.push(`${indent}};`);
|
|
75
|
+
} else {
|
|
76
|
+
lines.push(`${indent}export declare const ${name}: any;`);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
for (const key of exportKeys) {
|
|
81
|
+
generateType(exports[key], key);
|
|
82
|
+
lines.push("");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Add createUploadStream helper type
|
|
86
|
+
lines.push("export declare function createUploadStream(): Promise<{");
|
|
87
|
+
lines.push(" stream: WritableStream<any>;");
|
|
88
|
+
lines.push(" writableId: number;");
|
|
89
|
+
lines.push("}>;");
|
|
90
|
+
|
|
91
|
+
return lines.join("\n");
|
|
92
|
+
};
|
|
93
|
+
|
|
19
94
|
export const createHandler = (exports) => {
|
|
20
95
|
const exportKeys = Object.keys(exports);
|
|
21
96
|
const iteratorStore = new Map();
|
|
22
97
|
const instanceStore = new Map();
|
|
98
|
+
const streamStore = new Map();
|
|
99
|
+
const writableStreamStore = new Map();
|
|
23
100
|
let nextIteratorId = 1;
|
|
24
101
|
let nextInstanceId = 1;
|
|
102
|
+
let nextStreamId = 1;
|
|
25
103
|
|
|
26
104
|
const send = (ws, data) => {
|
|
27
105
|
ws.send(stringify(data));
|
|
@@ -43,6 +121,12 @@ export const createHandler = (exports) => {
|
|
|
43
121
|
const msg = parse(event.data);
|
|
44
122
|
const { type, id, path = [], args = [], iteratorId, instanceId } = msg;
|
|
45
123
|
|
|
124
|
+
// Keepalive ping/pong
|
|
125
|
+
if (type === "ping") {
|
|
126
|
+
send(server, { type: "pong", id });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
46
130
|
if (type === "construct") {
|
|
47
131
|
// Class instantiation
|
|
48
132
|
try {
|
|
@@ -86,7 +170,11 @@ export const createHandler = (exports) => {
|
|
|
86
170
|
// Await result to support both sync and async functions
|
|
87
171
|
const result = await target.apply(thisArg, args);
|
|
88
172
|
|
|
89
|
-
if (
|
|
173
|
+
if (isReadableStream(result)) {
|
|
174
|
+
const streamId = nextStreamId++;
|
|
175
|
+
streamStore.set(streamId, { stream: result, reader: null });
|
|
176
|
+
send(server, { type: "result", id, streamId, valueType: "readablestream" });
|
|
177
|
+
} else if (isAsyncIterable(result)) {
|
|
90
178
|
const iterId = nextIteratorId++;
|
|
91
179
|
iteratorStore.set(iterId, result[Symbol.asyncIterator]());
|
|
92
180
|
send(server, { type: "result", id, iteratorId: iterId, valueType: "asynciterator" });
|
|
@@ -152,6 +240,99 @@ export const createHandler = (exports) => {
|
|
|
152
240
|
if (iter?.return) await iter.return(undefined);
|
|
153
241
|
iteratorStore.delete(iteratorId);
|
|
154
242
|
send(server, { type: "iterate-result", id, value: undefined, done: true });
|
|
243
|
+
} else if (type === "stream-read") {
|
|
244
|
+
// ReadableStream chunk read
|
|
245
|
+
const { streamId } = msg;
|
|
246
|
+
const entry = streamStore.get(streamId);
|
|
247
|
+
if (!entry) {
|
|
248
|
+
send(server, { type: "error", id, error: "Stream not found" });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
// Get or create reader for this stream
|
|
253
|
+
let reader = entry.reader;
|
|
254
|
+
if (!reader) {
|
|
255
|
+
reader = entry.stream.getReader();
|
|
256
|
+
entry.reader = reader;
|
|
257
|
+
}
|
|
258
|
+
const { value, done } = await reader.read();
|
|
259
|
+
if (done) {
|
|
260
|
+
streamStore.delete(streamId);
|
|
261
|
+
}
|
|
262
|
+
// Convert Uint8Array to array for devalue serialization
|
|
263
|
+
const serializedValue = value instanceof Uint8Array ? Array.from(value) : value;
|
|
264
|
+
send(server, { type: "stream-result", id, value: serializedValue, done: !!done });
|
|
265
|
+
} catch (err) {
|
|
266
|
+
streamStore.delete(streamId);
|
|
267
|
+
send(server, { type: "error", id, error: String(err) });
|
|
268
|
+
}
|
|
269
|
+
} else if (type === "stream-cancel") {
|
|
270
|
+
// Cancel ReadableStream
|
|
271
|
+
const { streamId } = msg;
|
|
272
|
+
const entry = streamStore.get(streamId);
|
|
273
|
+
if (entry) {
|
|
274
|
+
try {
|
|
275
|
+
if (entry.reader) {
|
|
276
|
+
await entry.reader.cancel();
|
|
277
|
+
} else {
|
|
278
|
+
await entry.stream.cancel();
|
|
279
|
+
}
|
|
280
|
+
} catch (e) { /* ignore */ }
|
|
281
|
+
streamStore.delete(streamId);
|
|
282
|
+
}
|
|
283
|
+
send(server, { type: "result", id, value: true });
|
|
284
|
+
} else if (type === "writable-create") {
|
|
285
|
+
// Create a WritableStream on server side
|
|
286
|
+
const { targetPath, targetInstanceId } = msg;
|
|
287
|
+
let chunks = [];
|
|
288
|
+
const writableId = nextStreamId++;
|
|
289
|
+
|
|
290
|
+
const writable = new WritableStream({
|
|
291
|
+
write(chunk) {
|
|
292
|
+
chunks.push(chunk);
|
|
293
|
+
},
|
|
294
|
+
close() {
|
|
295
|
+
// Resolve with all chunks when stream closes
|
|
296
|
+
},
|
|
297
|
+
abort(reason) {
|
|
298
|
+
chunks = [];
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
writableStreamStore.set(writableId, { writable, chunks, targetPath, targetInstanceId });
|
|
303
|
+
send(server, { type: "result", id, writableId, valueType: "writablestream" });
|
|
304
|
+
} else if (type === "writable-write") {
|
|
305
|
+
// Write chunk to WritableStream
|
|
306
|
+
const { writableId, chunk } = msg;
|
|
307
|
+
const entry = writableStreamStore.get(writableId);
|
|
308
|
+
if (!entry) {
|
|
309
|
+
send(server, { type: "error", id, error: "WritableStream not found" });
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
// Convert array back to Uint8Array if needed
|
|
314
|
+
const data = Array.isArray(chunk) ? new Uint8Array(chunk) : chunk;
|
|
315
|
+
entry.chunks.push(data);
|
|
316
|
+
send(server, { type: "result", id, value: true });
|
|
317
|
+
} catch (err) {
|
|
318
|
+
send(server, { type: "error", id, error: String(err) });
|
|
319
|
+
}
|
|
320
|
+
} else if (type === "writable-close") {
|
|
321
|
+
// Close WritableStream and return collected chunks
|
|
322
|
+
const { writableId } = msg;
|
|
323
|
+
const entry = writableStreamStore.get(writableId);
|
|
324
|
+
if (!entry) {
|
|
325
|
+
send(server, { type: "error", id, error: "WritableStream not found" });
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
writableStreamStore.delete(writableId);
|
|
329
|
+
// Return the collected data
|
|
330
|
+
send(server, { type: "result", id, value: entry.chunks });
|
|
331
|
+
} else if (type === "writable-abort") {
|
|
332
|
+
// Abort WritableStream
|
|
333
|
+
const { writableId } = msg;
|
|
334
|
+
writableStreamStore.delete(writableId);
|
|
335
|
+
send(server, { type: "result", id, value: true });
|
|
155
336
|
}
|
|
156
337
|
} catch (err) {
|
|
157
338
|
console.error("WebSocket message error:", err);
|
|
@@ -161,6 +342,8 @@ export const createHandler = (exports) => {
|
|
|
161
342
|
server.addEventListener("close", () => {
|
|
162
343
|
iteratorStore.clear();
|
|
163
344
|
instanceStore.clear();
|
|
345
|
+
streamStore.clear();
|
|
346
|
+
writableStreamStore.clear();
|
|
164
347
|
});
|
|
165
348
|
|
|
166
349
|
return new Response(null, { status: 101, webSocket: client });
|
|
@@ -169,6 +352,18 @@ export const createHandler = (exports) => {
|
|
|
169
352
|
const wsProtocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
170
353
|
const wsUrl = `${wsProtocol}//${url.host}${url.pathname}`;
|
|
171
354
|
|
|
355
|
+
// Serve TypeScript type definitions
|
|
356
|
+
if (url.searchParams.has("types") || url.pathname.endsWith(".d.ts")) {
|
|
357
|
+
const typeDefinitions = generateTypeDefinitions(exports, exportKeys);
|
|
358
|
+
return new Response(typeDefinitions, {
|
|
359
|
+
headers: {
|
|
360
|
+
"Content-Type": "application/typescript; charset=utf-8",
|
|
361
|
+
"Access-Control-Allow-Origin": "*",
|
|
362
|
+
"Cache-Control": "no-cache",
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
172
367
|
// Generate named exports
|
|
173
368
|
const namedExports = exportKeys
|
|
174
369
|
.map((key) => `export const ${key} = createProxy([${JSON.stringify(key)}]);`)
|
|
@@ -180,11 +375,15 @@ export const createHandler = (exports) => {
|
|
|
180
375
|
.replace("__DEVALUE_PARSE__", DEVALUE_PARSE)
|
|
181
376
|
.replace("__NAMED_EXPORTS__", namedExports);
|
|
182
377
|
|
|
378
|
+
// Build types URL for X-TypeScript-Types header
|
|
379
|
+
const typesUrl = `${url.protocol}//${url.host}${url.pathname}?types`;
|
|
380
|
+
|
|
183
381
|
return new Response(clientCode, {
|
|
184
382
|
headers: {
|
|
185
383
|
"Content-Type": "application/javascript; charset=utf-8",
|
|
186
384
|
"Access-Control-Allow-Origin": "*",
|
|
187
385
|
"Cache-Control": "no-cache",
|
|
386
|
+
"X-TypeScript-Types": typesUrl,
|
|
188
387
|
},
|
|
189
388
|
});
|
|
190
389
|
},
|