export-runtime 0.0.2 → 0.0.3
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 +112 -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,6 +13,9 @@ 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
|
|
|
@@ -20,8 +23,11 @@ export const createHandler = (exports) => {
|
|
|
20
23
|
const exportKeys = Object.keys(exports);
|
|
21
24
|
const iteratorStore = new Map();
|
|
22
25
|
const instanceStore = new Map();
|
|
26
|
+
const streamStore = new Map();
|
|
27
|
+
const writableStreamStore = new Map();
|
|
23
28
|
let nextIteratorId = 1;
|
|
24
29
|
let nextInstanceId = 1;
|
|
30
|
+
let nextStreamId = 1;
|
|
25
31
|
|
|
26
32
|
const send = (ws, data) => {
|
|
27
33
|
ws.send(stringify(data));
|
|
@@ -43,6 +49,12 @@ export const createHandler = (exports) => {
|
|
|
43
49
|
const msg = parse(event.data);
|
|
44
50
|
const { type, id, path = [], args = [], iteratorId, instanceId } = msg;
|
|
45
51
|
|
|
52
|
+
// Keepalive ping/pong
|
|
53
|
+
if (type === "ping") {
|
|
54
|
+
send(server, { type: "pong", id });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
46
58
|
if (type === "construct") {
|
|
47
59
|
// Class instantiation
|
|
48
60
|
try {
|
|
@@ -86,7 +98,11 @@ export const createHandler = (exports) => {
|
|
|
86
98
|
// Await result to support both sync and async functions
|
|
87
99
|
const result = await target.apply(thisArg, args);
|
|
88
100
|
|
|
89
|
-
if (
|
|
101
|
+
if (isReadableStream(result)) {
|
|
102
|
+
const streamId = nextStreamId++;
|
|
103
|
+
streamStore.set(streamId, { stream: result, reader: null });
|
|
104
|
+
send(server, { type: "result", id, streamId, valueType: "readablestream" });
|
|
105
|
+
} else if (isAsyncIterable(result)) {
|
|
90
106
|
const iterId = nextIteratorId++;
|
|
91
107
|
iteratorStore.set(iterId, result[Symbol.asyncIterator]());
|
|
92
108
|
send(server, { type: "result", id, iteratorId: iterId, valueType: "asynciterator" });
|
|
@@ -152,6 +168,99 @@ export const createHandler = (exports) => {
|
|
|
152
168
|
if (iter?.return) await iter.return(undefined);
|
|
153
169
|
iteratorStore.delete(iteratorId);
|
|
154
170
|
send(server, { type: "iterate-result", id, value: undefined, done: true });
|
|
171
|
+
} else if (type === "stream-read") {
|
|
172
|
+
// ReadableStream chunk read
|
|
173
|
+
const { streamId } = msg;
|
|
174
|
+
const entry = streamStore.get(streamId);
|
|
175
|
+
if (!entry) {
|
|
176
|
+
send(server, { type: "error", id, error: "Stream not found" });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
// Get or create reader for this stream
|
|
181
|
+
let reader = entry.reader;
|
|
182
|
+
if (!reader) {
|
|
183
|
+
reader = entry.stream.getReader();
|
|
184
|
+
entry.reader = reader;
|
|
185
|
+
}
|
|
186
|
+
const { value, done } = await reader.read();
|
|
187
|
+
if (done) {
|
|
188
|
+
streamStore.delete(streamId);
|
|
189
|
+
}
|
|
190
|
+
// Convert Uint8Array to array for devalue serialization
|
|
191
|
+
const serializedValue = value instanceof Uint8Array ? Array.from(value) : value;
|
|
192
|
+
send(server, { type: "stream-result", id, value: serializedValue, done: !!done });
|
|
193
|
+
} catch (err) {
|
|
194
|
+
streamStore.delete(streamId);
|
|
195
|
+
send(server, { type: "error", id, error: String(err) });
|
|
196
|
+
}
|
|
197
|
+
} else if (type === "stream-cancel") {
|
|
198
|
+
// Cancel ReadableStream
|
|
199
|
+
const { streamId } = msg;
|
|
200
|
+
const entry = streamStore.get(streamId);
|
|
201
|
+
if (entry) {
|
|
202
|
+
try {
|
|
203
|
+
if (entry.reader) {
|
|
204
|
+
await entry.reader.cancel();
|
|
205
|
+
} else {
|
|
206
|
+
await entry.stream.cancel();
|
|
207
|
+
}
|
|
208
|
+
} catch (e) { /* ignore */ }
|
|
209
|
+
streamStore.delete(streamId);
|
|
210
|
+
}
|
|
211
|
+
send(server, { type: "result", id, value: true });
|
|
212
|
+
} else if (type === "writable-create") {
|
|
213
|
+
// Create a WritableStream on server side
|
|
214
|
+
const { targetPath, targetInstanceId } = msg;
|
|
215
|
+
let chunks = [];
|
|
216
|
+
const writableId = nextStreamId++;
|
|
217
|
+
|
|
218
|
+
const writable = new WritableStream({
|
|
219
|
+
write(chunk) {
|
|
220
|
+
chunks.push(chunk);
|
|
221
|
+
},
|
|
222
|
+
close() {
|
|
223
|
+
// Resolve with all chunks when stream closes
|
|
224
|
+
},
|
|
225
|
+
abort(reason) {
|
|
226
|
+
chunks = [];
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
writableStreamStore.set(writableId, { writable, chunks, targetPath, targetInstanceId });
|
|
231
|
+
send(server, { type: "result", id, writableId, valueType: "writablestream" });
|
|
232
|
+
} else if (type === "writable-write") {
|
|
233
|
+
// Write chunk to WritableStream
|
|
234
|
+
const { writableId, chunk } = msg;
|
|
235
|
+
const entry = writableStreamStore.get(writableId);
|
|
236
|
+
if (!entry) {
|
|
237
|
+
send(server, { type: "error", id, error: "WritableStream not found" });
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
// Convert array back to Uint8Array if needed
|
|
242
|
+
const data = Array.isArray(chunk) ? new Uint8Array(chunk) : chunk;
|
|
243
|
+
entry.chunks.push(data);
|
|
244
|
+
send(server, { type: "result", id, value: true });
|
|
245
|
+
} catch (err) {
|
|
246
|
+
send(server, { type: "error", id, error: String(err) });
|
|
247
|
+
}
|
|
248
|
+
} else if (type === "writable-close") {
|
|
249
|
+
// Close WritableStream and return collected chunks
|
|
250
|
+
const { writableId } = msg;
|
|
251
|
+
const entry = writableStreamStore.get(writableId);
|
|
252
|
+
if (!entry) {
|
|
253
|
+
send(server, { type: "error", id, error: "WritableStream not found" });
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
writableStreamStore.delete(writableId);
|
|
257
|
+
// Return the collected data
|
|
258
|
+
send(server, { type: "result", id, value: entry.chunks });
|
|
259
|
+
} else if (type === "writable-abort") {
|
|
260
|
+
// Abort WritableStream
|
|
261
|
+
const { writableId } = msg;
|
|
262
|
+
writableStreamStore.delete(writableId);
|
|
263
|
+
send(server, { type: "result", id, value: true });
|
|
155
264
|
}
|
|
156
265
|
} catch (err) {
|
|
157
266
|
console.error("WebSocket message error:", err);
|
|
@@ -161,6 +270,8 @@ export const createHandler = (exports) => {
|
|
|
161
270
|
server.addEventListener("close", () => {
|
|
162
271
|
iteratorStore.clear();
|
|
163
272
|
instanceStore.clear();
|
|
273
|
+
streamStore.clear();
|
|
274
|
+
writableStreamStore.clear();
|
|
164
275
|
});
|
|
165
276
|
|
|
166
277
|
return new Response(null, { status: 101, webSocket: client });
|