arcane-os 0.54.1 → 0.55.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.54.1",
3
+ "version": "0.55.0",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -48,6 +48,13 @@
48
48
  "./targets": "./src/targets/index.mjs",
49
49
  "./native": "./src/native-plan.mjs",
50
50
  "./native-provider": "./src/native-provider-loader.mjs",
51
+ "./native/portable-provider": "./src/native/portable-provider.mjs",
52
+ "./core/client": "./browser-runtime/core/client.mjs",
53
+ "./core/contracts": "./browser-runtime/core/contracts.mjs",
54
+ "./core/classic-source": "./src/core-classic-source.mjs",
55
+ "./core/runtime": "./src/core/runtime.mjs",
56
+ "./core/stdio": "./src/core/stdio.mjs",
57
+ "./core/host": "./src/core/host.mjs",
51
58
  "./integrated-provider": "./src/integrated-provider-loader.mjs",
52
59
  "./packager": "./src/packager/core.mjs",
53
60
  "./release-bundle": "./src/release-bundle.mjs",
@@ -89,8 +96,8 @@
89
96
  "test": "npm run test:unit && npm run test:functional && npm run test:integration && npm run test:regression",
90
97
  "test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
91
98
  "test:unit": "node ./bin/arcane-test.mjs test/app-descriptor.test.mjs test/app-schema.test.mjs test/app-selection.test.mjs test/contracts.test.mjs test/conversation-closing-report.test.mjs test/doctor.test.mjs test/mail-credentials.test.mjs test/mail-outbox.test.mjs test/mail-public-api.test.mjs test/mail-send.test.mjs test/mail-transport.test.mjs test/targets.test.mjs test/workspace-operation-lock.test.mjs",
92
- "test:functional": "node ./bin/arcane-test.mjs test/ai-request-model.test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-decisions.test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-input.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.test.mjs test/chat-transient-retention.test.mjs test/cli.test.mjs test/continuous-voice-capture.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs-png.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/file-preview.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/markdown-editor.test.mjs test/markdown-media.test.mjs test/markdown-source-map.test.mjs test/modal.test.mjs test/native-module-packaging.test.mjs test/native-module-resource-references.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/rendered-print.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/theme-manager.test.mjs test/twin-cloud-images.test.mjs test/twin-cloud.test.mjs test/websocket-client-import-map.test.mjs test/websocket.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
93
- "test:integration": "node ./bin/arcane-test.mjs test/installed-package-runtime.test.mjs test/root-app-layout.test.mjs test/integrated-shared.test.mjs test/integrated-workspace.test.mjs test/mail-browser.test.mjs test/native-plan.test.mjs test/native-provider-loader.test.mjs test/npm-release.test.mjs test/release-bundle.test.mjs test/release-capability-smoke.test.mjs test/shared-payload-batch.test.mjs test/tarball.test.mjs test/browser-wasm-cpu.test.mjs test/wllama-webgpu-runtime.test.mjs",
99
+ "test:functional": "node ./bin/arcane-test.mjs test/ai-request-model.test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-decisions.test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-input.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.test.mjs test/chat-transient-retention.test.mjs test/cli.test.mjs test/continuous-voice-capture.test.mjs test/core-classic-source.test.mjs test/core-client.test.mjs test/core-host.test.mjs test/core-runtime.test.mjs test/core-stdio.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs-png.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/file-preview.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/markdown-editor.test.mjs test/markdown-media.test.mjs test/markdown-source-map.test.mjs test/modal.test.mjs test/native-module-packaging.test.mjs test/native-module-resource-references.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/rendered-print.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/theme-manager.test.mjs test/twin-cloud-images.test.mjs test/twin-cloud.test.mjs test/websocket-client-import-map.test.mjs test/websocket.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
100
+ "test:integration": "node ./bin/arcane-test.mjs test/installed-package-runtime.test.mjs test/root-app-layout.test.mjs test/integrated-shared.test.mjs test/integrated-workspace.test.mjs test/mail-browser.test.mjs test/native-plan.test.mjs test/native-portable-provider.test.mjs test/native-provider-loader.test.mjs test/npm-release.test.mjs test/release-bundle.test.mjs test/release-capability-smoke.test.mjs test/shared-payload-batch.test.mjs test/tarball.test.mjs test/browser-wasm-cpu.test.mjs test/wllama-webgpu-runtime.test.mjs",
94
101
  "test:regression": "node ./bin/arcane-test.mjs test/channel-workflows.test.mjs test/html-import-registration.test.mjs test/html-import-packaged-script.test.mjs test/logging-regression.test.mjs test/markdown-speech.test.mjs test/prepared-speech.test.mjs test/native-provider-generation.test.mjs test/speech-queue-regression.test.mjs test/testing.test.mjs test/test-sets.test.mjs",
95
102
  "check": "node tools/check-source.mjs && npm test",
96
103
  "check:release": "node tools/check-source.mjs --package-only && npm run test:release",
@@ -23,6 +23,14 @@
23
23
  "toolchainRoot": { "type": "string", "minLength": 1 },
24
24
  "toolchain": { "type": "object" },
25
25
  "appDescriptor": { "type": "object" },
26
+ "selectedSdk": {
27
+ "description": "Actual selected app SDK installation used to complete a partial native package projection without replacing selected files.",
28
+ "type": "object",
29
+ "properties": {
30
+ "packageRoot": { "type": "string" },
31
+ "packageSource": { "type": "string" }
32
+ }
33
+ },
26
34
  "application": { "$ref": "#/$defs/releaseSelection" },
27
35
  "dependencies": {
28
36
  "type": "array",
package/src/cli/main.mjs CHANGED
@@ -75,8 +75,8 @@ Usage:
75
75
  ${CLI_NAME} verify [--app <id>]
76
76
  ${CLI_NAME} bundle [--app <id>] [--artifact <file>${APP_BUNDLE_EXTENSION}] [--overwrite]
77
77
  ${CLI_NAME} verify-bundle <file${APP_BUNDLE_EXTENSION}>
78
- ${CLI_NAME} native-doctor --target <native-target> --arcane-root <directory>
79
- ${CLI_NAME} native-prepare --target <native-target> --arcane-root <directory>
78
+ ${CLI_NAME} native-doctor --target <native-target> [--arcane-root <directory>]
79
+ ${CLI_NAME} native-prepare --target <native-target> [--arcane-root <directory>]
80
80
  ${CLI_NAME} build --target <target> [--arcane-root <directory>] [--output-root <directory>] [--format <format>] [--signing <mode>]
81
81
  ${CLI_NAME} run [--target <target>] [--app <id>] [--cert <pem>] [--key <pem>] [--port 8000] [--http-port 0] [--arcane-root <directory>] [--output-root <directory>] [--format <format>] [--signing <mode>]
82
82
  ${CLI_NAME} update-check
@@ -105,9 +105,10 @@ Global:
105
105
  --version Show the SDK version.
106
106
 
107
107
  The npm package is ${SDK_NAME}. Both the ${CLI_NAME} and arcane-os executables
108
- invoke this same headless toolchain. Every native operation requires one explicit
109
- --arcane-root. Available providers build portable directories, Windows x64 EXE
110
- bundles, Linux x64 or ARM64 DEBs, and development-signed Android APKs.`;
108
+ invoke this same headless toolchain. Portable uses the installed SDK provider
109
+ by default and produces a non-executable app payload. An explicit --arcane-root
110
+ selects the Arcane OS provider instead. Windows EXE, Linux DEB, and Android APK
111
+ targets still require an explicit --arcane-root and their platform toolchains.`;
111
112
 
112
113
  function usage(message){
113
114
  throw new ArcaneError(ERROR_CODES.usage,message);
@@ -810,7 +811,7 @@ async function pairNativeProvider(command,options,loadProvider,{signal,onEvent}=
810
811
  usage(`${command} requires one native target.`);
811
812
  }
812
813
  const targetRequest=createNativeTargetRequest(options);
813
- if(!options.arcaneRoot){
814
+ if(!options.arcaneRoot&&options.target!=='portable'){
814
815
  usage(`${command} for target ${options.target} requires --arcane-root <directory>.`);
815
816
  }
816
817
  const loaded=await loadProvider({
@@ -823,6 +824,7 @@ async function pairNativeProvider(command,options,loadProvider,{signal,onEvent}=
823
824
  ...options,
824
825
  nativeBuilder:loaded.nativeBuilder,
825
826
  toolchainRoot:loaded.toolchainRoot,
827
+ providerSource:loaded.providerSource,
826
828
  targetRequest
827
829
  };
828
830
  }
@@ -0,0 +1,23 @@
1
+ import {createCoreRuntime} from './runtime.mjs';
2
+ import {startCoreStdio} from './stdio.mjs';
3
+
4
+ /** Compose app services with the shared dispatcher and draining transport. */
5
+ export function startCoreHost({application, version, services = [], input, output, onError, signal} = {}) {
6
+ const runtime = createCoreRuntime({application, version, services});
7
+ const host = startCoreStdio({runtime, input, output, onError});
8
+ if (!signal) return host;
9
+
10
+ function closeHostOnAbort() {
11
+ // The transport owns cancellation, accepted-service work and final output.
12
+ host.close();
13
+ }
14
+
15
+ function releaseAbortListener() {
16
+ signal.removeEventListener('abort', closeHostOnAbort);
17
+ }
18
+
19
+ signal.addEventListener('abort', closeHostOnAbort, {once: true});
20
+ host.closed.then(releaseAbortListener, releaseAbortListener);
21
+ if (signal.aborted) closeHostOnAbort();
22
+ return host;
23
+ }
@@ -0,0 +1,333 @@
1
+ import Is from 'strong-type';
2
+ import {createArcaneEventSource} from '../event-manager.mjs';
3
+ import {CORE_PROTOCOL, CoreError, serializeCoreError} from '../../browser-runtime/core/contracts.mjs';
4
+
5
+ const is = new Is(false);
6
+ const STATE_EVENT = 'core.runtime.state';
7
+ const FRAME_EVENT = 'core.runtime.frame';
8
+
9
+ function failure(code, message) {
10
+ return new CoreError({code, message});
11
+ }
12
+
13
+ function requireFunction(value, name) {
14
+ if (!is.function(value)) throw new TypeError(`${name} must be a function.`);
15
+ }
16
+
17
+ function aborted(controller) {
18
+ if (controller.signal.aborted) throw controller.signal.reason;
19
+ }
20
+
21
+ /** App-neutral dispatch. Hosts own services, privileges and process lifetime. */
22
+ class CoreRuntime {
23
+ #application;
24
+ #version;
25
+ #events;
26
+ #state = 'created';
27
+ #services = new Map();
28
+ #methods = new Map();
29
+ #requests = new Map();
30
+ #responses = new Set();
31
+ #publications = [];
32
+ #publishing = false;
33
+ #closing = null;
34
+
35
+ constructor({application, version, services = []} = {}) {
36
+ this.#application = application;
37
+ this.#version = version;
38
+ this.#events = createArcaneEventSource(
39
+ this,
40
+ {source: 'core-runtime', eventTypes: [STATE_EVENT, FRAME_EVENT]}
41
+ );
42
+ for (const service of services) this.registerService(service);
43
+ }
44
+
45
+ current() {
46
+ return {
47
+ state: this.#state,
48
+ application: this.#application,
49
+ version: this.#version,
50
+ services: [...this.#services.values()].map(
51
+ function serviceState(service) {
52
+ return {name: service.name, state: service.state, error: service.error};
53
+ }
54
+ ),
55
+ activeRequests: [...this.#requests.values()].map(
56
+ function requestState(request) {
57
+ return {id: request.id, method: request.method, lifetime: request.lifetime};
58
+ }
59
+ )
60
+ };
61
+ }
62
+
63
+ subscribe(listener, {emitCurrent = true, signal} = {}) {
64
+ requireFunction(listener, 'listener');
65
+ function deliverState(occurrence) {
66
+ listener(occurrence.detail);
67
+ }
68
+ const unsubscribe = this.#events.on(STATE_EVENT, deliverState, {signal});
69
+ try {
70
+ if (emitCurrent && !signal?.aborted) listener(this.current());
71
+ } catch (error) {
72
+ unsubscribe();
73
+ throw error;
74
+ }
75
+ return unsubscribe;
76
+ }
77
+
78
+ onFrame(listener, {signal} = {}) {
79
+ requireFunction(listener, 'listener');
80
+ function deliverFrame(occurrence) {
81
+ listener(occurrence.detail);
82
+ }
83
+ return this.#events.on(FRAME_EVENT, deliverFrame, {signal});
84
+ }
85
+
86
+ #publish() {
87
+ this.#publications.push(this.current());
88
+ if (this.#publishing) return;
89
+ this.#publishing = true;
90
+ try {
91
+ // A synchronous subscriber can change the lifecycle. Finish each
92
+ // state publication before delivering its reentrant successor.
93
+ while (this.#publications.length) {
94
+ const state = this.#publications.shift();
95
+ this.#events.dispatch(STATE_EVENT, state);
96
+ this.emit('core.state', state);
97
+ }
98
+ } finally {
99
+ this.#publishing = false;
100
+ }
101
+ }
102
+
103
+ emit(event, data) {
104
+ const frame = {protocol: CORE_PROTOCOL, type: 'event', event, data, time: new Date().toISOString()};
105
+ this.#events.dispatch(FRAME_EVENT, frame);
106
+ return frame;
107
+ }
108
+
109
+ registerService(definition) {
110
+ if (this.#closing) throw failure('CORE_CLOSING', 'Core is closing.');
111
+ const {name, methods = {}, start, drain, dispose} = definition;
112
+ if (!is.string(name) || !name) throw new TypeError('Service name must be a nonempty string.');
113
+ if (this.#services.has(name)) throw failure('CORE_SERVICE_EXISTS', `Service ${name} is already registered.`);
114
+ for (const [hook, value] of Object.entries({start, drain, dispose})) {
115
+ if (value !== undefined) requireFunction(value, `Service ${hook}`);
116
+ }
117
+ const registrations = [];
118
+ for (const [method, value] of Object.entries(methods)) {
119
+ if (this.#methods.has(method) || ['system.ping', 'app.current', 'version.current'].includes(method)) {
120
+ throw failure('CORE_METHOD_EXISTS', `Method ${method} is already registered.`);
121
+ }
122
+ const handle = is.function(value) ? value : value.handle;
123
+ const lifetime = is.function(value) ? 'request' : value.lifetime ?? 'request';
124
+ requireFunction(handle, `Method ${method}`);
125
+ if (lifetime !== 'request' && lifetime !== 'service') {
126
+ throw new TypeError('Method lifetime must be request or service.');
127
+ }
128
+ registrations.push({method, handle, lifetime});
129
+ }
130
+ const service = {name, definition, state: 'registered', error: null, task: null};
131
+ this.#services.set(name, service);
132
+ for (const registration of registrations) this.#methods.set(registration.method, {...registration, service});
133
+ if (this.#state === 'ready') this.#startService(service);
134
+ const runtime = this;
135
+ return {
136
+ get ready() {
137
+ return runtime.#startService(service);
138
+ },
139
+ current() {
140
+ return {name: service.name, state: service.state, error: service.error};
141
+ }
142
+ };
143
+ }
144
+
145
+ #serviceContext(service) {
146
+ const runtime = this;
147
+ return {
148
+ application: this.#application,
149
+ service: service.name,
150
+ emit(event, data) {
151
+ return runtime.emit(event, data);
152
+ }
153
+ };
154
+ }
155
+
156
+ #serviceState(service, state, error) {
157
+ service.state = state;
158
+ service.error = state === 'failed' || (error !== undefined && error !== null)
159
+ ? serializeCoreError(error)
160
+ : null;
161
+ this.emit('core.service.state', {name: service.name, state, error: service.error});
162
+ this.#publish();
163
+ }
164
+
165
+ #startService(service) {
166
+ if (service.task) return service.task;
167
+ if (this.#closing) return Promise.reject(failure('CORE_CLOSING', 'Core is closing.'));
168
+ const runtime = this;
169
+ // Publish after task assignment so a synchronous state subscriber cannot
170
+ // start the same service twice.
171
+ service.task = Promise.resolve().then(
172
+ async function initializeService() {
173
+ runtime.#serviceState(service, 'starting');
174
+ try {
175
+ await service.definition.start?.(runtime.#serviceContext(service));
176
+ runtime.#serviceState(service, 'ready');
177
+ } catch (error) {
178
+ runtime.#serviceState(service, 'failed', error);
179
+ throw error;
180
+ }
181
+ }
182
+ );
183
+ service.task.catch(function observeServiceStartupFailure() {});
184
+ return service.task;
185
+ }
186
+
187
+ start() {
188
+ if (this.#closing) throw failure('CORE_CLOSING', 'Core is closing.');
189
+ if (this.#state === 'ready') return this.current();
190
+ this.#state = 'ready';
191
+ for (const service of this.#services.values()) this.#startService(service);
192
+ this.emit('core.ready', {version: this.#version, app: this.#application});
193
+ this.#publish();
194
+ return this.current();
195
+ }
196
+
197
+ async handle(frame) {
198
+ if (frame?.protocol !== CORE_PROTOCOL) throw failure('INVALID_RPC_REQUEST', 'Unknown Core protocol.');
199
+ if (frame.type === 'control') {
200
+ if (frame.control === 'request.cancel') return this.#cancel(frame.requestId);
201
+ if (frame.control === 'requests.cancelAll') {
202
+ for (const request of this.#requests.values()) this.#cancel(request.id);
203
+ return;
204
+ }
205
+ throw failure('INVALID_RPC_CONTROL', 'Unknown Core control.');
206
+ }
207
+ if (frame.type !== 'request' || !is.string(frame.id) || !frame.id || !is.string(frame.method)) {
208
+ throw failure('INVALID_RPC_REQUEST', 'A Core request requires an id and method.');
209
+ }
210
+ const task = this.#answer(frame);
211
+ this.#responses.add(task);
212
+ try {
213
+ return await task;
214
+ } finally {
215
+ this.#responses.delete(task);
216
+ }
217
+ }
218
+
219
+ async #answer(frame) {
220
+ let result;
221
+ let error;
222
+ let ok = true;
223
+ try {
224
+ if (this.#state !== 'ready') throw failure('CORE_NOT_READY', 'Core is not accepting requests.');
225
+ if (this.#requests.has(frame.id)) throw failure('RPC_REQUEST_ID_ACTIVE', 'The request id is already active.');
226
+ if (frame.method === 'system.ping') result = {ok: true};
227
+ else if (frame.method === 'version.current') result = this.#version;
228
+ else if (frame.method === 'app.current') result = this.#application;
229
+ else result = await this.#request(frame);
230
+ } catch (caught) {
231
+ ok = false;
232
+ error = caught;
233
+ }
234
+ const response = {
235
+ protocol: CORE_PROTOCOL,
236
+ type: 'response',
237
+ id: frame.id,
238
+ ...(ok ? {ok: true, result} : {ok: false, error: serializeCoreError(error)}),
239
+ time: new Date().toISOString()
240
+ };
241
+ this.#events.dispatch(FRAME_EVENT, response);
242
+ return response;
243
+ }
244
+
245
+ #request(frame) {
246
+ const registration = this.#methods.get(frame.method);
247
+ if (!registration) throw failure('METHOD_NOT_ALLOWED', `Core does not expose ${frame.method}.`);
248
+ const {service, handle, lifetime} = registration;
249
+ const controller = new AbortController();
250
+ const request = {id: frame.id, method: frame.method, lifetime, controller, task: null};
251
+ const runtime = this;
252
+ this.#requests.set(frame.id, request);
253
+ request.task = Promise.resolve().then(
254
+ async function executeCoreRequest() {
255
+ try {
256
+ await runtime.#startService(service);
257
+ aborted(controller);
258
+ const result = await handle.call(
259
+ service.definition,
260
+ frame.parameters,
261
+ {...runtime.#serviceContext(service), requestId: frame.id, signal: controller.signal}
262
+ );
263
+ aborted(controller);
264
+ return result;
265
+ } finally {
266
+ if (runtime.#requests.get(frame.id) === request) runtime.#requests.delete(frame.id);
267
+ runtime.#publish();
268
+ }
269
+ }
270
+ );
271
+ this.#publish();
272
+ return request.task;
273
+ }
274
+
275
+ #cancel(id) {
276
+ const request = this.#requests.get(id);
277
+ if (!request || request.lifetime === 'service' || request.controller.signal.aborted) return false;
278
+ const error = failure('REQUEST_ABORTED', 'The Core request was cancelled.');
279
+ error.name = 'AbortError';
280
+ request.controller.abort(error);
281
+ return true;
282
+ }
283
+
284
+ close() {
285
+ if (this.#closing) return this.#closing;
286
+ this.#state = 'draining';
287
+ const runtime = this;
288
+ // A synchronous state listener may call close again. Retain the owner
289
+ // before announcing draining, and never let a renderer cancel saves.
290
+ this.#closing = Promise.resolve().then(
291
+ async function drainCoreRuntime() {
292
+ const failures = [];
293
+ for (const request of runtime.#requests.values()) runtime.#cancel(request.id);
294
+ await Promise.allSettled([...runtime.#responses]);
295
+ await Promise.all(
296
+ [...runtime.#services.values()].map(
297
+ async function closeCoreService(service) {
298
+ const serviceFailures = [];
299
+ if (service.task) {
300
+ try { await service.task; } catch (error) { serviceFailures.push(error); }
301
+ }
302
+ runtime.#serviceState(service, 'draining');
303
+ for (const hook of ['drain', 'dispose']) {
304
+ try {
305
+ await service.definition[hook]?.(runtime.#serviceContext(service));
306
+ } catch (error) {
307
+ serviceFailures.push(error);
308
+ runtime.emit('core.error', serializeCoreError(error));
309
+ }
310
+ }
311
+ const error = serviceFailures.length
312
+ ? new AggregateError(serviceFailures, `Service ${service.name} shutdown failed.`)
313
+ : null;
314
+ failures.push(...serviceFailures);
315
+ runtime.#serviceState(service, 'closed', error);
316
+ }
317
+ )
318
+ );
319
+ runtime.#state = 'closed';
320
+ runtime.#publish();
321
+ runtime.#events.dispose();
322
+ if (failures.length) throw new AggregateError(failures, 'Core service shutdown failed.');
323
+ return runtime.current();
324
+ }
325
+ );
326
+ this.#publish();
327
+ return this.#closing;
328
+ }
329
+ }
330
+
331
+ export function createCoreRuntime(options) {
332
+ return new CoreRuntime(options);
333
+ }
@@ -0,0 +1,209 @@
1
+ import {finished} from 'node:stream';
2
+ import Is from 'strong-type';
3
+ import {CoreError, serializeCoreError} from '../../browser-runtime/core/contracts.mjs';
4
+
5
+ const is = new Is(false);
6
+
7
+ /** Content-Length is used only for the existing native transport framing. */
8
+ export function encodeCoreFrame(frame) {
9
+ const body = Buffer.from(JSON.stringify(frame), 'utf8');
10
+ return Buffer.concat([Buffer.from(`Content-Length: ${body.length}\r\n\r\n`, 'ascii'), body]);
11
+ }
12
+
13
+ export function createCoreFrameDecoder(onFrame) {
14
+ let header = Buffer.alloc(0);
15
+ let remaining = null;
16
+ let parts = [];
17
+ const utf8 = new TextDecoder('utf-8', {fatal: true});
18
+
19
+ function push(chunk) {
20
+ let input = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
21
+ while (input.length || remaining === 0) {
22
+ if (remaining === null) {
23
+ header = Buffer.concat([header, input]);
24
+ const marker = header.indexOf('\r\n\r\n');
25
+ if (marker < 0) return;
26
+ const match = header.subarray(0, marker).toString('ascii').match(
27
+ /(?:^|\r\n)Content-Length:\s*(\d+)(?:\r\n|$)/i
28
+ );
29
+ if (!match) throw new CoreError({code: 'IPC_LENGTH_MISSING', message: 'IPC content length is missing.'});
30
+ remaining = Number(match[1]);
31
+ if (!is.safeInteger(remaining) || remaining < 0) {
32
+ throw new CoreError({code: 'IPC_LENGTH_INVALID', message: 'IPC content length is invalid.'});
33
+ }
34
+ input = header.subarray(marker + 4);
35
+ header = Buffer.alloc(0);
36
+ }
37
+ const consumed = Math.min(remaining, input.length);
38
+ if (consumed) parts.push(input.subarray(0, consumed));
39
+ input = input.subarray(consumed);
40
+ remaining -= consumed;
41
+ if (remaining > 0) return;
42
+ const body = Buffer.concat(parts);
43
+ parts = [];
44
+ remaining = null;
45
+ onFrame(JSON.parse(utf8.decode(body)));
46
+ }
47
+ }
48
+
49
+ function finish() {
50
+ if (header.length || remaining !== null) {
51
+ throw new CoreError({code: 'IPC_FRAME_INCOMPLETE', message: 'IPC ended during a frame.'});
52
+ }
53
+ }
54
+
55
+ return {push, finish};
56
+ }
57
+
58
+ function reportStdioError(error) {
59
+ console.error('Arcane Core transport failed:', error);
60
+ }
61
+
62
+ /** Attach an existing runtime; EOF drains accepted service work, never exits. */
63
+ export function startCoreStdio({runtime, input = process.stdin, output = process.stdout, onError = reportStdioError}) {
64
+ let closing = null;
65
+ let writes = Promise.resolve();
66
+ let outputFailed = false;
67
+ let inputCompleted = false;
68
+ let resolveInputCompletion;
69
+ const inputCompletion = new Promise(function observeInputCompletion(resolve) {
70
+ resolveInputCompletion = resolve;
71
+ });
72
+ const pending = new Set();
73
+ const failures = [];
74
+ let resolveClosed;
75
+ let rejectClosed;
76
+ const closed = new Promise(
77
+ function createClosedPromise(resolve, reject) {
78
+ resolveClosed = resolve;
79
+ rejectClosed = reject;
80
+ }
81
+ );
82
+ // Every background error is reported to onError and retained by closed.
83
+ closed.catch(function observeReportedTransportFailure() {});
84
+
85
+ function report(error) {
86
+ if (failures.includes(error)) return;
87
+ failures.push(error);
88
+ try { onError(error); } catch (reportError) { failures.push(reportError); }
89
+ }
90
+
91
+ function failOutput(error) {
92
+ outputFailed = true;
93
+ report(error);
94
+ close();
95
+ }
96
+
97
+ function queueFrame(frame) {
98
+ if (outputFailed) return;
99
+ let encoded;
100
+ try { encoded = encodeCoreFrame(frame); } catch (error) { failOutput(error); return; }
101
+ writes = writes.then(
102
+ function writeNextFrame() {
103
+ if (outputFailed) return;
104
+ return new Promise(
105
+ function writeFrame(resolve, reject) {
106
+ output.write(
107
+ encoded,
108
+ function frameWritten(error) {
109
+ if (error) reject(error);
110
+ else resolve();
111
+ }
112
+ );
113
+ }
114
+ );
115
+ }
116
+ ).catch(failOutput);
117
+ }
118
+
119
+ const unsubscribe = runtime.onFrame(queueFrame);
120
+
121
+ function receiveFrame(frame) {
122
+ const task = runtime.handle(frame);
123
+ pending.add(task);
124
+ task.then(
125
+ function requestSettled() {
126
+ pending.delete(task);
127
+ },
128
+ function requestFailed(error) {
129
+ pending.delete(task);
130
+ report(error);
131
+ try { runtime.emit('core.error', serializeCoreError(error)); } catch (eventError) { report(eventError); }
132
+ }
133
+ );
134
+ }
135
+
136
+ const decoder = createCoreFrameDecoder(receiveFrame);
137
+
138
+ function receiveInput(chunk) {
139
+ try { decoder.push(chunk); } catch (error) { report(error); close(); }
140
+ }
141
+
142
+ function inputEnded() {
143
+ try { decoder.finish(); } catch (error) { report(error); }
144
+ close();
145
+ }
146
+
147
+ function inputFailed(error) {
148
+ report(error);
149
+ close();
150
+ }
151
+
152
+ function detachInput() {
153
+ input.off('data', receiveInput);
154
+ input.off('end', inputEnded);
155
+ input.off('close', inputEnded);
156
+ input.pause();
157
+ }
158
+
159
+ async function releaseTransportListeners() {
160
+ unsubscribe();
161
+ output.off('error', failOutput);
162
+ // destroy(error) marks a stream destroyed before its scheduled error
163
+ // and close events. The native completion owner observes their delivery;
164
+ // stream flags alone do not establish that pending errors were observed.
165
+ if (input.destroyed && !inputCompleted) await inputCompletion;
166
+ cleanupInputCompletion();
167
+ input.off('error', inputFailed);
168
+ }
169
+
170
+ function close() {
171
+ if (closing) return closed;
172
+ detachInput();
173
+ closing = Promise.resolve().then(
174
+ async function drainTransport() {
175
+ try { await runtime.close(); } catch (error) { report(error); }
176
+ await Promise.allSettled([...pending]);
177
+ await writes;
178
+ await releaseTransportListeners();
179
+ if (failures.length) rejectClosed(new AggregateError(failures, 'Core transport closed with errors.'));
180
+ else resolveClosed(runtime.current());
181
+ }
182
+ );
183
+ closing.catch(
184
+ async function shutdownFailed(error) {
185
+ report(error);
186
+ await releaseTransportListeners();
187
+ rejectClosed(new AggregateError(failures, 'Core transport shutdown failed.'));
188
+ }
189
+ );
190
+ return closed;
191
+ }
192
+
193
+ const cleanupInputCompletion = finished(input, {readable: true, writable: false}, function inputLifecycleComplete() {
194
+ inputCompleted = true;
195
+ resolveInputCompletion();
196
+ });
197
+ input.on('data', receiveInput);
198
+ input.on('end', inputEnded);
199
+ input.on('close', inputEnded);
200
+ input.on('error', inputFailed);
201
+ output.on('error', failOutput);
202
+ try { runtime.start(); } catch (error) { report(error); close(); }
203
+ if (!closing) {
204
+ if (input.errored) report(input.errored);
205
+ if (input.readableEnded || input.destroyed) inputEnded();
206
+ else input.resume();
207
+ }
208
+ return {runtime, closed, close};
209
+ }