export-runtime 0.0.1
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 +229 -0
- package/entry.js +4 -0
- package/handler.js +116 -0
- package/package.json +33 -0
package/client.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// Minimal devalue stringify implementation (compatible with sveltejs/devalue)
|
|
2
|
+
export const DEVALUE_STRINGIFY = `
|
|
3
|
+
const stringify = (value) => {
|
|
4
|
+
const stringified = [];
|
|
5
|
+
const indexes = new Map();
|
|
6
|
+
let p = 0;
|
|
7
|
+
|
|
8
|
+
const flatten = (thing) => {
|
|
9
|
+
if (typeof thing === 'function') {
|
|
10
|
+
throw new Error('Cannot stringify a function');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (indexes.has(thing)) return indexes.get(thing);
|
|
14
|
+
|
|
15
|
+
if (thing === undefined) return -1;
|
|
16
|
+
if (Number.isNaN(thing)) return -3;
|
|
17
|
+
if (thing === Infinity) return -4;
|
|
18
|
+
if (thing === -Infinity) return -5;
|
|
19
|
+
if (thing === 0 && 1 / thing < 0) return -6;
|
|
20
|
+
|
|
21
|
+
const index = p++;
|
|
22
|
+
indexes.set(thing, index);
|
|
23
|
+
|
|
24
|
+
if (typeof thing === 'boolean' || typeof thing === 'number' || typeof thing === 'string' || thing === null) {
|
|
25
|
+
stringified[index] = thing;
|
|
26
|
+
} else if (thing instanceof Date) {
|
|
27
|
+
stringified[index] = ['Date', thing.toISOString()];
|
|
28
|
+
} else if (thing instanceof URL) {
|
|
29
|
+
stringified[index] = ['URL', thing.href];
|
|
30
|
+
} else if (thing instanceof URLSearchParams) {
|
|
31
|
+
stringified[index] = ['URLSearchParams', thing.toString()];
|
|
32
|
+
} else if (thing instanceof RegExp) {
|
|
33
|
+
stringified[index] = ['RegExp', thing.source, thing.flags];
|
|
34
|
+
} else if (typeof thing === 'bigint') {
|
|
35
|
+
stringified[index] = ['BigInt', thing.toString()];
|
|
36
|
+
} else if (thing instanceof Set) {
|
|
37
|
+
stringified[index] = ['Set', ...[...thing].map(flatten)];
|
|
38
|
+
} else if (thing instanceof Map) {
|
|
39
|
+
stringified[index] = ['Map', ...[...thing].map(([k, v]) => [flatten(k), flatten(v)])];
|
|
40
|
+
} else if (thing instanceof Int8Array) {
|
|
41
|
+
stringified[index] = ['Int8Array', ...[...thing].map(flatten)];
|
|
42
|
+
} else if (thing instanceof Uint8Array) {
|
|
43
|
+
stringified[index] = ['Uint8Array', ...[...thing].map(flatten)];
|
|
44
|
+
} else if (thing instanceof Uint8ClampedArray) {
|
|
45
|
+
stringified[index] = ['Uint8ClampedArray', ...[...thing].map(flatten)];
|
|
46
|
+
} else if (thing instanceof Int16Array) {
|
|
47
|
+
stringified[index] = ['Int16Array', ...[...thing].map(flatten)];
|
|
48
|
+
} else if (thing instanceof Uint16Array) {
|
|
49
|
+
stringified[index] = ['Uint16Array', ...[...thing].map(flatten)];
|
|
50
|
+
} else if (thing instanceof Int32Array) {
|
|
51
|
+
stringified[index] = ['Int32Array', ...[...thing].map(flatten)];
|
|
52
|
+
} else if (thing instanceof Uint32Array) {
|
|
53
|
+
stringified[index] = ['Uint32Array', ...[...thing].map(flatten)];
|
|
54
|
+
} else if (thing instanceof Float32Array) {
|
|
55
|
+
stringified[index] = ['Float32Array', ...[...thing].map(flatten)];
|
|
56
|
+
} else if (thing instanceof Float64Array) {
|
|
57
|
+
stringified[index] = ['Float64Array', ...[...thing].map(flatten)];
|
|
58
|
+
} else if (thing instanceof BigInt64Array) {
|
|
59
|
+
stringified[index] = ['BigInt64Array', ...[...thing].map(flatten)];
|
|
60
|
+
} else if (thing instanceof BigUint64Array) {
|
|
61
|
+
stringified[index] = ['BigUint64Array', ...[...thing].map(flatten)];
|
|
62
|
+
} else if (thing instanceof ArrayBuffer) {
|
|
63
|
+
stringified[index] = ['ArrayBuffer', ...[...new Uint8Array(thing)].map(flatten)];
|
|
64
|
+
} else if (Array.isArray(thing)) {
|
|
65
|
+
stringified[index] = thing.map(flatten);
|
|
66
|
+
} else if (typeof thing === 'object') {
|
|
67
|
+
const obj = {};
|
|
68
|
+
for (const key of Object.keys(thing)) {
|
|
69
|
+
obj[key] = flatten(thing[key]);
|
|
70
|
+
}
|
|
71
|
+
stringified[index] = obj;
|
|
72
|
+
} else {
|
|
73
|
+
throw new Error('Cannot stringify ' + typeof thing);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return index;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
flatten(value);
|
|
80
|
+
return JSON.stringify(stringified);
|
|
81
|
+
};
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
// Minimal devalue parse implementation (compatible with sveltejs/devalue)
|
|
85
|
+
export const DEVALUE_PARSE = `
|
|
86
|
+
const UNDEFINED = -1;
|
|
87
|
+
const HOLE = -2;
|
|
88
|
+
const NAN = -3;
|
|
89
|
+
const POSITIVE_INFINITY = -4;
|
|
90
|
+
const NEGATIVE_INFINITY = -5;
|
|
91
|
+
const NEGATIVE_ZERO = -6;
|
|
92
|
+
|
|
93
|
+
const parse = (serialized) => {
|
|
94
|
+
if (serialized === '') return undefined;
|
|
95
|
+
const values = JSON.parse(serialized);
|
|
96
|
+
const hydrated = new Array(values.length);
|
|
97
|
+
|
|
98
|
+
const hydrate = (index) => {
|
|
99
|
+
if (index === UNDEFINED) return undefined;
|
|
100
|
+
if (index === HOLE) return undefined;
|
|
101
|
+
if (index === NAN) return NaN;
|
|
102
|
+
if (index === POSITIVE_INFINITY) return Infinity;
|
|
103
|
+
if (index === NEGATIVE_INFINITY) return -Infinity;
|
|
104
|
+
if (index === NEGATIVE_ZERO) return -0;
|
|
105
|
+
|
|
106
|
+
if (hydrated[index] !== undefined) return hydrated[index];
|
|
107
|
+
|
|
108
|
+
const value = values[index];
|
|
109
|
+
|
|
110
|
+
if (typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean' || value === null) {
|
|
111
|
+
hydrated[index] = value;
|
|
112
|
+
} else if (Array.isArray(value)) {
|
|
113
|
+
if (typeof value[0] === 'string') {
|
|
114
|
+
const type = value[0];
|
|
115
|
+
switch (type) {
|
|
116
|
+
case 'Date': hydrated[index] = new Date(value[1]); break;
|
|
117
|
+
case 'Set': hydrated[index] = new Set(value.slice(1).map(hydrate)); break;
|
|
118
|
+
case 'Map': hydrated[index] = new Map(value.slice(1).map(([k, v]) => [hydrate(k), hydrate(v)])); break;
|
|
119
|
+
case 'RegExp': hydrated[index] = new RegExp(value[1], value[2]); break;
|
|
120
|
+
case 'BigInt': hydrated[index] = BigInt(value[1]); break;
|
|
121
|
+
case 'URL': hydrated[index] = new URL(value[1]); break;
|
|
122
|
+
case 'URLSearchParams': hydrated[index] = new URLSearchParams(value[1]); break;
|
|
123
|
+
case 'Int8Array': case 'Uint8Array': case 'Uint8ClampedArray':
|
|
124
|
+
case 'Int16Array': case 'Uint16Array': case 'Int32Array': case 'Uint32Array':
|
|
125
|
+
case 'Float32Array': case 'Float64Array': case 'BigInt64Array': case 'BigUint64Array':
|
|
126
|
+
hydrated[index] = new globalThis[type](value.slice(1).map(hydrate));
|
|
127
|
+
break;
|
|
128
|
+
case 'ArrayBuffer': {
|
|
129
|
+
const bytes = value.slice(1).map(hydrate);
|
|
130
|
+
const buffer = new ArrayBuffer(bytes.length);
|
|
131
|
+
new Uint8Array(buffer).set(bytes);
|
|
132
|
+
hydrated[index] = buffer;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
default:
|
|
136
|
+
const arr = new Array(value.length);
|
|
137
|
+
hydrated[index] = arr;
|
|
138
|
+
for (let i = 0; i < value.length; i++) arr[i] = hydrate(value[i]);
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
const arr = new Array(value.length);
|
|
142
|
+
hydrated[index] = arr;
|
|
143
|
+
for (let i = 0; i < value.length; i++) arr[i] = hydrate(value[i]);
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
const obj = {};
|
|
147
|
+
hydrated[index] = obj;
|
|
148
|
+
for (const key in value) obj[key] = hydrate(value[key]);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return hydrated[index];
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
return hydrate(0);
|
|
155
|
+
};
|
|
156
|
+
`;
|
|
157
|
+
|
|
158
|
+
export const CLIENT_CODE = `
|
|
159
|
+
__DEVALUE_STRINGIFY__
|
|
160
|
+
__DEVALUE_PARSE__
|
|
161
|
+
|
|
162
|
+
const ws = new WebSocket(__WS_URL__);
|
|
163
|
+
const pending = new Map();
|
|
164
|
+
let nextId = 1;
|
|
165
|
+
|
|
166
|
+
const ready = new Promise((resolve, reject) => {
|
|
167
|
+
ws.onopen = () => resolve(undefined);
|
|
168
|
+
ws.onerror = (e) => reject(e);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
ws.onmessage = (event) => {
|
|
172
|
+
const msg = parse(event.data);
|
|
173
|
+
const resolver = pending.get(msg.id);
|
|
174
|
+
if (!resolver) return;
|
|
175
|
+
|
|
176
|
+
if (msg.type === "error") {
|
|
177
|
+
resolver.reject(new Error(msg.error));
|
|
178
|
+
pending.delete(msg.id);
|
|
179
|
+
} else if (msg.type === "result") {
|
|
180
|
+
if (msg.valueType === "function") {
|
|
181
|
+
resolver.resolve(createProxy(msg.path));
|
|
182
|
+
} else if (msg.valueType === "asynciterator") {
|
|
183
|
+
const iteratorProxy = {
|
|
184
|
+
[Symbol.asyncIterator]() { return this; },
|
|
185
|
+
async next() {
|
|
186
|
+
await ready;
|
|
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
|
+
});
|
|
192
|
+
},
|
|
193
|
+
async return(value) {
|
|
194
|
+
await ready;
|
|
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
|
+
});
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
resolver.resolve(iteratorProxy);
|
|
203
|
+
} else {
|
|
204
|
+
resolver.resolve(msg.value);
|
|
205
|
+
}
|
|
206
|
+
pending.delete(msg.id);
|
|
207
|
+
} else if (msg.type === "iterate-result") {
|
|
208
|
+
resolver.resolve({ value: msg.value, done: msg.done });
|
|
209
|
+
pending.delete(msg.id);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const createProxy = (path = []) => new Proxy(function(){}, {
|
|
214
|
+
get(_, prop) {
|
|
215
|
+
if (prop === "then" || prop === Symbol.toStringTag) return undefined;
|
|
216
|
+
return createProxy([...path, prop]);
|
|
217
|
+
},
|
|
218
|
+
async apply(_, __, args) {
|
|
219
|
+
await ready;
|
|
220
|
+
const id = nextId++;
|
|
221
|
+
return new Promise((resolve, reject) => {
|
|
222
|
+
pending.set(id, { resolve, reject });
|
|
223
|
+
ws.send(stringify({ type: "call", id, path, args }));
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
__NAMED_EXPORTS__
|
|
229
|
+
`;
|
package/entry.js
ADDED
package/handler.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { stringify, parse } from "devalue";
|
|
2
|
+
import { CLIENT_CODE, DEVALUE_PARSE, DEVALUE_STRINGIFY } from "./client.js";
|
|
3
|
+
|
|
4
|
+
const getByPath = (obj, path) => {
|
|
5
|
+
let current = obj;
|
|
6
|
+
for (const key of path) {
|
|
7
|
+
if (current == null) return undefined;
|
|
8
|
+
current = current[key];
|
|
9
|
+
}
|
|
10
|
+
return current;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const isAsyncIterable = (value) =>
|
|
14
|
+
value != null && typeof value[Symbol.asyncIterator] === "function";
|
|
15
|
+
|
|
16
|
+
export const createHandler = (exports) => {
|
|
17
|
+
const exportKeys = Object.keys(exports);
|
|
18
|
+
const iteratorStore = new Map();
|
|
19
|
+
let nextIteratorId = 1;
|
|
20
|
+
|
|
21
|
+
const send = (ws, data) => {
|
|
22
|
+
ws.send(stringify(data));
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
async fetch(request) {
|
|
27
|
+
const url = new URL(request.url);
|
|
28
|
+
const upgradeHeader = request.headers.get("Upgrade");
|
|
29
|
+
|
|
30
|
+
if (upgradeHeader === "websocket") {
|
|
31
|
+
const pair = new WebSocketPair();
|
|
32
|
+
const [client, server] = Object.values(pair);
|
|
33
|
+
|
|
34
|
+
server.accept();
|
|
35
|
+
|
|
36
|
+
server.addEventListener("message", async (event) => {
|
|
37
|
+
try {
|
|
38
|
+
const msg = parse(event.data);
|
|
39
|
+
const { type, id, path = [], args = [], iteratorId } = msg;
|
|
40
|
+
|
|
41
|
+
if (type === "call") {
|
|
42
|
+
try {
|
|
43
|
+
const fn = getByPath(exports, path);
|
|
44
|
+
if (typeof fn !== "function") {
|
|
45
|
+
send(server, { type: "error", id, error: `${path.join(".")} is not a function` });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const result = await fn.apply(undefined, args);
|
|
49
|
+
|
|
50
|
+
if (isAsyncIterable(result)) {
|
|
51
|
+
const iterId = nextIteratorId++;
|
|
52
|
+
iteratorStore.set(iterId, result[Symbol.asyncIterator]());
|
|
53
|
+
send(server, { type: "result", id, iteratorId: iterId, valueType: "asynciterator" });
|
|
54
|
+
} else if (typeof result === "function") {
|
|
55
|
+
send(server, { type: "result", id, path: [...path], valueType: "function" });
|
|
56
|
+
} else {
|
|
57
|
+
send(server, { type: "result", id, value: result });
|
|
58
|
+
}
|
|
59
|
+
} catch (err) {
|
|
60
|
+
send(server, { type: "error", id, error: String(err) });
|
|
61
|
+
}
|
|
62
|
+
} else if (type === "iterate-next") {
|
|
63
|
+
const iter = iteratorStore.get(iteratorId);
|
|
64
|
+
if (!iter) {
|
|
65
|
+
send(server, { type: "error", id, error: "Iterator not found" });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const { value, done } = await iter.next();
|
|
70
|
+
if (done) iteratorStore.delete(iteratorId);
|
|
71
|
+
send(server, { type: "iterate-result", id, value, done: !!done });
|
|
72
|
+
} catch (err) {
|
|
73
|
+
send(server, { type: "error", id, error: String(err) });
|
|
74
|
+
}
|
|
75
|
+
} else if (type === "iterate-return") {
|
|
76
|
+
const iter = iteratorStore.get(iteratorId);
|
|
77
|
+
if (iter?.return) await iter.return(undefined);
|
|
78
|
+
iteratorStore.delete(iteratorId);
|
|
79
|
+
send(server, { type: "iterate-result", id, value: undefined, done: true });
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.error("WebSocket message error:", err);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
server.addEventListener("close", () => {
|
|
87
|
+
iteratorStore.clear();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return new Response(null, { status: 101, webSocket: client });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const wsProtocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
94
|
+
const wsUrl = `${wsProtocol}//${url.host}${url.pathname}`;
|
|
95
|
+
|
|
96
|
+
// Generate named exports
|
|
97
|
+
const namedExports = exportKeys
|
|
98
|
+
.map((key) => `export const ${key} = createProxy([${JSON.stringify(key)}]);`)
|
|
99
|
+
.join("\n");
|
|
100
|
+
|
|
101
|
+
const clientCode = CLIENT_CODE
|
|
102
|
+
.replace("__WS_URL__", JSON.stringify(wsUrl))
|
|
103
|
+
.replace("__DEVALUE_STRINGIFY__", DEVALUE_STRINGIFY)
|
|
104
|
+
.replace("__DEVALUE_PARSE__", DEVALUE_PARSE)
|
|
105
|
+
.replace("__NAMED_EXPORTS__", namedExports);
|
|
106
|
+
|
|
107
|
+
return new Response(clientCode, {
|
|
108
|
+
headers: {
|
|
109
|
+
"Content-Type": "application/javascript; charset=utf-8",
|
|
110
|
+
"Access-Control-Allow-Origin": "*",
|
|
111
|
+
"Cache-Control": "no-cache",
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "export-runtime",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Cloudflare Workers ESM Export Framework Runtime",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cloudflare",
|
|
7
|
+
"workers",
|
|
8
|
+
"esm",
|
|
9
|
+
"rpc",
|
|
10
|
+
"websocket"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/ihasq/export#readme",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/ihasq/export.git",
|
|
16
|
+
"directory": "packages/export-runtime"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "ihasq",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"main": "./entry.js",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": "./entry.js"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"entry.js",
|
|
27
|
+
"handler.js",
|
|
28
|
+
"client.js"
|
|
29
|
+
],
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"devalue": "^5.1.1"
|
|
32
|
+
}
|
|
33
|
+
}
|