@fungi.computer/agar 0.0.0-stage → 0.0.1

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 (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +766 -2
  3. package/dist/agent-message-meta.d.ts +74 -0
  4. package/dist/agent-message-meta.js +63 -0
  5. package/dist/agents.d.ts +170 -0
  6. package/dist/agents.js +218 -0
  7. package/dist/app-build-contract.d.ts +43 -0
  8. package/dist/app-build-contract.js +24 -0
  9. package/dist/apps.d.ts +66 -0
  10. package/dist/apps.js +47 -0
  11. package/dist/backend-declaration.d.ts +6 -0
  12. package/dist/backend-declaration.js +65 -0
  13. package/dist/backend-definition.d.ts +88 -0
  14. package/dist/backend-definition.js +158 -0
  15. package/dist/backend-refusal.d.ts +10 -0
  16. package/dist/backend-refusal.js +10 -0
  17. package/dist/backend.d.ts +30 -0
  18. package/dist/backend.js +28 -0
  19. package/dist/capabilities.d.ts +30 -0
  20. package/dist/capabilities.js +50 -0
  21. package/dist/channels.d.ts +142 -0
  22. package/dist/channels.js +97 -0
  23. package/dist/commands.d.ts +33 -0
  24. package/dist/commands.js +46 -0
  25. package/dist/computer-contracts.d.ts +73 -0
  26. package/dist/computer-contracts.js +57 -0
  27. package/dist/computer.d.ts +294 -0
  28. package/dist/computer.js +319 -0
  29. package/dist/connect.d.ts +11 -0
  30. package/dist/connect.js +211 -0
  31. package/dist/errors.d.ts +34 -0
  32. package/dist/errors.js +106 -0
  33. package/dist/failure.d.ts +2 -0
  34. package/dist/failure.js +9 -0
  35. package/dist/files.d.ts +25 -0
  36. package/dist/files.js +84 -0
  37. package/dist/game-servers.d.ts +530 -0
  38. package/dist/game-servers.js +244 -0
  39. package/dist/gills.d.ts +43 -0
  40. package/dist/gills.js +174 -0
  41. package/dist/git-commit.d.ts +143 -0
  42. package/dist/git-commit.js +251 -0
  43. package/dist/git-rpc.d.ts +310 -0
  44. package/dist/git-rpc.js +166 -0
  45. package/dist/git-steps.d.ts +579 -0
  46. package/dist/git-steps.js +804 -0
  47. package/dist/git-wire.d.ts +33 -0
  48. package/dist/git-wire.js +240 -0
  49. package/dist/git.d.ts +322 -0
  50. package/dist/git.js +98 -0
  51. package/dist/host.d.ts +97 -0
  52. package/dist/host.js +732 -0
  53. package/dist/inbox.d.ts +99 -0
  54. package/dist/inbox.js +184 -0
  55. package/dist/index.d.ts +31 -0
  56. package/dist/index.js +15 -0
  57. package/dist/inference.d.ts +17 -0
  58. package/dist/inference.js +115 -0
  59. package/dist/internal/app-error.d.ts +8 -0
  60. package/dist/internal/app-error.js +36 -0
  61. package/dist/internal/app-session.d.ts +8 -0
  62. package/dist/internal/app-session.js +551 -0
  63. package/dist/internal/back-inputs.d.ts +22 -0
  64. package/dist/internal/back-inputs.js +93 -0
  65. package/dist/internal/browser-bridge.d.ts +30 -0
  66. package/dist/internal/browser-bridge.js +203 -0
  67. package/dist/internal/capabilities.d.ts +13 -0
  68. package/dist/internal/capabilities.js +138 -0
  69. package/dist/internal/commands.d.ts +9 -0
  70. package/dist/internal/commands.js +257 -0
  71. package/dist/internal/disconnect-host.d.ts +7 -0
  72. package/dist/internal/disconnect-host.js +19 -0
  73. package/dist/internal/git-edit-tree.d.ts +6 -0
  74. package/dist/internal/git-edit-tree.js +46 -0
  75. package/dist/internal/memory-fs.d.ts +39 -0
  76. package/dist/internal/memory-fs.js +154 -0
  77. package/dist/internal/protocol.d.ts +116 -0
  78. package/dist/internal/protocol.js +6 -0
  79. package/dist/internal/query-feed.d.ts +23 -0
  80. package/dist/internal/query-feed.js +111 -0
  81. package/dist/internal/result.d.ts +22 -0
  82. package/dist/internal/result.js +61 -0
  83. package/dist/internal/rpc-contract.d.ts +24 -0
  84. package/dist/internal/rpc-contract.js +78 -0
  85. package/dist/internal/schemas.d.ts +307 -0
  86. package/dist/internal/schemas.js +524 -0
  87. package/dist/internal/store.d.ts +8 -0
  88. package/dist/internal/store.js +30 -0
  89. package/dist/internal/terminal-gate.d.ts +17 -0
  90. package/dist/internal/terminal-gate.js +94 -0
  91. package/dist/lifecycle.d.ts +44 -0
  92. package/dist/lifecycle.js +1 -0
  93. package/dist/marketplace.d.ts +159 -0
  94. package/dist/marketplace.js +102 -0
  95. package/dist/media-json.d.ts +181 -0
  96. package/dist/media-json.js +204 -0
  97. package/dist/media-library.d.ts +11 -0
  98. package/dist/media-library.js +15 -0
  99. package/dist/media-request-id.d.ts +7 -0
  100. package/dist/media-request-id.js +16 -0
  101. package/dist/media.d.ts +354 -0
  102. package/dist/media.js +654 -0
  103. package/dist/method-refusal.d.ts +64 -0
  104. package/dist/method-refusal.js +48 -0
  105. package/dist/pending.d.ts +10 -0
  106. package/dist/pending.js +10 -0
  107. package/dist/query.d.ts +76 -0
  108. package/dist/query.js +195 -0
  109. package/dist/react-mount.d.ts +23 -0
  110. package/dist/react-mount.js +108 -0
  111. package/dist/react.d.ts +41 -0
  112. package/dist/react.js +111 -0
  113. package/dist/read-copy.d.ts +103 -0
  114. package/dist/read-copy.js +195 -0
  115. package/dist/roles/files.d.ts +295 -0
  116. package/dist/roles/files.js +184 -0
  117. package/dist/root.d.ts +21 -0
  118. package/dist/root.js +1 -0
  119. package/dist/rpc.d.ts +1 -0
  120. package/dist/rpc.js +1 -0
  121. package/dist/status.d.ts +17 -0
  122. package/dist/status.js +50 -0
  123. package/dist/storage-wire.d.ts +167 -0
  124. package/dist/storage-wire.js +115 -0
  125. package/dist/storage.d.ts +72 -0
  126. package/dist/storage.js +143 -0
  127. package/dist/store.d.ts +11 -0
  128. package/dist/store.js +1 -0
  129. package/dist/test-kit/git.d.ts +31 -0
  130. package/dist/test-kit/git.js +174 -0
  131. package/dist/ui.d.ts +169 -0
  132. package/dist/ui.js +10 -0
  133. package/dist/window.d.ts +41 -0
  134. package/dist/window.js +15 -0
  135. package/dist/world.d.ts +16 -0
  136. package/dist/world.js +17 -0
  137. package/docs/api/README.md +28 -0
  138. package/docs/api/app-build-contract.md +113 -0
  139. package/docs/api/backend-declaration.md +62 -0
  140. package/docs/api/backend-definition.md +215 -0
  141. package/docs/api/backend.md +269 -0
  142. package/docs/api/errors.md +169 -0
  143. package/docs/api/files.md +91 -0
  144. package/docs/api/gills.md +164 -0
  145. package/docs/api/git-commit.md +279 -0
  146. package/docs/api/git-rpc.md +243 -0
  147. package/docs/api/git-steps.md +1010 -0
  148. package/docs/api/git-wire.md +197 -0
  149. package/docs/api/git.md +579 -0
  150. package/docs/api/index.md +2880 -0
  151. package/docs/api/media-json.md +507 -0
  152. package/docs/api/media-request-id.md +32 -0
  153. package/docs/api/modules.md +32 -0
  154. package/docs/api/query.md +253 -0
  155. package/docs/api/react.md +216 -0
  156. package/docs/api/read-copy.md +204 -0
  157. package/docs/api/rpc.md +128 -0
  158. package/docs/api/storage.md +620 -0
  159. package/package.json +197 -3
package/dist/host.js ADDED
@@ -0,0 +1,732 @@
1
+ import * as Predicate from "effect/Predicate";
2
+ import * as Schema from "effect/Schema";
3
+ /**
4
+ * WHY: the host adapter is the sole authority for iframe identity, epochs,
5
+ * and Cap'n Web resource lifetime; apps never authorize themselves (2026-08-27).
6
+ */
7
+ import { newMessagePortRpcSession, RpcStub, RpcTarget, } from "@fungi.computer/capnweb";
8
+ import { WhistleActionError } from "@fungi.computer/whistle";
9
+ import { AppError } from "./errors.js";
10
+ import { reportCatchFailure } from "./failure.js";
11
+ import { disposeRpcValue } from "./internal/capabilities.js";
12
+ import { disconnectHost } from "./internal/disconnect-host.js";
13
+ import { APP_CONNECT_TYPE, APP_PROTOCOL_VERSION, APP_READY_TYPE, } from "./internal/protocol.js";
14
+ import { callRpc, rpcFailure, rpcSuccess, } from "./internal/result.js";
15
+ import { parseBrowserAction, parseChordInvocation, parseCommandResultRpc, parseBackDecisionResult, parseCloseDecisionResult, parseCommand, parseCommandId, parseConnectRequest, parseContextFact, parseFocus, parseAgentSessionSelection, parseLocalHostBootstrap, parseHostLifecycleFact, parseNavigation, parseNavigationState, parseStatusAction, parseStatusItems, parsePermissionRequest, parseReady, parseReservedChordsFact, parseThemeFact, parseToast, parseVoidResult, } from "./internal/schemas.js";
16
+ import { createTerminalGate, } from "./internal/terminal-gate.js";
17
+ /** One pending or live mount belongs to each iframe WindowProxy. */
18
+ const liveMounts = new WeakMap();
19
+ const retainedCommandHandlerSchema = Schema.declare((value) => value instanceof RpcStub);
20
+ class MountedHost extends RpcTarget {
21
+ epoch;
22
+ #commands = new Map();
23
+ #pendingCommands = new Map();
24
+ #pendingUnregistrations = new Set();
25
+ #contribution;
26
+ #sync = Promise.resolve();
27
+ #connected = false;
28
+ #disposed = false;
29
+ #handlers;
30
+ #launch;
31
+ #onConnected;
32
+ #onFatal;
33
+ constructor(launch, handlers, epoch) {
34
+ super();
35
+ this.epoch = epoch;
36
+ this.#launch = parseLocalHostBootstrap(launch);
37
+ this.#handlers = handlers;
38
+ }
39
+ connect(value) {
40
+ try {
41
+ this.#assertLive();
42
+ const request = parseConnectRequest(value);
43
+ if (this.#connected || request.epoch !== this.epoch)
44
+ throw identityError();
45
+ this.#connected = true;
46
+ this.#onConnected?.();
47
+ return Promise.resolve(rpcSuccess(this.#launch));
48
+ }
49
+ catch (cause) {
50
+ return Promise.resolve(rpcFailure(cause));
51
+ }
52
+ }
53
+ async invokeReservedChord(id, repeat) {
54
+ return this.#call(() => this.#handlers.invokeReservedChord(parseChordInvocation({ id, repeat })));
55
+ }
56
+ isConnected() {
57
+ return this.#connected;
58
+ }
59
+ onConnected(callback) {
60
+ this.#onConnected = callback;
61
+ }
62
+ onFatal(callback) {
63
+ this.#onFatal = callback;
64
+ }
65
+ async requestComputerChoice() {
66
+ return this.#call(() => {
67
+ if (this.#handlers.requestComputerChoice === undefined)
68
+ throw new AppError("Computer choice is unavailable in this host", {
69
+ code: "CAPABILITY_DENIED",
70
+ retryable: false,
71
+ });
72
+ return this.#handlers.requestComputerChoice();
73
+ });
74
+ }
75
+ async requestBrowserAction(value) {
76
+ return this.#answer(async () => {
77
+ const parsed = parseBrowserAction(value);
78
+ if (parsed.kind === "refused")
79
+ return parsed;
80
+ return (this.#handlers.requestBrowserAction?.(parsed) ?? {
81
+ kind: "refused",
82
+ reason: "unavailable",
83
+ });
84
+ });
85
+ }
86
+ async openNavigation(value) {
87
+ return this.#call(() => this.#handlers.openNavigation(parseNavigation(value)));
88
+ }
89
+ async openTeamBilling() {
90
+ return this.#call(() => {
91
+ if (this.#handlers.openTeamBilling === undefined)
92
+ throw new AppError("Billing is unavailable in this host", {
93
+ code: "CAPABILITY_DENIED",
94
+ retryable: false,
95
+ });
96
+ return this.#handlers.openTeamBilling();
97
+ });
98
+ }
99
+ async openDesktopSettings() {
100
+ return this.#call(() => {
101
+ if (this.#handlers.openDesktopSettings === undefined)
102
+ throw new AppError("Desktop settings are unavailable in this host", {
103
+ code: "CAPABILITY_DENIED",
104
+ retryable: false,
105
+ });
106
+ return this.#handlers.openDesktopSettings();
107
+ });
108
+ }
109
+ async requestAppPermissions(value) {
110
+ return this.#answer(() => {
111
+ if (this.#handlers.requestAppPermissions === undefined)
112
+ throw new AppError("App permissions are unavailable in this host", {
113
+ code: "CAPABILITY_DENIED",
114
+ retryable: false,
115
+ });
116
+ return this.#handlers.requestAppPermissions(parsePermissionRequest(value));
117
+ });
118
+ }
119
+ async registerCommand(value, handler) {
120
+ let registration;
121
+ try {
122
+ registration = this.#admitCommand(value, handler);
123
+ await this.#registerPendingCommand(registration);
124
+ return rpcSuccess(Object.freeze({ commandId: registration.commandId }));
125
+ }
126
+ catch (cause) {
127
+ if (registration)
128
+ this.#discardPending(registration);
129
+ return rpcFailure(cause);
130
+ }
131
+ }
132
+ async reportFocus(value) {
133
+ return this.#call(() => this.#handlers.reportFocus(parseFocus(value)));
134
+ }
135
+ async reportStatus(value) {
136
+ return this.#call(() => {
137
+ const items = parseStatusItems(value);
138
+ return this.#handlers.reportStatus?.(items) ?? Promise.resolve();
139
+ });
140
+ }
141
+ async reportNavigation(value) {
142
+ return this.#call(() => {
143
+ const state = parseNavigationState(value);
144
+ return this.#handlers.reportNavigation?.(state) ?? Promise.resolve();
145
+ });
146
+ }
147
+ async reportAgentSessionSelection(value) {
148
+ return this.#call(() => this.#handlers.reportAgentSessionSelection(parseAgentSessionSelection(value)));
149
+ }
150
+ async showToast(value) {
151
+ return this.#call(() => this.#handlers.showToast(parseToast(value)));
152
+ }
153
+ [Symbol.dispose]() {
154
+ if (this.#disposed)
155
+ return;
156
+ this.#disposed = true;
157
+ for (const command of this.#commands.values()) {
158
+ try {
159
+ this.#release(command);
160
+ }
161
+ catch (cause) {
162
+ console.warn("App host command cleanup failed", cause);
163
+ }
164
+ }
165
+ this.#commands.clear();
166
+ for (const command of this.#pendingCommands.values()) {
167
+ try {
168
+ this.#release(command);
169
+ }
170
+ catch (cause) {
171
+ console.warn("App host pending command cleanup failed", cause);
172
+ }
173
+ }
174
+ this.#pendingCommands.clear();
175
+ this.#pendingUnregistrations.clear();
176
+ this.#releaseContribution();
177
+ }
178
+ unregisterCommand(value) {
179
+ try {
180
+ this.#assertConnected();
181
+ const commandId = parseCommandId(value);
182
+ const command = this.#commands.get(commandId);
183
+ if (!command)
184
+ throw new AppError("Unknown command", {
185
+ code: "INVALID_ARGUMENT",
186
+ retryable: false,
187
+ });
188
+ if (this.#pendingUnregistrations.has(commandId))
189
+ throw new AppError("Command is already unregistering", {
190
+ code: "INVALID_ARGUMENT",
191
+ retryable: false,
192
+ });
193
+ this.#pendingUnregistrations.add(commandId);
194
+ return this.#syncUnregistration(commandId, command)
195
+ .then(() => {
196
+ this.#pendingUnregistrations.delete(commandId);
197
+ if (this.#commands.get(commandId) === command) {
198
+ this.#commands.delete(commandId);
199
+ this.#release(command);
200
+ }
201
+ return rpcSuccess(undefined);
202
+ })
203
+ .catch((cause) => {
204
+ this.#pendingUnregistrations.delete(commandId);
205
+ return rpcFailure(cause);
206
+ });
207
+ }
208
+ catch (cause) {
209
+ return Promise.resolve(rpcFailure(cause));
210
+ }
211
+ }
212
+ #assertConnected() {
213
+ this.#assertLive();
214
+ if (!this.#connected)
215
+ throw disconnected();
216
+ }
217
+ #assertLive() {
218
+ if (this.#disposed)
219
+ throw disconnected();
220
+ }
221
+ #admitCommand(value, handler) {
222
+ this.#assertConnected();
223
+ const command = parseCommand(value);
224
+ const commandId = `${this.#launch.identity.appId}/${command.id}`;
225
+ if (this.#commands.size + this.#pendingCommands.size >= 128 ||
226
+ this.#commands.has(commandId) ||
227
+ this.#pendingCommands.has(commandId))
228
+ throw new AppError("Command already registered", {
229
+ code: "INVALID_ARGUMENT",
230
+ retryable: false,
231
+ });
232
+ const retainedCommand = {
233
+ callback: duplicateHandler(handler),
234
+ requests: new Set(),
235
+ command,
236
+ };
237
+ this.#pendingCommands.set(commandId, retainedCommand);
238
+ return { commandId, command: retainedCommand };
239
+ }
240
+ #discardPending(registration) {
241
+ const { commandId, command } = registration;
242
+ if (this.#pendingCommands.get(commandId) !== command)
243
+ return;
244
+ this.#pendingCommands.delete(commandId);
245
+ this.#release(command);
246
+ }
247
+ async #call(operation) {
248
+ return this.#answer(async () => {
249
+ await operation();
250
+ return undefined;
251
+ });
252
+ }
253
+ async #answer(operation) {
254
+ try {
255
+ this.#assertConnected();
256
+ return rpcSuccess(await operation());
257
+ }
258
+ catch (cause) {
259
+ return rpcFailure(cause);
260
+ }
261
+ }
262
+ #registerPendingCommand(registration) {
263
+ return this.#enqueueSync(async () => {
264
+ const { commandId, command } = registration;
265
+ this.#assertConnected();
266
+ if (this.#pendingCommands.get(commandId) !== command)
267
+ throw disconnected();
268
+ const next = new Map(this.#commands);
269
+ next.set(commandId, command);
270
+ try {
271
+ await this.#applyCommands(next);
272
+ }
273
+ catch (cause) {
274
+ this.#discardPending(registration);
275
+ throw cause;
276
+ }
277
+ if (this.#disposed || this.#pendingCommands.get(commandId) !== command)
278
+ throw disconnected();
279
+ this.#pendingCommands.delete(commandId);
280
+ this.#commands.set(commandId, command);
281
+ });
282
+ }
283
+ #syncUnregistration(commandId, command) {
284
+ return this.#enqueueSync(async () => {
285
+ this.#assertConnected();
286
+ if (this.#commands.get(commandId) !== command)
287
+ throw disconnected();
288
+ const next = new Map(this.#commands);
289
+ next.delete(commandId);
290
+ try {
291
+ await this.#applyCommands(next, next);
292
+ }
293
+ catch (cause) {
294
+ if (this.#commands.get(commandId) === command) {
295
+ this.#commands.delete(commandId);
296
+ this.#release(command);
297
+ }
298
+ throw cause;
299
+ }
300
+ });
301
+ }
302
+ #enqueueSync(operation) {
303
+ const queued = this.#sync.then(operation);
304
+ this.#sync = queued.catch((cause) => {
305
+ reportCatchFailure("host.command-sync", cause);
306
+ });
307
+ return queued;
308
+ }
309
+ async #applyCommands(next, recovery = this.#commands) {
310
+ const commands = [...next.values()].map((retained) => retained.command);
311
+ if (this.#contribution === undefined) {
312
+ if (commands.length === 0)
313
+ return;
314
+ await this.#registerContribution(commands);
315
+ return;
316
+ }
317
+ const contribution = this.#contribution;
318
+ try {
319
+ contribution.update(commands);
320
+ if (this.#disposed || this.#contribution !== contribution)
321
+ throw disconnected();
322
+ }
323
+ catch (cause) {
324
+ this.#releaseContribution();
325
+ try {
326
+ await this.#registerContribution([...recovery.values()].map((retained) => retained.command));
327
+ }
328
+ catch (recoveryCause) {
329
+ this.#abandonCommands();
330
+ this.#onFatal?.();
331
+ throw recoveryCause;
332
+ }
333
+ throw cause;
334
+ }
335
+ }
336
+ async #registerContribution(commands) {
337
+ if (commands.length === 0)
338
+ return;
339
+ const contribution = await this.#handlers.registerCommands({
340
+ commands,
341
+ invoke: (id, input) => {
342
+ const commandId = `${this.#launch.identity.appId}/${id}`;
343
+ const command = this.#commands.get(commandId);
344
+ if (command === undefined)
345
+ throw disconnected();
346
+ return this.#invoke(command.callback, commandId, input);
347
+ },
348
+ });
349
+ if (!Predicate.isObject(contribution))
350
+ throw new AppError("Host command registration returned no lease", {
351
+ code: "HOST_FAILURE",
352
+ retryable: false,
353
+ });
354
+ if (!Predicate.isFunction(contribution.update) ||
355
+ !Predicate.isFunction(contribution.dispose)) {
356
+ throw new AppError("Host command registration returned an invalid lease", {
357
+ code: "HOST_FAILURE",
358
+ retryable: false,
359
+ });
360
+ }
361
+ if (this.#disposed || !this.#connected) {
362
+ contribution.dispose();
363
+ throw disconnected();
364
+ }
365
+ this.#contribution = contribution;
366
+ }
367
+ #abandonCommands() {
368
+ for (const command of this.#commands.values())
369
+ this.#release(command);
370
+ this.#commands.clear();
371
+ for (const command of this.#pendingCommands.values())
372
+ this.#release(command);
373
+ this.#pendingCommands.clear();
374
+ this.#pendingUnregistrations.clear();
375
+ }
376
+ #cancel(callback, requestId) {
377
+ try {
378
+ void callRpc(callback.cancel(requestId), parseVoidResult).catch((cause) => {
379
+ console.warn("App host command cancellation failed", cause);
380
+ this.#onFatal?.();
381
+ });
382
+ }
383
+ catch (cause) {
384
+ console.warn("App host command cancellation failed", cause);
385
+ this.#onFatal?.();
386
+ }
387
+ }
388
+ async #invoke(callback, commandId, input) {
389
+ this.#assertConnected();
390
+ const command = this.#commands.get(commandId);
391
+ if (command?.callback !== callback)
392
+ throw disconnected();
393
+ if (command.requests.size >= 64) {
394
+ throw new AppError("Too many command requests", {
395
+ code: "HOST_FAILURE",
396
+ retryable: true,
397
+ });
398
+ }
399
+ if (input.signal?.aborted)
400
+ throw cancelled();
401
+ const requestId = newRequestId();
402
+ command.requests.add(requestId);
403
+ const cancel = () => {
404
+ this.#cancel(callback, requestId);
405
+ };
406
+ input.signal?.addEventListener("abort", cancel, { once: true });
407
+ try {
408
+ const invocation = {
409
+ commandId,
410
+ origin: input.origin,
411
+ requestId,
412
+ };
413
+ if (input.arguments !== undefined)
414
+ Object.assign(invocation, { arguments: input.arguments });
415
+ const outcome = await callRpc(callback.invoke(invocation), parseCommandResultRpc);
416
+ if (outcome.status === "failed")
417
+ throw new WhistleActionError(outcome.error.code, outcome.error.message, outcome.error.details);
418
+ return outcome.result;
419
+ }
420
+ finally {
421
+ input.signal?.removeEventListener("abort", cancel);
422
+ command.requests.delete(requestId);
423
+ }
424
+ }
425
+ #release(command) {
426
+ for (const requestId of command.requests)
427
+ this.#cancel(command.callback, requestId);
428
+ command.requests.clear();
429
+ disposeRpcValue(command.callback);
430
+ }
431
+ #releaseContribution() {
432
+ const contribution = this.#contribution;
433
+ this.#contribution = undefined;
434
+ if (contribution === undefined)
435
+ return;
436
+ try {
437
+ contribution.dispose();
438
+ }
439
+ catch (cause) {
440
+ console.warn("App host command contribution cleanup failed", cause);
441
+ }
442
+ }
443
+ }
444
+ class PendingHostMount {
445
+ options;
446
+ origin;
447
+ #gate;
448
+ #host;
449
+ #reject;
450
+ #resolve;
451
+ constructor(options, origin) {
452
+ this.options = options;
453
+ this.origin = origin;
454
+ this.#gate = createTerminalGate({
455
+ dispose: () => {
456
+ this.#disposeHost();
457
+ },
458
+ onDisconnect: options.handlers.onDisconnect,
459
+ reject: (error) => this.#reject?.(error),
460
+ release: () => {
461
+ this.#releaseMount();
462
+ },
463
+ });
464
+ }
465
+ start() {
466
+ return new Promise((resolve, reject) => {
467
+ this.#resolve = resolve;
468
+ this.#reject = reject;
469
+ this.options.hostWindow.addEventListener("message", this.#onMessage);
470
+ this.options.signal?.addEventListener("abort", this.#onAbort, {
471
+ once: true,
472
+ });
473
+ });
474
+ }
475
+ #disposeHost() {
476
+ try {
477
+ this.#host?.dispose();
478
+ }
479
+ finally {
480
+ this.#releaseMount();
481
+ }
482
+ }
483
+ #fail(error) {
484
+ this.#stopWaiting();
485
+ // Publication is a one-way door. A failure after it must use the
486
+ // terminal path so disposal and notification remain exactly-once.
487
+ if (this.#gate.isPublished())
488
+ this.#gate.unexpected();
489
+ else
490
+ this.#gate.failBeforePublication(error);
491
+ }
492
+ #onAbort = () => {
493
+ this.#fail(cancelled());
494
+ };
495
+ #onMessage = (event) => {
496
+ if (!isReadyMessage(event.data))
497
+ return;
498
+ performance.mark("fungi.app-open.host-ready-received");
499
+ if (event.source !== this.options.appWindow)
500
+ return;
501
+ if (event.origin !== this.origin) {
502
+ this.#fail(identityError());
503
+ return;
504
+ }
505
+ if (event.ports.length !== 0) {
506
+ this.#fail(protocolError("The app ready message transferred a port"));
507
+ return;
508
+ }
509
+ try {
510
+ parseReady(event.data);
511
+ this.#stopReady();
512
+ performance.mark("fungi.app-open.host-connect-sent");
513
+ const session = this.#openSession();
514
+ this.#postConnect(session.target.epoch, session.transfer);
515
+ this.#probe(session.client);
516
+ }
517
+ catch (cause) {
518
+ this.#fail(toProtocolError(cause));
519
+ }
520
+ };
521
+ #openSession() {
522
+ const channel = new MessageChannel();
523
+ const target = new MountedHost(this.options.launch, this.options.handlers, newEpoch());
524
+ const client = newMessagePortRpcSession(channel.port1, target);
525
+ this.#host = createHost({
526
+ client,
527
+ onBroken: () => {
528
+ this.#gate.unexpected();
529
+ },
530
+ onDisposed: () => {
531
+ this.#gate.release();
532
+ this.#releaseMount();
533
+ },
534
+ onGracefulDisconnect: () => this.#gate.graceful(),
535
+ port: channel.port1,
536
+ target,
537
+ });
538
+ target.onConnected(() => {
539
+ this.#publish();
540
+ });
541
+ return { client, port: channel.port1, target, transfer: channel.port2 };
542
+ }
543
+ #postConnect(epoch, transfer) {
544
+ this.options.appWindow.postMessage({
545
+ epoch,
546
+ protocolVersion: APP_PROTOCOL_VERSION,
547
+ type: APP_CONNECT_TYPE,
548
+ },
549
+ // Opaque sandbox origins cannot be named as a postMessage target. The
550
+ // ready door above still binds this transfer to the exact iframe Window.
551
+ this.origin === "null" ? "*" : this.origin, [transfer]);
552
+ }
553
+ #probe(client) {
554
+ void callRpc(client.updateContext(this.options.launch.context), parseVoidResult).catch((cause) => {
555
+ if (this.#gate.isPublished())
556
+ this.#gate.unexpected();
557
+ else
558
+ this.#fail(cause instanceof AppError ? cause : disconnected());
559
+ });
560
+ }
561
+ #publish() {
562
+ if (!this.#host || !this.#gate.publish())
563
+ return;
564
+ performance.mark("fungi.app-open.host-connected");
565
+ this.#stopWaiting();
566
+ this.#resolve?.(this.#host);
567
+ }
568
+ #releaseMount() {
569
+ if (liveMounts.get(this.options.appWindow) === this)
570
+ liveMounts.delete(this.options.appWindow);
571
+ }
572
+ #stopReady() {
573
+ this.options.hostWindow.removeEventListener("message", this.#onMessage);
574
+ }
575
+ #stopWaiting() {
576
+ this.#stopReady();
577
+ this.options.signal?.removeEventListener("abort", this.#onAbort);
578
+ }
579
+ }
580
+ /** The host-only adapter owns iframe authentication and the hidden Cap'n Web epoch. */
581
+ export function connectAppHost(options) {
582
+ const origin = expectedOrigin(options.expectedAppOrigin);
583
+ if (!origin)
584
+ return Promise.reject(invalidOrigin());
585
+ if (options.signal?.aborted)
586
+ return Promise.reject(cancelled());
587
+ if (liveMounts.has(options.appWindow))
588
+ return Promise.reject(alreadyMounted());
589
+ const mount = new PendingHostMount(options, origin);
590
+ liveMounts.set(options.appWindow, mount);
591
+ return mount.start();
592
+ }
593
+ function alreadyMounted() {
594
+ return new AppError("An app host is already live for this iframe", {
595
+ code: "IDENTITY",
596
+ retryable: false,
597
+ });
598
+ }
599
+ function cancelled() {
600
+ return new AppError("App host connection was cancelled", {
601
+ code: "CANCELLED",
602
+ retryable: false,
603
+ });
604
+ }
605
+ function createFactUpdater(assertLive) {
606
+ return async (send, fact, parse) => {
607
+ assertLive();
608
+ await callRpc(send(parse(fact)), parseVoidResult);
609
+ };
610
+ }
611
+ function createHost(options) {
612
+ let disposed = false;
613
+ const assertLive = () => {
614
+ if (disposed)
615
+ throw disconnected();
616
+ };
617
+ const dispose = createHostDisposer(options, () => disposed, (value) => {
618
+ disposed = value;
619
+ });
620
+ const onBroken = () => {
621
+ try {
622
+ options.onBroken();
623
+ }
624
+ finally {
625
+ dispose();
626
+ }
627
+ };
628
+ options.target.onFatal(onBroken);
629
+ options.client.onRpcBroken(onBroken);
630
+ const update = createFactUpdater(assertLive);
631
+ return Object.freeze({
632
+ disconnect: (error) => disconnectHost(error, assertLive, options, dispose),
633
+ dispose,
634
+ requestClose: async () => {
635
+ assertLive();
636
+ return await callRpc(options.client.requestClose(), parseCloseDecisionResult);
637
+ },
638
+ requestBack: async () => {
639
+ assertLive();
640
+ return await callRpc(options.client.requestBack(), parseBackDecisionResult);
641
+ },
642
+ activateStatus: async (action) => {
643
+ assertLive();
644
+ return await callRpc(options.client.activateStatus(parseStatusAction(action)), parseBackDecisionResult);
645
+ },
646
+ updateContext: (fact) => update((fact) => options.client.updateContext(fact), fact, parseContextFact),
647
+ reconfigure: async (value) => {
648
+ assertLive();
649
+ const context = parseContextFact(value.context);
650
+ const capabilities = {
651
+ capabilities: value.capabilities,
652
+ context,
653
+ };
654
+ await callRpc(options.client.reconfigure(capabilities), parseVoidResult);
655
+ },
656
+ updateLifecycle: (fact) => update((fact) => options.client.updateLifecycle(fact), fact, parseHostLifecycleFact),
657
+ updateReservedChords: (fact) => update((fact) => options.client.updateReservedChords(fact), fact, parseReservedChordsFact),
658
+ updateTheme: (fact) => update((fact) => options.client.updateTheme(fact), fact, parseThemeFact),
659
+ });
660
+ }
661
+ function createHostDisposer(options, isDisposed, setDisposed) {
662
+ return () => {
663
+ if (isDisposed())
664
+ return;
665
+ setDisposed(true);
666
+ // Mark the private epoch terminal before closing transport; Cap'n Web may
667
+ // synchronously report the close as a break.
668
+ options.onDisposed();
669
+ try {
670
+ options.target[Symbol.dispose]();
671
+ }
672
+ finally {
673
+ disposeRpcValue(options.client);
674
+ options.port.close();
675
+ }
676
+ };
677
+ }
678
+ function disconnected() {
679
+ return new AppError("The app host is disconnected", {
680
+ code: "DISCONNECTED",
681
+ retryable: true,
682
+ });
683
+ }
684
+ function duplicateHandler(value) {
685
+ if (!Schema.is(retainedCommandHandlerSchema)(value)) {
686
+ throw new AppError("Command callback is invalid", {
687
+ code: "PROTOCOL",
688
+ retryable: false,
689
+ });
690
+ }
691
+ return value.dup();
692
+ }
693
+ function expectedOrigin(value) {
694
+ if (value === "null")
695
+ return value;
696
+ if (!URL.canParse(value))
697
+ return undefined;
698
+ const origin = new URL(value).origin;
699
+ return origin === value ? origin : undefined;
700
+ }
701
+ const isReadyMessage = Schema.is(Schema.Struct({ type: Schema.Literal(APP_READY_TYPE) }));
702
+ function identityError() {
703
+ return new AppError("The app connection identity is invalid", {
704
+ code: "IDENTITY",
705
+ retryable: false,
706
+ });
707
+ }
708
+ function invalidOrigin() {
709
+ return new AppError("expectedAppOrigin must be an origin", {
710
+ code: "INVALID_ARGUMENT",
711
+ retryable: false,
712
+ });
713
+ }
714
+ function newEpoch() {
715
+ return randomToken(24);
716
+ }
717
+ function newRequestId() {
718
+ return randomToken(18);
719
+ }
720
+ function protocolError(message) {
721
+ return new AppError(message, { code: "PROTOCOL", retryable: false });
722
+ }
723
+ function randomToken(bytesLength) {
724
+ const bytes = new Uint8Array(bytesLength);
725
+ crypto.getRandomValues(bytes);
726
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
727
+ }
728
+ function toProtocolError(cause) {
729
+ return cause instanceof AppError
730
+ ? cause
731
+ : protocolError("Unable to establish the app host");
732
+ }