arcane-os 0.69.0 → 0.70.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 (32) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +1 -1
  3. package/browser-runtime/ai/browser-decisions.mjs +46 -2
  4. package/browser-runtime/ai/browser-speech-artifacts.mjs +58 -62
  5. package/browser-runtime/ai/browser-speech-providers.mjs +3 -0
  6. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +76 -164
  7. package/browser-runtime/ai/dbopfs-model-parts.mjs +344 -0
  8. package/browser-runtime/ai/dbopfs-model-resources.mjs +190 -0
  9. package/browser-runtime/ai/decision-runtime.mjs +17 -1
  10. package/browser-runtime/ai/decision-worker.mjs +21 -1
  11. package/browser-runtime/ai/model-resource-bridge.mjs +231 -0
  12. package/browser-runtime/ai/speech-worker-client.mjs +68 -28
  13. package/browser-runtime/ai/speech-worker-runtime.mjs +212 -97
  14. package/browser-runtime/core/contracts.mjs +1 -1
  15. package/docs/reference/ai/browser-decisions.md +57 -3
  16. package/docs/reference/ai/browser-speech.md +45 -17
  17. package/docs/reference/ai/browser-wasm.md +28 -20
  18. package/docs/reference/core-native-packaging.md +13 -0
  19. package/docs/reference/core-runtime.md +9 -0
  20. package/docs/reference/core-shared-host.md +190 -0
  21. package/docs/reference/inventory/runtime-modules.json +3 -3
  22. package/docs/reference/runtime-modules.md +97 -7
  23. package/docs/reference/sdk-api.md +41 -30
  24. package/package.json +2 -2
  25. package/runtime/arcane/modules/AI.js +2 -18
  26. package/runtime/arcane/modules/AIProviderRuntime.js +77 -3
  27. package/runtime/arcane/modules/DBOPFS.js +180 -26
  28. package/src/core/host.mjs +2 -0
  29. package/src/core/runtime.mjs +9 -4
  30. package/src/core/services/packaged-web.mjs +8 -1
  31. package/src/core/shared-host.mjs +500 -0
  32. package/src/native/portable-layout.mjs +66 -34
@@ -8,7 +8,8 @@ import {
8
8
  const is=new Is(false);
9
9
 
10
10
  const dbopfsEventTypes={
11
- ready:'dbopfs-ready'
11
+ ready:'dbopfs-ready',
12
+ change:'dbopfs-change'
12
13
  };
13
14
  const dbopfsReasons={
14
15
  ready:'opfs-database-ready'
@@ -117,6 +118,13 @@ if(navigator.storage?.persist){
117
118
  class DBOPFS {
118
119
 
119
120
  #events;
121
+ #changeChannel=null;
122
+ #changeOriginId=globalThis.crypto?.randomUUID?.()
123
+ ||`dbopfs-${Date.now()}-${Math.random().toString(36).slice(2)}`;
124
+ #changeSequence=0;
125
+ #changeWindow=window;
126
+ #changesSuspended=false;
127
+ #cacheStates=new Map();
120
128
 
121
129
  /** @type {FileSystemDirectoryHandle|Object} */
122
130
  #db={}
@@ -286,6 +294,113 @@ class DBOPFS {
286
294
  return `${tableName}:${fileName}`
287
295
  }
288
296
 
297
+ #fileCacheState(tableName,fileName){
298
+ const directoryName=directoryNameForTable(tableName);
299
+ let table=this.#cacheStates.get(directoryName);
300
+ if(!table){
301
+ table=new Map();
302
+ this.#cacheStates.set(directoryName,table);
303
+ }
304
+ let state=table.get(fileName);
305
+ if(!state){
306
+ state={revision:0};
307
+ table.set(fileName,state);
308
+ }
309
+ return state;
310
+ }
311
+
312
+ #forgetFile(tableName,fileName,retire=false){
313
+ const directoryName=directoryNameForTable(tableName);
314
+ const registeredTableName=tableNameForDirectory(directoryName);
315
+ const states=this.#cacheStates.get(directoryName);
316
+ const state=states?.get(fileName);
317
+ if(state)state.revision+=1;
318
+ if(retire&&states){
319
+ states.delete(fileName);
320
+ if(states.size===0)this.#cacheStates.delete(directoryName);
321
+ }
322
+ for(const name of new Set([tableName,directoryName,registeredTableName])){
323
+ if(this.#tables[name])delete this.#tables[name][fileName];
324
+ }
325
+ }
326
+
327
+ #openChangeChannel(){
328
+ if(this.#changesSuspended||this.#changeChannel||!this.#applicationId)return;
329
+ const Channel=this.#changeWindow.BroadcastChannel;
330
+ if(!is.function(Channel))return;
331
+ try{
332
+ const channel=new Channel(`arcane.dbopfs.changes:${this.#storagePath}`);
333
+ const database=this;
334
+ channel.onmessage=function receiveCommittedChange(event){
335
+ const change=event.data;
336
+ if(!change||change.applicationId!==database.#applicationId
337
+ ||change.storagePath!==database.#storagePath
338
+ ||change.originId===database.#changeOriginId)return;
339
+ if(!['write','append','delete','table-delete'].includes(change.action)
340
+ ||typeof change.tableName!=='string'
341
+ ||typeof change.directoryName!=='string'
342
+ ||typeof change.changeId!=='string'
343
+ ||(change.action!=='table-delete'&&typeof change.fileName!=='string'))return;
344
+ database.#receiveChange(change);
345
+ };
346
+ channel.onmessageerror=function reportChangeMessageError(event){
347
+ arcaneLogging.error('DBOPFS could not receive a committed-change notification.',event);
348
+ };
349
+ this.#changeChannel=channel;
350
+ }catch(error){
351
+ arcaneLogging.error('DBOPFS cross-document notifications are unavailable; local storage remains available.',error);
352
+ }
353
+ }
354
+
355
+ #closeChangeChannel(){
356
+ if(!this.#changeChannel)return;
357
+ this.#changeChannel.onmessage=null;
358
+ this.#changeChannel.onmessageerror=null;
359
+ this.#changeChannel.close();
360
+ this.#changeChannel=null;
361
+ }
362
+
363
+ #receiveChange(change){
364
+ if(change.action==='table-delete'){
365
+ this.#forgetTable(change.tableName,change.directoryName,tableNameForDirectory(change.directoryName));
366
+ }else{
367
+ this.#forgetFile(change.tableName,change.fileName,change.action==='delete');
368
+ }
369
+ this.#events.dispatch(dbopfsEventTypes.change,{...change,remote:true},{operationId:change.changeId});
370
+ }
371
+
372
+ #commitChange(tableName,fileName,action){
373
+ const directoryName=directoryNameForTable(tableName);
374
+ const registeredTableName=tableNameForDirectory(directoryName);
375
+ if(action==='table-delete'){
376
+ this.#forgetTable(tableName,directoryName,registeredTableName);
377
+ }else{
378
+ this.#forgetFile(tableName,fileName,action==='delete');
379
+ }
380
+ this.#changeSequence+=1;
381
+ const change={
382
+ applicationId:this.#applicationId,
383
+ storagePath:this.#storagePath,
384
+ tableName:registeredTableName,
385
+ directoryName,
386
+ fileName,
387
+ action,
388
+ originId:this.#changeOriginId,
389
+ sequence:this.#changeSequence,
390
+ changeId:`${this.#changeOriginId}:${this.#changeSequence}`
391
+ };
392
+ // Notify the remote transport before synchronous local observers can
393
+ // start another mutation. Transport failure cannot undo a saved record.
394
+ if(this.#changeChannel){
395
+ try{
396
+ this.#changeChannel.postMessage(change);
397
+ }catch(error){
398
+ arcaneLogging.error('DBOPFS saved a change whose cross-document notification failed.',error);
399
+ }
400
+ }
401
+ this.#events.dispatch(dbopfsEventTypes.change,{...change,remote:false},{operationId:change.changeId});
402
+ }
403
+
289
404
  /**
290
405
  * Invalidates every known cache key for one logical/physical table pair.
291
406
  * @private
@@ -294,6 +409,7 @@ class DBOPFS {
294
409
  * @param {string} registeredTableName
295
410
  */
296
411
  #forgetTable(tableName,directoryName,registeredTableName){
412
+ this.#cacheStates.delete(directoryName);
297
413
  for(const name of new Set([
298
414
  tableName,
299
415
  directoryName,
@@ -320,6 +436,22 @@ class DBOPFS {
320
436
  source:'dbopfs',
321
437
  eventTypes:Object.values(dbopfsEventTypes)
322
438
  });
439
+ const database=this;
440
+ this.#changeWindow.addEventListener('pagehide',function suspendDBOPFSChanges(){
441
+ database.#changesSuspended=true;
442
+ database.#closeChangeChannel();
443
+ });
444
+ this.#changeWindow.addEventListener('pageshow',function resumeDBOPFSChanges(){
445
+ if(!database.#changesSuspended)return;
446
+ database.#changesSuspended=false;
447
+ // Notifications have no history. Discard only this page's cached
448
+ // views after suspension; storage and committed events are unchanged.
449
+ database.#tables={};
450
+ database.#tableHandles={};
451
+ database.#tableHandlePromises={};
452
+ database.#cacheStates.clear();
453
+ database.#openChangeChannel();
454
+ });
323
455
  this.readyPromise=this.#init(options);
324
456
  }
325
457
 
@@ -340,6 +472,7 @@ class DBOPFS {
340
472
  this.#applicationId=scope.applicationId;
341
473
  this.#storagePath=scope.path;
342
474
  this.#db=scope.directory;
475
+ this.#openChangeChannel();
343
476
 
344
477
  this.ready=true;
345
478
 
@@ -382,6 +515,17 @@ class DBOPFS {
382
515
  return this.#storagePath;
383
516
  }
384
517
 
518
+ /**
519
+ * Observes committed record/table changes through the canonical event owner.
520
+ * Subscription is live-only; it does not replay records or load a table.
521
+ * @param {function} listener Receives an event whose detail is change metadata.
522
+ * @param {{signal?:AbortSignal,once?:boolean}} options
523
+ * @returns {function} Idempotent unsubscribe with a matching dispose method.
524
+ */
525
+ subscribeChanges(listener,options={}){
526
+ return this.#events.subscribe(dbopfsEventTypes.change,listener,options);
527
+ }
528
+
385
529
  /**
386
530
  * Returns in-memory table cache.
387
531
  *
@@ -444,7 +588,9 @@ class DBOPFS {
444
588
  {create:true}
445
589
  ).then(
446
590
  function registerRequestedTable(handle){
447
- this.#tableHandles[registeredTableName]=handle;
591
+ if(this.#tableHandlePromises[registeredTableName]===handlePromise){
592
+ this.#tableHandles[registeredTableName]=handle;
593
+ }
448
594
  return handle;
449
595
  }.bind(this)
450
596
  );
@@ -489,6 +635,10 @@ class DBOPFS {
489
635
  dataToWrite=JSON.stringify(dataToWrite)
490
636
  }
491
637
 
638
+ const cacheState=this.#fileCacheState(tableName,fileName);
639
+ const cacheRevision=cacheState.revision;
640
+ const result=append?true:parseFileValue(fileName,String(dataToWrite));
641
+
492
642
  try{
493
643
  await this.writeFile(
494
644
  tableName,
@@ -497,20 +647,20 @@ class DBOPFS {
497
647
  append
498
648
  );
499
649
 
500
- if(append){
501
- delete this.#tables[tableName][fileName];
502
- }else{
503
- this.#tables[tableName][fileName]=parseFileValue(
504
- fileName,
505
- String(dataToWrite)
506
- );
650
+ if(!append&&this.#fileCacheState(tableName,fileName)===cacheState
651
+ &&cacheState.revision<=cacheRevision+1){
652
+ if(!this.#tables[tableName])this.#tables[tableName]={};
653
+ this.#tables[tableName][fileName]=result;
654
+ }
655
+ if(append&&cacheState.revision===cacheRevision){
656
+ this.#forgetFile(tableName,fileName);
507
657
  }
508
658
  }catch(error){
509
659
  arcaneLogging.error(`Error writing file '${fileName}' to table '${tableName}':`,error)
510
660
  throw error
511
661
  }
512
662
 
513
- return append?true:this.#tables[tableName][fileName]
663
+ return result
514
664
  }.bind(this)
515
665
  )
516
666
 
@@ -547,12 +697,14 @@ class DBOPFS {
547
697
  );
548
698
 
549
699
  if(!is.function(handle.createWritable)){
550
- return this.#writeFileWithWorker(
700
+ await this.#writeFileWithWorker(
551
701
  table.name,
552
702
  fileName,
553
703
  fileData,
554
704
  append
555
705
  );
706
+ this.#commitChange(tableName,fileName,append?'append':'write');
707
+ return true;
556
708
  }
557
709
 
558
710
  const writable=await handle.createWritable(
@@ -572,6 +724,7 @@ class DBOPFS {
572
724
 
573
725
  await writable.write(blob);
574
726
  await writable.close();
727
+ this.#commitChange(tableName,fileName,append?'append':'write');
575
728
 
576
729
  return true
577
730
  }
@@ -669,6 +822,8 @@ class DBOPFS {
669
822
  */
670
823
  async get(tableName='',fileName='',force=false){
671
824
  if(force||!this.#tables[tableName]?.[fileName]){
825
+ const cacheState=this.#fileCacheState(tableName,fileName);
826
+ const cacheRevision=cacheState.revision;
672
827
  try{
673
828
  const file=await this.readFile(tableName,fileName)
674
829
  const textContent=await file.text()
@@ -677,12 +832,15 @@ class DBOPFS {
677
832
  this.#tables[tableName]={}
678
833
  }
679
834
 
680
- this.#tables[tableName][fileName]=parseFileValue(
681
- fileName,
682
- textContent
683
- );
835
+ const value=parseFileValue(fileName,textContent);
836
+ if(this.#fileCacheState(tableName,fileName)===cacheState
837
+ &&cacheState.revision===cacheRevision){
838
+ this.#tables[tableName][fileName]=value;
839
+ }
840
+ return value;
684
841
  }catch(error){
685
842
  if(error.name==='NotFoundError'){
843
+ this.#forgetFile(tableName,fileName,true);
686
844
  return null
687
845
  }
688
846
 
@@ -788,15 +946,13 @@ class DBOPFS {
788
946
 
789
947
  try{
790
948
  await table.removeEntry(fileName)
949
+ this.#commitChange(tableName,fileName,'delete');
791
950
  }catch(error){
792
951
  if(error.name!=='NotFoundError'){
793
952
  arcaneLogging.error(error)
794
953
  throw error
795
954
  }
796
- }
797
-
798
- if(this.#tables[tableName]){
799
- delete this.#tables[tableName][fileName]
955
+ this.#forgetFile(tableName,fileName,true);
800
956
  }
801
957
 
802
958
  return true
@@ -831,13 +987,9 @@ class DBOPFS {
831
987
 
832
988
  try{
833
989
  await this.#db.removeEntry(directoryName,{recursive:true})
834
-
835
- delete this.#tables[tableName]
836
- delete this.#tables[directoryName]
837
- delete this.#tables[registeredTableName]
838
- delete this.#tableHandles[registeredTableName]
839
- delete this.#tableHandlePromises[registeredTableName]
990
+ this.#commitChange(tableName,null,'table-delete');
840
991
  }catch(error){
992
+ if(error.name==='NotFoundError')this.#forgetTable(tableName,directoryName,registeredTableName);
841
993
  arcaneLogging.error(error)
842
994
  }
843
995
 
@@ -894,7 +1046,7 @@ class DBOPFS {
894
1046
  }
895
1047
  }
896
1048
 
897
- this.#forgetTable(tableName,directoryName,registeredTableName)
1049
+ this.#commitChange(tableName,null,'table-delete');
898
1050
 
899
1051
  return {
900
1052
  status:'removed',
@@ -916,11 +1068,13 @@ class DBOPFS {
916
1068
 
917
1069
  for await(const [name]of this.#db.entries()){
918
1070
  await this.#db.removeEntry(name,{recursive:true})
1071
+ this.#commitChange(name,null,'table-delete');
919
1072
  }
920
1073
 
921
1074
  this.#tables={}
922
1075
  this.#tableHandles={}
923
1076
  this.#tableHandlePromises={}
1077
+ this.#cacheStates.clear();
924
1078
  this.#writeLocks={}
925
1079
 
926
1080
  return this
package/src/core/host.mjs CHANGED
@@ -3,6 +3,8 @@ import Is from 'strong-type';
3
3
  import {createCoreRuntime} from './runtime.mjs';
4
4
  import {startCoreStdio} from './stdio.mjs';
5
5
 
6
+ export {startSharedCoreHost, connectSharedCoreHost, runSharedCoreHost, startSharedCoreBridge} from './shared-host.mjs';
7
+
6
8
  const is = new Is(false);
7
9
 
8
10
  /**
@@ -109,7 +109,12 @@ class CoreRuntime {
109
109
  }
110
110
 
111
111
  emit(event, data) {
112
- const frame = {protocol: CORE_PROTOCOL, type: 'event', event, data, time: new Date().toISOString()};
112
+ return this.#emit(event, data);
113
+ }
114
+
115
+ #emit(event, data, requestId) {
116
+ const frame = {protocol: CORE_PROTOCOL, type: 'event', event, data,
117
+ ...(requestId === undefined ? {} : {requestId}), time: new Date().toISOString()};
113
118
  this.#events.dispatch(FRAME_EVENT, frame);
114
119
  return frame;
115
120
  }
@@ -158,7 +163,7 @@ class CoreRuntime {
158
163
  return service.definition;
159
164
  }
160
165
 
161
- #serviceContext(service) {
166
+ #serviceContext(service, requestId) {
162
167
  const runtime = this;
163
168
  return {
164
169
  application: this.#application,
@@ -167,7 +172,7 @@ class CoreRuntime {
167
172
  return runtime.getService(name);
168
173
  },
169
174
  emit(event, data) {
170
- return runtime.emit(event, data);
175
+ return runtime.#emit(event, data, requestId);
171
176
  }
172
177
  };
173
178
  }
@@ -295,7 +300,7 @@ class CoreRuntime {
295
300
  const result = await handle.call(
296
301
  service.definition,
297
302
  frame.parameters,
298
- {...runtime.#serviceContext(service), requestId: frame.id, signal: controller.signal}
303
+ {...runtime.#serviceContext(service, frame.id), requestId: frame.id, signal: controller.signal}
299
304
  );
300
305
  aborted(controller);
301
306
  return result;
@@ -8,8 +8,10 @@ export function createPackagedWebService({artifactRoot}, context = {}) {
8
8
  let server;
9
9
  let closing = false;
10
10
  let lifetime;
11
+ let current = null;
11
12
  return {
12
13
  name: 'packaged-web',
14
+ current() { return current === null ? null : {...current}; },
13
15
  async start(runtime) {
14
16
  const manifest = JSON.parse(await readFile(path.join(artifactRoot, 'arcane-native.json'), 'utf8'));
15
17
  // The launcher chooses app state independently of its installation.
@@ -48,9 +50,11 @@ export function createPackagedWebService({artifactRoot}, context = {}) {
48
50
  await writeFile(originFile, JSON.stringify({host: '127.0.0.1', port: server.port}) + '\n', {flag: 'wx'});
49
51
  }
50
52
  const url = new URL(manifest.start, server.origin).href;
51
- runtime.emit('core.web.ready', {origin: server.origin, url, port: server.port});
53
+ current = {origin: server.origin, url, port: server.port};
54
+ runtime.emit('core.web.ready', {...current});
52
55
  lifetime = server.lifecycle.then(
53
56
  function packagedServerEnded() {
57
+ current = null;
54
58
  if (!closing) {
55
59
  runtime.emit('core.web.failed', serializeCoreError(
56
60
  new Error('The packaged application listener closed unexpectedly.')
@@ -58,6 +62,7 @@ export function createPackagedWebService({artifactRoot}, context = {}) {
58
62
  }
59
63
  },
60
64
  function packagedServerFailed(error) {
65
+ current = null;
61
66
  if (!closing) runtime.emit('core.web.failed', serializeCoreError(error));
62
67
  throw error;
63
68
  }
@@ -66,6 +71,7 @@ export function createPackagedWebService({artifactRoot}, context = {}) {
66
71
  // now; the native diagnostic event owns immediate reporting.
67
72
  lifetime.catch(function observePackagedServerFailure() {});
68
73
  } catch (error) {
74
+ current = null;
69
75
  closing = true;
70
76
  if (server) {
71
77
  try { await server.close(); }
@@ -78,6 +84,7 @@ export function createPackagedWebService({artifactRoot}, context = {}) {
78
84
  },
79
85
  async drain() {
80
86
  closing = true;
87
+ current = null;
81
88
  if (!server) return;
82
89
  const results = await Promise.allSettled([server.close(), server.closed, lifetime]);
83
90
  const failures = [...new Set(results.filter(function rejected(result) {