arcane-os 0.54.1 → 0.56.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +35 -28
  3. package/browser-runtime/core/client.mjs +552 -0
  4. package/browser-runtime/core/contracts.mjs +85 -0
  5. package/docs/architecture.md +31 -23
  6. package/docs/compatibility.md +10 -1
  7. package/docs/consumer-updates.md +82 -0
  8. package/docs/platform-targets.md +20 -11
  9. package/docs/publishing.md +41 -18
  10. package/docs/reference/README.md +19 -7
  11. package/docs/reference/availability-and-normalization.md +15 -5
  12. package/docs/reference/behavioral-testing.md +7 -6
  13. package/docs/reference/cli.md +12 -4
  14. package/docs/reference/core-client.md +163 -0
  15. package/docs/reference/core-native-packaging.md +149 -0
  16. package/docs/reference/core-runtime.md +185 -0
  17. package/docs/reference/inventory/package-api.json +271 -5
  18. package/docs/reference/protocols.md +38 -14
  19. package/docs/reference/pwa.md +17 -0
  20. package/docs/reference/sdk-api.md +772 -15
  21. package/package.json +10 -3
  22. package/schemas/arcane-app.schema.json +10 -0
  23. package/schemas/arcane-package.schema.json +10 -0
  24. package/schemas/native-build-plan.schema.json +8 -0
  25. package/src/app-descriptor.mjs +18 -1
  26. package/src/app-layout.mjs +6 -2
  27. package/src/cli/main.mjs +8 -6
  28. package/src/core/host.mjs +23 -0
  29. package/src/core/hosts/arcane-core-loader.cjs +2 -0
  30. package/src/core/hosts/linux/arcane_core_process.c +568 -0
  31. package/src/core/hosts/linux/arcane_core_process.h +127 -0
  32. package/src/core/hosts/sea-launcher.cjs +17 -0
  33. package/src/core/hosts/windows/ArcaneCoreProcess.cs +416 -0
  34. package/src/core/runtime.mjs +345 -0
  35. package/src/core/stdio.mjs +209 -0
  36. package/src/core-classic-source.mjs +43 -0
  37. package/src/import-map.mjs +62 -0
  38. package/src/native/portable-layout.mjs +184 -0
  39. package/src/native/portable-provider.mjs +211 -0
  40. package/src/native-plan.mjs +10 -4
  41. package/src/native-provider-loader.mjs +15 -4
  42. package/src/packager/core.mjs +131 -32
  43. package/src/process.mjs +8 -4
  44. package/src/targets/index.mjs +3 -1
  45. package/src/toolchain.mjs +23 -6
@@ -0,0 +1,345 @@
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(replay = false) {
87
+ this.#publications.push({state: this.current(), replay});
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, replay: replaying} = this.#publications.shift();
95
+ // Finish one replay snapshot before publishing lifecycle changes
96
+ // triggered reentrantly by its listeners.
97
+ if (replaying && state.state === 'ready') {
98
+ this.emit('core.ready', {version: state.version, app: state.application});
99
+ }
100
+ this.#events.dispatch(STATE_EVENT, state);
101
+ this.emit('core.state', state);
102
+ if (replaying) {
103
+ for (const service of state.services) this.emit('core.service.state', service);
104
+ }
105
+ }
106
+ } finally {
107
+ this.#publishing = false;
108
+ }
109
+ }
110
+
111
+ emit(event, data) {
112
+ const frame = {protocol: CORE_PROTOCOL, type: 'event', event, data, time: new Date().toISOString()};
113
+ this.#events.dispatch(FRAME_EVENT, frame);
114
+ return frame;
115
+ }
116
+
117
+ registerService(definition) {
118
+ if (this.#closing) throw failure('CORE_CLOSING', 'Core is closing.');
119
+ const {name, methods = {}, start, drain, dispose} = definition;
120
+ if (!is.string(name) || !name) throw new TypeError('Service name must be a nonempty string.');
121
+ if (this.#services.has(name)) throw failure('CORE_SERVICE_EXISTS', `Service ${name} is already registered.`);
122
+ for (const [hook, value] of Object.entries({start, drain, dispose})) {
123
+ if (value !== undefined) requireFunction(value, `Service ${hook}`);
124
+ }
125
+ const registrations = [];
126
+ for (const [method, value] of Object.entries(methods)) {
127
+ if (this.#methods.has(method) || ['system.ping', 'app.current', 'version.current'].includes(method)) {
128
+ throw failure('CORE_METHOD_EXISTS', `Method ${method} is already registered.`);
129
+ }
130
+ const handle = is.function(value) ? value : value.handle;
131
+ const lifetime = is.function(value) ? 'request' : value.lifetime ?? 'request';
132
+ requireFunction(handle, `Method ${method}`);
133
+ if (lifetime !== 'request' && lifetime !== 'service') {
134
+ throw new TypeError('Method lifetime must be request or service.');
135
+ }
136
+ registrations.push({method, handle, lifetime});
137
+ }
138
+ const service = {name, definition, state: 'registered', error: null, task: null};
139
+ this.#services.set(name, service);
140
+ for (const registration of registrations) this.#methods.set(registration.method, {...registration, service});
141
+ if (this.#state === 'ready') this.#startService(service);
142
+ const runtime = this;
143
+ return {
144
+ get ready() {
145
+ return runtime.#startService(service);
146
+ },
147
+ current() {
148
+ return {name: service.name, state: service.state, error: service.error};
149
+ }
150
+ };
151
+ }
152
+
153
+ #serviceContext(service) {
154
+ const runtime = this;
155
+ return {
156
+ application: this.#application,
157
+ service: service.name,
158
+ emit(event, data) {
159
+ return runtime.emit(event, data);
160
+ }
161
+ };
162
+ }
163
+
164
+ #serviceState(service, state, error) {
165
+ service.state = state;
166
+ service.error = state === 'failed' || (error !== undefined && error !== null)
167
+ ? serializeCoreError(error)
168
+ : null;
169
+ this.emit('core.service.state', {name: service.name, state, error: service.error});
170
+ this.#publish();
171
+ }
172
+
173
+ #startService(service) {
174
+ if (service.task) return service.task;
175
+ if (this.#closing) return Promise.reject(failure('CORE_CLOSING', 'Core is closing.'));
176
+ const runtime = this;
177
+ // Publish after task assignment so a synchronous state subscriber cannot
178
+ // start the same service twice.
179
+ service.task = Promise.resolve().then(
180
+ async function initializeService() {
181
+ runtime.#serviceState(service, 'starting');
182
+ try {
183
+ await service.definition.start?.(runtime.#serviceContext(service));
184
+ runtime.#serviceState(service, 'ready');
185
+ } catch (error) {
186
+ runtime.#serviceState(service, 'failed', error);
187
+ throw error;
188
+ }
189
+ }
190
+ );
191
+ service.task.catch(function observeServiceStartupFailure() {});
192
+ return service.task;
193
+ }
194
+
195
+ start() {
196
+ if (this.#closing) throw failure('CORE_CLOSING', 'Core is closing.');
197
+ if (this.#state === 'ready') return this.current();
198
+ this.#state = 'ready';
199
+ for (const service of this.#services.values()) this.#startService(service);
200
+ this.emit('core.ready', {version: this.#version, app: this.#application});
201
+ this.#publish();
202
+ return this.current();
203
+ }
204
+
205
+ async handle(frame) {
206
+ if (frame?.protocol !== CORE_PROTOCOL) throw failure('INVALID_RPC_REQUEST', 'Unknown Core protocol.');
207
+ if (frame.type === 'control') {
208
+ if (frame.control === 'runtime.replay') {
209
+ this.#publish(true);
210
+ return;
211
+ }
212
+ if (frame.control === 'request.cancel') return this.#cancel(frame.requestId);
213
+ if (frame.control === 'requests.cancelAll') {
214
+ for (const request of this.#requests.values()) this.#cancel(request.id);
215
+ return;
216
+ }
217
+ throw failure('INVALID_RPC_CONTROL', 'Unknown Core control.');
218
+ }
219
+ if (frame.type !== 'request' || !is.string(frame.id) || !frame.id || !is.string(frame.method)) {
220
+ throw failure('INVALID_RPC_REQUEST', 'A Core request requires an id and method.');
221
+ }
222
+ const task = this.#answer(frame);
223
+ this.#responses.add(task);
224
+ try {
225
+ return await task;
226
+ } finally {
227
+ this.#responses.delete(task);
228
+ }
229
+ }
230
+
231
+ async #answer(frame) {
232
+ let result;
233
+ let error;
234
+ let ok = true;
235
+ try {
236
+ if (this.#state !== 'ready') throw failure('CORE_NOT_READY', 'Core is not accepting requests.');
237
+ if (this.#requests.has(frame.id)) throw failure('RPC_REQUEST_ID_ACTIVE', 'The request id is already active.');
238
+ if (frame.method === 'system.ping') result = {ok: true};
239
+ else if (frame.method === 'version.current') result = this.#version;
240
+ else if (frame.method === 'app.current') result = this.#application;
241
+ else result = await this.#request(frame);
242
+ } catch (caught) {
243
+ ok = false;
244
+ error = caught;
245
+ }
246
+ const response = {
247
+ protocol: CORE_PROTOCOL,
248
+ type: 'response',
249
+ id: frame.id,
250
+ ...(ok ? {ok: true, result} : {ok: false, error: serializeCoreError(error)}),
251
+ time: new Date().toISOString()
252
+ };
253
+ this.#events.dispatch(FRAME_EVENT, response);
254
+ return response;
255
+ }
256
+
257
+ #request(frame) {
258
+ const registration = this.#methods.get(frame.method);
259
+ if (!registration) throw failure('METHOD_NOT_ALLOWED', `Core does not expose ${frame.method}.`);
260
+ const {service, handle, lifetime} = registration;
261
+ const controller = new AbortController();
262
+ const request = {id: frame.id, method: frame.method, lifetime, controller, task: null};
263
+ const runtime = this;
264
+ this.#requests.set(frame.id, request);
265
+ request.task = Promise.resolve().then(
266
+ async function executeCoreRequest() {
267
+ try {
268
+ await runtime.#startService(service);
269
+ aborted(controller);
270
+ const result = await handle.call(
271
+ service.definition,
272
+ frame.parameters,
273
+ {...runtime.#serviceContext(service), requestId: frame.id, signal: controller.signal}
274
+ );
275
+ aborted(controller);
276
+ return result;
277
+ } finally {
278
+ if (runtime.#requests.get(frame.id) === request) runtime.#requests.delete(frame.id);
279
+ runtime.#publish();
280
+ }
281
+ }
282
+ );
283
+ this.#publish();
284
+ return request.task;
285
+ }
286
+
287
+ #cancel(id) {
288
+ const request = this.#requests.get(id);
289
+ if (!request || request.lifetime === 'service' || request.controller.signal.aborted) return false;
290
+ const error = failure('REQUEST_ABORTED', 'The Core request was cancelled.');
291
+ error.name = 'AbortError';
292
+ request.controller.abort(error);
293
+ return true;
294
+ }
295
+
296
+ close() {
297
+ if (this.#closing) return this.#closing;
298
+ this.#state = 'draining';
299
+ const runtime = this;
300
+ // A synchronous state listener may call close again. Retain the owner
301
+ // before announcing draining, and never let a renderer cancel saves.
302
+ this.#closing = Promise.resolve().then(
303
+ async function drainCoreRuntime() {
304
+ const failures = [];
305
+ for (const request of runtime.#requests.values()) runtime.#cancel(request.id);
306
+ await Promise.allSettled([...runtime.#responses]);
307
+ await Promise.all(
308
+ [...runtime.#services.values()].map(
309
+ async function closeCoreService(service) {
310
+ const serviceFailures = [];
311
+ if (service.task) {
312
+ try { await service.task; } catch (error) { serviceFailures.push(error); }
313
+ }
314
+ runtime.#serviceState(service, 'draining');
315
+ for (const hook of ['drain', 'dispose']) {
316
+ try {
317
+ await service.definition[hook]?.(runtime.#serviceContext(service));
318
+ } catch (error) {
319
+ serviceFailures.push(error);
320
+ runtime.emit('core.error', serializeCoreError(error));
321
+ }
322
+ }
323
+ const error = serviceFailures.length
324
+ ? new AggregateError(serviceFailures, `Service ${service.name} shutdown failed.`)
325
+ : null;
326
+ failures.push(...serviceFailures);
327
+ runtime.#serviceState(service, 'closed', error);
328
+ }
329
+ )
330
+ );
331
+ runtime.#state = 'closed';
332
+ runtime.#publish();
333
+ runtime.#events.dispose();
334
+ if (failures.length) throw new AggregateError(failures, 'Core service shutdown failed.');
335
+ return runtime.current();
336
+ }
337
+ );
338
+ this.#publish();
339
+ return this.#closing;
340
+ }
341
+ }
342
+
343
+ export function createCoreRuntime(options) {
344
+ return new CoreRuntime(options);
345
+ }
@@ -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
+ }
@@ -0,0 +1,43 @@
1
+ import {readFile} from 'node:fs/promises';
2
+
3
+ /**
4
+ * Generate the native host's classic script from the canonical ESM client.
5
+ * The two owned modules have declaration exports and two explicit imports;
6
+ * this projection is deliberately not a general JavaScript bundler.
7
+ */
8
+ export async function createCoreClassicSource({eventOwnerModuleURL,replayRuntimeState=false}={}){
9
+ if(typeof eventOwnerModuleURL!=='string'||!eventOwnerModuleURL){
10
+ throw new TypeError('The classic Core client needs the served SDK event-manager module URL.');
11
+ }
12
+ const [contracts,client]=await Promise.all([
13
+ readFile(new URL('../browser-runtime/core/contracts.mjs',import.meta.url),'utf8'),
14
+ readFile(new URL('../browser-runtime/core/client.mjs',import.meta.url),'utf8')
15
+ ]);
16
+ const declarations=contracts.replace(/^export (?=(?:const|class|function) )/gmu,'');
17
+ const implementation=client
18
+ .replace("import {arcaneEvents} from '../event-manager.mjs';",'')
19
+ .replace("import {CORE_PROTOCOL,CORE_READY_EVENTS,CoreError,serializeCoreError} from './contracts.mjs';",'')
20
+ .replace(/^export (?=function )/gmu,'');
21
+ return `(function installArcaneCoreClassic(global){
22
+ 'use strict';
23
+ const arcaneEvents=global.arcaneEvents??null;
24
+ ${declarations}
25
+ ${implementation}
26
+ if(global[CORE_CLIENT_KEY])return;
27
+ function loadSharedEventOwner(){
28
+ return import(${JSON.stringify(eventOwnerModuleURL)}).then(function sharedEventOwner(namespace){return namespace.arcaneEvents;});
29
+ }
30
+ // Document-created native injection precedes the page's managed import map.
31
+ // Only the module import waits for parsing; facade/RPC setup below is immediate.
32
+ const eventOwnerReady=arcaneEvents?Promise.resolve(arcaneEvents)
33
+ :global.document?.readyState==='loading'
34
+ ?new Promise(function afterImportMap(resolve,reject){
35
+ global.document.addEventListener('DOMContentLoaded',function connectSharedEvents(){
36
+ loadSharedEventOwner().then(resolve,reject);
37
+ },{once:true});
38
+ })
39
+ :loadSharedEventOwner();
40
+ installCoreClient(global,{eventOwner:arcaneEvents,eventOwnerReady,replayRuntimeState:${replayRuntimeState===true}});
41
+ })(globalThis);
42
+ `;
43
+ }
@@ -42,6 +42,8 @@ const STATIC_RUNTIME_PACKAGE_IMPORTS=new Map([
42
42
  const SDK_BROWSER_SELF_IMPORTS=new Map([
43
43
  ['arcane-os/event-manager',SDK_BROWSER_ENTRY],
44
44
  ['arcane-os/websocket-client','sdk/websocket-client.mjs'],
45
+ ['arcane-os/core/client','sdk/core/client.mjs'],
46
+ ['arcane-os/core/contracts','sdk/core/contracts.mjs'],
45
47
  ['arcane-os/logging','sdk/logging.mjs'],
46
48
  ['arcane-os/browser-device','sdk/browser-device.mjs'],
47
49
  ['arcane-os/pwa','sdk/pwa.mjs'],
@@ -2087,6 +2089,66 @@ function scanHtmlStructure(html){
2087
2089
  return {scripts,links,bases,metas,elements,styles,headClose,bodyClose};
2088
2090
  }
2089
2091
 
2092
+ /** Remove generated PWA references from a selected output document without changing authored content. */
2093
+ export function removePwaEntryReferences(html, {documentUrl, manifestUrl} = {}) {
2094
+ if (!is.string(html)) throw new TypeError('PWA entry HTML must be a string.');
2095
+ const structure = scanHtmlStructure(html);
2096
+ const manifest = new URL(manifestUrl, documentUrl);
2097
+ const base = structure.bases.find(
2098
+ function baseWithHref(element) {
2099
+ return parseTagAttributes(element.open).has('href');
2100
+ }
2101
+ );
2102
+ const baseHref = base ? structuralAttribute(parseTagAttributes(base.open), 'href', 'base') : null;
2103
+ const resourceBase = baseHref ? new URL(baseHref, documentUrl) : documentUrl;
2104
+ const edits = [];
2105
+ for (const link of structure.links) {
2106
+ const attributes = parseTagAttributes(link.open);
2107
+ const href = htmlAttributeView(attributes.get('href') ?? '');
2108
+ const rel = htmlAttributeView(attributes.get('rel') ?? '');
2109
+ if (!href?.decoded || !rel) continue;
2110
+ let target;
2111
+ try {
2112
+ target = new URL(href.decoded, resourceBase);
2113
+ } catch (error) {
2114
+ // An unrelated non-URL reference stays under its authored owner.
2115
+ continue;
2116
+ }
2117
+ if (target.origin !== manifest.origin || target.pathname !== manifest.pathname) continue;
2118
+ const tokens = [...rel.decoded.matchAll(/[^\t\n\f\r ]+/gu)];
2119
+ const manifestTokens = tokens.filter(
2120
+ function manifestRelationship(token) {
2121
+ return asciiLower(token[0]) === 'manifest';
2122
+ }
2123
+ );
2124
+ if (manifestTokens.length === 0) continue;
2125
+ if (manifestTokens.length === tokens.length) {
2126
+ edits.push(
2127
+ {start: link.start, end: link.end, value: ''}
2128
+ );
2129
+ continue;
2130
+ }
2131
+ const position = attributes.positions.get('rel');
2132
+ for (const token of manifestTokens) {
2133
+ edits.push(
2134
+ {
2135
+ start: link.start + position.start + rel.positions[token.index],
2136
+ end: link.start + position.start + rel.positions[token.index + token[0].length],
2137
+ value: ''
2138
+ }
2139
+ );
2140
+ }
2141
+ }
2142
+ for (const script of structure.scripts) {
2143
+ if (parseTagAttributes(script.open).has('data-arcane-pwa')) {
2144
+ edits.push(
2145
+ {start: script.start, end: script.end, value: ''}
2146
+ );
2147
+ }
2148
+ }
2149
+ return applyReferenceEdits(html, edits);
2150
+ }
2151
+
2090
2152
  /** Update only active PWA entry references; registration never orders application startup. */
2091
2153
  export function applyPwaEntryReferences(html,{manifestUrl,bootstrapUrl}={}){
2092
2154
  if(!is.string(html))throw new TypeError('PWA entry HTML must be a string.');