arcane-os 0.68.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.
- package/CHANGELOG.md +36 -0
- package/README.md +9 -4
- package/browser-runtime/ai/browser-decisions.mjs +46 -2
- package/browser-runtime/ai/browser-speech-artifacts.mjs +58 -62
- package/browser-runtime/ai/browser-speech-providers.mjs +8 -3
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +76 -164
- package/browser-runtime/ai/dbopfs-model-parts.mjs +344 -0
- package/browser-runtime/ai/dbopfs-model-resources.mjs +190 -0
- package/browser-runtime/ai/decision-runtime.mjs +17 -1
- package/browser-runtime/ai/decision-worker.mjs +21 -1
- package/browser-runtime/ai/model-resource-bridge.mjs +231 -0
- package/browser-runtime/ai/speech-worker-client.mjs +68 -28
- package/browser-runtime/ai/speech-worker-runtime.mjs +212 -97
- package/browser-runtime/core/contracts.mjs +1 -1
- package/docs/reference/README.md +2 -0
- package/docs/reference/ai/browser-decisions.md +57 -3
- package/docs/reference/ai/browser-speech.md +94 -26
- package/docs/reference/ai/browser-wasm.md +28 -20
- package/docs/reference/core-native-packaging.md +54 -0
- package/docs/reference/core-runtime.md +9 -0
- package/docs/reference/core-shared-host.md +190 -0
- package/docs/reference/inventory/package-api.json +3 -3
- package/docs/reference/inventory/runtime-modules.json +3 -3
- package/docs/reference/runtime-components.md +4 -2
- package/docs/reference/runtime-modules.md +120 -20
- package/docs/reference/sdk-api.md +64 -30
- package/package.json +2 -2
- package/runtime/arcane/components/theme-switcher.html +1 -1
- package/runtime/arcane/modules/AI.js +7 -22
- package/runtime/arcane/modules/AIProviderRuntime.js +84 -6
- package/runtime/arcane/modules/DBOPFS.js +180 -26
- package/schemas/arcane-app.schema.json +10 -0
- package/src/app-descriptor.mjs +23 -1
- package/src/core/host.mjs +2 -0
- package/src/core/hosts/windows/ArcaneHost.cs +46 -0
- package/src/core/hosts/windows/ArcaneLauncher.cs +21 -0
- package/src/core/runtime.mjs +9 -4
- package/src/core/services/packaged-web.mjs +8 -1
- package/src/core/shared-host.mjs +500 -0
- package/src/native/portable-layout.mjs +66 -34
- package/src/native/portable-provider.mjs +1 -0
|
@@ -1688,6 +1688,7 @@ export class AIProviderRuntime {
|
|
|
1688
1688
|
#speechDesiredMuted = true;
|
|
1689
1689
|
#speechTransition = Promise.resolve();
|
|
1690
1690
|
#speechTransitionOwners = 0;
|
|
1691
|
+
#speechActivation = null;
|
|
1691
1692
|
#unsubscribeIntents = null;
|
|
1692
1693
|
#closed = false;
|
|
1693
1694
|
#closing = false;
|
|
@@ -2784,6 +2785,11 @@ export class AIProviderRuntime {
|
|
|
2784
2785
|
}
|
|
2785
2786
|
if (slot.loadPromise) {
|
|
2786
2787
|
if (this.#sameSelection(slot.selection, targetSelection)) {
|
|
2788
|
+
const activation = this.#speechActivation;
|
|
2789
|
+
if (role === 'tts' && activation?.loadPromise === slot.loadPromise
|
|
2790
|
+
&& signal !== activation.controller.signal) {
|
|
2791
|
+
return this.#observeSpeechActivation(activation, signal);
|
|
2792
|
+
}
|
|
2787
2793
|
return slot.loadPromise;
|
|
2788
2794
|
}
|
|
2789
2795
|
return Promise.reject(
|
|
@@ -2957,6 +2963,10 @@ export class AIProviderRuntime {
|
|
|
2957
2963
|
}
|
|
2958
2964
|
);
|
|
2959
2965
|
slot.loadPromise = loadOperation;
|
|
2966
|
+
if (role === 'tts' && this.#speechActivation
|
|
2967
|
+
&& signal === this.#speechActivation.controller.signal) {
|
|
2968
|
+
this.#speechActivation.loadPromise = loadOperation;
|
|
2969
|
+
}
|
|
2960
2970
|
publishAIRuntimeRoleState(
|
|
2961
2971
|
role,
|
|
2962
2972
|
roleRecord(
|
|
@@ -3386,15 +3396,19 @@ export class AIProviderRuntime {
|
|
|
3386
3396
|
if (options.signal?.aborted) {
|
|
3387
3397
|
return Promise.reject(normalizedAbort());
|
|
3388
3398
|
}
|
|
3389
|
-
if (role === 'tts' && !queued
|
|
3399
|
+
if (role === 'tts' && !queued) {
|
|
3390
3400
|
const payload = options.payload;
|
|
3391
3401
|
const field = Object.hasOwn(payload ?? {}, 'input') ? 'input' : 'text';
|
|
3392
3402
|
const text = Object.getOwnPropertyDescriptor(payload ?? {}, field)?.value;
|
|
3393
3403
|
if (is.string(text)) {
|
|
3394
|
-
const
|
|
3395
|
-
|
|
3404
|
+
const textFormat = Object.getOwnPropertyDescriptor(payload, 'textFormat')?.value;
|
|
3405
|
+
const input = speechInputPrepared === true || textFormat === 'plain'
|
|
3406
|
+
? text
|
|
3407
|
+
: stripSpeechFormatting(text);
|
|
3408
|
+
if (input !== text || Object.hasOwn(payload, 'textFormat')) {
|
|
3396
3409
|
const payloadProperties = Object.getOwnPropertyDescriptors(payload);
|
|
3397
3410
|
payloadProperties[field] = {...payloadProperties[field], value: input};
|
|
3411
|
+
delete payloadProperties.textFormat;
|
|
3398
3412
|
const requestProperties = Object.getOwnPropertyDescriptors(options);
|
|
3399
3413
|
requestProperties.payload = {
|
|
3400
3414
|
...requestProperties.payload,
|
|
@@ -4266,19 +4280,34 @@ export class AIProviderRuntime {
|
|
|
4266
4280
|
return true;
|
|
4267
4281
|
}
|
|
4268
4282
|
|
|
4269
|
-
async setSpeechMuted(muted) {
|
|
4283
|
+
async setSpeechMuted(muted, {signal = null} = {}) {
|
|
4270
4284
|
arcaneLogging.debug('[Arcane speech runtime] setSpeechMuted', muted);
|
|
4271
4285
|
this.#assertOpen();
|
|
4272
4286
|
this.#assertNotConfiguring();
|
|
4273
4287
|
if (!is.boolean(muted)) {
|
|
4274
4288
|
fail('AI speech muted state must be a boolean.');
|
|
4275
4289
|
}
|
|
4290
|
+
assertAbortSignal(signal);
|
|
4291
|
+
if (signal?.aborted) throw normalizedAbort(signal.reason);
|
|
4292
|
+
if (!muted && !this.#speechDesiredMuted && this.#speechActivation
|
|
4293
|
+
&& !this.#speechActivation.controller.signal.aborted) {
|
|
4294
|
+
return this.#observeSpeechActivation(this.#speechActivation, signal);
|
|
4295
|
+
}
|
|
4276
4296
|
this.#speechDesiredMuted = muted;
|
|
4277
4297
|
if (muted) {
|
|
4278
4298
|
this.#speechMuted = true;
|
|
4299
|
+
this.#speechActivation?.controller.abort();
|
|
4279
4300
|
this.#slots.tts.loadController?.abort();
|
|
4280
4301
|
this.cancel('tts');
|
|
4281
4302
|
}
|
|
4303
|
+
const activation = muted ? null : {
|
|
4304
|
+
controller: new AbortController(),
|
|
4305
|
+
interests: new Set(),
|
|
4306
|
+
promise: null,
|
|
4307
|
+
loadPromise: null,
|
|
4308
|
+
settled: false
|
|
4309
|
+
};
|
|
4310
|
+
this.#speechActivation = activation;
|
|
4282
4311
|
const runtime = this;
|
|
4283
4312
|
this.#speechTransitionOwners += 1;
|
|
4284
4313
|
const transition = this.#speechTransition.catch(
|
|
@@ -4286,6 +4315,9 @@ export class AIProviderRuntime {
|
|
|
4286
4315
|
).then(
|
|
4287
4316
|
async function reconcileLatestAISpeechPreference() {
|
|
4288
4317
|
while (true) {
|
|
4318
|
+
if (activation?.controller.signal.aborted) {
|
|
4319
|
+
throw normalizedAbort(activation.controller.signal.reason);
|
|
4320
|
+
}
|
|
4289
4321
|
const desiredMuted = runtime.#speechDesiredMuted;
|
|
4290
4322
|
if (desiredMuted) {
|
|
4291
4323
|
runtime.cancel('tts');
|
|
@@ -4302,7 +4334,11 @@ export class AIProviderRuntime {
|
|
|
4302
4334
|
if (runtime.#speechDesiredMuted) {
|
|
4303
4335
|
continue;
|
|
4304
4336
|
}
|
|
4305
|
-
|
|
4337
|
+
const currentActivation = runtime.#speechActivation;
|
|
4338
|
+
if (currentActivation?.controller.signal.aborted) {
|
|
4339
|
+
throw normalizedAbort(currentActivation.controller.signal.reason);
|
|
4340
|
+
}
|
|
4341
|
+
await runtime.load('tts', {signal: currentActivation?.controller.signal ?? null});
|
|
4306
4342
|
}
|
|
4307
4343
|
if (runtime.#speechDesiredMuted === desiredMuted) {
|
|
4308
4344
|
runtime.#speechMuted = desiredMuted;
|
|
@@ -4314,10 +4350,52 @@ export class AIProviderRuntime {
|
|
|
4314
4350
|
).finally(
|
|
4315
4351
|
function releaseAIProviderSpeechTransitionOwnership() {
|
|
4316
4352
|
runtime.#speechTransitionOwners -= 1;
|
|
4353
|
+
if (activation) {
|
|
4354
|
+
activation.settled = true;
|
|
4355
|
+
if (runtime.#speechActivation === activation) {
|
|
4356
|
+
runtime.#speechActivation = null;
|
|
4357
|
+
}
|
|
4358
|
+
}
|
|
4317
4359
|
}
|
|
4318
4360
|
);
|
|
4319
4361
|
this.#speechTransition = transition;
|
|
4320
|
-
return transition;
|
|
4362
|
+
if (!activation) return transition;
|
|
4363
|
+
activation.promise = transition;
|
|
4364
|
+
return this.#observeSpeechActivation(activation, signal);
|
|
4365
|
+
}
|
|
4366
|
+
|
|
4367
|
+
#observeSpeechActivation(activation, signal) {
|
|
4368
|
+
return new Promise(function observeAIProviderSpeechActivation(resolve, reject) {
|
|
4369
|
+
const interest = {};
|
|
4370
|
+
let settled = false;
|
|
4371
|
+
activation.interests.add(interest);
|
|
4372
|
+
function releaseSpeechActivationInterest() {
|
|
4373
|
+
activation.interests.delete(interest);
|
|
4374
|
+
signal?.removeEventListener('abort', cancelSpeechActivationInterest);
|
|
4375
|
+
}
|
|
4376
|
+
function cancelSpeechActivationInterest() {
|
|
4377
|
+
if (settled) return;
|
|
4378
|
+
settled = true;
|
|
4379
|
+
releaseSpeechActivationInterest();
|
|
4380
|
+
if (!activation.settled && activation.interests.size === 0) {
|
|
4381
|
+
activation.controller.abort(signal?.reason);
|
|
4382
|
+
}
|
|
4383
|
+
reject(normalizedAbort(signal?.reason));
|
|
4384
|
+
}
|
|
4385
|
+
signal?.addEventListener('abort', cancelSpeechActivationInterest, {once: true});
|
|
4386
|
+
activation.promise.then(function speechActivationReady(status) {
|
|
4387
|
+
if (settled) return;
|
|
4388
|
+
settled = true;
|
|
4389
|
+
releaseSpeechActivationInterest();
|
|
4390
|
+
resolve(status);
|
|
4391
|
+
}, function speechActivationFailed(error) {
|
|
4392
|
+
if (settled) return;
|
|
4393
|
+
settled = true;
|
|
4394
|
+
releaseSpeechActivationInterest();
|
|
4395
|
+
reject(error);
|
|
4396
|
+
});
|
|
4397
|
+
if (signal?.aborted) cancelSpeechActivationInterest();
|
|
4398
|
+
});
|
|
4321
4399
|
}
|
|
4322
4400
|
|
|
4323
4401
|
#providerFor(slot) {
|
|
@@ -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.#
|
|
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
|
-
|
|
502
|
-
|
|
503
|
-
this.#tables[tableName][fileName]=
|
|
504
|
-
|
|
505
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
681
|
-
|
|
682
|
-
|
|
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.#
|
|
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
|
|
@@ -132,6 +132,16 @@
|
|
|
132
132
|
}
|
|
133
133
|
},
|
|
134
134
|
"localAI": { "$ref": "#/$defs/localAI" },
|
|
135
|
+
"window": {
|
|
136
|
+
"type": "object",
|
|
137
|
+
"additionalProperties": false,
|
|
138
|
+
"description": "Initial native client dimensions in logical pixels at 96 DPI, and whether the user can resize the window. Omitted fields retain the host defaults.",
|
|
139
|
+
"properties": {
|
|
140
|
+
"width": { "type": "integer", "minimum": 1 },
|
|
141
|
+
"height": { "type": "integer", "minimum": 1 },
|
|
142
|
+
"resizable": { "type": "boolean" }
|
|
143
|
+
}
|
|
144
|
+
},
|
|
135
145
|
"bundledApps": {
|
|
136
146
|
"type": "array",
|
|
137
147
|
"uniqueItems": true,
|
package/src/app-descriptor.mjs
CHANGED
|
@@ -235,8 +235,26 @@ function validateDocumentCatalog(value){
|
|
|
235
235
|
});
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
function validateNativeWindow(value){
|
|
239
|
+
if(value===undefined)return undefined;
|
|
240
|
+
assertOnlyKeys(value,new Set(['width','height','resizable']),'descriptor.native.window');
|
|
241
|
+
for(const dimension of ['width','height']){
|
|
242
|
+
if(value[dimension]!==undefined&&(!is.integer(value[dimension])||value[dimension]<=0)){
|
|
243
|
+
fail(`descriptor.native.window.${dimension} must be a positive integer.`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if(value.resizable!==undefined&&!is.boolean(value.resizable)){
|
|
247
|
+
fail('descriptor.native.window.resizable must be a boolean.');
|
|
248
|
+
}
|
|
249
|
+
return completeValue({
|
|
250
|
+
...(value.width===undefined?{}:{width:value.width}),
|
|
251
|
+
...(value.height===undefined?{}:{height:value.height}),
|
|
252
|
+
...(value.resizable===undefined?{}:{resizable:value.resizable})
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
238
256
|
function validateNative(value,appId){
|
|
239
|
-
assertOnlyKeys(value,new Set(['type','icon','order','bundledApps','documentCatalog','localAI','services']),'descriptor.native');
|
|
257
|
+
assertOnlyKeys(value,new Set(['type','icon','order','bundledApps','documentCatalog','localAI','services','window']),'descriptor.native');
|
|
240
258
|
if(!NATIVE_TYPES.has(value.type))fail('descriptor.native.type is unsupported.');
|
|
241
259
|
const icon=value.icon===null?null:normalizeRelativePath(value.icon,'descriptor.native.icon');
|
|
242
260
|
if(!is.integer(value.order)||value.order<0){
|
|
@@ -246,6 +264,7 @@ function validateNative(value,appId){
|
|
|
246
264
|
pattern:APP_ID_PATTERN
|
|
247
265
|
});
|
|
248
266
|
const documentCatalog=validateDocumentCatalog(value.documentCatalog);
|
|
267
|
+
const window=validateNativeWindow(value.window);
|
|
249
268
|
let services;
|
|
250
269
|
if(value.services!==undefined){
|
|
251
270
|
if(!is.array(value.services))fail('descriptor.native.services must be an array.');
|
|
@@ -259,6 +278,7 @@ function validateNative(value,appId){
|
|
|
259
278
|
icon,
|
|
260
279
|
order:value.order,
|
|
261
280
|
bundledApps,
|
|
281
|
+
...(window===undefined?{}:{window}),
|
|
262
282
|
...(services===undefined?{}:{services}),
|
|
263
283
|
...(value.localAI===undefined?{}:{localAI:normalizeLocalAIConfig(value.localAI)}),
|
|
264
284
|
...(documentCatalog?{documentCatalog}:{})
|
|
@@ -395,6 +415,7 @@ export function projectNativeDescriptor(descriptor,{source}={}){
|
|
|
395
415
|
...(value.native.bundledApps.length?{bundledApps:[...value.native.bundledApps]}:{}),
|
|
396
416
|
...(value.native.documentCatalog?{documentCatalog:value.native.documentCatalog}:{}),
|
|
397
417
|
...(value.native.services===undefined?{}:{services:value.native.services}),
|
|
418
|
+
...(value.native.window===undefined?{}:{window:value.native.window}),
|
|
398
419
|
...(value.native.localAI===undefined?{}:{localAI:value.native.localAI}),
|
|
399
420
|
include:[...value.package.include]
|
|
400
421
|
};
|
|
@@ -440,6 +461,7 @@ function synthesizedDescriptor(packageManifest,nativeDescriptor){
|
|
|
440
461
|
order:is.integer(native.order)?native.order:100,
|
|
441
462
|
bundledApps:[...(native.bundledApps??[])],
|
|
442
463
|
...(native.services===undefined?{}:{services:native.services}),
|
|
464
|
+
...(native.window===undefined?{}:{window:native.window}),
|
|
443
465
|
...((packageManifest.localAI??native.localAI)===undefined?{}:{localAI:packageManifest.localAI??native.localAI}),
|
|
444
466
|
...(native.documentCatalog?{documentCatalog:native.documentCatalog}:{})
|
|
445
467
|
},
|
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
|
/**
|
|
@@ -26,6 +26,10 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
26
26
|
public string CoreExecutable { get; set; }
|
|
27
27
|
public string CoreArguments { get; set; }
|
|
28
28
|
public string CoreWorkingDirectory { get; set; }
|
|
29
|
+
/// <summary>Optional initial client dimensions in logical pixels at 96 DPI.</summary>
|
|
30
|
+
public double? InitialClientWidth { get; set; }
|
|
31
|
+
public double? InitialClientHeight { get; set; }
|
|
32
|
+
public bool? Resizable { get; set; }
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
public static class ArcaneHost
|
|
@@ -128,6 +132,8 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
128
132
|
{
|
|
129
133
|
if (options == null) throw new ArgumentNullException("options");
|
|
130
134
|
if (onDiagnostic == null) throw new ArgumentNullException("onDiagnostic");
|
|
135
|
+
ValidateInitialDimension(options.InitialClientWidth, "InitialClientWidth");
|
|
136
|
+
ValidateInitialDimension(options.InitialClientHeight, "InitialClientHeight");
|
|
131
137
|
this.options = options;
|
|
132
138
|
this.onDiagnostic = onDiagnostic;
|
|
133
139
|
this.onError = onError;
|
|
@@ -135,6 +141,11 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
135
141
|
completion.Task.ContinueWith(ObserveReportedFailure, TaskContinuationOptions.OnlyOnFaulted);
|
|
136
142
|
Text = options.Title;
|
|
137
143
|
AutoScaleMode = AutoScaleMode.Dpi;
|
|
144
|
+
if (options.Resizable.HasValue)
|
|
145
|
+
{
|
|
146
|
+
FormBorderStyle = options.Resizable.Value ? FormBorderStyle.Sizable : FormBorderStyle.FixedSingle;
|
|
147
|
+
MaximizeBox = options.Resizable.Value;
|
|
148
|
+
}
|
|
138
149
|
webView = new WebView2 { Dock = DockStyle.Fill };
|
|
139
150
|
Controls.Add(webView);
|
|
140
151
|
}
|
|
@@ -145,6 +156,9 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
145
156
|
|
|
146
157
|
protected override void OnLoad(EventArgs args)
|
|
147
158
|
{
|
|
159
|
+
// Apply the initial choice once. A composing launcher's Load handler
|
|
160
|
+
// can then restore its own saved state; later user sizing is untouched.
|
|
161
|
+
if (!started && !closing) ApplyInitialWindowSize();
|
|
148
162
|
base.OnLoad(args);
|
|
149
163
|
if (started || closing) return;
|
|
150
164
|
started = true;
|
|
@@ -152,6 +166,38 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
152
166
|
initialization.ContinueWith(ObserveUnexpectedFailure, TaskContinuationOptions.OnlyOnFaulted);
|
|
153
167
|
}
|
|
154
168
|
|
|
169
|
+
private static void ValidateInitialDimension(double? value, string name)
|
|
170
|
+
{
|
|
171
|
+
if (value.HasValue && (Double.IsNaN(value.Value) || Double.IsInfinity(value.Value)
|
|
172
|
+
|| value.Value <= 0 || Math.Truncate(value.Value) != value.Value))
|
|
173
|
+
throw new ArgumentOutOfRangeException(name, "Select a positive integral logical client dimension.");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private void ApplyInitialWindowSize()
|
|
177
|
+
{
|
|
178
|
+
if ((!options.InitialClientWidth.HasValue && !options.InitialClientHeight.HasValue)
|
|
179
|
+
|| WindowState != FormWindowState.Normal) return;
|
|
180
|
+
|
|
181
|
+
Rectangle workArea = Screen.FromControl(this).WorkingArea;
|
|
182
|
+
int availableWidth = Math.Max(1, workArea.Width - (Width - ClientSize.Width));
|
|
183
|
+
int availableHeight = Math.Max(1, workArea.Height - (Height - ClientSize.Height));
|
|
184
|
+
using (Graphics display = CreateGraphics())
|
|
185
|
+
{
|
|
186
|
+
double width = options.InitialClientWidth.HasValue
|
|
187
|
+
? options.InitialClientWidth.Value * display.DpiX / 96.0 : ClientSize.Width;
|
|
188
|
+
double height = options.InitialClientHeight.HasValue
|
|
189
|
+
? options.InitialClientHeight.Value * display.DpiY / 96.0 : ClientSize.Height;
|
|
190
|
+
// Fit before converting to native integer coordinates. This is
|
|
191
|
+
// an initial screen fit, not a persistent maximum window size.
|
|
192
|
+
ClientSize = new Size(
|
|
193
|
+
(int)Math.Max(1, Math.Min(availableWidth, Math.Round(width))),
|
|
194
|
+
(int)Math.Max(1, Math.Min(availableHeight, Math.Round(height))));
|
|
195
|
+
}
|
|
196
|
+
Location = new Point(
|
|
197
|
+
Math.Max(workArea.Left, Math.Min(Left, workArea.Right - Width)),
|
|
198
|
+
Math.Max(workArea.Top, Math.Min(Top, workArea.Bottom - Height)));
|
|
199
|
+
}
|
|
200
|
+
|
|
155
201
|
private async Task InitializeAsync()
|
|
156
202
|
{
|
|
157
203
|
try
|