arcane-os 0.60.0 → 0.61.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.
@@ -6936,6 +6936,19 @@ no engine. The native routes of `AI.fetchTTS(payload, signal)` and
6936
6936
  `AI.fetchSTT(audio, signal)` forward their signals through these calls and
6937
6937
  normalize cancellation to `AbortError` with `ARCANE_AI_REQUEST_ABORTED`.
6938
6938
 
6939
+ `facade.window.setTheme(presentation, {signal}?)` forwards the complete
6940
+ presentation to the current native window, independently of OS-user appearance.
6941
+ Optional `backgroundColor` and `textColor` accept `{red,green,blue,alpha}` or
6942
+ `null` for a platform-default reset; omitted fields remain unchanged. The
6943
+ result is `{platform,supported,applied,unsupported}` with actual per-field
6944
+ acceptance, rather than a measured-pixel or atomic-update claim. Cancellation
6945
+ uses the existing request lifetime and does not undo settings already accepted.
6946
+ Shared ThemeBootstrap forwards the app's computed semantic colors without
6947
+ blocking rendering; applications using it need no duplicate listener. See the
6948
+ [current-window contract](core-client.md#current-window-theme) for channel
6949
+ values, errors, navigation, disposal and current host support. The facade does
6950
+ not add this operation to an older executable or an unsupported host.
6951
+
6939
6952
  ### Availability and normalization
6940
6953
 
6941
6954
  **Browser and native WebView JavaScript hosts with a Core client.** Creating the
@@ -7402,6 +7415,25 @@ including accepted service work, without a forced termination deadline. Missing
7402
7415
  prerequisites, download/extraction failures and host failures remain observable
7403
7416
  through the operation's error/event boundary.
7404
7417
 
7418
+ During assembly, `appDescriptor.native.icon` selects the app's existing image.
7419
+ PNG and ICO inputs produce a derived `runtime/arcane-app.ico` and update only
7420
+ the copied application's executable icon. Source assets, the selected host
7421
+ cache, other executable resources and the Core executable remain unchanged.
7422
+ `manifest.host.icon` identifies the selected runtime icon, while
7423
+ `manifest.host.executableIcon` records whether embedding succeeded.
7424
+
7425
+ The builder's `onEvent` receives `native.icon.completed` after successful
7426
+ embedding or `native.icon.unsupported` when the selected format or executable
7427
+ layout cannot provide it. Both carry `target`, `appId`, `icon` and `executable`;
7428
+ the unsupported event also carries the original `code` and complete `message`.
7429
+ Unsupported embedding preserves ordinary assembly and the original icon path.
7430
+ Unreadable files or malformed supported image data retain their ordinary errors.
7431
+ The matching host loads the window/taskbar icon independently of page and Core
7432
+ startup. PNG/ICO embedding and runtime codec support are separate boundaries;
7433
+ see [application icon and window colors](core-native-packaging.md#application-icon-and-window-colors)
7434
+ for formats, per-window colors, errors and platform limits. npm publication
7435
+ alone does not establish delivery or execution of the matching native archive.
7436
+
7405
7437
  ### Example
7406
7438
 
7407
7439
  ```javascript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.60.0",
3
+ "version": "0.61.0",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -41,6 +41,8 @@
41
41
  "./ai/browser-wasm": "./browser-runtime/ai/browser-wasm.mjs",
42
42
  "./ai/core-local": "./browser-runtime/ai/core-local.mjs",
43
43
  "./ai/core-onnx": "./browser-runtime/ai/core-onnx.mjs",
44
+ "./ai/core-image": "./browser-runtime/ai/core-image.mjs",
45
+ "./ai/core-model-assets": "./browser-runtime/ai/core-model-assets.mjs",
44
46
  "./ai/browser-decisions": "./browser-runtime/ai/browser-decisions.mjs",
45
47
  "./ai/tool-text-stream": "./browser-runtime/ai/tool-text-stream.mjs",
46
48
  "./ai/twin-cloud": "./browser-runtime/ai/twin-cloud.mjs",
@@ -63,10 +65,13 @@
63
65
  "./core/preferences": "./src/core/services/preferences.mjs",
64
66
  "./core/diarization": "./src/diarization/service.mjs",
65
67
  "./core/local-ai": "./src/core/services/local-ai.mjs",
68
+ "./core/image": "./src/core/services/image.mjs",
69
+ "./core/model-assets": "./src/core/services/model-assets.mjs",
66
70
  "./diarization": "./src/diarization/index.mjs",
67
71
  "./diarization/build": "./src/diarization/build.mjs",
68
72
  "./local-ai": "./src/local-ai/install.mjs",
69
73
  "./local-ai/onnx": "./src/local-ai/onnx.mjs",
74
+ "./local-ai/image": "./src/local-ai/image-runtime.mjs",
70
75
  "./integrated-provider": "./src/integrated-provider-loader.mjs",
71
76
  "./packager": "./src/packager/core.mjs",
72
77
  "./release-bundle": "./src/release-bundle.mjs",
@@ -262,12 +262,12 @@
262
262
  "type": "array",
263
263
  "items": {
264
264
  "oneOf": [
265
- { "enum": ["llama.cpp", "ollama", "onnx"] },
265
+ { "enum": ["llama.cpp", "ollama", "onnx", "stable-diffusion.cpp"] },
266
266
  {
267
267
  "type": "object",
268
268
  "required": ["id"],
269
269
  "properties": {
270
- "id": { "enum": ["llama.cpp", "ollama", "onnx"] },
270
+ "id": { "enum": ["llama.cpp", "ollama", "onnx", "stable-diffusion.cpp"] },
271
271
  "version": { "type": "string" },
272
272
  "url": { "type": "string" }
273
273
  }
@@ -156,12 +156,12 @@
156
156
  "type": "array",
157
157
  "items": {
158
158
  "oneOf": [
159
- { "enum": ["llama.cpp", "ollama", "onnx"] },
159
+ { "enum": ["llama.cpp", "ollama", "onnx", "stable-diffusion.cpp"] },
160
160
  {
161
161
  "type": "object",
162
162
  "required": ["id"],
163
163
  "properties": {
164
- "id": { "enum": ["llama.cpp", "ollama", "onnx"] },
164
+ "id": { "enum": ["llama.cpp", "ollama", "onnx", "stable-diffusion.cpp"] },
165
165
  "version": { "type": "string" },
166
166
  "url": { "type": "string" }
167
167
  }
@@ -0,0 +1,563 @@
1
+ import {Buffer} from 'node:buffer';
2
+ import {isDeepStrictEqual} from 'node:util';
3
+ import Is from 'strong-type';
4
+ import {createArcaneEventSource} from '../../event-manager.mjs';
5
+ import {CoreError, serializeCoreError} from '../../../browser-runtime/core/contracts.mjs';
6
+ import {normalizeLocalAIConfig} from '../../local-ai/config.mjs';
7
+ import {createImageRuntime} from '../../local-ai/image-runtime.mjs';
8
+
9
+ const is = new Is(false);
10
+
11
+ function failure(code, message) {
12
+ return new CoreError(
13
+ {code, message}
14
+ );
15
+ }
16
+
17
+ function waitForPreparation(task, signal) {
18
+ signal.throwIfAborted();
19
+ return new Promise(
20
+ function awaitImagePreparation(resolve, reject) {
21
+ function cancelled() {
22
+ signal.removeEventListener('abort', cancelled);
23
+ reject(signal.reason);
24
+ }
25
+ signal.addEventListener(
26
+ 'abort',
27
+ cancelled,
28
+ {once: true}
29
+ );
30
+ task.then(
31
+ function prepared(runtime) {
32
+ signal.removeEventListener('abort', cancelled);
33
+ if (signal.aborted) {
34
+ reject(signal.reason);
35
+ } else {
36
+ resolve(runtime);
37
+ }
38
+ },
39
+ function preparationFailed(error) {
40
+ signal.removeEventListener('abort', cancelled);
41
+ reject(error);
42
+ }
43
+ );
44
+ }
45
+ );
46
+ }
47
+
48
+ /** PNG encoding here belongs exclusively to the JSON Core transport. */
49
+ function encodeImage(image) {
50
+ const {data, ...metadata} = image;
51
+ return {
52
+ ...metadata,
53
+ encoding: 'base64',
54
+ data: Buffer.from(data).toString('base64')
55
+ };
56
+ }
57
+
58
+ /** One selected native image runtime; applications own prompts and persistence. */
59
+ export function createLocalImageService(
60
+ configuration,
61
+ {appRoot, runtimes = [], signal, onEvent, prepare, modelAssets} = {}
62
+ ) {
63
+ const config = normalizeLocalAIConfig(configuration) ?? {runtimes: []};
64
+ const requirement = config.runtimes.find(
65
+ function selectedImageRuntime(record) {
66
+ return record.id === 'stable-diffusion.cpp';
67
+ }
68
+ );
69
+ const lifetime = new AbortController();
70
+ const lifetimeSignal = signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal;
71
+ const owner = {};
72
+ const events = createArcaneEventSource(
73
+ owner,
74
+ {source: 'core-image', eventTypes: ['image.state']}
75
+ );
76
+ const jobs = new Set();
77
+ const projections = new Map();
78
+ const releases = new Set();
79
+ const releaseFailures = [];
80
+ let record = runtimes.find(
81
+ function installedImageRuntime(value) {
82
+ return value.id === 'stable-diffusion.cpp';
83
+ }
84
+ );
85
+ let state = {
86
+ id: 'stable-diffusion.cpp',
87
+ installed: Boolean(record?.libraryPath && record?.bindingModulePath),
88
+ available: false,
89
+ selectedModel: null,
90
+ loaded: false,
91
+ state: requirement ? 'created' : 'unavailable',
92
+ backend: record?.backend ?? null,
93
+ devices: [],
94
+ resources: {},
95
+ progress: null,
96
+ error: null,
97
+ models: record?.models ?? requirement?.models ?? []
98
+ };
99
+ let context;
100
+ let runtime;
101
+ let preparation;
102
+ let stopRuntimeSubscription;
103
+ let closing;
104
+ let closed = false;
105
+
106
+ function current() {
107
+ return {...state, closed};
108
+ }
109
+
110
+ function publish() {
111
+ const snapshot = current();
112
+ events.dispatch('image.state', snapshot);
113
+ context?.emit('image.state', snapshot);
114
+ return snapshot;
115
+ }
116
+
117
+ function acceptRuntimeState(value) {
118
+ state = {
119
+ ...state,
120
+ ...value,
121
+ available: value.available ?? (!value.closed && value.state !== 'error'),
122
+ installed: Boolean(record?.libraryPath && record?.bindingModulePath),
123
+ models: record?.models ?? requirement?.models ?? []
124
+ };
125
+ if (closing) {
126
+ state.available = false;
127
+ state.state = closed ? 'closed' : 'closing';
128
+ }
129
+ publish();
130
+ releaseUnusedProjections();
131
+ }
132
+
133
+ function diagnostic(event) {
134
+ if (!onEvent) {
135
+ return;
136
+ }
137
+ function reportDiagnosticFailure(error) {
138
+ console.error('Local image diagnostic observer failed.', error);
139
+ }
140
+ try {
141
+ Promise.resolve(onEvent(event)).catch(reportDiagnosticFailure);
142
+ } catch (error) {
143
+ reportDiagnosticFailure(error);
144
+ }
145
+ }
146
+
147
+ function notifyListener(listener, value) {
148
+ const result = listener(value);
149
+ if (result && is.function(result.then)) {
150
+ Promise.resolve(result).catch(
151
+ function reportImageListenerFailure(error) {
152
+ console.error('Local image state listener failed.', error);
153
+ }
154
+ );
155
+ }
156
+ }
157
+
158
+ function preparationProgress(event) {
159
+ if (!closing) {
160
+ state = {...state, progress: event};
161
+ publish();
162
+ }
163
+ diagnostic(event);
164
+ }
165
+
166
+ function beginPreparation() {
167
+ if (preparation || !requirement || closing) {
168
+ return preparation;
169
+ }
170
+ state = {...state, state: 'preparing', error: null};
171
+ preparation = Promise.resolve().then(
172
+ async function prepareSelectedImageRuntime() {
173
+ lifetimeSignal.throwIfAborted();
174
+ if (prepare) {
175
+ record = await prepare(
176
+ {
177
+ appRoot,
178
+ configuration: config,
179
+ requirement,
180
+ signal: lifetimeSignal,
181
+ onEvent: preparationProgress
182
+ }
183
+ );
184
+ }
185
+ lifetimeSignal.throwIfAborted();
186
+ if (!record?.libraryPath || !record?.bindingModulePath) {
187
+ throw failure(
188
+ 'LOCAL_AI_RUNTIME_UNAVAILABLE',
189
+ 'The selected local image runtime has not been prepared.'
190
+ );
191
+ }
192
+ runtime = createImageRuntime(
193
+ {
194
+ libraryPath: record.libraryPath,
195
+ bindingModulePath: record.bindingModulePath,
196
+ models: record.models ?? [],
197
+ backend: record.backend,
198
+ variants: record.variants,
199
+ signal: lifetimeSignal,
200
+ onEvent: diagnostic
201
+ }
202
+ );
203
+ stopRuntimeSubscription = runtime.subscribe(
204
+ acceptRuntimeState,
205
+ {replay: true}
206
+ );
207
+ return runtime;
208
+ }
209
+ );
210
+ preparation.catch(
211
+ function preparationFailed(error) {
212
+ state = {
213
+ ...state,
214
+ installed: Boolean(record?.libraryPath && record?.bindingModulePath),
215
+ available: false,
216
+ state: lifetimeSignal.aborted ? 'closing' : 'error',
217
+ error: lifetimeSignal.aborted ? null : serializeCoreError(error)
218
+ };
219
+ publish();
220
+ }
221
+ );
222
+ publish();
223
+ return preparation;
224
+ }
225
+
226
+ async function selectedRuntime(operationSignal) {
227
+ operationSignal.throwIfAborted();
228
+ if (!requirement) {
229
+ throw failure(
230
+ 'LOCAL_AI_RUNTIME_NOT_SELECTED',
231
+ 'stable-diffusion.cpp is not selected in this application configuration.'
232
+ );
233
+ }
234
+ const selected = await waitForPreparation(beginPreparation(), operationSignal);
235
+ operationSignal.throwIfAborted();
236
+ return selected;
237
+ }
238
+
239
+ async function projectedModel(model, assetProjectionId, resourcePaths, operationSignal) {
240
+ if (assetProjectionId === undefined) {
241
+ return model;
242
+ }
243
+ if (!modelAssets) {
244
+ throw failure(
245
+ 'LOCAL_AI_IMAGE_ASSETS_UNAVAILABLE',
246
+ 'This Core has no model asset projection service.'
247
+ );
248
+ }
249
+ let projection = projections.get(assetProjectionId);
250
+ if (!projection) {
251
+ projection = {id: assetProjectionId, lease: null, task: null, models: new Map()};
252
+ projection.task = Promise.resolve().then(
253
+ async function retainModelAssets() {
254
+ projection.lease = await modelAssets.retain(assetProjectionId);
255
+ return projection.lease;
256
+ }
257
+ );
258
+ projections.set(assetProjectionId, projection);
259
+ }
260
+ const lease = await projection.task;
261
+ operationSignal.throwIfAborted();
262
+ if (!resourcePaths || !is.object(resourcePaths) || is.array(resourcePaths)) {
263
+ throw new TypeError('Projected image models require resourcePaths mapping native roles to projection members.');
264
+ }
265
+ const entries = Object.entries(resourcePaths);
266
+ const modelId = is.string(model) ? model : model?.id;
267
+ const cached = projection.models.get(modelId);
268
+ if (cached && isDeepStrictEqual(cached.model, model)
269
+ && Object.keys(cached.resourcePaths).length === entries.length
270
+ && entries.every(
271
+ function matchingResource([role, memberPath]) {
272
+ return cached.resourcePaths[role] === memberPath;
273
+ }
274
+ )) {
275
+ return cached.prepared;
276
+ }
277
+ const definition = is.string(model)
278
+ ? record.models?.find(
279
+ function selectedModelDefinition(value) {
280
+ return value.id === model;
281
+ }
282
+ ) : model;
283
+ if (!definition) {
284
+ throw failure(
285
+ 'LOCAL_AI_IMAGE_MODEL_UNAVAILABLE',
286
+ `The selected image model ${String(model)} has no model definition.`
287
+ );
288
+ }
289
+ const resources = {...definition.resources};
290
+ for (const [role, memberPath] of entries) {
291
+ const member = lease.members.find(
292
+ function selectedProjectionMember(value) {
293
+ return value.path === memberPath;
294
+ }
295
+ );
296
+ if (!member) {
297
+ throw failure(
298
+ 'LOCAL_AI_IMAGE_RESOURCE_UNAVAILABLE',
299
+ `The retained model projection has no member ${String(memberPath)} for ${role}.`
300
+ );
301
+ }
302
+ resources[role] = member.nativePath;
303
+ }
304
+ const prepared = {...definition, resources};
305
+ projection.models.set(
306
+ modelId,
307
+ {model, resourcePaths: {...resourcePaths}, prepared}
308
+ );
309
+ return prepared;
310
+ }
311
+
312
+ function releaseUnusedProjections(all = false) {
313
+ // Native calls share one context. Retain every provisional projection
314
+ // until those calls settle, then compare with the actual loaded inputs.
315
+ if (jobs.size > 0) {
316
+ return;
317
+ }
318
+ const resources = all ? [] : Object.values(runtime?.current().resources ?? {});
319
+ for (const [id, projection] of projections) {
320
+ const active = projection.lease?.members.some(
321
+ function activeNativeResource(member) {
322
+ return resources.includes(member.nativePath);
323
+ }
324
+ );
325
+ if (active) {
326
+ continue;
327
+ }
328
+ projections.delete(id);
329
+ if (!projection.lease) {
330
+ continue;
331
+ }
332
+ const task = Promise.resolve().then(
333
+ function releaseModelProjection() {
334
+ return projection.lease.release();
335
+ }
336
+ );
337
+ releases.add(task);
338
+ task.then(
339
+ function projectionReleased() {
340
+ releases.delete(task);
341
+ },
342
+ function projectionReleaseFailed(error) {
343
+ releases.delete(task);
344
+ releaseFailures.push(error);
345
+ console.error('Local image model projection release failed.', error);
346
+ }
347
+ );
348
+ }
349
+ }
350
+
351
+ function track(task, operationSignal) {
352
+ jobs.add(task);
353
+ function settled() {
354
+ jobs.delete(task);
355
+ releaseUnusedProjections();
356
+ }
357
+ task.then(
358
+ settled,
359
+ function imageOperationFailed(error) {
360
+ if (!closing && !operationSignal.aborted) {
361
+ state = {...state, error: serializeCoreError(error)};
362
+ publish();
363
+ }
364
+ settled();
365
+ }
366
+ );
367
+ return task;
368
+ }
369
+
370
+ function subscribe(listener, {replay = true, signal: subscriptionSignal} = {}) {
371
+ if (!is.function(listener)) {
372
+ throw new TypeError('An image state listener must be a function.');
373
+ }
374
+ if (closed) {
375
+ if (replay && !subscriptionSignal?.aborted) {
376
+ notifyListener(listener, current());
377
+ }
378
+ return function closedSubscription() {};
379
+ }
380
+ const unsubscribe = events.on(
381
+ 'image.state',
382
+ function imageStateChanged(event) {
383
+ notifyListener(listener, event.detail);
384
+ },
385
+ {signal: subscriptionSignal}
386
+ );
387
+ try {
388
+ if (replay && !subscriptionSignal?.aborted) {
389
+ notifyListener(listener, current());
390
+ }
391
+ } catch (error) {
392
+ unsubscribe();
393
+ throw error;
394
+ }
395
+ return unsubscribe;
396
+ }
397
+
398
+ function close() {
399
+ if (closing) {
400
+ return closing;
401
+ }
402
+ closing = Promise.resolve().then(
403
+ async function closeImageService() {
404
+ const runtimeClosing = Promise.resolve().then(
405
+ function closeNativeImageRuntime() {
406
+ return runtime?.close();
407
+ }
408
+ );
409
+ const results = await Promise.allSettled(
410
+ [runtimeClosing, ...jobs, ...(preparation ? [preparation] : [])]
411
+ );
412
+ try {
413
+ const errors = [];
414
+ if (results[0].status === 'rejected') {
415
+ errors.push(results[0].reason);
416
+ }
417
+ // Native close joins unload or worker exit even when it
418
+ // rejects. Its files can now complete their own cleanup.
419
+ try {
420
+ releaseUnusedProjections(true);
421
+ } catch (error) {
422
+ errors.push(error);
423
+ }
424
+ await Promise.allSettled([...releases]);
425
+ errors.push(...releaseFailures);
426
+ if (errors.length === 1) {
427
+ throw errors[0];
428
+ }
429
+ if (errors.length > 1) {
430
+ throw new AggregateError(
431
+ errors,
432
+ 'Local image service cleanup failed.',
433
+ {cause: errors[0]}
434
+ );
435
+ }
436
+ } catch (error) {
437
+ state = {...state, error: serializeCoreError(error)};
438
+ throw error;
439
+ } finally {
440
+ stopRuntimeSubscription?.();
441
+ lifetimeSignal.removeEventListener('abort', abortService);
442
+ closed = true;
443
+ state = {...state, available: false, state: 'closed'};
444
+ publish();
445
+ }
446
+ }
447
+ );
448
+ lifetime.abort();
449
+ state = {...state, available: false, state: 'closing'};
450
+ publish();
451
+ return closing;
452
+ }
453
+
454
+ function abortService() {
455
+ close().catch(
456
+ function reportImageShutdownFailure(error) {
457
+ console.error('Local image service shutdown failed.', error);
458
+ }
459
+ );
460
+ }
461
+
462
+ lifetimeSignal.addEventListener(
463
+ 'abort',
464
+ abortService,
465
+ {once: true}
466
+ );
467
+
468
+ return {
469
+ name: 'image',
470
+ current,
471
+ subscribe,
472
+ start(currentContext) {
473
+ context = currentContext;
474
+ if (lifetimeSignal.aborted) {
475
+ abortService();
476
+ return;
477
+ }
478
+ beginPreparation();
479
+ publish();
480
+ },
481
+ methods: {
482
+ 'image.status': current,
483
+ 'image.load': function loadImage(
484
+ {model, context: modelContext, assetProjectionId, resourcePaths} = {},
485
+ request
486
+ ) {
487
+ const operationSignal = AbortSignal.any([lifetimeSignal, request.signal]);
488
+ return track(
489
+ (async function loadSelectedImageModel() {
490
+ const selected = await selectedRuntime(operationSignal);
491
+ const prepared = await projectedModel(model, assetProjectionId, resourcePaths, operationSignal);
492
+ await selected.load(
493
+ {model: prepared, context: modelContext, signal: operationSignal}
494
+ );
495
+ operationSignal.throwIfAborted();
496
+ return current();
497
+ })(),
498
+ operationSignal
499
+ );
500
+ },
501
+ 'image.generate': function generateImage(
502
+ {model, prompt, parameters, streamId, assetProjectionId, resourcePaths} = {},
503
+ request
504
+ ) {
505
+ const operationSignal = AbortSignal.any([lifetimeSignal, request.signal]);
506
+ operationSignal.throwIfAborted();
507
+ request.emit(
508
+ 'image.progress',
509
+ {streamId, requestId: request.requestId, status: 'Thinking', progress: {phase: 'accepted'}}
510
+ );
511
+ return track(
512
+ (async function generateSelectedImage() {
513
+ const selected = await selectedRuntime(operationSignal);
514
+ const prepared = await projectedModel(model, assetProjectionId, resourcePaths, operationSignal);
515
+ const result = await selected.generate(
516
+ {
517
+ model: prepared,
518
+ prompt,
519
+ parameters,
520
+ signal: operationSignal,
521
+ onProgress(progress) {
522
+ if (!operationSignal.aborted) {
523
+ request.emit(
524
+ 'image.progress',
525
+ {streamId, requestId: request.requestId, status: 'Thinking', progress}
526
+ );
527
+ }
528
+ }
529
+ }
530
+ );
531
+ operationSignal.throwIfAborted();
532
+ return {...result, images: result.images.map(encodeImage)};
533
+ })(),
534
+ operationSignal
535
+ );
536
+ },
537
+ 'image.unload': function unloadImage(_parameters, request) {
538
+ const operationSignal = AbortSignal.any([lifetimeSignal, request.signal]);
539
+ return track(
540
+ (async function unloadSelectedImageModel() {
541
+ const selected = await selectedRuntime(operationSignal);
542
+ await selected.unload(
543
+ {signal: operationSignal}
544
+ );
545
+ operationSignal.throwIfAborted();
546
+ return current();
547
+ })(),
548
+ operationSignal
549
+ );
550
+ }
551
+ },
552
+ drain: close,
553
+ async dispose() {
554
+ try {
555
+ await close();
556
+ } finally {
557
+ events.dispose();
558
+ }
559
+ }
560
+ };
561
+ }
562
+
563
+ export default createLocalImageService;