export-runtime 0.0.1 → 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 +126 -19
- package/handler.js +193 -6
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -162,14 +162,43 @@ __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
|
+
|
|
187
|
+
const sendRequest = async (msg) => {
|
|
188
|
+
await ready;
|
|
189
|
+
const id = nextId++;
|
|
190
|
+
return new Promise((resolve, reject) => {
|
|
191
|
+
pending.set(id, { resolve, reject });
|
|
192
|
+
ws.send(stringify({ ...msg, id }));
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
|
|
171
196
|
ws.onmessage = (event) => {
|
|
172
197
|
const msg = parse(event.data);
|
|
198
|
+
|
|
199
|
+
// Ignore pong responses (keepalive)
|
|
200
|
+
if (msg.type === "pong") return;
|
|
201
|
+
|
|
173
202
|
const resolver = pending.get(msg.id);
|
|
174
203
|
if (!resolver) return;
|
|
175
204
|
|
|
@@ -179,27 +208,56 @@ ws.onmessage = (event) => {
|
|
|
179
208
|
} else if (msg.type === "result") {
|
|
180
209
|
if (msg.valueType === "function") {
|
|
181
210
|
resolver.resolve(createProxy(msg.path));
|
|
211
|
+
} else if (msg.valueType === "instance") {
|
|
212
|
+
resolver.resolve(createInstanceProxy(msg.instanceId));
|
|
182
213
|
} else if (msg.valueType === "asynciterator") {
|
|
183
214
|
const iteratorProxy = {
|
|
184
215
|
[Symbol.asyncIterator]() { return this; },
|
|
185
216
|
async next() {
|
|
186
|
-
|
|
187
|
-
const id = nextId++;
|
|
188
|
-
return new Promise((resolve, reject) => {
|
|
189
|
-
pending.set(id, { resolve, reject });
|
|
190
|
-
ws.send(stringify({ type: "iterate-next", id, iteratorId: msg.iteratorId }));
|
|
191
|
-
});
|
|
217
|
+
return sendRequest({ type: "iterate-next", iteratorId: msg.iteratorId });
|
|
192
218
|
},
|
|
193
219
|
async return(value) {
|
|
194
|
-
|
|
195
|
-
const id = nextId++;
|
|
196
|
-
return new Promise((resolve, reject) => {
|
|
197
|
-
pending.set(id, { resolve, reject });
|
|
198
|
-
ws.send(stringify({ type: "iterate-return", id, iteratorId: msg.iteratorId, value }));
|
|
199
|
-
});
|
|
220
|
+
return sendRequest({ type: "iterate-return", iteratorId: msg.iteratorId, value });
|
|
200
221
|
}
|
|
201
222
|
};
|
|
202
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);
|
|
203
261
|
} else {
|
|
204
262
|
resolver.resolve(msg.value);
|
|
205
263
|
}
|
|
@@ -207,23 +265,72 @@ ws.onmessage = (event) => {
|
|
|
207
265
|
} else if (msg.type === "iterate-result") {
|
|
208
266
|
resolver.resolve({ value: msg.value, done: msg.done });
|
|
209
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);
|
|
210
273
|
}
|
|
211
274
|
};
|
|
212
275
|
|
|
276
|
+
// Proxy for remote class instances
|
|
277
|
+
const createInstanceProxy = (instanceId, path = []) => {
|
|
278
|
+
const proxy = new Proxy(function(){}, {
|
|
279
|
+
get(_, prop) {
|
|
280
|
+
if (prop === "then" || prop === Symbol.toStringTag) return undefined;
|
|
281
|
+
if (prop === Symbol.dispose || prop === Symbol.asyncDispose) {
|
|
282
|
+
return () => sendRequest({ type: "release", instanceId });
|
|
283
|
+
}
|
|
284
|
+
if (prop === "[release]") {
|
|
285
|
+
return () => sendRequest({ type: "release", instanceId });
|
|
286
|
+
}
|
|
287
|
+
return createInstanceProxy(instanceId, [...path, prop]);
|
|
288
|
+
},
|
|
289
|
+
set(_, prop, value) {
|
|
290
|
+
sendRequest({ type: "set", instanceId, path: [...path, prop], args: [value] });
|
|
291
|
+
return true;
|
|
292
|
+
},
|
|
293
|
+
async apply(_, __, args) {
|
|
294
|
+
return sendRequest({ type: "call", instanceId, path, args });
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
return proxy;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// Proxy for exports (functions, classes, objects)
|
|
213
301
|
const createProxy = (path = []) => new Proxy(function(){}, {
|
|
214
302
|
get(_, prop) {
|
|
215
303
|
if (prop === "then" || prop === Symbol.toStringTag) return undefined;
|
|
216
304
|
return createProxy([...path, prop]);
|
|
217
305
|
},
|
|
218
306
|
async apply(_, __, args) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
ws.send(stringify({ type: "call", id, path, args }));
|
|
224
|
-
});
|
|
307
|
+
return sendRequest({ type: "call", path, args });
|
|
308
|
+
},
|
|
309
|
+
construct(_, args) {
|
|
310
|
+
return sendRequest({ type: "construct", path, args });
|
|
225
311
|
}
|
|
226
312
|
});
|
|
227
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
|
+
|
|
228
335
|
__NAMED_EXPORTS__
|
|
229
336
|
`;
|
package/handler.js
CHANGED
|
@@ -13,10 +13,21 @@ 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
|
+
|
|
19
|
+
const isClass = (fn) =>
|
|
20
|
+
typeof fn === "function" && /^class\s/.test(Function.prototype.toString.call(fn));
|
|
21
|
+
|
|
16
22
|
export const createHandler = (exports) => {
|
|
17
23
|
const exportKeys = Object.keys(exports);
|
|
18
24
|
const iteratorStore = new Map();
|
|
25
|
+
const instanceStore = new Map();
|
|
26
|
+
const streamStore = new Map();
|
|
27
|
+
const writableStreamStore = new Map();
|
|
19
28
|
let nextIteratorId = 1;
|
|
29
|
+
let nextInstanceId = 1;
|
|
30
|
+
let nextStreamId = 1;
|
|
20
31
|
|
|
21
32
|
const send = (ws, data) => {
|
|
22
33
|
ws.send(stringify(data));
|
|
@@ -36,18 +47,62 @@ export const createHandler = (exports) => {
|
|
|
36
47
|
server.addEventListener("message", async (event) => {
|
|
37
48
|
try {
|
|
38
49
|
const msg = parse(event.data);
|
|
39
|
-
const { type, id, path = [], args = [], iteratorId } = msg;
|
|
50
|
+
const { type, id, path = [], args = [], iteratorId, instanceId } = msg;
|
|
51
|
+
|
|
52
|
+
// Keepalive ping/pong
|
|
53
|
+
if (type === "ping") {
|
|
54
|
+
send(server, { type: "pong", id });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
40
57
|
|
|
41
|
-
if (type === "
|
|
58
|
+
if (type === "construct") {
|
|
59
|
+
// Class instantiation
|
|
42
60
|
try {
|
|
43
|
-
const
|
|
44
|
-
if (
|
|
61
|
+
const Ctor = getByPath(exports, path);
|
|
62
|
+
if (!isClass(Ctor)) {
|
|
63
|
+
send(server, { type: "error", id, error: `${path.join(".")} is not a class` });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const instance = new Ctor(...args);
|
|
67
|
+
const instId = nextInstanceId++;
|
|
68
|
+
instanceStore.set(instId, instance);
|
|
69
|
+
send(server, { type: "result", id, instanceId: instId, valueType: "instance" });
|
|
70
|
+
} catch (err) {
|
|
71
|
+
send(server, { type: "error", id, error: String(err) });
|
|
72
|
+
}
|
|
73
|
+
} else if (type === "call") {
|
|
74
|
+
try {
|
|
75
|
+
let target;
|
|
76
|
+
let thisArg;
|
|
77
|
+
|
|
78
|
+
if (instanceId !== undefined) {
|
|
79
|
+
// Method call on instance
|
|
80
|
+
const instance = instanceStore.get(instanceId);
|
|
81
|
+
if (!instance) {
|
|
82
|
+
send(server, { type: "error", id, error: "Instance not found" });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
target = getByPath(instance, path);
|
|
86
|
+
thisArg = path.length > 1 ? getByPath(instance, path.slice(0, -1)) : instance;
|
|
87
|
+
} else {
|
|
88
|
+
// Regular function call
|
|
89
|
+
target = getByPath(exports, path);
|
|
90
|
+
thisArg = path.length > 1 ? getByPath(exports, path.slice(0, -1)) : undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (typeof target !== "function") {
|
|
45
94
|
send(server, { type: "error", id, error: `${path.join(".")} is not a function` });
|
|
46
95
|
return;
|
|
47
96
|
}
|
|
48
|
-
const result = await fn.apply(undefined, args);
|
|
49
97
|
|
|
50
|
-
|
|
98
|
+
// Await result to support both sync and async functions
|
|
99
|
+
const result = await target.apply(thisArg, args);
|
|
100
|
+
|
|
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)) {
|
|
51
106
|
const iterId = nextIteratorId++;
|
|
52
107
|
iteratorStore.set(iterId, result[Symbol.asyncIterator]());
|
|
53
108
|
send(server, { type: "result", id, iteratorId: iterId, valueType: "asynciterator" });
|
|
@@ -59,6 +114,42 @@ export const createHandler = (exports) => {
|
|
|
59
114
|
} catch (err) {
|
|
60
115
|
send(server, { type: "error", id, error: String(err) });
|
|
61
116
|
}
|
|
117
|
+
} else if (type === "get") {
|
|
118
|
+
// Property access on instance
|
|
119
|
+
try {
|
|
120
|
+
const instance = instanceStore.get(instanceId);
|
|
121
|
+
if (!instance) {
|
|
122
|
+
send(server, { type: "error", id, error: "Instance not found" });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const value = getByPath(instance, path);
|
|
126
|
+
if (typeof value === "function") {
|
|
127
|
+
send(server, { type: "result", id, valueType: "function" });
|
|
128
|
+
} else {
|
|
129
|
+
send(server, { type: "result", id, value });
|
|
130
|
+
}
|
|
131
|
+
} catch (err) {
|
|
132
|
+
send(server, { type: "error", id, error: String(err) });
|
|
133
|
+
}
|
|
134
|
+
} else if (type === "set") {
|
|
135
|
+
// Property assignment on instance
|
|
136
|
+
try {
|
|
137
|
+
const instance = instanceStore.get(instanceId);
|
|
138
|
+
if (!instance) {
|
|
139
|
+
send(server, { type: "error", id, error: "Instance not found" });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const parent = path.length > 1 ? getByPath(instance, path.slice(0, -1)) : instance;
|
|
143
|
+
const prop = path[path.length - 1];
|
|
144
|
+
parent[prop] = args[0];
|
|
145
|
+
send(server, { type: "result", id, value: true });
|
|
146
|
+
} catch (err) {
|
|
147
|
+
send(server, { type: "error", id, error: String(err) });
|
|
148
|
+
}
|
|
149
|
+
} else if (type === "release") {
|
|
150
|
+
// Release instance
|
|
151
|
+
instanceStore.delete(instanceId);
|
|
152
|
+
send(server, { type: "result", id, value: true });
|
|
62
153
|
} else if (type === "iterate-next") {
|
|
63
154
|
const iter = iteratorStore.get(iteratorId);
|
|
64
155
|
if (!iter) {
|
|
@@ -77,6 +168,99 @@ export const createHandler = (exports) => {
|
|
|
77
168
|
if (iter?.return) await iter.return(undefined);
|
|
78
169
|
iteratorStore.delete(iteratorId);
|
|
79
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 });
|
|
80
264
|
}
|
|
81
265
|
} catch (err) {
|
|
82
266
|
console.error("WebSocket message error:", err);
|
|
@@ -85,6 +269,9 @@ export const createHandler = (exports) => {
|
|
|
85
269
|
|
|
86
270
|
server.addEventListener("close", () => {
|
|
87
271
|
iteratorStore.clear();
|
|
272
|
+
instanceStore.clear();
|
|
273
|
+
streamStore.clear();
|
|
274
|
+
writableStreamStore.clear();
|
|
88
275
|
});
|
|
89
276
|
|
|
90
277
|
return new Response(null, { status: 101, webSocket: client });
|