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