@zhin.js/isolate 1.0.7 → 1.0.10
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/README.md +4 -7
- package/lib/contracts.d.ts +1 -1
- package/lib/runtime.d.ts +2 -2
- package/lib/runtime.js +55 -66
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
下一代 Zhin Runtime 的可选 Node 隔离适配器。它为每个 `runtime: isolated` Plugin generation 创建独立 Worker 或 child process,并只通过 structured-clone RPC 与 Host 通信。
|
|
4
4
|
|
|
5
|
-
> 当前包属于 `feature/next` 绿地实现,版本仍为 `0.0.0` 且未作为稳定 API 发布。
|
|
6
|
-
|
|
7
5
|
## 为什么独立成包
|
|
8
6
|
|
|
9
7
|
`@zhin.js/runtime` 只定义 `IsolatedPluginRuntimePort`,不强制默认 IM 安装携带隔离实现。需要故障或并发隔离的 Root 显式安装本包;trusted Plugin 的启动路径和生产闭包不变。
|
|
@@ -71,13 +69,12 @@ Host 从对应 owner 的 Resource snapshot 取得 `isolatedPluginToken`,再调
|
|
|
71
69
|
|
|
72
70
|
## 代际切换
|
|
73
71
|
|
|
74
|
-
|
|
72
|
+
隔离进程参与 Runtime generation readiness:
|
|
75
73
|
|
|
76
74
|
1. `prepare` 创建候选 isolate、import entry 并校验 definition,但不执行 setup。
|
|
77
|
-
2. `
|
|
78
|
-
3.
|
|
79
|
-
4.
|
|
80
|
-
5. 旧 generation 的最后一个 lease 释放后,Scope disposer 终止旧 isolate。
|
|
75
|
+
2. `activateNext(signal)` 在候选 isolate 内执行 setup;失败会清理候选,旧代从未被触碰。
|
|
76
|
+
3. Runtime 原子提交 snapshot 后,新请求才能从新 snapshot 取得 handle。
|
|
77
|
+
4. 旧 generation 的在途请求继续持有旧 lease;最后一个 lease 释放后,Scope disposer 终止旧 isolate。
|
|
81
78
|
|
|
82
79
|
RPC 超时会把整个实例标记为 failed 并终止 transport,因为远端工作可能仍在运行,不能把超时伪装成安全 drain。Worker/进程意外退出会拒绝全部 pending RPC,并通过 `onCrash` 上报;当前 generation 内不静默重启。
|
|
83
80
|
|
package/lib/contracts.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Dispose, type PluginId } from '@zhin.js/plugin-runtime';
|
|
2
|
-
export type IsolatedPluginStatus = 'prepared' | 'active' | '
|
|
2
|
+
export type IsolatedPluginStatus = 'prepared' | 'active' | 'failed' | 'closed';
|
|
3
3
|
export interface IsolatedPluginEvent {
|
|
4
4
|
readonly name: string;
|
|
5
5
|
readonly payload: unknown;
|
package/lib/runtime.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type PluginId } from '@zhin.js/plugin-runtime';
|
|
2
2
|
import type { IsolatedPluginPrepareRequest, IsolatedPluginRuntimePort, PreparedIsolatedPlugin } from '@zhin.js/runtime';
|
|
3
3
|
import { type IsolateMode } from './transport.js';
|
|
4
4
|
export interface IsolatedHostCallContext {
|
|
@@ -15,5 +15,5 @@ export interface NodeIsolatedPluginRuntimeOptions {
|
|
|
15
15
|
export declare class NodeIsolatedPluginRuntime implements IsolatedPluginRuntimePort {
|
|
16
16
|
#private;
|
|
17
17
|
constructor(options?: NodeIsolatedPluginRuntimeOptions);
|
|
18
|
-
prepare(request: IsolatedPluginPrepareRequest): Promise<PreparedIsolatedPlugin>;
|
|
18
|
+
prepare(request: IsolatedPluginPrepareRequest, signal: AbortSignal): Promise<PreparedIsolatedPlugin>;
|
|
19
19
|
}
|
package/lib/runtime.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { pathToFileURL } from 'node:url';
|
|
2
|
+
import { createGenerationAdmissionGate, generationAdmissionSource, } from '@zhin.js/plugin-runtime';
|
|
2
3
|
import { isolatedPluginToken, } from './contracts.js';
|
|
3
4
|
import { deserializeError, serializeError, } from './protocol.js';
|
|
4
5
|
import { createTransport } from './transport.js';
|
|
@@ -8,43 +9,28 @@ export class NodeIsolatedPluginRuntime {
|
|
|
8
9
|
#requestTimeoutMs;
|
|
9
10
|
#hostMethods;
|
|
10
11
|
#onCrash;
|
|
11
|
-
#quiescedPrevious = new Map();
|
|
12
12
|
constructor(options = {}) {
|
|
13
13
|
this.#mode = options.mode ?? 'worker';
|
|
14
14
|
this.#requestTimeoutMs = options.requestTimeoutMs ?? 10_000;
|
|
15
15
|
this.#hostMethods = Object.freeze({ ...options.hostMethods });
|
|
16
16
|
this.#onCrash = options.onCrash;
|
|
17
17
|
}
|
|
18
|
-
async prepare(request) {
|
|
18
|
+
async prepare(request, signal) {
|
|
19
|
+
signal.throwIfAborted();
|
|
19
20
|
assertCloneable(request.config, `Config for ${request.owner}`);
|
|
20
|
-
const
|
|
21
|
+
const admission = createGenerationAdmissionGate();
|
|
22
|
+
const instance = new IsolatedInstance(request.owner, createTransport(this.#mode), this.#requestTimeoutMs, this.#hostMethods, this.#onCrash, admission);
|
|
21
23
|
try {
|
|
22
|
-
const descriptor = await instance.
|
|
24
|
+
const descriptor = await instance.requestAbortable('prepare', {
|
|
23
25
|
...request,
|
|
24
26
|
entry: pathToFileURL(request.entry).href,
|
|
25
|
-
});
|
|
27
|
+
}, signal);
|
|
26
28
|
return {
|
|
27
29
|
descriptor,
|
|
28
30
|
resources: [{ token: isolatedPluginToken, value: instance }],
|
|
29
31
|
handoff: {
|
|
30
|
-
|
|
31
|
-
const handle = previous.resources.get(request.owner)?.get(isolatedPluginToken.id);
|
|
32
|
-
if (!(handle instanceof IsolatedInstance))
|
|
33
|
-
return;
|
|
34
|
-
await handle.quiesce();
|
|
35
|
-
this.#quiescedPrevious.set(request.owner, handle);
|
|
36
|
-
},
|
|
37
|
-
activateNext: () => instance.activate(),
|
|
32
|
+
activateNext: (activationSignal) => instance.activate(activationSignal),
|
|
38
33
|
deactivateNext: () => instance.deactivate(),
|
|
39
|
-
resumePrevious: () => {
|
|
40
|
-
const previous = this.#quiescedPrevious.get(request.owner);
|
|
41
|
-
this.#quiescedPrevious.delete(request.owner);
|
|
42
|
-
previous?.resume();
|
|
43
|
-
},
|
|
44
|
-
openNext: () => {
|
|
45
|
-
instance.open();
|
|
46
|
-
this.#quiescedPrevious.delete(request.owner);
|
|
47
|
-
},
|
|
48
34
|
},
|
|
49
35
|
dispose: () => instance.close(),
|
|
50
36
|
};
|
|
@@ -61,25 +47,28 @@ class IsolatedInstance {
|
|
|
61
47
|
timeoutMs;
|
|
62
48
|
hostMethods;
|
|
63
49
|
onCrash;
|
|
50
|
+
admission;
|
|
51
|
+
[generationAdmissionSource];
|
|
64
52
|
#status = 'prepared';
|
|
65
53
|
#nextRequest = 1;
|
|
66
|
-
#inFlight = 0;
|
|
67
|
-
#drainWaiters = [];
|
|
68
54
|
#closed = false;
|
|
69
55
|
#pending = new Map();
|
|
70
56
|
#listeners = new Set();
|
|
71
|
-
constructor(owner, transport, timeoutMs, hostMethods, onCrash) {
|
|
57
|
+
constructor(owner, transport, timeoutMs, hostMethods, onCrash, admission = createGenerationAdmissionGate()) {
|
|
72
58
|
this.owner = owner;
|
|
73
59
|
this.transport = transport;
|
|
74
60
|
this.timeoutMs = timeoutMs;
|
|
75
61
|
this.hostMethods = hostMethods;
|
|
76
62
|
this.onCrash = onCrash;
|
|
63
|
+
this.admission = admission;
|
|
64
|
+
this[generationAdmissionSource] = Object.freeze([admission]);
|
|
77
65
|
transport.onMessage((message) => this.#onMessage(message));
|
|
78
66
|
transport.onExit((error) => this.#fail(error));
|
|
79
67
|
}
|
|
80
68
|
get status() { return this.#status; }
|
|
81
|
-
async activate() {
|
|
82
|
-
await this.
|
|
69
|
+
async activate(signal) {
|
|
70
|
+
await this.requestAbortable('activate', undefined, signal);
|
|
71
|
+
this.#status = 'active';
|
|
83
72
|
}
|
|
84
73
|
async deactivate() {
|
|
85
74
|
if (this.#closed || this.#status === 'failed')
|
|
@@ -87,40 +76,14 @@ class IsolatedInstance {
|
|
|
87
76
|
await this.request('deactivate');
|
|
88
77
|
this.#status = 'prepared';
|
|
89
78
|
}
|
|
90
|
-
open() {
|
|
91
|
-
if (this.#closed || this.#status === 'failed')
|
|
92
|
-
throw new Error(`Cannot open ${this.owner}`);
|
|
93
|
-
this.#status = 'active';
|
|
94
|
-
}
|
|
95
|
-
async quiesce() {
|
|
96
|
-
if (this.#status !== 'active')
|
|
97
|
-
return;
|
|
98
|
-
this.#status = 'quiesced';
|
|
99
|
-
if (this.#inFlight === 0)
|
|
100
|
-
return;
|
|
101
|
-
await new Promise((resolve) => this.#drainWaiters.push(resolve));
|
|
102
|
-
}
|
|
103
|
-
resume() {
|
|
104
|
-
if (this.#status === 'quiesced')
|
|
105
|
-
this.#status = 'active';
|
|
106
|
-
}
|
|
107
79
|
async call(method, input) {
|
|
108
80
|
if (this.#status !== 'active') {
|
|
109
81
|
throw new Error(`Isolated Plugin ${this.owner} is not accepting calls: ${this.#status}`);
|
|
110
82
|
}
|
|
111
83
|
assertMethod(method);
|
|
112
84
|
assertCloneable(input, `Input for ${method}`);
|
|
113
|
-
this
|
|
114
|
-
|
|
115
|
-
return await this.request('call', { method, input });
|
|
116
|
-
}
|
|
117
|
-
finally {
|
|
118
|
-
this.#inFlight -= 1;
|
|
119
|
-
if (this.#inFlight === 0) {
|
|
120
|
-
for (const resolve of this.#drainWaiters.splice(0))
|
|
121
|
-
resolve();
|
|
122
|
-
}
|
|
123
|
-
}
|
|
85
|
+
return this.admission.enter(() => this.request('call', { method, input }))
|
|
86
|
+
?? Promise.reject(new Error(`Isolated Plugin ${this.owner} generation is not admitted`));
|
|
124
87
|
}
|
|
125
88
|
onEvent(listener) {
|
|
126
89
|
this.#listeners.add(listener);
|
|
@@ -148,6 +111,26 @@ class IsolatedInstance {
|
|
|
148
111
|
this.transport.send({ type: 'request', id, method, payload });
|
|
149
112
|
});
|
|
150
113
|
}
|
|
114
|
+
async requestAbortable(method, payload, signal) {
|
|
115
|
+
signal.throwIfAborted();
|
|
116
|
+
let abort;
|
|
117
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
118
|
+
abort = () => {
|
|
119
|
+
const reason = signal.reason instanceof Error
|
|
120
|
+
? signal.reason
|
|
121
|
+
: new Error(`Isolated request aborted: ${method}`);
|
|
122
|
+
this.#fail(reason);
|
|
123
|
+
void this.transport.close().then(() => reject(reason), (closeError) => reject(new AggregateError([reason, closeError], `Isolated request cancellation failed: ${method}`)));
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
127
|
+
try {
|
|
128
|
+
return await Promise.race([this.request(method, payload), aborted]);
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
signal.removeEventListener('abort', abort);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
151
134
|
async close() {
|
|
152
135
|
if (this.#closed)
|
|
153
136
|
return;
|
|
@@ -176,21 +159,29 @@ class IsolatedInstance {
|
|
|
176
159
|
return;
|
|
177
160
|
}
|
|
178
161
|
if (message.type === 'event') {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
listener
|
|
162
|
+
this.admission.enter(() => {
|
|
163
|
+
const event = Object.freeze({ name: message.name, payload: message.payload });
|
|
164
|
+
for (const listener of this.#listeners)
|
|
165
|
+
listener(event);
|
|
166
|
+
});
|
|
182
167
|
return;
|
|
183
168
|
}
|
|
184
169
|
void this.#handleHostCall(message);
|
|
185
170
|
}
|
|
186
171
|
async #handleHostCall(message) {
|
|
187
|
-
const method = this.hostMethods[message.method];
|
|
188
172
|
try {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
173
|
+
const handled = this.admission.enter(async () => {
|
|
174
|
+
const method = this.hostMethods[message.method];
|
|
175
|
+
if (!method)
|
|
176
|
+
throw new Error(`Host RPC method is not allowed: ${message.method}`);
|
|
177
|
+
const value = await method(message.input, { owner: this.owner });
|
|
178
|
+
assertCloneable(value, `Host RPC result for ${message.method}`);
|
|
179
|
+
this.transport.send({ type: 'host-result', id: message.id, ok: true, value });
|
|
180
|
+
});
|
|
181
|
+
if (!handled) {
|
|
182
|
+
throw new Error(`Isolated Plugin ${this.owner} generation is not admitted`);
|
|
183
|
+
}
|
|
184
|
+
await handled;
|
|
194
185
|
}
|
|
195
186
|
catch (error) {
|
|
196
187
|
this.transport.send({
|
|
@@ -206,8 +197,6 @@ class IsolatedInstance {
|
|
|
206
197
|
return;
|
|
207
198
|
this.#status = 'failed';
|
|
208
199
|
this.#rejectPending(error);
|
|
209
|
-
for (const resolve of this.#drainWaiters.splice(0))
|
|
210
|
-
resolve();
|
|
211
200
|
this.onCrash?.(this.owner, error);
|
|
212
201
|
}
|
|
213
202
|
#rejectPending(error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/isolate",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "Worker and child-process isolation adapter for the Zhin Plugin Runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
"lib"
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
20
|
-
"@zhin.js/runtime": "1.0.
|
|
19
|
+
"@zhin.js/plugin-runtime": "1.1.6",
|
|
20
|
+
"@zhin.js/runtime": "1.0.10"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@types/node": "^26.1.2",
|