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.
@@ -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
+ }
@@ -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
- const task = this.#answer(frame);
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), requestId: frame.id, signal: controller.signal}
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;
@@ -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
- for (const record of peer.requests.values()) cancel(record);
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 route(frame) {
93
- if (frame.type === 'response') {
94
- const record = requests.get(frame.id);
95
- if (record) send(record.peer, {...frame, id: record.id});
96
- return;
97
- }
98
- if (frame.type !== 'event') return;
99
- if (Object.hasOwn(frame, 'requestId')) {
100
- const owned = requests.get(frame.requestId);
101
- if (owned) send(owned.peer, {...frame, requestId: owned.id,
102
- data: frame.data?.requestId === owned.internalId ? {...frame.data, requestId: owned.id} : frame.data});
103
- return;
104
- }
105
- const id = frame.data?.requestId;
106
- const record = requests.get(id);
107
- if (record) {
108
- send(record.peer, {...frame, data: {...frame.data, requestId: record.id}});
109
- return;
110
- }
111
- // A retired transport correlation is not a new app-wide event.
112
- if (typeof id === 'string' && id.startsWith(prefix)) return;
113
- for (const peer of peers) send(peer, frame.event === 'core.state'
114
- ? {...frame, data: stateFor(peer, frame.data)} : frame);
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
- if (frame.control === 'runtime.replay') { replay(peer); return; }
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.requests.has(frame.id)) throw failure('RPC_REQUEST_ID_ACTIVE', 'The request id is already active on this connection.');
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
- const record = {peer, id: frame.id, internalId: `${prefix}${++sequence}`};
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, requests: new Map(), shutdownIds: [], closed: false, writes: Promise.resolve()};
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
- unsubscribe = runtime.onFrame(route);
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();
@@ -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 unsubscribe = runtime.onFrame(queueFrame);
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 = runtime.handle(frame);
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
- unsubscribe();
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;