arcane-os 0.57.0 → 0.59.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 (60) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +6 -2
  3. package/browser-runtime/ai/core-onnx.mjs +96 -0
  4. package/browser-runtime/ai/onnx-tensors.mjs +85 -0
  5. package/browser-runtime/core/client.mjs +152 -17
  6. package/docs/architecture.md +5 -2
  7. package/docs/platform-targets.md +30 -13
  8. package/docs/reference/README.md +22 -3
  9. package/docs/reference/availability-and-normalization.md +14 -7
  10. package/docs/reference/cli.md +22 -10
  11. package/docs/reference/core-client.md +87 -0
  12. package/docs/reference/core-native-packaging.md +129 -9
  13. package/docs/reference/core-preferences.md +49 -0
  14. package/docs/reference/diarization.md +206 -0
  15. package/docs/reference/git-text-snapshot.md +104 -0
  16. package/docs/reference/inventory/package-api.json +205 -23
  17. package/docs/reference/local-ai.md +145 -11
  18. package/docs/reference/protocols.md +28 -19
  19. package/docs/reference/pwa.md +74 -4
  20. package/docs/reference/sdk-api.md +589 -39
  21. package/package.json +11 -3
  22. package/schemas/arcane-app.schema.json +3 -3
  23. package/schemas/arcane-package.schema.json +2 -2
  24. package/schemas/native-build-plan.schema.json +27 -1
  25. package/schemas/target-adapter.schema.json +2 -0
  26. package/src/app-descriptor.mjs +1 -1
  27. package/src/cli/main.mjs +14 -4
  28. package/src/constants.mjs +2 -0
  29. package/src/core/host.mjs +14 -5
  30. package/src/core/hosts/macos/ArcaneHost.h +86 -0
  31. package/src/core/hosts/macos/ArcaneHost.m +455 -0
  32. package/src/core/hosts/macos/ArcaneLauncher.m +432 -0
  33. package/src/core/hosts/windows/ArcaneLauncher.cs +4 -1
  34. package/src/core/services/local-ai.mjs +82 -4
  35. package/src/core/services/packaged-web.mjs +89 -0
  36. package/src/core/services/preferences.mjs +201 -0
  37. package/src/core-classic-source.mjs +2 -2
  38. package/src/dev-server.mjs +2 -5
  39. package/src/diarization/build.mjs +35 -0
  40. package/src/diarization/index.mjs +432 -0
  41. package/src/diarization/native/CMakeLists.txt +9 -0
  42. package/src/diarization/native/main.cpp +269 -0
  43. package/src/diarization/service.mjs +129 -0
  44. package/src/git-text-snapshot.mjs +265 -0
  45. package/src/import-map.mjs +9 -4
  46. package/src/index.mjs +1 -0
  47. package/src/local-ai/config.mjs +1 -1
  48. package/src/local-ai/install.mjs +134 -19
  49. package/src/local-ai/onnx-worker.mjs +85 -0
  50. package/src/local-ai/onnx.mjs +368 -0
  51. package/src/native/macos-provider.mjs +291 -0
  52. package/src/native/portable-layout.mjs +10 -2
  53. package/src/native/portable-provider.mjs +10 -5
  54. package/src/native-plan.mjs +9 -1
  55. package/src/native-provider-loader.mjs +3 -1
  56. package/src/process.mjs +123 -17
  57. package/src/scaffold.mjs +1 -1
  58. package/src/targets/index.mjs +11 -1
  59. package/src/templates/workspace-template.mjs +6 -5
  60. package/src/toolchain.mjs +42 -13
@@ -0,0 +1,455 @@
1
+ #import "ArcaneHost.h"
2
+ #import <dispatch/dispatch.h>
3
+
4
+ typedef void (^ArcaneHostCompletion)(void);
5
+ typedef void (^ArcaneHostOperation)(ArcaneHostCompletion completion);
6
+ typedef void (^ArcaneHostReply)(id _Nullable reply, NSString * _Nullable errorMessage);
7
+
8
+ static NSError *ArcaneHostError(NSString *code, NSString *message, NSError *underlying) {
9
+ NSMutableDictionary *details = [@{NSLocalizedDescriptionKey: message, @"code": code} mutableCopy];
10
+ if (underlying) details[NSUnderlyingErrorKey] = underlying;
11
+ return [NSError errorWithDomain:@"ArcaneHost" code:1 userInfo:details];
12
+ }
13
+
14
+ static id ArcaneDiagnosticValue(id value, NSMutableArray *ancestors, NSError **error) {
15
+ if (!value) return NSNull.null;
16
+ if ([value isKindOfClass:[NSString class]] || [value isKindOfClass:[NSNumber class]]
17
+ || [value isKindOfClass:[NSDate class]] || [value isKindOfClass:[NSNull class]]) return value;
18
+ for (id ancestor in ancestors) {
19
+ if (ancestor == value) {
20
+ *error = ArcaneHostError(@"CORE_NATIVE_DIAGNOSTIC_UNREPRESENTABLE",
21
+ @"A native diagnostic contains a cyclic object graph that WebKit arguments cannot represent. The complete error remains with the native host delegate.", nil);
22
+ return nil;
23
+ }
24
+ }
25
+ [ancestors addObject:value];
26
+ id result = nil;
27
+ if ([value isKindOfClass:[NSError class]]) {
28
+ NSError *nativeError = value;
29
+ result = ArcaneDiagnosticValue(@{
30
+ @"code": nativeError.userInfo[@"code"] ?: @"CORE_HOST_FAILED",
31
+ @"message": nativeError.localizedDescription,
32
+ @"domain": nativeError.domain, @"nativeCode": @(nativeError.code),
33
+ @"userInfo": nativeError.userInfo
34
+ }, ancestors, error);
35
+ } else if ([value isKindOfClass:[NSDictionary class]]) {
36
+ NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
37
+ for (id key in value) {
38
+ if (![key isKindOfClass:[NSString class]]) {
39
+ *error = ArcaneHostError(@"CORE_NATIVE_DIAGNOSTIC_UNREPRESENTABLE",
40
+ @"A native diagnostic dictionary has a non-string key that WebKit arguments cannot represent. The complete error remains with the native host delegate.", nil);
41
+ break;
42
+ }
43
+ id item = ArcaneDiagnosticValue(value[key], ancestors, error);
44
+ if (!item) break;
45
+ dictionary[key] = item;
46
+ }
47
+ if (!*error) result = dictionary;
48
+ } else if ([value isKindOfClass:[NSArray class]]) {
49
+ NSMutableArray *array = [NSMutableArray array];
50
+ for (id item in value) {
51
+ id converted = ArcaneDiagnosticValue(item, ancestors, error);
52
+ if (!converted) break;
53
+ [array addObject:converted];
54
+ }
55
+ if (!*error) result = array;
56
+ } else if ([value isKindOfClass:[NSURL class]]) {
57
+ NSURL *url = value;
58
+ // Reversible Foundation values that are not WebKit argument types stay
59
+ // complete in explicit transport records, including relative URLs.
60
+ result = ArcaneDiagnosticValue(@{@"nativeType": @"NSURL",
61
+ @"relativeString": url.relativeString, @"baseURL": (id)url.baseURL ?: NSNull.null}, ancestors, error);
62
+ } else if ([value isKindOfClass:[NSData class]]) {
63
+ result = @{@"nativeType": @"NSData", @"base64": [value base64EncodedStringWithOptions:0]};
64
+ } else {
65
+ *error = ArcaneHostError(@"CORE_NATIVE_DIAGNOSTIC_UNREPRESENTABLE",
66
+ [NSString stringWithFormat:@"The native diagnostic class %@ cannot be passed as a WebKit argument. The complete error remains with the native host delegate.", NSStringFromClass([value class])], nil);
67
+ }
68
+ [ancestors removeLastObject];
69
+ return result;
70
+ }
71
+
72
+ static NSDictionary *ArcaneHostErrorRecord(NSError *error, NSError **representationError) {
73
+ *representationError = nil;
74
+ return ArcaneDiagnosticValue(error, [NSMutableArray array], representationError);
75
+ }
76
+
77
+ static NSData *ArcaneCancelRequestsData(void) {
78
+ return [@"{\"protocol\":\"arcane/1\",\"type\":\"control\",\"control\":\"requests.cancelAll\"}"
79
+ dataUsingEncoding:NSUTF8StringEncoding];
80
+ }
81
+
82
+ @interface ArcaneHost () <NSWindowDelegate, WKNavigationDelegate,
83
+ WKScriptMessageHandlerWithReply, ArcaneCoreProcessDelegate> {
84
+ WKWebView *_webView;
85
+ WKUserContentController *_contentController;
86
+ ArcaneCoreProcess *_process;
87
+ id<ArcaneHostDelegate> _hostDelegate;
88
+ NSMutableArray<NSError *> *_errors;
89
+ NSMutableArray<ArcaneHostOperation> *_ingressOperations;
90
+ NSMutableArray<ArcaneHostOperation> *_deliveries;
91
+ NSMutableArray<void (^)(NSArray<NSError *> *)> *_closeCompletions;
92
+ NSMutableDictionary<NSString *, NSString *> *_requestActivations;
93
+ NSString *_activation;
94
+ NSError *_terminalError;
95
+ BOOL _ingressRunning;
96
+ BOOL _deliveryRunning;
97
+ BOOL _processComplete;
98
+ BOOL _closing;
99
+ BOOL _closed;
100
+ BOOL _windowClosed;
101
+ ArcaneHost *_closingOwner;
102
+ }
103
+ @end
104
+
105
+ @implementation ArcaneHost
106
+
107
+ + (instancetype)openApplicationURL:(NSURL *)applicationURL
108
+ websiteDataStore:(WKWebsiteDataStore *)websiteDataStore
109
+ title:(NSString *)title
110
+ contentFrame:(NSRect)contentFrame
111
+ classicSource:(NSString *)classicSource
112
+ executableURL:(NSURL *)executableURL
113
+ arguments:(NSArray<NSString *> *)arguments
114
+ workingDirectoryURL:(NSURL *)workingDirectoryURL
115
+ delegate:(id<ArcaneHostDelegate>)delegate {
116
+ if (![NSThread isMainThread] || !websiteDataStore || !title
117
+ || !classicSource || !executableURL || !arguments || !workingDirectoryURL || !delegate) {
118
+ [NSException raise:NSInvalidArgumentException
119
+ format:@"Open the host on the main thread with a store, title, classic source, Core configuration and delegate."];
120
+ }
121
+ NSWindow *window = [[NSWindow alloc] initWithContentRect:contentFrame
122
+ styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable
123
+ | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable
124
+ backing:NSBackingStoreBuffered defer:NO];
125
+ ArcaneHost *host = [[self alloc] initWithWindow:window];
126
+ host->_hostDelegate = delegate;
127
+ host->_errors = [NSMutableArray array];
128
+ host->_ingressOperations = [NSMutableArray array];
129
+ host->_deliveries = [NSMutableArray array];
130
+ host->_closeCompletions = [NSMutableArray array];
131
+ host->_requestActivations = [NSMutableDictionary dictionary];
132
+ window.title = title;
133
+ window.releasedWhenClosed = NO;
134
+ window.delegate = host;
135
+
136
+ host->_contentController = [[WKUserContentController alloc] init];
137
+ [host->_contentController addScriptMessageHandlerWithReply:host
138
+ contentWorld:WKContentWorld.pageWorld name:@"arcane"];
139
+ [host->_contentController addUserScript:[[WKUserScript alloc] initWithSource:classicSource
140
+ injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES
141
+ inContentWorld:WKContentWorld.pageWorld]];
142
+ WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
143
+ configuration.websiteDataStore = websiteDataStore;
144
+ configuration.userContentController = host->_contentController;
145
+ host->_webView = [[WKWebView alloc] initWithFrame:window.contentView.bounds configuration:configuration];
146
+ host->_webView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
147
+ host->_webView.navigationDelegate = host;
148
+ window.contentView = host->_webView;
149
+ [host showWindow:nil];
150
+ if (applicationURL) [host loadApplicationURL:applicationURL];
151
+ host->_process = [ArcaneCoreProcess startExecutableURL:executableURL arguments:arguments
152
+ workingDirectoryURL:workingDirectoryURL delegate:host];
153
+ return host;
154
+ }
155
+
156
+ - (WKWebView *)webView { return _webView; }
157
+ - (BOOL)isClosing { return _closing; }
158
+
159
+ - (WKNavigation *)loadApplicationURL:(NSURL *)applicationURL {
160
+ if (_closing) return nil;
161
+ return [_webView loadRequest:[NSURLRequest requestWithURL:applicationURL]];
162
+ }
163
+
164
+ - (void)reportError:(NSError *)error {
165
+ [_errors addObject:error];
166
+ [_hostDelegate host:self didEncounterError:error];
167
+ }
168
+
169
+ - (void)enqueueOperation:(ArcaneHostOperation)operation ingress:(BOOL)ingress {
170
+ NSMutableArray<ArcaneHostOperation> *operations = ingress ? _ingressOperations : _deliveries;
171
+ [operations addObject:[operation copy]];
172
+ [self runNextOperationForIngress:ingress];
173
+ }
174
+
175
+ - (void)runNextOperationForIngress:(BOOL)ingress {
176
+ if (ingress ? _ingressRunning : _deliveryRunning) return;
177
+ NSMutableArray<ArcaneHostOperation> *operations = ingress ? _ingressOperations : _deliveries;
178
+ if (!operations.count) { [self finishCloseIfReady]; return; }
179
+ if (ingress) _ingressRunning = YES;
180
+ else _deliveryRunning = YES;
181
+ ArcaneHostOperation operation = operations.firstObject;
182
+ [operations removeObjectAtIndex:0];
183
+ operation(^{
184
+ if (ingress) self->_ingressRunning = NO;
185
+ else self->_deliveryRunning = NO;
186
+ // Advance on the main queue without recursively walking a run of
187
+ // immediately settled messages. Ingress waits only for its own probe
188
+ // and ordered writes; page delivery never holds subsequent stdin work.
189
+ dispatch_async(dispatch_get_main_queue(), ^{ [self runNextOperationForIngress:ingress]; });
190
+ });
191
+ }
192
+
193
+ - (void)rejectReply:(ArcaneHostReply)reply error:(NSError *)error {
194
+ [self reportError:error];
195
+ NSError *representationError = nil;
196
+ NSDictionary *record = ArcaneHostErrorRecord(error, &representationError);
197
+ if (record) reply(@{@"accepted": @NO, @"error": record}, nil);
198
+ else {
199
+ [self reportError:representationError];
200
+ reply(nil, representationError.localizedDescription);
201
+ }
202
+ }
203
+
204
+ - (void)writeData:(NSData *)data completion:(void (^)(NSError *error))completion {
205
+ NSError *error = nil;
206
+ BOOL queued = [_process sendJSONData:data completion:^(NSError *writeError) {
207
+ dispatch_async(dispatch_get_main_queue(), ^{ completion(writeError); });
208
+ } error:&error];
209
+ if (!queued) completion(error ?: ArcaneHostError(@"CORE_PROCESS_INPUT_CLOSED", @"Core input is closed.", nil));
210
+ }
211
+
212
+ - (void)userContentController:(WKUserContentController *)controller
213
+ didReceiveScriptMessage:(WKScriptMessage *)message replyHandler:(ArcaneHostReply)replyHandler {
214
+ id body = message.body;
215
+ if (![body isKindOfClass:[NSDictionary class]]
216
+ || ![body[@"type"] isKindOfClass:[NSString class]]
217
+ || ![body[@"activation"] isKindOfClass:[NSString class]]) {
218
+ [self rejectReply:replyHandler error:ArcaneHostError(@"CORE_WEBKIT_RECORD_INVALID",
219
+ @"The selected classic source must supply WebKit document lifecycle records.", nil)];
220
+ return;
221
+ }
222
+ NSDictionary *record = [body copy];
223
+ ArcaneHostReply reply = [replyHandler copy];
224
+ [self enqueueOperation:^(ArcaneHostCompletion done) {
225
+ if (self->_closing || self->_processComplete) {
226
+ [self rejectReply:reply error:self->_terminalError ?: ArcaneHostError(
227
+ @"CORE_PROCESS_INPUT_CLOSED", @"Core input is closed.", nil)];
228
+ done();
229
+ return;
230
+ }
231
+ if ([record[@"type"] isEqual:@"activate"]) {
232
+ [self activate:record[@"activation"] reply:reply completion:done];
233
+ } else if ([record[@"type"] isEqual:@"frame"] || [record[@"type"] isEqual:@"retire"]) {
234
+ [self writeRecord:record reply:reply completion:done];
235
+ } else {
236
+ [self rejectReply:reply error:ArcaneHostError(@"CORE_WEBKIT_RECORD_INVALID",
237
+ @"The WebKit transport record has no supported operation.", nil)];
238
+ done();
239
+ }
240
+ } ingress:YES];
241
+ }
242
+
243
+ - (void)activate:(NSString *)activation reply:(ArcaneHostReply)reply completion:(ArcaneHostCompletion)done {
244
+ // WKFrameInfo identifies a frame, not its document. Probe the currently
245
+ // executing realm; every later delivery also checks its activation there.
246
+ [_webView callAsyncJavaScript:@"return typeof globalThis.__arcaneWebKitDocumentCurrent === 'function' && globalThis.__arcaneWebKitDocumentCurrent(activation) === true;"
247
+ arguments:@{@"activation": activation} inFrame:nil inContentWorld:WKContentWorld.pageWorld
248
+ completionHandler:^(id result, NSError *error) {
249
+ if (error || ![result isEqual:@YES] || self->_closing || self->_processComplete) {
250
+ [self rejectReply:reply error:error ?: ArcaneHostError(@"CORE_DOCUMENT_RETIRED",
251
+ @"The requesting document activation is no longer current.", nil)];
252
+ done();
253
+ return;
254
+ }
255
+ void (^accept)(NSError *) = ^(NSError *writeError) {
256
+ if (writeError || self->_closing || self->_processComplete) {
257
+ [self rejectReply:reply error:writeError ?: ArcaneHostError(
258
+ @"CORE_PROCESS_INPUT_CLOSED", @"Core input is closed.", nil)];
259
+ done();
260
+ return;
261
+ }
262
+ self->_activation = [activation copy];
263
+ reply(@{@"accepted": @YES}, nil);
264
+ done();
265
+ };
266
+ if (self->_activation && ![self->_activation isEqual:activation]) {
267
+ self->_activation = nil;
268
+ // Existing control cancels request lifetime only. It precedes
269
+ // opening the new ingress; accepted service work stays alive.
270
+ [self writeData:ArcaneCancelRequestsData() completion:accept];
271
+ } else accept(nil);
272
+ }];
273
+ }
274
+
275
+ - (void)writeRecord:(NSDictionary *)record reply:(ArcaneHostReply)reply completion:(ArcaneHostCompletion)done {
276
+ NSString *activation = record[@"activation"];
277
+ if (![_activation isEqual:activation]) {
278
+ [self rejectReply:reply error:ArcaneHostError(@"CORE_DOCUMENT_RETIRED",
279
+ @"The requesting document activation has retired.", nil)];
280
+ done();
281
+ return;
282
+ }
283
+ NSString *json = record[@"json"];
284
+ NSData *data = [json isKindOfClass:[NSString class]] ? [json dataUsingEncoding:NSUTF8StringEncoding] : nil;
285
+ NSError *parseError = nil;
286
+ id frame = data ? [NSJSONSerialization JSONObjectWithData:data options:0 error:&parseError] : nil;
287
+ if (![frame isKindOfClass:[NSDictionary class]]) {
288
+ [self rejectReply:reply error:ArcaneHostError(@"CORE_WEBKIT_JSON_INVALID",
289
+ @"The WebKit transport requires a complete serialized Core frame.", parseError)];
290
+ done();
291
+ return;
292
+ }
293
+ NSString *requestID = [frame[@"type"] isEqual:@"request"]
294
+ && [frame[@"id"] isKindOfClass:[NSString class]] ? frame[@"id"] : nil;
295
+ if (requestID) _requestActivations[requestID] = activation;
296
+ if ([record[@"type"] isEqual:@"retire"]) _activation = nil;
297
+ // The original string is encoded only at the stdio boundary. Parsing above
298
+ // reads routing fields; it never reconstructs or changes the Core payload.
299
+ [self writeData:data completion:^(NSError *error) {
300
+ if (error) {
301
+ if (requestID) [self->_requestActivations removeObjectForKey:requestID];
302
+ [self rejectReply:reply error:error];
303
+ } else reply(@{@"accepted": @YES}, nil);
304
+ done();
305
+ }];
306
+ }
307
+
308
+ - (void)receiveFrameData:(NSData *)data {
309
+ [_hostDelegate host:self receivedFrameData:data];
310
+ NSString *json = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
311
+ NSError *error = nil;
312
+ id frame = json ? [NSJSONSerialization JSONObjectWithData:data options:0 error:&error] : nil;
313
+ if (![frame isKindOfClass:[NSDictionary class]]) {
314
+ [self reportError:ArcaneHostError(@"CORE_OUTPUT_JSON_INVALID", @"Core output is not a readable JSON frame.", error)];
315
+ return;
316
+ }
317
+ NSString *activation = nil;
318
+ if ([frame[@"type"] isEqual:@"response"] && [frame[@"id"] isKindOfClass:[NSString class]]) {
319
+ activation = _requestActivations[frame[@"id"]];
320
+ [_requestActivations removeObjectForKey:frame[@"id"]];
321
+ } else if ([frame[@"type"] isEqual:@"event"]) {
322
+ activation = _activation;
323
+ } else {
324
+ [self reportError:ArcaneHostError(@"CORE_OUTPUT_FRAME_INVALID", @"Core output has no supported frame type.", nil)];
325
+ return;
326
+ }
327
+ // No current subscriber/response owner: native diagnostics own the complete
328
+ // frame. A later activation requests current state, not transient history.
329
+ if (!activation) return;
330
+ [self enqueueOperation:^(ArcaneHostCompletion done) {
331
+ [self deliverJSON:json activation:activation completion:done];
332
+ } ingress:NO];
333
+ }
334
+
335
+ - (void)deliverJSON:(NSString *)json activation:(NSString *)activation completion:(ArcaneHostCompletion)done {
336
+ [_webView callAsyncJavaScript:@"return typeof globalThis.__arcaneReceive === 'function' && globalThis.__arcaneReceive(json, activation);"
337
+ arguments:@{@"json": json, @"activation": activation}
338
+ inFrame:nil inContentWorld:WKContentWorld.pageWorld completionHandler:^(id result, NSError *error) {
339
+ if (error) [self reportError:error];
340
+ // false includes an activation retired while WebKit queued this
341
+ // evaluation. The complete frame already reached native diagnostics.
342
+ done();
343
+ }];
344
+ }
345
+
346
+ - (void)deliverFailure:(NSError *)error completion:(ArcaneHostCompletion)done {
347
+ if (!_activation) { done(); return; }
348
+ NSError *representationError = nil;
349
+ NSDictionary *record = ArcaneHostErrorRecord(error, &representationError);
350
+ if (!record) {
351
+ [self reportError:representationError];
352
+ // Report the actual transport representation failure rather than send
353
+ // a partial version of the original native error as if it were complete.
354
+ record = ArcaneHostErrorRecord(representationError, &representationError);
355
+ }
356
+ [_webView callAsyncJavaScript:@"return typeof globalThis.__arcaneTransportFailed === 'function' && globalThis.__arcaneTransportFailed(error, activation);"
357
+ arguments:@{@"error": record, @"activation": _activation}
358
+ inFrame:nil inContentWorld:WKContentWorld.pageWorld completionHandler:^(id result, NSError *evaluationError) {
359
+ if (evaluationError) [self reportError:evaluationError];
360
+ done();
361
+ }];
362
+ }
363
+
364
+ - (void)coreProcess:(ArcaneCoreProcess *)process didLaunch:(pid_t)processIdentifier {
365
+ dispatch_async(dispatch_get_main_queue(), ^{ [self->_hostDelegate host:self didLaunchCoreProcess:processIdentifier]; });
366
+ }
367
+
368
+ - (void)coreProcess:(ArcaneCoreProcess *)process receivedOutputData:(NSData *)data stream:(ArcaneCoreOutputStream)stream {
369
+ dispatch_async(dispatch_get_main_queue(), ^{ [self->_hostDelegate host:self receivedOutputData:data stream:stream]; });
370
+ }
371
+
372
+ - (void)coreProcess:(ArcaneCoreProcess *)process receivedFrameData:(NSData *)data {
373
+ dispatch_async(dispatch_get_main_queue(), ^{ [self receiveFrameData:data]; });
374
+ }
375
+
376
+ - (void)coreProcess:(ArcaneCoreProcess *)process didEncounterError:(NSError *)error {
377
+ dispatch_async(dispatch_get_main_queue(), ^{ [self reportError:error]; });
378
+ }
379
+
380
+ - (void)coreProcess:(ArcaneCoreProcess *)process didExitWithStatus:(int)status reason:(NSTaskTerminationReason)reason {
381
+ dispatch_async(dispatch_get_main_queue(), ^{ [self->_hostDelegate host:self didExitCoreProcessWithStatus:status reason:reason]; });
382
+ }
383
+
384
+ - (void)coreProcess:(ArcaneCoreProcess *)process didCompleteWithErrors:(NSArray<NSError *> *)errors {
385
+ dispatch_async(dispatch_get_main_queue(), ^{
386
+ self->_processComplete = YES;
387
+ self->_terminalError = errors.firstObject ?: ArcaneHostError(@"CORE_PROCESS_CLOSED", @"Core has completed and closed its transport.", nil);
388
+ [self enqueueOperation:^(ArcaneHostCompletion done) {
389
+ [self deliverFailure:self->_terminalError completion:^{
390
+ [self->_hostDelegate host:self coreDidCompleteWithErrors:errors];
391
+ done();
392
+ }];
393
+ } ingress:NO];
394
+ });
395
+ }
396
+
397
+ - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error {
398
+ // An unsuccessful provisional navigation may leave the current document
399
+ // alive. Its connection is owned by shared page lifecycle, not URL changes.
400
+ [self reportError:error];
401
+ }
402
+
403
+ - (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error {
404
+ [self reportError:error];
405
+ }
406
+
407
+ - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView {
408
+ [self reportError:ArcaneHostError(@"CORE_WEB_CONTENT_PROCESS_TERMINATED", @"The WebKit content process terminated.", nil)];
409
+ // The callback has no document activation. Keep routing decisions at the
410
+ // shared lifecycle seam: a later confirmed activation cancels prior request
411
+ // lifetime before opening its ingress; the caller can also close the host.
412
+ }
413
+
414
+ - (void)close { [self closeWithCompletion:nil]; }
415
+
416
+ - (void)closeWithCompletion:(void (^)(NSArray<NSError *> *errors))completion {
417
+ if (_closed) { if (completion) completion([_errors copy]); return; }
418
+ if (completion) [_closeCompletions addObject:[completion copy]];
419
+ if (!_closing) {
420
+ _closing = YES;
421
+ _closingOwner = self;
422
+ [_process closeInput];
423
+ }
424
+ [self finishCloseIfReady];
425
+ }
426
+
427
+ - (BOOL)windowShouldClose:(NSWindow *)sender {
428
+ [self closeWithCompletion:nil];
429
+ return _closed;
430
+ }
431
+
432
+ - (void)windowWillClose:(NSNotification *)notification {
433
+ _windowClosed = YES;
434
+ [self closeWithCompletion:nil];
435
+ }
436
+
437
+ - (void)finishCloseIfReady {
438
+ if (!_closing || _closed || !_processComplete || _ingressRunning || _deliveryRunning
439
+ || _ingressOperations.count || _deliveries.count) return;
440
+ _closed = YES;
441
+ if (!_windowClosed) [super close];
442
+ [_contentController removeScriptMessageHandlerForName:@"arcane" contentWorld:WKContentWorld.pageWorld];
443
+ _webView.navigationDelegate = nil;
444
+ self.window.delegate = nil;
445
+ _activation = nil;
446
+ [_requestActivations removeAllObjects];
447
+ _process = nil;
448
+ NSArray *completions = [_closeCompletions copy];
449
+ [_closeCompletions removeAllObjects];
450
+ NSArray<NSError *> *errors = [_errors copy];
451
+ for (void (^completion)(NSArray<NSError *> *) in completions) completion(errors);
452
+ _hostDelegate = nil;
453
+ _closingOwner = nil;
454
+ }
455
+ @end