@wuyuchentr/webassembly-runtime-embed 2.0.0 → 3.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/index.js +5 -0
- package/src/pool.js +204 -0
- package/src/runtime.js +14 -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": "3.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/index.js
CHANGED
|
@@ -3,6 +3,8 @@ 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');
|
|
6
8
|
const loader = require('./loader.js');
|
|
7
9
|
|
|
8
10
|
function createRuntime(options = {}) {
|
|
@@ -24,5 +26,8 @@ module.exports = {
|
|
|
24
26
|
HostRegistry,
|
|
25
27
|
ResourceLimits,
|
|
26
28
|
WasiContext,
|
|
29
|
+
WasmPool,
|
|
30
|
+
InstanceLease,
|
|
31
|
+
HotReload,
|
|
27
32
|
loader,
|
|
28
33
|
};
|
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,8 @@
|
|
|
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');
|
|
4
6
|
const loader = require('./loader.js');
|
|
5
7
|
|
|
6
8
|
class WasmInstance {
|
|
@@ -135,6 +137,18 @@ class WasmRuntime {
|
|
|
135
137
|
this.instances = [];
|
|
136
138
|
}
|
|
137
139
|
|
|
140
|
+
createPool(options = {}) {
|
|
141
|
+
if (!options.source && !options.sourceOptions) {
|
|
142
|
+
options.sourceOptions = { ...this.options };
|
|
143
|
+
}
|
|
144
|
+
return new WasmPool(this, options);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
watch(source, options = {}) {
|
|
148
|
+
const hr = new HotReload(this, source, options);
|
|
149
|
+
return hr;
|
|
150
|
+
}
|
|
151
|
+
|
|
138
152
|
getStats() {
|
|
139
153
|
return {
|
|
140
154
|
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 };
|