arcane-os 0.83.0 → 0.84.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/CHANGELOG.md +29 -0
- package/README.md +1 -1
- package/docs/reference/core-native-packaging.md +21 -0
- package/docs/reference/core-runtime.md +28 -1
- package/docs/reference/core-shared-host.md +71 -8
- package/docs/reference/inventory/package-api.json +20 -6
- package/docs/reference/local-ai.md +24 -3
- package/docs/reference/native-decisions.md +13 -0
- package/docs/reference/sdk-api.md +87 -3
- package/package.json +1 -1
- package/src/core/host.mjs +1 -1
- package/src/core/runtime-connection.mjs +165 -0
- package/src/core/runtime.mjs +10 -6
- package/src/core/shared-host.mjs +270 -70
- package/src/core/stdio.mjs +18 -3
- package/src/local-ai/onnx-worker.mjs +39 -5
- package/src/local-ai/onnx.mjs +3 -3
- package/src/native/portable-layout.mjs +30 -1
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import {randomUUID} from 'node:crypto';
|
|
2
|
+
import Is from 'strong-type';
|
|
3
|
+
import {CORE_PROTOCOL, CoreError, serializeCoreError} from '../../browser-runtime/core/contracts.mjs';
|
|
4
|
+
|
|
5
|
+
const is = new Is(false);
|
|
6
|
+
const owners = new WeakMap();
|
|
7
|
+
|
|
8
|
+
function failure(code, message) {
|
|
9
|
+
return new CoreError({code, message});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function event(event, data) {
|
|
13
|
+
return {protocol: CORE_PROTOCOL, type: 'event', event, data, time: new Date().toISOString()};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function runtimeConnections(runtime) {
|
|
17
|
+
const existing = owners.get(runtime);
|
|
18
|
+
if (existing) return existing;
|
|
19
|
+
const connections = new Set();
|
|
20
|
+
const requests = new Map();
|
|
21
|
+
const prefix = `connection-${randomUUID()}:`;
|
|
22
|
+
let sequence = 0;
|
|
23
|
+
|
|
24
|
+
function route(frame) {
|
|
25
|
+
if (frame.type === 'response') {
|
|
26
|
+
const record = requests.get(frame.id);
|
|
27
|
+
if (record) {
|
|
28
|
+
record.release();
|
|
29
|
+
record.connection.send({...frame, id: record.id}, record);
|
|
30
|
+
}
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (frame.type !== 'event') return;
|
|
34
|
+
if (Object.hasOwn(frame, 'requestId')) {
|
|
35
|
+
const record = requests.get(frame.requestId);
|
|
36
|
+
if (record) record.connection.send({...frame, requestId: record.id}, record);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
for (const connection of connections) connection.send(frame);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const unsubscribe = runtime.onFrame(route);
|
|
43
|
+
const owner = {
|
|
44
|
+
connections,
|
|
45
|
+
requests,
|
|
46
|
+
allocate(requestPrefix) {
|
|
47
|
+
// Every acceptance gets a fresh transport correlation. A retired
|
|
48
|
+
// service context must never target a later use of its client ID.
|
|
49
|
+
const active = runtime.current().activeRequests;
|
|
50
|
+
function inUse(candidate) {
|
|
51
|
+
return requests.has(candidate) || active.some(
|
|
52
|
+
function activeRequest(request) { return request.id === candidate; }
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
let internalId;
|
|
56
|
+
do {
|
|
57
|
+
internalId = `${requestPrefix ?? ''}${prefix}${++sequence}`;
|
|
58
|
+
} while (inUse(internalId));
|
|
59
|
+
return internalId;
|
|
60
|
+
},
|
|
61
|
+
release() {
|
|
62
|
+
if (connections.size || requests.size) return;
|
|
63
|
+
unsubscribe();
|
|
64
|
+
owners.delete(runtime);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
owners.set(runtime, owner);
|
|
68
|
+
return owner;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** One transport connection borrows dispatch; only its host owns runtime.close. */
|
|
72
|
+
export function createCoreRuntimeConnection({
|
|
73
|
+
runtime, send, requestPrefix, mapFrame, getReplayEvents, preserveContextRequestId = true
|
|
74
|
+
}) {
|
|
75
|
+
const owner = runtimeConnections(runtime);
|
|
76
|
+
const requests = new Map();
|
|
77
|
+
const internalRequests = new Map();
|
|
78
|
+
let disconnected = false;
|
|
79
|
+
|
|
80
|
+
function deliver(frame, record) {
|
|
81
|
+
if (disconnected) return;
|
|
82
|
+
const outgoing = mapFrame ? mapFrame(frame, record) : frame;
|
|
83
|
+
if (outgoing !== undefined) send(outgoing);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function cancel(record) {
|
|
87
|
+
return runtime.handle(
|
|
88
|
+
{protocol: CORE_PROTOCOL, type: 'control', control: 'request.cancel', requestId: record.internalId}
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function cancelAll() {
|
|
93
|
+
return Promise.all([...requests.values()].map(cancel));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function replay() {
|
|
97
|
+
const state = runtime.current();
|
|
98
|
+
if (state.state === 'ready') deliver(event('core.ready', {version: state.version, app: state.application}));
|
|
99
|
+
deliver(event('core.state', state));
|
|
100
|
+
for (const service of state.services) deliver(event('core.service.state', service));
|
|
101
|
+
// Only the existing owner's current snapshots, never requests/history.
|
|
102
|
+
for (const snapshot of getReplayEvents?.() ?? []) deliver(event(snapshot.event, snapshot.data));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function handle(frame) {
|
|
106
|
+
if (disconnected) throw failure('CORE_CONNECTION_CLOSED', 'The Core connection is closed.');
|
|
107
|
+
if (frame?.protocol !== CORE_PROTOCOL) throw failure('INVALID_RPC_REQUEST', 'Unknown Core protocol.');
|
|
108
|
+
if (frame.type === 'control') {
|
|
109
|
+
if (frame.control === 'runtime.replay') return replay();
|
|
110
|
+
if (frame.control === 'requests.cancelAll') return cancelAll();
|
|
111
|
+
if (frame.control === 'request.cancel') {
|
|
112
|
+
const record = requests.get(frame.requestId);
|
|
113
|
+
return record ? cancel(record) : undefined;
|
|
114
|
+
}
|
|
115
|
+
throw failure('INVALID_RPC_CONTROL', 'Unknown Core control.');
|
|
116
|
+
}
|
|
117
|
+
if (frame.type !== 'request' || !is.string(frame.id) || !frame.id || !is.string(frame.method)) {
|
|
118
|
+
throw failure('INVALID_RPC_REQUEST', 'A Core request requires an id and method.');
|
|
119
|
+
}
|
|
120
|
+
if (requests.has(frame.id)) {
|
|
121
|
+
const answer = {protocol: CORE_PROTOCOL, type: 'response', id: frame.id, ok: false,
|
|
122
|
+
error: serializeCoreError(failure('RPC_REQUEST_ID_ACTIVE', 'The request id is already active on this connection.')),
|
|
123
|
+
time: new Date().toISOString()};
|
|
124
|
+
deliver(answer);
|
|
125
|
+
return answer;
|
|
126
|
+
}
|
|
127
|
+
const record = {connection, id: frame.id, internalId: owner.allocate(requestPrefix), release: releaseRequest};
|
|
128
|
+
function releaseRequest() {
|
|
129
|
+
if (requests.get(record.id) === record) requests.delete(record.id);
|
|
130
|
+
internalRequests.delete(record.internalId);
|
|
131
|
+
owner.requests.delete(record.internalId);
|
|
132
|
+
owner.release();
|
|
133
|
+
}
|
|
134
|
+
requests.set(record.id, record);
|
|
135
|
+
internalRequests.set(record.internalId, record);
|
|
136
|
+
owner.requests.set(record.internalId, record);
|
|
137
|
+
try {
|
|
138
|
+
return await runtime.handle(
|
|
139
|
+
{...frame, id: record.internalId},
|
|
140
|
+
preserveContextRequestId ? {contextRequestId: record.id} : undefined
|
|
141
|
+
);
|
|
142
|
+
} finally {
|
|
143
|
+
releaseRequest();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function close() {
|
|
148
|
+
if (disconnected) return Promise.resolve();
|
|
149
|
+
disconnected = true;
|
|
150
|
+
owner.connections.delete(connection);
|
|
151
|
+
const cancellation = cancelAll();
|
|
152
|
+
owner.release();
|
|
153
|
+
return cancellation;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const connection = {
|
|
157
|
+
handle,
|
|
158
|
+
close,
|
|
159
|
+
send: deliver,
|
|
160
|
+
has(id) { return requests.has(id); },
|
|
161
|
+
request(internalId) { return internalRequests.get(internalId); }
|
|
162
|
+
};
|
|
163
|
+
owner.connections.add(connection);
|
|
164
|
+
return connection;
|
|
165
|
+
}
|
package/src/core/runtime.mjs
CHANGED
|
@@ -218,7 +218,7 @@ class CoreRuntime {
|
|
|
218
218
|
return this.current();
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
async handle(frame) {
|
|
221
|
+
async handle(frame, {contextRequestId} = {}) {
|
|
222
222
|
if (frame?.protocol !== CORE_PROTOCOL) throw failure('INVALID_RPC_REQUEST', 'Unknown Core protocol.');
|
|
223
223
|
if (frame.type === 'control') {
|
|
224
224
|
if (frame.control === 'runtime.replay') {
|
|
@@ -235,7 +235,10 @@ class CoreRuntime {
|
|
|
235
235
|
if (frame.type !== 'request' || !is.string(frame.id) || !frame.id || !is.string(frame.method)) {
|
|
236
236
|
throw failure('INVALID_RPC_REQUEST', 'A Core request requires an id and method.');
|
|
237
237
|
}
|
|
238
|
-
|
|
238
|
+
if (contextRequestId !== undefined && (!is.string(contextRequestId) || !contextRequestId)) {
|
|
239
|
+
throw failure('INVALID_RPC_REQUEST', 'A transport context request id must be a nonempty string.');
|
|
240
|
+
}
|
|
241
|
+
const task = this.#answer(frame, contextRequestId);
|
|
239
242
|
this.#responses.add(task);
|
|
240
243
|
try {
|
|
241
244
|
return await task;
|
|
@@ -244,7 +247,7 @@ class CoreRuntime {
|
|
|
244
247
|
}
|
|
245
248
|
}
|
|
246
249
|
|
|
247
|
-
async #answer(frame) {
|
|
250
|
+
async #answer(frame, contextRequestId) {
|
|
248
251
|
let result;
|
|
249
252
|
let error;
|
|
250
253
|
let ok = true;
|
|
@@ -254,7 +257,7 @@ class CoreRuntime {
|
|
|
254
257
|
if (frame.method === 'system.ping') result = {ok: true};
|
|
255
258
|
else if (frame.method === 'version.current') result = this.#version;
|
|
256
259
|
else if (frame.method === 'app.current') result = this.#application;
|
|
257
|
-
else result = await this.#request(frame);
|
|
260
|
+
else result = await this.#request(frame, contextRequestId);
|
|
258
261
|
} catch (caught) {
|
|
259
262
|
ok = false;
|
|
260
263
|
error = caught;
|
|
@@ -270,7 +273,7 @@ class CoreRuntime {
|
|
|
270
273
|
return response;
|
|
271
274
|
}
|
|
272
275
|
|
|
273
|
-
#request(frame) {
|
|
276
|
+
#request(frame, contextRequestId) {
|
|
274
277
|
const registration = this.#methods.get(frame.method);
|
|
275
278
|
if (!registration) {
|
|
276
279
|
const namespace = frame.method.split('.')[0];
|
|
@@ -300,7 +303,8 @@ class CoreRuntime {
|
|
|
300
303
|
const result = await handle.call(
|
|
301
304
|
service.definition,
|
|
302
305
|
frame.parameters,
|
|
303
|
-
{...runtime.#serviceContext(service, frame.id),
|
|
306
|
+
{...runtime.#serviceContext(service, frame.id),
|
|
307
|
+
requestId: contextRequestId ?? frame.id, coreRequestId: frame.id, signal: controller.signal}
|
|
304
308
|
);
|
|
305
309
|
aborted(controller);
|
|
306
310
|
return result;
|
package/src/core/shared-host.mjs
CHANGED
|
@@ -2,10 +2,14 @@ import {spawn} from 'node:child_process';
|
|
|
2
2
|
import {open} from 'node:fs/promises';
|
|
3
3
|
import {createServer} from 'node:net';
|
|
4
4
|
import {randomUUID} from 'node:crypto';
|
|
5
|
+
import Is from 'strong-type';
|
|
5
6
|
import {CORE_PROTOCOL, CoreError, serializeCoreError} from '../../browser-runtime/core/contracts.mjs';
|
|
6
7
|
import {createCoreRuntime} from './runtime.mjs';
|
|
7
8
|
import {createCoreFrameDecoder, encodeCoreFrame} from './stdio.mjs';
|
|
8
9
|
import {connectCoreSocket, createCoreSocketConnection, writeCoreSocket as write} from './socket-connection.mjs';
|
|
10
|
+
import {createCoreRuntimeConnection} from './runtime-connection.mjs';
|
|
11
|
+
|
|
12
|
+
const is = new Is(false);
|
|
9
13
|
|
|
10
14
|
function failure(code, message) { return new CoreError({code, message}); }
|
|
11
15
|
function reportError(error) { console.error('Shared Arcane Core failed:', error); }
|
|
@@ -23,6 +27,233 @@ function response(id, result, error) {
|
|
|
23
27
|
time: new Date().toISOString()};
|
|
24
28
|
}
|
|
25
29
|
|
|
30
|
+
/** Add local IPC to an existing runtime without acquiring its lifetime. */
|
|
31
|
+
export async function startCoreListener({runtime, endpoint, onError = reportError} = {}) {
|
|
32
|
+
if (!is.string(endpoint) || !endpoint) throw new TypeError('A Core listener endpoint must be a nonempty local pipe/socket path.');
|
|
33
|
+
if (!runtime || !is.function(runtime.handle) || !is.function(runtime.onFrame)
|
|
34
|
+
|| !is.function(runtime.current) || !is.function(runtime.subscribe)) {
|
|
35
|
+
throw new TypeError('A Core listener requires an existing Core runtime.');
|
|
36
|
+
}
|
|
37
|
+
if (['draining', 'closed'].includes(runtime.current().state)) throw failure('CORE_CLOSING', 'Core is closing.');
|
|
38
|
+
const peers = new Set();
|
|
39
|
+
const prefix = `listener-${randomUUID()}:`;
|
|
40
|
+
let accepting = true;
|
|
41
|
+
let closing;
|
|
42
|
+
let terminalError;
|
|
43
|
+
let resolveClosed;
|
|
44
|
+
let rejectClosed;
|
|
45
|
+
let resolveRuntimeClosed;
|
|
46
|
+
const runtimeClosed = new Promise(
|
|
47
|
+
function observeRuntimeClosure(resolve) { resolveRuntimeClosed = resolve; }
|
|
48
|
+
);
|
|
49
|
+
const closed = new Promise(
|
|
50
|
+
function ownListener(resolve, reject) {
|
|
51
|
+
resolveClosed = resolve;
|
|
52
|
+
rejectClosed = reject;
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
closed.catch(function observeReportedListenerFailure() {});
|
|
56
|
+
|
|
57
|
+
function diagnostic(error) {
|
|
58
|
+
report(onError, error);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function disconnect(peer) {
|
|
62
|
+
if (peer.closed) return;
|
|
63
|
+
peer.closed = true;
|
|
64
|
+
peers.delete(peer);
|
|
65
|
+
peer.connection.close().catch(diagnostic);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function send(peer, frame) {
|
|
69
|
+
if (peer.closed) return;
|
|
70
|
+
let content;
|
|
71
|
+
try {
|
|
72
|
+
content = encodeCoreFrame(frame);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
diagnostic(error);
|
|
75
|
+
peer.socket.destroy();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
peer.writes = peer.writes.then(
|
|
79
|
+
function writeNext() {
|
|
80
|
+
if (!peer.closed) return write(peer.socket, content);
|
|
81
|
+
}
|
|
82
|
+
).catch(
|
|
83
|
+
function outputFailed(error) {
|
|
84
|
+
diagnostic(error);
|
|
85
|
+
peer.socket.destroy();
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function accept(socket) {
|
|
91
|
+
const peer = {socket, closed: false, writes: Promise.resolve(), connection: null};
|
|
92
|
+
peers.add(peer);
|
|
93
|
+
peer.connection = createCoreRuntimeConnection(
|
|
94
|
+
{
|
|
95
|
+
runtime,
|
|
96
|
+
requestPrefix: prefix,
|
|
97
|
+
send(frame) { send(peer, frame); }
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
const decoder = createCoreFrameDecoder(
|
|
101
|
+
function receiveFrame(frame) {
|
|
102
|
+
if (peer.closed) return;
|
|
103
|
+
if (!accepting && frame?.type === 'request') {
|
|
104
|
+
send(peer, response(frame.id, undefined, failure('CORE_CLOSING', 'The Core listener is closing.')));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
// No owning-host shutdown command or application dispatcher.
|
|
108
|
+
peer.connection.handle(frame).catch(
|
|
109
|
+
function requestFailed(error) {
|
|
110
|
+
if (frame?.type === 'request') send(peer, response(frame.id, undefined, error));
|
|
111
|
+
else send(peer, event('core.error', serializeCoreError(error)));
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
socket.on(
|
|
117
|
+
'data',
|
|
118
|
+
function receiveData(chunk) {
|
|
119
|
+
try {
|
|
120
|
+
decoder.push(chunk);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
diagnostic(error);
|
|
123
|
+
socket.destroy();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
socket.on('error', diagnostic);
|
|
128
|
+
socket.on(
|
|
129
|
+
'end',
|
|
130
|
+
function inputEnded() {
|
|
131
|
+
try {
|
|
132
|
+
decoder.finish();
|
|
133
|
+
} catch (error) {
|
|
134
|
+
diagnostic(error);
|
|
135
|
+
}
|
|
136
|
+
disconnect(peer);
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
socket.on('close', function connectionClosed() { disconnect(peer); });
|
|
140
|
+
socket.resume();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const server = createServer({pauseOnConnect: true}, accept);
|
|
144
|
+
const binding = new Promise(
|
|
145
|
+
function claimEndpoint(resolve, reject) {
|
|
146
|
+
function failed(error) {
|
|
147
|
+
server.off('listening', listening);
|
|
148
|
+
reject(error);
|
|
149
|
+
}
|
|
150
|
+
function listening() {
|
|
151
|
+
server.off('error', failed);
|
|
152
|
+
resolve();
|
|
153
|
+
}
|
|
154
|
+
server.once('error', failed);
|
|
155
|
+
server.once('listening', listening);
|
|
156
|
+
server.listen({path: endpoint});
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
function close() {
|
|
161
|
+
if (closing) return closed;
|
|
162
|
+
accepting = false;
|
|
163
|
+
closing = Promise.resolve().then(
|
|
164
|
+
async function closeListener() {
|
|
165
|
+
try {
|
|
166
|
+
await binding;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
terminalError ??= error;
|
|
169
|
+
}
|
|
170
|
+
// Window/Core shutdown retains the endpoint through service
|
|
171
|
+
// drain. Closing this listener never initiates runtime.close().
|
|
172
|
+
if (runtime.current().state === 'draining') await runtimeClosed;
|
|
173
|
+
const stopped = new Promise(
|
|
174
|
+
function stopListening(resolve, reject) {
|
|
175
|
+
server.close(
|
|
176
|
+
function listenerClosed(error) {
|
|
177
|
+
if (error && error.code !== 'ERR_SERVER_NOT_RUNNING') reject(error);
|
|
178
|
+
else resolve();
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
stopped.catch(function observeListenerStopFailure() {});
|
|
184
|
+
const connected = [...peers];
|
|
185
|
+
const cancellations = await Promise.allSettled(
|
|
186
|
+
connected.map(
|
|
187
|
+
function detachConnection(peer) {
|
|
188
|
+
peer.socket.pause();
|
|
189
|
+
return peer.connection.close();
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
);
|
|
193
|
+
for (const outcome of cancellations) {
|
|
194
|
+
if (outcome.status === 'rejected') {
|
|
195
|
+
terminalError = terminalError
|
|
196
|
+
? new AggregateError([terminalError, outcome.reason], 'Core listener closure failed.')
|
|
197
|
+
: outcome.reason;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
await Promise.all(connected.map(function flushConnection(peer) { return peer.writes; }));
|
|
201
|
+
for (const peer of connected) {
|
|
202
|
+
peer.socket.resume();
|
|
203
|
+
peer.socket.end();
|
|
204
|
+
}
|
|
205
|
+
await stopped;
|
|
206
|
+
if (terminalError) throw terminalError;
|
|
207
|
+
return {state: 'closed', endpoint};
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
closing.then(
|
|
211
|
+
function listenerClosed(result) {
|
|
212
|
+
unsubscribe();
|
|
213
|
+
resolveClosed(result);
|
|
214
|
+
},
|
|
215
|
+
function listenerFailed(error) {
|
|
216
|
+
unsubscribe();
|
|
217
|
+
diagnostic(error);
|
|
218
|
+
rejectClosed(error);
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
return closed;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const unsubscribe = runtime.subscribe(
|
|
225
|
+
function runtimeChanged(state) {
|
|
226
|
+
if (state.state === 'draining') accepting = false;
|
|
227
|
+
if (state.state === 'closed') {
|
|
228
|
+
resolveRuntimeClosed();
|
|
229
|
+
close();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
server.on(
|
|
234
|
+
'error',
|
|
235
|
+
function listenerFailed(error) {
|
|
236
|
+
terminalError = error;
|
|
237
|
+
close();
|
|
238
|
+
}
|
|
239
|
+
);
|
|
240
|
+
try {
|
|
241
|
+
await binding;
|
|
242
|
+
if (!accepting) {
|
|
243
|
+
await close();
|
|
244
|
+
throw failure('CORE_CLOSING', 'Core closed while its listener was starting.');
|
|
245
|
+
}
|
|
246
|
+
return {endpoint, closed, close};
|
|
247
|
+
} catch (error) {
|
|
248
|
+
try {
|
|
249
|
+
await close();
|
|
250
|
+
} catch (closeError) {
|
|
251
|
+
if (closeError !== error) throw new AggregateError([error, closeError], 'Core listener startup and cleanup failed.');
|
|
252
|
+
}
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
26
257
|
/** Claim one app-selected pipe/socket before importing or creating services. */
|
|
27
258
|
export async function startSharedCoreHost({
|
|
28
259
|
endpoint, application, version, configure, getReplayEvents, signal, onError = reportError
|
|
@@ -30,13 +261,10 @@ export async function startSharedCoreHost({
|
|
|
30
261
|
signal?.throwIfAborted();
|
|
31
262
|
if (typeof endpoint !== 'string' || !endpoint) throw new TypeError('A shared Core endpoint must be a nonempty local pipe/socket path.');
|
|
32
263
|
const peers = new Set();
|
|
33
|
-
const requests = new Map();
|
|
34
264
|
const incoming = new Set();
|
|
35
265
|
const lifetime = new AbortController();
|
|
36
266
|
const prefix = `shared-${randomUUID()}:`;
|
|
37
|
-
let sequence = 0;
|
|
38
267
|
let runtime;
|
|
39
|
-
let unsubscribe;
|
|
40
268
|
let closing;
|
|
41
269
|
let preparation = Promise.resolve();
|
|
42
270
|
let ready = false;
|
|
@@ -65,89 +293,61 @@ export async function startSharedCoreHost({
|
|
|
65
293
|
if (!peer.closed) return write(peer.socket, content);
|
|
66
294
|
}).catch(function outputFailed(error) { diagnostic(error); peer.socket.destroy(); });
|
|
67
295
|
}
|
|
68
|
-
function cancel(record) {
|
|
69
|
-
observe(runtime.handle({protocol: CORE_PROTOCOL, type: 'control',
|
|
70
|
-
control: 'request.cancel', requestId: record.internalId}));
|
|
71
|
-
}
|
|
72
296
|
function disconnect(peer) {
|
|
73
297
|
if (peer.closed) return;
|
|
74
298
|
peer.closed = true;
|
|
75
299
|
peers.delete(peer);
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
function stateFor(peer, state) {
|
|
79
|
-
return {...state, activeRequests: state.activeRequests.map(function requestState(record) {
|
|
80
|
-
const owned = requests.get(record.id);
|
|
81
|
-
return owned?.peer === peer ? {...record, id: owned.id} : record;
|
|
82
|
-
})};
|
|
83
|
-
}
|
|
84
|
-
function replay(peer) {
|
|
85
|
-
const state = runtime.current();
|
|
86
|
-
if (state.state === 'ready') send(peer, event('core.ready', {version: state.version, app: state.application}));
|
|
87
|
-
send(peer, event('core.state', stateFor(peer, state)));
|
|
88
|
-
for (const service of state.services) send(peer, event('core.service.state', service));
|
|
89
|
-
// Domain snapshots belong to the app/service owner, never event history.
|
|
90
|
-
for (const snapshot of getReplayEvents?.() ?? []) send(peer, event(snapshot.event, snapshot.data));
|
|
300
|
+
if (peer.connection) observe(peer.connection.close());
|
|
91
301
|
}
|
|
92
|
-
function
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
302
|
+
function attach(peer) {
|
|
303
|
+
peer.connection = createCoreRuntimeConnection(
|
|
304
|
+
{
|
|
305
|
+
runtime,
|
|
306
|
+
requestPrefix: prefix,
|
|
307
|
+
preserveContextRequestId: false,
|
|
308
|
+
getReplayEvents,
|
|
309
|
+
send(frame) { send(peer, frame); },
|
|
310
|
+
mapFrame(frame, record) {
|
|
311
|
+
// Preserve this owning adapter's established data correlation.
|
|
312
|
+
// The borrowed listener and stdio never apply this mapping.
|
|
313
|
+
if (record) {
|
|
314
|
+
return frame.type === 'event' && frame.data?.requestId === record.internalId
|
|
315
|
+
? {...frame, data: {...frame.data, requestId: record.id}} : frame;
|
|
316
|
+
}
|
|
317
|
+
if (frame.event === 'core.state') {
|
|
318
|
+
return {...frame, data: {...frame.data, activeRequests: frame.data.activeRequests.map(
|
|
319
|
+
function requestState(request) {
|
|
320
|
+
const owned = peer.connection.request(request.id);
|
|
321
|
+
return owned ? {...request, id: owned.id} : request;
|
|
322
|
+
}
|
|
323
|
+
)}};
|
|
324
|
+
}
|
|
325
|
+
const id = frame.data?.requestId;
|
|
326
|
+
const owned = peer.connection.request(id);
|
|
327
|
+
if (owned) return {...frame, data: {...frame.data, requestId: owned.id}};
|
|
328
|
+
// A retired transport correlation is not a new app-wide event.
|
|
329
|
+
if (is.string(id) && id.startsWith(prefix)) return;
|
|
330
|
+
return frame;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
);
|
|
115
334
|
}
|
|
116
335
|
async function dispatch(peer, frame) {
|
|
117
336
|
if (frame?.protocol !== CORE_PROTOCOL) throw failure('INVALID_RPC_REQUEST', 'Unknown Core protocol.');
|
|
118
337
|
if (frame.type === 'control') {
|
|
119
|
-
|
|
120
|
-
if (frame.control === 'request.cancel') {
|
|
121
|
-
const record = peer.requests.get(frame.requestId);
|
|
122
|
-
if (record) cancel(record);
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
if (frame.control === 'requests.cancelAll') {
|
|
126
|
-
for (const record of peer.requests.values()) cancel(record);
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
throw failure('INVALID_RPC_CONTROL', 'Unknown Core control.');
|
|
338
|
+
return peer.connection.handle(frame);
|
|
130
339
|
}
|
|
131
340
|
if (frame.type !== 'request' || typeof frame.id !== 'string' || !frame.id || typeof frame.method !== 'string') {
|
|
132
341
|
throw failure('INVALID_RPC_REQUEST', 'A Core request requires an id and method.');
|
|
133
342
|
}
|
|
134
|
-
if (peer.
|
|
343
|
+
if (peer.connection.has(frame.id)) throw failure('RPC_REQUEST_ID_ACTIVE', 'The request id is already active on this connection.');
|
|
135
344
|
if (frame.method === 'core.host.shutdown') {
|
|
136
345
|
peer.shutdownIds.push(frame.id);
|
|
137
346
|
close();
|
|
138
347
|
return;
|
|
139
348
|
}
|
|
140
349
|
if (closing) throw failure('CORE_CLOSING', 'Core is closing.');
|
|
141
|
-
|
|
142
|
-
peer.requests.set(record.id, record);
|
|
143
|
-
requests.set(record.internalId, record);
|
|
144
|
-
try {
|
|
145
|
-
// Only protocol correlation changes; parameters remain untouched.
|
|
146
|
-
await runtime.handle({...frame, id: record.internalId});
|
|
147
|
-
} finally {
|
|
148
|
-
peer.requests.delete(record.id);
|
|
149
|
-
requests.delete(record.internalId);
|
|
150
|
-
}
|
|
350
|
+
return peer.connection.handle(frame);
|
|
151
351
|
}
|
|
152
352
|
function receive(peer, frame) {
|
|
153
353
|
if (peer.closed) return;
|
|
@@ -157,8 +357,9 @@ export async function startSharedCoreHost({
|
|
|
157
357
|
}));
|
|
158
358
|
}
|
|
159
359
|
function accept(socket) {
|
|
160
|
-
const peer = {socket,
|
|
360
|
+
const peer = {socket, connection: null, shutdownIds: [], closed: false, writes: Promise.resolve()};
|
|
161
361
|
peers.add(peer);
|
|
362
|
+
if (runtime) attach(peer);
|
|
162
363
|
const decoder = createCoreFrameDecoder(function receiveFrame(frame) { receive(peer, frame); });
|
|
163
364
|
socket.on('data', function data(chunk) {
|
|
164
365
|
try { decoder.push(chunk); } catch (error) { diagnostic(error); socket.destroy(); }
|
|
@@ -196,7 +397,6 @@ export async function startSharedCoreHost({
|
|
|
196
397
|
await Promise.all([...peers].map(function drainWrites(peer) { return peer.writes; }));
|
|
197
398
|
for (const peer of peers) { peer.socket.resume(); peer.socket.end(); }
|
|
198
399
|
await stopped;
|
|
199
|
-
unsubscribe?.();
|
|
200
400
|
signal?.removeEventListener('abort', abortHost);
|
|
201
401
|
if (error) throw error;
|
|
202
402
|
return runtime?.current() ?? {state: 'closed'};
|
|
@@ -222,7 +422,7 @@ export async function startSharedCoreHost({
|
|
|
222
422
|
// The endpoint is already owned. A losing launcher never calls configure.
|
|
223
423
|
signal?.throwIfAborted();
|
|
224
424
|
runtime = createCoreRuntime({application, version});
|
|
225
|
-
|
|
425
|
+
for (const peer of peers) attach(peer);
|
|
226
426
|
signal?.addEventListener('abort', abortHost, {once: true});
|
|
227
427
|
preparation = Promise.resolve().then(function configureClaimedHost() {
|
|
228
428
|
lifetime.signal.throwIfAborted();
|
package/src/core/stdio.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {finished} from 'node:stream';
|
|
2
2
|
import Is from 'strong-type';
|
|
3
3
|
import {CoreError, serializeCoreError} from '../../browser-runtime/core/contracts.mjs';
|
|
4
|
+
import {createCoreRuntimeConnection} from './runtime-connection.mjs';
|
|
4
5
|
|
|
5
6
|
const is = new Is(false);
|
|
6
7
|
|
|
@@ -116,10 +117,24 @@ export function startCoreStdio({runtime, input = process.stdin, output = process
|
|
|
116
117
|
).catch(failOutput);
|
|
117
118
|
}
|
|
118
119
|
|
|
119
|
-
const
|
|
120
|
+
const connection = createCoreRuntimeConnection(
|
|
121
|
+
{
|
|
122
|
+
runtime,
|
|
123
|
+
send: queueFrame,
|
|
124
|
+
mapFrame(frame) {
|
|
125
|
+
if (frame.event !== 'core.state') return frame;
|
|
126
|
+
return {...frame, data: {...frame.data, activeRequests: frame.data.activeRequests.map(
|
|
127
|
+
function requestState(request) {
|
|
128
|
+
const owned = connection.request(request.id);
|
|
129
|
+
return owned ? {...request, id: owned.id} : request;
|
|
130
|
+
}
|
|
131
|
+
)}};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
);
|
|
120
135
|
|
|
121
136
|
function receiveFrame(frame) {
|
|
122
|
-
const task =
|
|
137
|
+
const task = connection.handle(frame);
|
|
123
138
|
pending.add(task);
|
|
124
139
|
task.then(
|
|
125
140
|
function requestSettled() {
|
|
@@ -157,7 +172,7 @@ export function startCoreStdio({runtime, input = process.stdin, output = process
|
|
|
157
172
|
}
|
|
158
173
|
|
|
159
174
|
async function releaseTransportListeners() {
|
|
160
|
-
|
|
175
|
+
await connection.close();
|
|
161
176
|
output.off('error', failOutput);
|
|
162
177
|
// destroy(error) marks a stream destroyed before its scheduled error
|
|
163
178
|
// and close events. The native completion owner observes their delivery;
|