@wuyuchentr/webassembly-runtime-embed 2.0.0 → 4.0.0
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/package.json +1 -1
- package/src/cluster.js +365 -0
- package/src/index.js +9 -0
- package/src/pool.js +204 -0
- package/src/runtime.js +27 -0
- package/src/watcher.js +128 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wuyuchentr/webassembly-runtime-embed",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Embed WebAssembly runtime in Node.js with WASI support, multi-instance isolation, resource limits, and remote loading with cache.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"webassembly-runtime-embed": "bin/cli.js"
|
package/src/cluster.js
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
const net = require('net');
|
|
2
|
+
const crypto = require('crypto');
|
|
3
|
+
const EventEmitter = require('events');
|
|
4
|
+
|
|
5
|
+
const PROTOCOL_VERSION = 1;
|
|
6
|
+
|
|
7
|
+
function pack(msg) {
|
|
8
|
+
return JSON.stringify(msg) + '\n';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function unpack(data) {
|
|
12
|
+
const lines = data.toString('utf8').trim().split('\n');
|
|
13
|
+
return lines.filter(Boolean).map(l => JSON.parse(l));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class WasmWorker extends EventEmitter {
|
|
17
|
+
constructor(runtime, options = {}) {
|
|
18
|
+
super();
|
|
19
|
+
this.runtime = runtime;
|
|
20
|
+
this.port = options.port || 0;
|
|
21
|
+
this.host = options.host || '0.0.0.0';
|
|
22
|
+
this.authToken = options.authToken || null;
|
|
23
|
+
this._instances = new Map();
|
|
24
|
+
this._server = null;
|
|
25
|
+
this._sockets = new Set();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
register(name, instance) {
|
|
29
|
+
this._instances.set(name, instance);
|
|
30
|
+
this.emit('register', name, instance);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
unregister(name) {
|
|
34
|
+
this._instances.delete(name);
|
|
35
|
+
this.emit('unregister', name);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
list() {
|
|
39
|
+
return Array.from(this._instances.keys());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async listen() {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
this._server = net.createServer((socket) => this._handleSocket(socket));
|
|
45
|
+
this._server.once('error', reject);
|
|
46
|
+
this._server.listen(this.port, this.host, () => {
|
|
47
|
+
const addr = this._server.address();
|
|
48
|
+
this.port = addr.port;
|
|
49
|
+
this.emit('listening', { port: addr.port, host: this.host });
|
|
50
|
+
resolve(addr);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async close() {
|
|
56
|
+
for (const socket of this._sockets) {
|
|
57
|
+
socket.destroy();
|
|
58
|
+
}
|
|
59
|
+
this._sockets.clear();
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
if (!this._server) { resolve(); return; }
|
|
62
|
+
this._server.close(() => resolve());
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
_handleSocket(socket) {
|
|
67
|
+
const conn = { socket, buffer: Buffer.alloc(0), authenticated: !this.authToken };
|
|
68
|
+
this._sockets.add(socket);
|
|
69
|
+
|
|
70
|
+
socket.on('data', (data) => this._onData(conn, data));
|
|
71
|
+
socket.on('close', () => this._sockets.delete(socket));
|
|
72
|
+
socket.on('error', () => this._sockets.delete(socket));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
_onData(conn, data) {
|
|
76
|
+
conn.buffer = Buffer.concat([conn.buffer, data]);
|
|
77
|
+
|
|
78
|
+
while (true) {
|
|
79
|
+
const nlIdx = conn.buffer.indexOf(0x0a);
|
|
80
|
+
if (nlIdx === -1) break;
|
|
81
|
+
|
|
82
|
+
const line = conn.buffer.slice(0, nlIdx);
|
|
83
|
+
conn.buffer = conn.buffer.slice(nlIdx + 1);
|
|
84
|
+
|
|
85
|
+
if (line.length === 0) continue;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const msg = JSON.parse(line.toString('utf8'));
|
|
89
|
+
this._handleMessage(conn, msg);
|
|
90
|
+
} catch {
|
|
91
|
+
this._sendError(conn.socket, null, 'invalid JSON');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async _handleMessage(conn, msg) {
|
|
97
|
+
if (msg.type === 'auth') {
|
|
98
|
+
if (this.authToken && msg.token !== this.authToken) {
|
|
99
|
+
this._sendError(conn.socket, msg.id, 'auth failed');
|
|
100
|
+
conn.socket.destroy();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
conn.authenticated = true;
|
|
104
|
+
this._sendResponse(conn.socket, msg.id, { authenticated: true, version: PROTOCOL_VERSION });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!conn.authenticated) {
|
|
109
|
+
this._sendError(conn.socket, msg.id, 'not authenticated');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
switch (msg.type) {
|
|
114
|
+
case 'call': {
|
|
115
|
+
await this._handleCall(conn, msg);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case 'list': {
|
|
119
|
+
this._sendResponse(conn.socket, msg.id, { instances: this.list() });
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
case 'ping': {
|
|
123
|
+
this._sendResponse(conn.socket, msg.id, 'pong');
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
default:
|
|
127
|
+
this._sendError(conn.socket, msg.id, `unknown type: ${msg.type}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async _handleCall(conn, msg) {
|
|
132
|
+
const { instance: name, function: funcName, args } = msg;
|
|
133
|
+
const inst = this._instances.get(name);
|
|
134
|
+
if (!inst) {
|
|
135
|
+
this._sendError(conn.socket, msg.id, `instance '${name}' not found`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (!inst.hasExport(funcName)) {
|
|
139
|
+
this._sendError(conn.socket, msg.id, `function '${funcName}' not found on '${name}'`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const convertedArgs = (args || []).map(a => {
|
|
144
|
+
if (typeof a === 'number') return a;
|
|
145
|
+
if (typeof a === 'bigint') return a;
|
|
146
|
+
return a;
|
|
147
|
+
});
|
|
148
|
+
const result = await inst.call(funcName, ...convertedArgs);
|
|
149
|
+
this._sendResponse(conn.socket, msg.id, { result });
|
|
150
|
+
} catch (err) {
|
|
151
|
+
this._sendError(conn.socket, msg.id, err.message);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
_sendResponse(socket, id, data) {
|
|
156
|
+
const msg = { id, type: 'response' };
|
|
157
|
+
if (data !== null && data !== undefined && typeof data === 'object' && !Array.isArray(data)) {
|
|
158
|
+
Object.assign(msg, data);
|
|
159
|
+
} else {
|
|
160
|
+
msg.result = data;
|
|
161
|
+
}
|
|
162
|
+
socket.write(pack(msg));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
_sendError(socket, id, message) {
|
|
166
|
+
socket.write(pack({ id, type: 'error', error: message }));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
class WasmClient {
|
|
171
|
+
constructor(options = {}) {
|
|
172
|
+
this.host = options.host || '127.0.0.1';
|
|
173
|
+
this.port = options.port;
|
|
174
|
+
this.authToken = options.authToken || null;
|
|
175
|
+
this.autoReconnect = options.autoReconnect !== false;
|
|
176
|
+
this.reconnectDelay = options.reconnectDelay || 1000;
|
|
177
|
+
this._socket = null;
|
|
178
|
+
this._buffer = Buffer.alloc(0);
|
|
179
|
+
this._pending = new Map();
|
|
180
|
+
this._nextId = 1;
|
|
181
|
+
this._connected = false;
|
|
182
|
+
this._reconnectTimer = null;
|
|
183
|
+
this._closed = false;
|
|
184
|
+
this._connectPromise = null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async connect() {
|
|
188
|
+
if (this._connected) return;
|
|
189
|
+
if (this._connectPromise) return this._connectPromise;
|
|
190
|
+
|
|
191
|
+
this._connectPromise = new Promise((resolve, reject) => {
|
|
192
|
+
const socket = new net.Socket();
|
|
193
|
+
socket.connect(this.port, this.host, async () => {
|
|
194
|
+
this._socket = socket;
|
|
195
|
+
this._connected = true;
|
|
196
|
+
this._onConnected();
|
|
197
|
+
if (this.authToken) {
|
|
198
|
+
await this._send({ type: 'auth', token: this.authToken });
|
|
199
|
+
}
|
|
200
|
+
this._connectPromise = null;
|
|
201
|
+
resolve();
|
|
202
|
+
});
|
|
203
|
+
socket.on('data', (data) => this._onData(data));
|
|
204
|
+
socket.on('close', () => this._onDisconnect());
|
|
205
|
+
socket.on('error', (err) => {
|
|
206
|
+
this._connectPromise = null;
|
|
207
|
+
reject(err);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
return this._connectPromise;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
_onConnected() {
|
|
215
|
+
this._socket.on('data', (data) => this._onData(data));
|
|
216
|
+
this._socket.on('close', () => this._onDisconnect());
|
|
217
|
+
this._socket.on('error', () => {});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
_onData(data) {
|
|
221
|
+
this._buffer = Buffer.concat([this._buffer, data]);
|
|
222
|
+
while (true) {
|
|
223
|
+
const nlIdx = this._buffer.indexOf(0x0a);
|
|
224
|
+
if (nlIdx === -1) break;
|
|
225
|
+
const line = this._buffer.slice(0, nlIdx);
|
|
226
|
+
this._buffer = this._buffer.slice(nlIdx + 1);
|
|
227
|
+
if (line.length === 0) continue;
|
|
228
|
+
try {
|
|
229
|
+
const msg = JSON.parse(line.toString('utf8'));
|
|
230
|
+
this._handleResponse(msg);
|
|
231
|
+
} catch {}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
_handleResponse(msg) {
|
|
236
|
+
const pending = this._pending.get(msg.id);
|
|
237
|
+
if (!pending) return;
|
|
238
|
+
this._pending.delete(msg.id);
|
|
239
|
+
if (msg.type === 'error') {
|
|
240
|
+
pending.reject(new Error(msg.error));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (msg.result !== undefined) {
|
|
244
|
+
pending.resolve(msg.result);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const { id, type, ...rest } = msg;
|
|
248
|
+
pending.resolve(Object.keys(rest).length === 1 ? Object.values(rest)[0] : rest);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
_onDisconnect() {
|
|
252
|
+
this._connected = false;
|
|
253
|
+
this._socket = null;
|
|
254
|
+
|
|
255
|
+
for (const [, pending] of this._pending) {
|
|
256
|
+
pending.reject(new Error('connection closed'));
|
|
257
|
+
}
|
|
258
|
+
this._pending.clear();
|
|
259
|
+
|
|
260
|
+
if (this.autoReconnect && !this._closed) {
|
|
261
|
+
this._reconnectTimer = setTimeout(() => this.connect(), this.reconnectDelay);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async close() {
|
|
266
|
+
this._closed = true;
|
|
267
|
+
if (this._reconnectTimer) clearTimeout(this._reconnectTimer);
|
|
268
|
+
if (this._socket) {
|
|
269
|
+
this._socket.destroy();
|
|
270
|
+
this._socket = null;
|
|
271
|
+
}
|
|
272
|
+
this._connected = false;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async _send(msg) {
|
|
276
|
+
if (!this._connected) await this.connect();
|
|
277
|
+
return new Promise((resolve, reject) => {
|
|
278
|
+
const id = this._nextId++;
|
|
279
|
+
msg.id = id;
|
|
280
|
+
this._pending.set(id, { resolve, reject });
|
|
281
|
+
try {
|
|
282
|
+
this._socket.write(pack(msg));
|
|
283
|
+
} catch (err) {
|
|
284
|
+
this._pending.delete(id);
|
|
285
|
+
reject(err);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async call(instance, func, args = []) {
|
|
291
|
+
const response = await this._send({ type: 'call', instance, function: func, args });
|
|
292
|
+
return response;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async list() {
|
|
296
|
+
const response = await this._send({ type: 'list' });
|
|
297
|
+
return Array.isArray(response) ? response : (response.instances || []);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async ping() {
|
|
301
|
+
const response = await this._send({ type: 'ping' });
|
|
302
|
+
return response === 'pong';
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
class WasmCluster {
|
|
307
|
+
constructor(options = {}) {
|
|
308
|
+
this.workers = options.workers || [];
|
|
309
|
+
this.strategy = options.strategy || 'round-robin';
|
|
310
|
+
this._clients = this.workers.map(w => {
|
|
311
|
+
const c = new WasmClient({ host: w.host, port: w.port, autoReconnect: true });
|
|
312
|
+
return { client: c, weight: w.weight || 1 };
|
|
313
|
+
});
|
|
314
|
+
this._rrIndex = 0;
|
|
315
|
+
this._connected = false;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async connect() {
|
|
319
|
+
const results = await Promise.allSettled(
|
|
320
|
+
this._clients.map(c => c.client.connect())
|
|
321
|
+
);
|
|
322
|
+
this._connected = results.some(r => r.status === 'fulfilled');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
_nextClient() {
|
|
326
|
+
const available = this._clients.filter(c => c.client._connected);
|
|
327
|
+
if (available.length === 0) throw new Error('no connected workers');
|
|
328
|
+
|
|
329
|
+
switch (this.strategy) {
|
|
330
|
+
case 'round-robin': {
|
|
331
|
+
const idx = this._rrIndex % available.length;
|
|
332
|
+
this._rrIndex++;
|
|
333
|
+
return available[idx];
|
|
334
|
+
}
|
|
335
|
+
case 'first': return available[0];
|
|
336
|
+
case 'random': return available[Math.floor(Math.random() * available.length)];
|
|
337
|
+
default: return available[0];
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async call(instance, func, args = []) {
|
|
342
|
+
const entry = this._nextClient();
|
|
343
|
+
return entry.client.call(instance, func, args);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async list() {
|
|
347
|
+
const results = {};
|
|
348
|
+
for (const entry of this._clients) {
|
|
349
|
+
try {
|
|
350
|
+
const instances = await entry.client.list();
|
|
351
|
+
results[`${entry.client.host}:${entry.client.port}`] = instances;
|
|
352
|
+
} catch {}
|
|
353
|
+
}
|
|
354
|
+
return results;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async close() {
|
|
358
|
+
for (const entry of this._clients) {
|
|
359
|
+
await entry.client.close();
|
|
360
|
+
}
|
|
361
|
+
this._connected = false;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
module.exports = { WasmWorker, WasmClient, WasmCluster };
|
package/src/index.js
CHANGED
|
@@ -3,6 +3,9 @@ const { MemoryView } = require('./memory.js');
|
|
|
3
3
|
const { HostRegistry } = require('./host.js');
|
|
4
4
|
const { ResourceLimits } = require('./limits.js');
|
|
5
5
|
const { WasiContext } = require('./wasi.js');
|
|
6
|
+
const { WasmPool, InstanceLease } = require('./pool.js');
|
|
7
|
+
const { HotReload } = require('./watcher.js');
|
|
8
|
+
const { WasmWorker, WasmClient, WasmCluster } = require('./cluster.js');
|
|
6
9
|
const loader = require('./loader.js');
|
|
7
10
|
|
|
8
11
|
function createRuntime(options = {}) {
|
|
@@ -24,5 +27,11 @@ module.exports = {
|
|
|
24
27
|
HostRegistry,
|
|
25
28
|
ResourceLimits,
|
|
26
29
|
WasiContext,
|
|
30
|
+
WasmPool,
|
|
31
|
+
InstanceLease,
|
|
32
|
+
HotReload,
|
|
33
|
+
WasmWorker,
|
|
34
|
+
WasmClient,
|
|
35
|
+
WasmCluster,
|
|
27
36
|
loader,
|
|
28
37
|
};
|
package/src/pool.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
class InstanceLease {
|
|
4
|
+
constructor(pool, instance) {
|
|
5
|
+
this.pool = pool;
|
|
6
|
+
this.instance = instance;
|
|
7
|
+
this._released = false;
|
|
8
|
+
this.id = crypto.randomUUID();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
release() {
|
|
12
|
+
if (this._released) return false;
|
|
13
|
+
this._released = true;
|
|
14
|
+
this.pool._release(this);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async call(name, ...args) {
|
|
19
|
+
return this.instance.call(name, ...args);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class WasmPool {
|
|
24
|
+
constructor(runtime, options = {}) {
|
|
25
|
+
this.runtime = runtime;
|
|
26
|
+
this.source = options.source || null;
|
|
27
|
+
this.sourceOptions = options.sourceOptions || {};
|
|
28
|
+
this.maxSize = options.maxSize || 10;
|
|
29
|
+
this.minSize = options.minSize || 0;
|
|
30
|
+
this.acquireTimeout = options.acquireTimeout || 0;
|
|
31
|
+
this.idleTimeout = options.idleTimeout || 60000;
|
|
32
|
+
this._idle = [];
|
|
33
|
+
this._busy = new Map();
|
|
34
|
+
this._pending = [];
|
|
35
|
+
this._closed = false;
|
|
36
|
+
this._warmPromise = null;
|
|
37
|
+
this._reaperTimer = null;
|
|
38
|
+
this._instanceIdCounter = 0;
|
|
39
|
+
this._creating = 0;
|
|
40
|
+
|
|
41
|
+
if (this.idleTimeout > 0) {
|
|
42
|
+
this._startReaper();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_startReaper() {
|
|
47
|
+
this._reaperTimer = setInterval(() => {
|
|
48
|
+
this._reapIdle();
|
|
49
|
+
}, Math.min(this.idleTimeout, 30000));
|
|
50
|
+
if (this._reaperTimer.unref) this._reaperTimer.unref();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async warm() {
|
|
54
|
+
if (this._warmPromise) return this._warmPromise;
|
|
55
|
+
this._warmPromise = this._warmInternal();
|
|
56
|
+
return this._warmPromise;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async _warmInternal() {
|
|
60
|
+
const target = this.minSize;
|
|
61
|
+
const toCreate = Math.max(0, target - this._countTotal());
|
|
62
|
+
const tasks = [];
|
|
63
|
+
for (let i = 0; i < toCreate; i++) {
|
|
64
|
+
tasks.push(this._createInstance());
|
|
65
|
+
}
|
|
66
|
+
const instances = await Promise.all(tasks);
|
|
67
|
+
for (const inst of instances) {
|
|
68
|
+
this._idle.push(inst);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async _createInstance() {
|
|
73
|
+
const id = ++this._instanceIdCounter;
|
|
74
|
+
const inst = await this.runtime.load(this.source, this.sourceOptions);
|
|
75
|
+
inst._poolId = id;
|
|
76
|
+
inst._createdAt = Date.now();
|
|
77
|
+
return inst;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
_countTotal() {
|
|
81
|
+
return this._idle.length + this._busy.size;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async acquire() {
|
|
85
|
+
if (this._closed) throw new Error('Pool is closed');
|
|
86
|
+
|
|
87
|
+
if (this._idle.length > 0) {
|
|
88
|
+
const inst = this._idle.pop();
|
|
89
|
+
this._busy.set(inst._poolId, inst);
|
|
90
|
+
return new InstanceLease(this, inst);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (this._countTotal() + this._creating >= this.maxSize) {
|
|
94
|
+
return this._enqueue();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this._creating++;
|
|
98
|
+
try {
|
|
99
|
+
const inst = await this._createInstance();
|
|
100
|
+
this._creating--;
|
|
101
|
+
this._busy.set(inst._poolId, inst);
|
|
102
|
+
return new InstanceLease(this, inst);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
this._creating--;
|
|
105
|
+
throw err;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
_enqueue() {
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
let timer = null;
|
|
112
|
+
const entry = { resolve, reject, timer: null };
|
|
113
|
+
|
|
114
|
+
if (this.acquireTimeout > 0) {
|
|
115
|
+
timer = setTimeout(() => {
|
|
116
|
+
const idx = this._pending.indexOf(entry);
|
|
117
|
+
if (idx !== -1) this._pending.splice(idx, 1);
|
|
118
|
+
reject(new Error(`Acquire timed out after ${this.acquireTimeout}ms`));
|
|
119
|
+
}, this.acquireTimeout);
|
|
120
|
+
entry.timer = timer;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this._pending.push(entry);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
_release(lease) {
|
|
128
|
+
const inst = lease.instance;
|
|
129
|
+
this._busy.delete(inst._poolId);
|
|
130
|
+
|
|
131
|
+
if (this._closed) return;
|
|
132
|
+
|
|
133
|
+
if (this._pending.length > 0) {
|
|
134
|
+
this._busy.set(inst._poolId, inst);
|
|
135
|
+
const entry = this._pending.shift();
|
|
136
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
137
|
+
entry.resolve(new InstanceLease(this, inst));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
inst._lastUsedAt = Date.now();
|
|
142
|
+
this._idle.push(inst);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async exec(fn) {
|
|
146
|
+
const lease = await this.acquire();
|
|
147
|
+
try {
|
|
148
|
+
return await fn(lease.instance, lease);
|
|
149
|
+
} finally {
|
|
150
|
+
lease.release();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async drain() {
|
|
155
|
+
this._closed = true;
|
|
156
|
+
|
|
157
|
+
for (const entry of this._pending) {
|
|
158
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
159
|
+
entry.reject(new Error('Pool is draining'));
|
|
160
|
+
}
|
|
161
|
+
this._pending = [];
|
|
162
|
+
|
|
163
|
+
const all = [...this._idle, ...Array.from(this._busy.values())];
|
|
164
|
+
this._idle = [];
|
|
165
|
+
this._busy.clear();
|
|
166
|
+
|
|
167
|
+
if (this._reaperTimer) {
|
|
168
|
+
clearInterval(this._reaperTimer);
|
|
169
|
+
this._reaperTimer = null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
_reapIdle() {
|
|
174
|
+
if (this._idle.length === 0) return;
|
|
175
|
+
const now = Date.now();
|
|
176
|
+
const keep = Math.max(0, this.minSize);
|
|
177
|
+
const reapable = this._idle
|
|
178
|
+
.filter(i => now - (i._lastUsedAt || i._createdAt) > this.idleTimeout)
|
|
179
|
+
.sort((a, b) => (a._lastUsedAt || a._createdAt) - (b._lastUsedAt || b._createdAt));
|
|
180
|
+
|
|
181
|
+
const toRemove = Math.max(0, reapable.length - keep);
|
|
182
|
+
const removeSet = new Set(reapable.slice(0, toRemove).map(i => i._poolId));
|
|
183
|
+
this._idle = this._idle.filter(i => {
|
|
184
|
+
if (removeSet.has(i._poolId)) {
|
|
185
|
+
this.runtime.removeInstance(i);
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
return true;
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
stats() {
|
|
193
|
+
return {
|
|
194
|
+
idle: this._idle.length,
|
|
195
|
+
busy: this._busy.size,
|
|
196
|
+
pending: this._pending.length,
|
|
197
|
+
total: this._countTotal(),
|
|
198
|
+
maxSize: this.maxSize,
|
|
199
|
+
minSize: this.minSize,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = { WasmPool, InstanceLease };
|
package/src/runtime.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
const { MemoryView } = require('./memory.js');
|
|
2
2
|
const { HostRegistry } = require('./host.js');
|
|
3
3
|
const { ResourceLimits } = require('./limits.js');
|
|
4
|
+
const { WasmPool } = require('./pool.js');
|
|
5
|
+
const { HotReload } = require('./watcher.js');
|
|
6
|
+
const { WasmWorker, WasmClient, WasmCluster } = require('./cluster.js');
|
|
4
7
|
const loader = require('./loader.js');
|
|
5
8
|
|
|
6
9
|
class WasmInstance {
|
|
@@ -135,6 +138,30 @@ class WasmRuntime {
|
|
|
135
138
|
this.instances = [];
|
|
136
139
|
}
|
|
137
140
|
|
|
141
|
+
createPool(options = {}) {
|
|
142
|
+
if (!options.source && !options.sourceOptions) {
|
|
143
|
+
options.sourceOptions = { ...this.options };
|
|
144
|
+
}
|
|
145
|
+
return new WasmPool(this, options);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
watch(source, options = {}) {
|
|
149
|
+
const hr = new HotReload(this, source, options);
|
|
150
|
+
return hr;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
createWorker(options = {}) {
|
|
154
|
+
return new WasmWorker(this, options);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
createClient(options = {}) {
|
|
158
|
+
return new WasmClient(options);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
createCluster(options = {}) {
|
|
162
|
+
return new WasmCluster(options);
|
|
163
|
+
}
|
|
164
|
+
|
|
138
165
|
getStats() {
|
|
139
166
|
return {
|
|
140
167
|
instances: this.instances.length,
|
package/src/watcher.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const EventEmitter = require('events');
|
|
4
|
+
|
|
5
|
+
class HotReload extends EventEmitter {
|
|
6
|
+
constructor(runtime, source, options = {}) {
|
|
7
|
+
super();
|
|
8
|
+
this.runtime = runtime;
|
|
9
|
+
this.source = path.resolve(source);
|
|
10
|
+
this.options = options;
|
|
11
|
+
this.pollInterval = options.pollInterval || 1000;
|
|
12
|
+
this._currentModule = null;
|
|
13
|
+
this._currentInstance = null;
|
|
14
|
+
this._watcher = null;
|
|
15
|
+
this._pollTimer = null;
|
|
16
|
+
this._active = false;
|
|
17
|
+
this._reloading = false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async start() {
|
|
21
|
+
if (this._active) return;
|
|
22
|
+
this._active = true;
|
|
23
|
+
|
|
24
|
+
const mode = this.options.mode || 'poll';
|
|
25
|
+
|
|
26
|
+
if (mode === 'watch') {
|
|
27
|
+
this._startWatcher();
|
|
28
|
+
} else {
|
|
29
|
+
this._startPolling();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await this._loadModule();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_startWatcher() {
|
|
36
|
+
try {
|
|
37
|
+
this._watcher = fs.watch(this.source, (eventType) => {
|
|
38
|
+
if (eventType === 'change') {
|
|
39
|
+
this._scheduleReload();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
} catch {
|
|
43
|
+
this._startPolling();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_startPolling() {
|
|
48
|
+
this._lastMtime = this._getMtime();
|
|
49
|
+
this._pollTimer = setInterval(() => {
|
|
50
|
+
const mtime = this._getMtime();
|
|
51
|
+
if (mtime && mtime !== this._lastMtime) {
|
|
52
|
+
this._lastMtime = mtime;
|
|
53
|
+
this._scheduleReload();
|
|
54
|
+
}
|
|
55
|
+
}, this.pollInterval);
|
|
56
|
+
if (this._pollTimer.unref) this._pollTimer.unref();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
_getMtime() {
|
|
60
|
+
try {
|
|
61
|
+
return fs.statSync(this.source).mtimeMs;
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
_scheduleReload() {
|
|
68
|
+
if (this._reloading) return;
|
|
69
|
+
this._reloading = true;
|
|
70
|
+
|
|
71
|
+
const debounceMs = this.options.debounce || 200;
|
|
72
|
+
if (this._reloadTimer) clearTimeout(this._reloadTimer);
|
|
73
|
+
|
|
74
|
+
this._reloadTimer = setTimeout(async () => {
|
|
75
|
+
try {
|
|
76
|
+
await this._loadModule();
|
|
77
|
+
this.emit('reload', this._currentInstance);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
this.emit('error', err);
|
|
80
|
+
} finally {
|
|
81
|
+
this._reloading = false;
|
|
82
|
+
}
|
|
83
|
+
}, debounceMs);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async _loadModule() {
|
|
87
|
+
const oldInstance = this._currentInstance;
|
|
88
|
+
const newInstance = await this.runtime.load(this.source, this.options.sourceOptions);
|
|
89
|
+
|
|
90
|
+
this._currentInstance = newInstance;
|
|
91
|
+
this._currentModule = true;
|
|
92
|
+
this.emit('load', newInstance);
|
|
93
|
+
|
|
94
|
+
if (oldInstance) {
|
|
95
|
+
this.runtime.removeInstance(oldInstance);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
getInstance() {
|
|
100
|
+
return this._currentInstance;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async call(name, ...args) {
|
|
104
|
+
if (!this._currentInstance) {
|
|
105
|
+
throw new Error('Module not loaded yet');
|
|
106
|
+
}
|
|
107
|
+
return this._currentInstance.call(name, ...args);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
close() {
|
|
111
|
+
this._active = false;
|
|
112
|
+
if (this._watcher) {
|
|
113
|
+
this._watcher.close();
|
|
114
|
+
this._watcher = null;
|
|
115
|
+
}
|
|
116
|
+
if (this._pollTimer) {
|
|
117
|
+
clearInterval(this._pollTimer);
|
|
118
|
+
this._pollTimer = null;
|
|
119
|
+
}
|
|
120
|
+
if (this._reloadTimer) {
|
|
121
|
+
clearTimeout(this._reloadTimer);
|
|
122
|
+
this._reloadTimer = null;
|
|
123
|
+
}
|
|
124
|
+
this.removeAllListeners();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { HotReload };
|