arcane-os 0.53.0 → 0.54.1

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 (38) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/NOTICE +4 -1
  3. package/README.md +1 -1
  4. package/bin/arcane-test.mjs +2 -1
  5. package/browser-runtime/ai/browser-decisions.mjs +406 -0
  6. package/browser-runtime/ai/decision-runtime.mjs +281 -0
  7. package/browser-runtime/ai/decision-worker.mjs +67 -0
  8. package/browser-runtime/dependencies/event-pubsub/dependencies/strong-type/index.js +1151 -0
  9. package/browser-runtime/dependencies/event-pubsub/dependencies/strong-type/licence +21 -0
  10. package/browser-runtime/dependencies/event-pubsub/dependencies/strong-type/package.json +61 -0
  11. package/browser-runtime/dependencies/event-pubsub/index.js +2 -2
  12. package/browser-runtime/dependencies/event-pubsub/package.json +11 -4
  13. package/browser-runtime/dependencies/ws-share/WS.js +128 -0
  14. package/browser-runtime/dependencies/ws-share/licence.md +27 -0
  15. package/browser-runtime/dependencies/ws-share/package.json +64 -0
  16. package/browser-runtime/websocket-client.mjs +1 -0
  17. package/docs/event-manager.md +7 -4
  18. package/docs/reference/ai/browser-decisions.md +184 -0
  19. package/docs/reference/inventory/package-api.json +30 -2
  20. package/docs/reference/inventory/runtime-modules.json +17 -4
  21. package/docs/reference/protocols.md +76 -1
  22. package/docs/reference/runtime-modules.md +45 -2
  23. package/docs/reference/sdk-api.md +133 -9
  24. package/docs/reference/websocket-client.md +132 -0
  25. package/node_modules/event-pubsub/CHANGELOG.md +12 -1
  26. package/node_modules/event-pubsub/MIGRATION.md +8 -5
  27. package/node_modules/event-pubsub/README.md +75 -19
  28. package/node_modules/event-pubsub/SECURITY.md +3 -3
  29. package/node_modules/event-pubsub/index.js +2 -2
  30. package/node_modules/event-pubsub/package.json +11 -4
  31. package/package.json +8 -5
  32. package/runtime/arcane/modules/HTMLImport.js +81 -26
  33. package/runtime/arcane/modules/HTMLImportScript.js +3 -0
  34. package/src/import-map.mjs +416 -59
  35. package/src/index.mjs +1 -0
  36. package/src/packager/core.mjs +24 -6
  37. package/src/packager/native-modules.mjs +458 -0
  38. package/src/testing-loader.mjs +41 -20
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.54.1
4
+
5
+ - Select the self-contained Transformers.js 4.3.0 browser CDN bundle for
6
+ `createBrowserDecisionModel`. The prior `.web.js` entry expected a bundler
7
+ and left `onnxruntime-web/webgpu` unresolved in the native module Worker.
8
+ The runtime URL correction preserves exact Laya FP16/Julia FP32 selection,
9
+ full inputs/results, activation and cancellation; no new runtime version,
10
+ backend fallback, application default change or data migration is introduced.
11
+
12
+ ## 0.54.0
13
+
14
+ - Add `WS` through `arcane-os/websocket-client` and the Node root export,
15
+ using published `ws-share@3.1.0`. Compatible callers in one loaded module
16
+ share the exact native socket; `WS.observe()` exposes global lifecycle
17
+ events and `WS.getConnections()` supplies current connection records.
18
+ Native per-socket events, complete payloads and `acceptWebSocket` remain
19
+ unchanged. No automatic reconnect or cross-realm sharing is introduced.
20
+ - Update `event-pubsub` to `6.1.1` and preserve its own `strong-type@2.0.0`
21
+ browser scope alongside the SDK's existing `strong-type@2.0.1`. Managed
22
+ maps, installed-package routes and application test contexts carry that
23
+ scope. Consumers regenerate their normal public SDK projections.
24
+ - Add opt-in `moduleFormat: 'native'` application packaging with operation
25
+ `outputDirectory`, reachable module graphs, scoped import-map resolution,
26
+ complete selected assets and external HTMLImport script lifecycle. Correct
27
+ named-expression lexical scope without changing default import-map output.
28
+ This is a packaging capability, not a complete browser-extension scaffold.
29
+ - Add `createBrowserDecisionModel` through `arcane-os/ai/browser-decisions`
30
+ for explicit Laya typed-decisions FP16 and Julia-1 FP32 browser operations.
31
+ Model activation is lazy and uses the selected upstream Transformers.js
32
+ runtime; complete inputs/results, ordered batches, lifecycle and cancellation
33
+ remain observable. No chat-provider replacement, automatic model loading,
34
+ precision fallback, application default switch or saved-data migration occurs.
35
+
3
36
  ## 0.53.0
4
37
 
5
38
  - Add `acceptWebSocket` through `arcane-os/websocket` and the Node root export.
package/NOTICE CHANGED
@@ -7,7 +7,10 @@ informational notice and is not itself a grant of commercial rights.
7
7
 
8
8
  Third-party material retains its own terms:
9
9
 
10
- - event-pubsub 6.1.0: MIT License; see its installed `licence` file.
10
+ - event-pubsub 6.1.1: MIT License; see browser-runtime/dependencies/event-pubsub/licence.
11
+ - event-pubsub's strong-type 2.0.0 dependency: MIT License; see
12
+ browser-runtime/dependencies/event-pubsub/dependencies/strong-type/licence.
13
+ - ws-share 3.1.0: DBAD License; see browser-runtime/dependencies/ws-share/licence.md.
11
14
  - js-queue 3.1.1: MIT License; see browser-runtime/dependencies/js-queue/licence.md.
12
15
  - js-queue's easy-stack 2.1.0 dependency: MIT License; see its installed
13
16
  `licence` file. The browser Queue entry does not import this stack module.
package/README.md CHANGED
@@ -20,7 +20,7 @@ selected SDK projection or an explicit live SDK source mount. Both profiles
20
20
  share the theme, packaging, event, cancellation,
21
21
  and browser run contracts while retaining their selected application layout.
22
22
 
23
- This checkout defines the `0.53.0` SDK contract. Applications pin one exact npm
23
+ This checkout defines the `0.54.1` SDK contract. Applications pin one exact npm
24
24
  version and lockfile; registry state is deliberately not baked into application
25
25
  artifacts.
26
26
 
@@ -115,7 +115,8 @@ async function readManagedImportMapContext(source,signal,testFile){
115
115
  protocol:MANAGED_IMPORT_MAP_PROTOCOL,
116
116
  boundary:context.boundary,
117
117
  baseURL:pathToFileURL(`${canonicalBase}${path.sep}`).href,
118
- imports:{...context.imports}
118
+ imports:{...context.imports},
119
+ scopes:context.scopes??{}
119
120
  };
120
121
  }
121
122
 
@@ -0,0 +1,406 @@
1
+ import Is from '../dependencies/strong-type/index.js';
2
+ import {createArcaneEventSource} from '../event-manager.mjs';
3
+
4
+ const is = new Is(false);
5
+ const STATE_EVENT = 'ai.decisions.state';
6
+ // The CDN bundle includes ONNX Runtime; the .web build leaves bare imports for bundlers.
7
+ const DEFAULT_RUNTIME_MODULE = 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.3.0/dist/transformers.min.js';
8
+
9
+ function requiredText(value, name) {
10
+ if (!is.string(value) || value.length === 0) {
11
+ throw new TypeError(`${name} must be a nonempty string.`);
12
+ }
13
+ return value;
14
+ }
15
+
16
+ function cancellationReason(signal, message) {
17
+ if (signal?.reason !== undefined) return signal.reason;
18
+ const error = new Error(message);
19
+ error.name = 'AbortError';
20
+ error.code = 'ARCANE_AI_REQUEST_ABORTED';
21
+ return error;
22
+ }
23
+
24
+ function reviveWorkerError(value, seen = new Map()) {
25
+ if (is.error(value) || value === null || !is.object(value)) return value;
26
+ if (!is.string(value.name) || !is.string(value.message)) return value;
27
+ if (seen.has(value)) return seen.get(value);
28
+ const error = new Error(value.message);
29
+ seen.set(value, error);
30
+ for (const key of Object.keys(value)) {
31
+ let item = value[key];
32
+ if (key === 'cause') item = reviveWorkerError(item, seen);
33
+ if (key === 'errors' && is.array(item)) {
34
+ item = item.map(
35
+ function reviveNestedError(nested) {
36
+ return reviveWorkerError(nested, seen);
37
+ }
38
+ );
39
+ }
40
+ Object.defineProperty(
41
+ error,
42
+ key,
43
+ {value: item, enumerable: true, configurable: true, writable: true}
44
+ );
45
+ }
46
+ return error;
47
+ }
48
+
49
+ function createDecisionOperation(op) {
50
+ const operation = {op};
51
+ operation.promise = new Promise(
52
+ function createDecisionOperationPromise(resolve, reject) {
53
+ operation.resolve = resolve;
54
+ operation.reject = reject;
55
+ }
56
+ );
57
+ return operation;
58
+ }
59
+
60
+ /**
61
+ * One lazily activated model Worker. Cancelling any active operation terminates
62
+ * that Worker and rejects all of its operations with the same cancellation
63
+ * reason. A later load/evaluate may activate a fresh Worker after cancellation,
64
+ * unloading, or failure; disposal is terminal.
65
+ */
66
+ class BrowserDecisionModel {
67
+ #configuration;
68
+ #events;
69
+ #activation = null;
70
+ #generation = 0;
71
+ #nextId = 1;
72
+ #state = 'unloaded';
73
+ #progress = null;
74
+ #error = null;
75
+ #disposed = false;
76
+
77
+ constructor({family, model, revision = 'main', device = 'webgpu', runtime = {}} = {}) {
78
+ if (family !== 'laya' && family !== 'julia') {
79
+ throw new TypeError('family must be "laya" or "julia".');
80
+ }
81
+ if (runtime === null || !is.object(runtime)) {
82
+ throw new TypeError('runtime must be an object.');
83
+ }
84
+ const moduleUrl = runtime.moduleUrl === undefined
85
+ ? DEFAULT_RUNTIME_MODULE
86
+ : runtime.moduleUrl;
87
+ this.#configuration = {
88
+ family,
89
+ model: requiredText(model, 'model'),
90
+ revision: requiredText(revision, 'revision'),
91
+ device: requiredText(device, 'device'),
92
+ dtype: family === 'laya' ? 'fp16' : 'fp32',
93
+ runtime: {...runtime, moduleUrl: requiredText(moduleUrl, 'runtime.moduleUrl')}
94
+ };
95
+ this.#events = createArcaneEventSource(
96
+ this,
97
+ {source: 'ai-browser-decisions', eventTypes: [STATE_EVENT]}
98
+ );
99
+ }
100
+
101
+ status() {
102
+ const {family, model, revision, device, dtype} = this.#configuration;
103
+ const activeRequests = this.#activation?.evaluations.size ?? 0;
104
+ return {
105
+ family,
106
+ model,
107
+ revision,
108
+ device,
109
+ dtype,
110
+ state: this.#state,
111
+ loaded: this.#state === 'ready',
112
+ busy: this.#state === 'loading' || activeRequests > 0,
113
+ activeRequests,
114
+ progress: this.#progress === null ? null : {...this.#progress},
115
+ error: this.#error
116
+ };
117
+ }
118
+
119
+ subscribe(listener, {emitCurrent = true, signal} = {}) {
120
+ if (!is.function(listener)) throw new TypeError('listener must be a function.');
121
+ if (!is.boolean(emitCurrent)) throw new TypeError('emitCurrent must be a boolean.');
122
+ function forwardDecisionState(event) {
123
+ listener(event.detail);
124
+ }
125
+ const unsubscribe = this.#events.on(
126
+ STATE_EVENT,
127
+ forwardDecisionState,
128
+ {signal}
129
+ );
130
+ try {
131
+ if (emitCurrent && !signal?.aborted) {
132
+ const current = this.status();
133
+ listener(current);
134
+ }
135
+ } catch (error) {
136
+ unsubscribe();
137
+ throw error;
138
+ }
139
+ return unsubscribe;
140
+ }
141
+
142
+ #publish() {
143
+ const current = this.status();
144
+ this.#events.dispatch(STATE_EVENT, current);
145
+ }
146
+
147
+ #assertOperational(signal) {
148
+ if (this.#disposed) {
149
+ const error = new Error('The browser decision model is disposed.');
150
+ error.code = 'ARCANE_AI_DISPOSED';
151
+ throw error;
152
+ }
153
+ if (signal !== undefined && signal !== null
154
+ && (!is.boolean(signal.aborted)
155
+ || !is.function(signal.addEventListener)
156
+ || !is.function(signal.removeEventListener))) {
157
+ throw new TypeError('signal must be an AbortSignal.');
158
+ }
159
+ if (signal?.aborted) {
160
+ throw cancellationReason(signal, 'The browser decision operation was cancelled.');
161
+ }
162
+ }
163
+
164
+ #isCurrent(activation) {
165
+ return this.#activation === activation && this.#generation === activation.generation;
166
+ }
167
+
168
+ #observeSignal(activation, signal) {
169
+ const model = this;
170
+ function cancelDecisionActivation() {
171
+ model.#stop(
172
+ activation,
173
+ cancellationReason(signal, 'The browser decision operation was cancelled.'),
174
+ 'unloaded'
175
+ );
176
+ }
177
+ if (signal) {
178
+ signal.addEventListener(
179
+ 'abort',
180
+ cancelDecisionActivation,
181
+ {once: true}
182
+ );
183
+ if (signal.aborted) cancelDecisionActivation();
184
+ }
185
+ function stopObservingDecisionSignal() {
186
+ signal?.removeEventListener('abort', cancelDecisionActivation);
187
+ }
188
+ return stopObservingDecisionSignal;
189
+ }
190
+
191
+ #activate() {
192
+ const activation = {
193
+ generation: ++this.#generation,
194
+ worker: null,
195
+ listeners: null,
196
+ load: createDecisionOperation('load'),
197
+ pending: new Map(),
198
+ evaluations: new Set(),
199
+ reason: undefined
200
+ };
201
+ this.#activation = activation;
202
+ this.#state = 'loading';
203
+ this.#progress = {phase: 'loading-runtime'};
204
+ this.#error = null;
205
+ this.#publish();
206
+ if (!this.#isCurrent(activation)) return activation;
207
+ const model = this;
208
+ try {
209
+ const worker = new globalThis.Worker(
210
+ new URL('./decision-worker.mjs', import.meta.url),
211
+ {type: 'module', name: 'arcane-decisions'}
212
+ );
213
+ activation.worker = worker;
214
+ function handleDecisionWorkerMessage(event) {
215
+ model.#receive(activation, event.data);
216
+ }
217
+ function handleDecisionWorkerError(event) {
218
+ const error = event.error ?? new Error(
219
+ event.message,
220
+ {cause: event}
221
+ );
222
+ model.#stop(activation, error, 'error');
223
+ }
224
+ function handleDecisionWorkerMessageError(event) {
225
+ const error = new Error(
226
+ 'The browser could not deserialize the decision worker response.',
227
+ {cause: event}
228
+ );
229
+ model.#stop(activation, error, 'error');
230
+ }
231
+ activation.listeners = {
232
+ message: handleDecisionWorkerMessage,
233
+ error: handleDecisionWorkerError,
234
+ messageerror: handleDecisionWorkerMessageError
235
+ };
236
+ for (const [type, listener] of Object.entries(activation.listeners)) {
237
+ worker.addEventListener(type, listener);
238
+ }
239
+ const id = this.#nextId++;
240
+ activation.pending.set(id, activation.load);
241
+ worker.postMessage(
242
+ {id, op: 'load', payload: this.#configuration}
243
+ );
244
+ } catch (error) {
245
+ this.#stop(activation, error, 'error');
246
+ }
247
+ return activation;
248
+ }
249
+
250
+ #receive(activation, message) {
251
+ if (!this.#isCurrent(activation)) return;
252
+ const operation = message && activation.pending.get(message.id);
253
+ if (!operation) {
254
+ this.#stop(
255
+ activation,
256
+ new Error(
257
+ 'The decision worker returned an unknown operation.',
258
+ {cause: message}
259
+ ),
260
+ 'error'
261
+ );
262
+ return;
263
+ }
264
+ if (Object.hasOwn(message, 'progress')) {
265
+ this.#progress = message.progress;
266
+ this.#publish();
267
+ return;
268
+ }
269
+ if (Object.hasOwn(message, 'error')) {
270
+ const error = reviveWorkerError(message.error);
271
+ // Transformers.js keeps a Worker-local inference promise chain.
272
+ // A rejected run can poison that chain; a fresh Worker resets it.
273
+ this.#stop(activation, error, 'error');
274
+ return;
275
+ }
276
+ if (!Object.hasOwn(message, 'result')) {
277
+ this.#stop(
278
+ activation,
279
+ new Error(
280
+ 'The decision worker response has no result.',
281
+ {cause: message}
282
+ ),
283
+ 'error'
284
+ );
285
+ return;
286
+ }
287
+ activation.pending.delete(message.id);
288
+ if (operation.op === 'load') {
289
+ this.#state = 'ready';
290
+ this.#progress = {phase: 'ready'};
291
+ this.#error = null;
292
+ this.#publish();
293
+ if (this.#isCurrent(activation)) {
294
+ const current = this.status();
295
+ operation.resolve(current);
296
+ }
297
+ return;
298
+ }
299
+ operation.resolve(message.result);
300
+ }
301
+
302
+ #stop(activation, reason, state) {
303
+ if (!this.#isCurrent(activation)) return;
304
+ activation.reason = reason;
305
+ this.#activation = null;
306
+ this.#generation += 1;
307
+ this.#state = state;
308
+ this.#progress = null;
309
+ this.#error = reason;
310
+ if (activation.worker) {
311
+ for (const [type, listener] of Object.entries(activation.listeners ?? {})) {
312
+ activation.worker.removeEventListener(type, listener);
313
+ }
314
+ activation.worker.terminate();
315
+ }
316
+ activation.load.reject(reason);
317
+ for (const operation of activation.pending.values()) operation.reject(reason);
318
+ activation.pending.clear();
319
+ this.#publish();
320
+ }
321
+
322
+ async load({signal} = {}) {
323
+ this.#assertOperational(signal);
324
+ const activation = this.#activation ?? this.#activate();
325
+ const stopObserving = this.#observeSignal(activation, signal);
326
+ try {
327
+ await activation.load.promise;
328
+ if (!this.#isCurrent(activation)) throw activation.reason;
329
+ return this.status();
330
+ } finally {
331
+ stopObserving();
332
+ }
333
+ }
334
+
335
+ async evaluate(rows, {signal} = {}) {
336
+ this.#assertOperational(signal);
337
+ const activation = this.#activation ?? this.#activate();
338
+ const evaluation = {};
339
+ activation.evaluations.add(evaluation);
340
+ const stopObserving = this.#observeSignal(activation, signal);
341
+ if (this.#isCurrent(activation)) {
342
+ this.#progress = {
343
+ phase: this.#state === 'ready' ? 'evaluating' : 'waiting-for-model'
344
+ };
345
+ this.#error = null;
346
+ this.#publish();
347
+ }
348
+ try {
349
+ await activation.load.promise;
350
+ if (!this.#isCurrent(activation)) throw activation.reason;
351
+ const operation = createDecisionOperation('evaluate');
352
+ const id = this.#nextId++;
353
+ activation.pending.set(id, operation);
354
+ try {
355
+ activation.worker.postMessage(
356
+ {id, op: 'evaluate', payload: rows}
357
+ );
358
+ } catch (error) {
359
+ activation.pending.delete(id);
360
+ this.#error = error;
361
+ operation.reject(error);
362
+ this.#publish();
363
+ }
364
+ return await operation.promise;
365
+ } finally {
366
+ stopObserving();
367
+ activation.evaluations.delete(evaluation);
368
+ if (this.#isCurrent(activation)) this.#publish();
369
+ }
370
+ }
371
+
372
+ unload() {
373
+ if (this.#disposed) return this.status();
374
+ const reason = cancellationReason(undefined, 'The browser decision model was unloaded.');
375
+ if (this.#activation) {
376
+ this.#stop(this.#activation, reason, 'unloaded');
377
+ } else {
378
+ this.#state = 'unloaded';
379
+ this.#progress = null;
380
+ this.#error = null;
381
+ this.#publish();
382
+ }
383
+ return this.status();
384
+ }
385
+
386
+ dispose() {
387
+ if (this.#disposed) return this.status();
388
+ this.#disposed = true;
389
+ const reason = cancellationReason(undefined, 'The browser decision model was disposed.');
390
+ if (this.#activation) {
391
+ this.#stop(this.#activation, reason, 'disposed');
392
+ } else {
393
+ this.#state = 'disposed';
394
+ this.#progress = null;
395
+ this.#error = null;
396
+ this.#publish();
397
+ }
398
+ this.#events.dispose();
399
+ return this.status();
400
+ }
401
+ }
402
+
403
+ /** Creates a client without starting a Worker, fetching a runtime, or loading a model. */
404
+ export function createBrowserDecisionModel(configuration) {
405
+ return new BrowserDecisionModel(configuration);
406
+ }