@agentshouse/app-bridge 0.1.0-alpha.9 → 0.1.0-c094380be07ea

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 (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +21 -79
  3. package/dist/calls.d.ts +117 -0
  4. package/dist/calls.js +1 -0
  5. package/dist/index.d.ts +152 -0
  6. package/dist/index.js +1 -0
  7. package/dist/mdmodel/blocks.d.ts +10 -0
  8. package/dist/mdmodel/condition.d.ts +149 -0
  9. package/dist/mdmodel/definition.d.ts +65 -0
  10. package/dist/mdmodel/divide.d.ts +2 -0
  11. package/dist/mdmodel/document.d.ts +15 -0
  12. package/dist/mdmodel/fields.d.ts +5 -0
  13. package/dist/mdmodel/index.d.ts +18 -0
  14. package/dist/mdmodel/paths.d.ts +17 -0
  15. package/dist/mdmodel/profile.d.ts +5 -0
  16. package/dist/mdmodel/project.d.ts +20 -0
  17. package/dist/mdmodel/references.d.ts +3 -0
  18. package/dist/mdmodel/types.d.ts +143 -0
  19. package/dist/mdmodel/validate.d.ts +7 -0
  20. package/dist/mdmodel/version.d.ts +1 -0
  21. package/dist/mdmodel/yaml.d.ts +55 -0
  22. package/dist/mdruntime/acquisition.d.ts +13 -0
  23. package/dist/mdruntime/adapter.d.ts +477 -0
  24. package/dist/mdruntime/binary.d.ts +22 -0
  25. package/dist/mdruntime/change-set.d.ts +34 -0
  26. package/dist/mdruntime/change.d.ts +88 -0
  27. package/dist/mdruntime/client.d.ts +81 -0
  28. package/dist/mdruntime/collection.d.ts +38 -0
  29. package/dist/mdruntime/connection.d.ts +138 -0
  30. package/dist/mdruntime/conversation.d.ts +103 -0
  31. package/dist/mdruntime/durability.d.ts +14 -0
  32. package/dist/mdruntime/failure.d.ts +12 -0
  33. package/dist/mdruntime/file.d.ts +19 -0
  34. package/dist/mdruntime/model.d.ts +56 -0
  35. package/dist/mdruntime/observation.d.ts +29 -0
  36. package/dist/mdruntime/open.d.ts +1 -0
  37. package/dist/mdruntime/outcome.d.ts +46 -0
  38. package/dist/mdruntime/overlay.d.ts +26 -0
  39. package/dist/mdruntime/query.d.ts +246 -0
  40. package/dist/mdruntime/reach.d.ts +45 -0
  41. package/dist/mdruntime/readiness.d.ts +52 -0
  42. package/dist/mdruntime/recovery.d.ts +24 -0
  43. package/dist/mdruntime/replica.d.ts +45 -0
  44. package/dist/mdruntime/room-draft.d.ts +143 -0
  45. package/dist/mdruntime/rows.d.ts +36 -0
  46. package/dist/mdruntime/save.d.ts +63 -0
  47. package/dist/mdruntime/search.d.ts +83 -0
  48. package/dist/mdruntime/shared.d.ts +10 -0
  49. package/dist/mdruntime/status.d.ts +26 -0
  50. package/dist/mdruntime/store.d.ts +159 -0
  51. package/dist/mdruntime/transfer.d.ts +9 -0
  52. package/dist/mdruntime/turn.d.ts +6 -0
  53. package/dist/mdruntime/wire.d.ts +104 -0
  54. package/dist/test-host.d.ts +33 -0
  55. package/dist/test-host.js +200 -0
  56. package/docs/families.md +136 -0
  57. package/package.json +11 -14
  58. package/client/src/bridge.d.ts +0 -35
  59. package/client/src/bridge.js +0 -229
  60. package/client/src/index.d.ts +0 -3
  61. package/client/src/index.js +0 -2
  62. package/client/src/test-host.d.ts +0 -30
  63. package/client/src/test-host.js +0 -159
  64. package/client/src/wire.d.ts +0 -69
  65. package/client/src/wire.js +0 -1
  66. package/src/apps/remainder.d.ts +0 -2
  67. package/src/apps/remainder.js +0 -16
  68. package/src/apps/tab.d.ts +0 -6
  69. package/src/apps/tab.js +0 -27
@@ -0,0 +1,136 @@
1
+ # The App bridge's families
2
+
3
+ - **Navigation** - the part of the shell's URL below the App's path, with its
4
+ query and fragment. The shell delivers it when the App starts and whenever the
5
+ User navigates; the App asks the shell to push or replace it. The App also
6
+ sets its tab's title and icon with `setTab({ title, icon })`, either or both:
7
+ a title of at most 200 characters and an icon given as a base64 `data:` URL
8
+ of an SVG, PNG, GIF or WebP image of at most 1,048,576 characters. The shell
9
+ shows them over the App's display name, or a Site's title, and its tab icon
10
+ until the next load, and ignores a request outside those bounds.
11
+ - **Ceremony** - the App asks the shell to leave for one of House's own
12
+ purpose-bound authenticated pages. The shell navigates only to a same-origin
13
+ absolute path, carries its current address as the return target, and the page
14
+ returns there when it completes. An App names an address House gave it in a
15
+ reading and composes none.
16
+ - **Installation** - `install()` offers the page as a Home-screen app. Where the
17
+ browser has an install prompt ready the shell shows it and the call resolves
18
+ `'accepted'` or `'dismissed'` with the User's choice, spending that prompt;
19
+ otherwise, as on every iPhone and iPad, it resolves `'manual'` and the App
20
+ shows its own instructions. A prompt the browser refuses to show, as for a
21
+ call made without a click, rejects with an error named and coded by the
22
+ browser's own error name, with `retryable` false, and stays ready. Call it from a click. The standard
23
+ `display-mode: standalone` media query tells the App it runs installed.
24
+ - **Notifications** - House's own panel alone; an App's or a Site's call
25
+ rejects with an error named `HouseCeremonyRefused`, carrying `code` and `retryable`. `notifications()` answers this browser
26
+ storage's push state: `'on'`, `'off'`, `'blocked'` where the browser refuses
27
+ notifications, or `'unavailable'` where it has no push, as on an iPhone or
28
+ iPad outside a Home-screen app. `notifications(true)` asks the browser's
29
+ permission and subscribes, `notifications(false)` unsubscribes, and each
30
+ answers the state it leaves; a failure of the browser's push service, and
31
+ of `setBadge`, rejects the same way. Call `notifications(true)` from a
32
+ click. `setBadge(unread)` takes the identifiers of the Human Notifications
33
+ unread in this storage and sets the icon's badge to their number, or clears
34
+ it at none, where the browser shows one.
35
+ - **Status** - the App reference the runtime client is attached under,
36
+ `preferences`: the `roomRef` and Path of the App's own preferences document,
37
+ or `null` until the runtime holds that Room, sync
38
+ state, credential state and its `expiresAt`, observed failure, `analytics`:
39
+ whether the page measures and the visitor's stored analytics choice, and
40
+ `serviceMode`: `open`, `write-halted` while House refuses writes, so the
41
+ App can stop retrying them, or `null` while the runtime holds no House
42
+ connection. Every status field is camelCase. `onStatus(listener)` and
43
+ `onAddress(listener)` hand a listener the status and the address the bridge
44
+ already holds, then each change.
45
+ The App reports the visitor's choice with `analyticsConsent('accept')` or
46
+ `analyticsConsent('decline')`; the shell hands it to House's analytics loader, and
47
+ the choice never reaches House.
48
+ - **Site image read** - given a Published Room and an original attachment
49
+ reference, it answers the web copy's path and metadata, or `null` when no
50
+ copy exists. The original bytes never cross this bridge at a Site.
51
+ - **App release code** - `sourceTop(app, release)` answers the top level of
52
+ the latest successful release of an App the User may use: its folders, its
53
+ files with their sizes and its root README's text, or `null` for none;
54
+ `sourceDownload(app, release)` answers every file of that release with its
55
+ path, its size and its `text`, or its bytes as `base64`. The browser keeps
56
+ each answer per App and release: a read naming a release it holds answers
57
+ without asking House, and keeping a newer release drops the earlier one. A
58
+ refusal is House's `{ error: { code, retryable } }`, `app_release_not_found` for an
59
+ App with no release; a read House does not answer, or one that outlived
60
+ the browser's signed-in User, is `null`.
61
+ - **External calls** - `callExternal({ url, method, headers, body, slot })`
62
+ has House's server send an HTTPS request to one of the release's declared
63
+ external origins and resolves with the service's `status`, `headers` and its
64
+ body as `body` text or `body_base64`, or a refusal as House's
65
+ `{ error: { code, retryable } }`, or `null` when House does not answer. House fills in the
66
+ App and its running release; naming an account slot places the key of the
67
+ account the User connected to it, which the App never sees, and naming none
68
+ sends no key. A Site's call names no slot, and the House panel names an
69
+ `account` instead. `body_base64` replaces `body` for bytes.
70
+ - **App messages** - `messages(listener)` hands the listener each message
71
+ another holder of this App sends to this User's running copies, as `from`,
72
+ the sender's User id, and `body`, the JSON value they sent, until the
73
+ function it returns is called. App code sends one through the Runtime's
74
+ `call('message.send', { to, body })` with the recipient's User id and
75
+ a body of at most 32 KiB of JSON; House fills in the App and relays it only
76
+ to copies running now, storing nothing. The panel sends none.
77
+ - **App screenshot read** - given an App reference, `readScreenshot(app)`
78
+ answers the image this browser captured of that App's screen the first time
79
+ it ran its current release, as a PNG `Blob` at most 1200 pixels wide, or
80
+ `null` when this browser holds none. The image stays in the browser; a Site
81
+ holds none.
82
+ - **Site screenshot read** - given one of the User's Site identifiers,
83
+ `readSiteScreenshot(site)` answers the image House captured of that Site
84
+ after its latest release, as a JPEG `Blob`, or `null` when House holds none
85
+ or does not answer. A Site's own renderer reads none.
86
+
87
+ - **Cooperation** - `cooperate({ checkpoint, persist, capture, restore,
88
+ ready, settle })` lets House activate a newer release of the App while it
89
+ runs, keeping the User's task; the function it returns withdraws it.
90
+ Register it in the entry script, before the first draw, or House treats the
91
+ page as not cooperating and the newer release waits for the App's next
92
+ open. When a newer release is stored, House calls `checkpoint()`, which
93
+ resolves at the App's next safe checkpoint and may never resolve, then
94
+ `persist()`, which makes authored work durable, then `capture()`, which
95
+ answers the meaningful task the App owns, such as the selected record,
96
+ filters, cursor, selection and scroll position, as JSON of at most 65,536
97
+ characters; House keeps the address itself. In the newer release House
98
+ calls `restore(task)`, or `restore(null)` when it opens the release without
99
+ a task, then `ready()`, the App's own check that its local data and
100
+ capabilities work, and finally `settle('committed')` once the release is
101
+ active or `settle('abandoned')` when House returns to the release before
102
+ it, restoring the same task there. A release House opens already active is
103
+ settled `'committed'` at once, after `restore(task)` when House hands it a
104
+ task, and a cooperation registered after House settled is settled at its
105
+ registration. Until `settle('committed')` House fails the release's outside
106
+ calls at once, as offline, those of the workers it starts included, and
107
+ refuses its Saves, write acts and external calls with the retryable code
108
+ `house_trial_withheld`, so a start-up effect is not repeated; run start-up
109
+ effects after `settle('committed')`.
110
+
111
+ ## The test host
112
+
113
+ `createHouseBridgeTestHost(options)` plays House in one page: it installs a
114
+ bridge object that `houseBridge()` returns, until the next host replaces it.
115
+ It delivers the address the App starts at and each address `open` plays as
116
+ Back or Forward, admitting nothing, so it delivers `//notes` exactly as a
117
+ browser at `/apps/<app>//notes` does. It records every push and replace and
118
+ answers each with the address the shell would then show, records an admitted
119
+ ceremony, and refuses a push, replace or ceremony that is not a same-origin
120
+ absolute path of at most 1,000 characters with an error named
121
+ `HouseNavigationRefused` or `HouseCeremonyRefused`. It publishes a status,
122
+ its `serviceMode` included, records in `consents` each `analyticsConsent` it
123
+ takes while that status measures and publishes the status the choice leaves,
124
+ records in `tabs` each `setTab`, answers a release code read from the
125
+ `sources` it was given, or `null`, answers each `callExternal` from the
126
+ `external` function it was given, or `null`, and records the request in
127
+ `externalCalls`, answers `readScreenshot` from the `screenshots` it was given
128
+ by App reference and `readSiteScreenshot` from the `siteScreenshots` it was
129
+ given by Site identifier, answers every `install()` `'manual'`, as a browser
130
+ with no install prompt does, answers every `notifications()` `'unavailable'`,
131
+ as a browser without push does, settles every `setBadge`, delivers each App
132
+ message `message({ from, body })` plays, and hands `bridge.runtime` the MD
133
+ Runtime client `attachRuntime(client)` gives it. `checkpoint()` runs the
134
+ registered cooperation's checkpoint, persist and capture and answers the task,
135
+ `restore(task)` runs its restore and answers its readiness check, and
136
+ `settle(activation)` hands it the outcome.
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@agentshouse/app-bridge",
3
- "version": "0.1.0-alpha.9",
4
- "description": "Renderer-side client of the agents.house App bridge: navigation, the tab title and icon, ceremonies, installation, status, Site image reads, App release code and App screenshot reads, with an in-page test host.",
5
- "license": "UNLICENSED",
3
+ "version": "0.1.0-c094380be07ea",
4
+ "description": "Types of the agents.house App bridge House hands every App: navigation, the tab title and icon, ceremonies, installation, notifications and the icon badge, status, Site image reads, App release code, App and Site screenshot reads, and App messages, with an accessor and an in-page test host.",
5
+ "license": "MIT",
6
6
  "type": "module",
7
7
  "sideEffects": false,
8
8
  "engines": {
@@ -10,21 +10,18 @@
10
10
  },
11
11
  "exports": {
12
12
  ".": {
13
- "types": "./client/src/index.d.ts",
14
- "default": "./client/src/index.js"
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
15
  },
16
16
  "./test-host": {
17
- "types": "./client/src/test-host.d.ts",
18
- "default": "./client/src/test-host.js"
17
+ "types": "./dist/test-host.d.ts",
18
+ "default": "./dist/test-host.js"
19
19
  }
20
20
  },
21
21
  "files": [
22
- "client/src/*.js",
23
- "client/src/*.d.ts",
24
- "src/apps/remainder.js",
25
- "src/apps/remainder.d.ts",
26
- "src/apps/tab.js",
27
- "src/apps/tab.d.ts",
28
- "README.md"
22
+ "dist",
23
+ "docs",
24
+ "README.md",
25
+ "LICENSE"
29
26
  ]
30
27
  }
@@ -1,35 +0,0 @@
1
- import type { HouseBridgePort, HouseGaChoice, HouseMessagePort, HouseSourceDownload, HouseSourceRefusal, HouseSourceTop, HouseStatus, HouseTab } from './wire.js';
2
- export declare class HouseCeremonyRefused extends Error {
3
- readonly path: string | null;
4
- constructor(path: string | null);
5
- }
6
- export declare class HouseNavigationRefused extends Error {
7
- readonly address: string | null;
8
- constructor(address: string | null);
9
- }
10
- export interface HouseImageCopy {
11
- readonly attachment: string;
12
- readonly copy: string;
13
- readonly name: string;
14
- readonly media_type: string;
15
- }
16
- type HouseInstallation = 'accepted' | 'dismissed' | 'manual';
17
- export interface HouseBridge {
18
- readonly runtime: Promise<HouseMessagePort>;
19
- address(): string | null;
20
- onAddress(listener: (address: string) => void): () => void;
21
- push(address: string): Promise<string>;
22
- replace(address: string): Promise<string>;
23
- ceremony(path: string): Promise<never>;
24
- install(): Promise<HouseInstallation>;
25
- setTab(tab: HouseTab): void;
26
- readAttachment(room: string, attachment: string): Promise<HouseImageCopy | null>;
27
- sourceTop(app: string, release?: string): Promise<HouseSourceTop | HouseSourceRefusal | null>;
28
- sourceDownload(app: string, release?: string): Promise<HouseSourceDownload | HouseSourceRefusal | null>;
29
- readScreenshot(app: string): Promise<Blob | null>;
30
- status(): HouseStatus | null;
31
- onStatus(listener: (status: HouseStatus) => void): () => void;
32
- gaConsent(choice: HouseGaChoice): void;
33
- }
34
- export declare function connectHouseBridge(source?: Promise<HouseBridgePort>): HouseBridge;
35
- export {};
@@ -1,229 +0,0 @@
1
- export class HouseCeremonyRefused extends Error {
2
- path;
3
- constructor(path) {
4
- super('house_ceremony_refused');
5
- this.name = 'HouseCeremonyRefused';
6
- this.path = path;
7
- }
8
- }
9
- export class HouseNavigationRefused extends Error {
10
- address;
11
- constructor(address) {
12
- super('house_navigation_refused');
13
- this.name = 'HouseNavigationRefused';
14
- this.address = address;
15
- }
16
- }
17
- function settled() {
18
- let settle = null;
19
- const promise = new Promise((resolve, reject) => {
20
- settle = { resolve, reject };
21
- });
22
- return { promise, settle: settle };
23
- }
24
- function heldBridge() {
25
- const held = globalThis.houseBridge;
26
- if (held === undefined)
27
- throw new Error('house_bridge_absent');
28
- return held;
29
- }
30
- export function connectHouseBridge(source = heldBridge()) {
31
- const runtime = settled();
32
- const ceremonies = [];
33
- const navigations = [];
34
- const attachments = new Map();
35
- let nextAttachment = 0;
36
- const sources = new Map();
37
- let nextSource = 0;
38
- const installations = new Map();
39
- let nextInstallation = 0;
40
- const screenshots = new Map();
41
- let nextScreenshot = 0;
42
- const addressListeners = new Set();
43
- const statusListeners = new Set();
44
- let outbox = [];
45
- let held = null;
46
- let address = null;
47
- let status = null;
48
- function send(message) {
49
- if (held === null)
50
- outbox?.push(message);
51
- else
52
- held.postMessage(message);
53
- }
54
- function receive(event) {
55
- const data = event.data;
56
- if (data === null || typeof data !== 'object')
57
- return;
58
- if (data.house === 'runtime') {
59
- const [port] = event.ports;
60
- if (port !== undefined)
61
- runtime.settle.resolve(port);
62
- return;
63
- }
64
- if (data.house === 'status') {
65
- status = data.status;
66
- for (const listener of statusListeners)
67
- listener(status);
68
- return;
69
- }
70
- if (data.house === 'address') {
71
- address = String(data.address);
72
- if (data.navigated === true)
73
- navigations.shift()?.settle.resolve(address);
74
- for (const listener of addressListeners)
75
- listener(address);
76
- return;
77
- }
78
- if (data.house === 'navigate-refused') {
79
- const refused = data.address;
80
- const named = typeof refused === 'string' ? refused : null;
81
- const index = named === null ? 0 : navigations.findIndex((one) => one.address === named);
82
- const [pending] = navigations.splice(index < 0 ? 0 : index, 1);
83
- pending?.settle.reject(new HouseNavigationRefused(named));
84
- return;
85
- }
86
- if (data.house === 'attachment') {
87
- const answer = data;
88
- if (typeof answer.id !== 'number')
89
- return;
90
- const pending = attachments.get(answer.id);
91
- if (pending === undefined)
92
- return;
93
- attachments.delete(answer.id);
94
- pending.resolve(answer.image ?? null);
95
- return;
96
- }
97
- if (data.house === 'install') {
98
- const answer = data;
99
- if (typeof answer.id !== 'number')
100
- return;
101
- const pending = installations.get(answer.id);
102
- if (pending === undefined)
103
- return;
104
- installations.delete(answer.id);
105
- if (answer.outcome !== undefined)
106
- pending.resolve(answer.outcome);
107
- else
108
- pending.reject(new DOMException(answer.refusal?.message, answer.refusal?.name));
109
- return;
110
- }
111
- if (data.house === 'source') {
112
- const answer = data;
113
- if (typeof answer.id !== 'number')
114
- return;
115
- const pending = sources.get(answer.id);
116
- if (pending === undefined)
117
- return;
118
- sources.delete(answer.id);
119
- pending.resolve(answer.answer ?? null);
120
- return;
121
- }
122
- if (data.house === 'screenshot') {
123
- const answer = data;
124
- if (typeof answer.id !== 'number')
125
- return;
126
- const pending = screenshots.get(answer.id);
127
- if (pending === undefined)
128
- return;
129
- screenshots.delete(answer.id);
130
- pending.resolve(answer.image ?? null);
131
- return;
132
- }
133
- if (data.house === 'ceremony-refused') {
134
- const refused = data.path;
135
- const named = typeof refused === 'string' ? refused : null;
136
- const index = named === null ? 0 : ceremonies.findIndex((one) => one.path === named);
137
- const [pending] = ceremonies.splice(index < 0 ? 0 : index, 1);
138
- pending?.settle.reject(new HouseCeremonyRefused(named));
139
- }
140
- }
141
- void source.then((port) => {
142
- held = port;
143
- port.addEventListener('message', receive);
144
- port.start();
145
- const waiting = outbox ?? [];
146
- outbox = null;
147
- for (const message of waiting)
148
- port.postMessage(message);
149
- });
150
- function readSource(house, app, release) {
151
- const pending = settled();
152
- const id = ++nextSource;
153
- sources.set(id, pending.settle);
154
- send({ house, id, app, release });
155
- return pending.promise;
156
- }
157
- function navigate(mode, asked) {
158
- const pending = settled();
159
- navigations.push({ address: asked, settle: pending.settle });
160
- send({ house: 'navigate', mode, address: asked });
161
- return pending.promise;
162
- }
163
- return {
164
- runtime: runtime.promise,
165
- address() {
166
- return address;
167
- },
168
- onAddress(listener) {
169
- addressListeners.add(listener);
170
- return () => {
171
- addressListeners.delete(listener);
172
- };
173
- },
174
- push(asked) {
175
- return navigate('push', asked);
176
- },
177
- replace(asked) {
178
- return navigate('replace', asked);
179
- },
180
- ceremony(path) {
181
- const pending = settled();
182
- ceremonies.push({ path, settle: pending.settle });
183
- send({ house: 'ceremony', path });
184
- return pending.promise;
185
- },
186
- install() {
187
- const pending = settled();
188
- const id = ++nextInstallation;
189
- installations.set(id, pending.settle);
190
- send({ house: 'install', id });
191
- return pending.promise;
192
- },
193
- setTab(tab) {
194
- send({ house: 'tab', title: tab.title, icon: tab.icon });
195
- },
196
- readAttachment(room, attachment) {
197
- const pending = settled();
198
- const id = ++nextAttachment;
199
- attachments.set(id, pending.settle);
200
- send({ house: 'attachment-read', id, room, attachment });
201
- return pending.promise;
202
- },
203
- sourceTop(app, release) {
204
- return readSource('source-top', app, release);
205
- },
206
- sourceDownload(app, release) {
207
- return readSource('source-download', app, release);
208
- },
209
- readScreenshot(app) {
210
- const pending = settled();
211
- const id = ++nextScreenshot;
212
- screenshots.set(id, pending.settle);
213
- send({ house: 'screenshot-read', id, app });
214
- return pending.promise;
215
- },
216
- status() {
217
- return status;
218
- },
219
- onStatus(listener) {
220
- statusListeners.add(listener);
221
- return () => {
222
- statusListeners.delete(listener);
223
- };
224
- },
225
- gaConsent(choice) {
226
- send({ house: 'ga/consent', choice });
227
- },
228
- };
229
- }
@@ -1,3 +0,0 @@
1
- export { connectHouseBridge, HouseCeremonyRefused, HouseNavigationRefused, type HouseBridge, type HouseImageCopy, } from './bridge.js';
2
- export { HOUSE_ADDRESS_MAX_CHARS } from './wire.js';
3
- export type { HouseBridgeEvent, HouseBridgePort, HouseCredentialState, HouseGa, HouseGaChoice, HouseMessagePort, HouseNavigationMode, HouseSourceDownload, HouseSourceFile, HouseSourceRefusal, HouseSourceTop, HouseStatus, HouseSyncState, } from './wire.js';
@@ -1,2 +0,0 @@
1
- export { connectHouseBridge, HouseCeremonyRefused, HouseNavigationRefused, } from './bridge.js';
2
- export { HOUSE_ADDRESS_MAX_CHARS } from './wire.js';
@@ -1,30 +0,0 @@
1
- import type { HouseImageCopy } from './bridge.js';
2
- import type { HouseBridgePort, HouseGaChoice, HouseMessagePort, HouseNavigationMode, HouseSourceDownload, HouseSourceTop, HouseStatus, HouseTab } from './wire.js';
3
- export interface HouseBridgeNavigation {
4
- readonly mode: HouseNavigationMode;
5
- readonly address: string;
6
- }
7
- export interface HouseBridgeTestHost {
8
- readonly port: Promise<HouseBridgePort>;
9
- readonly navigations: readonly HouseBridgeNavigation[];
10
- readonly ceremonies: readonly string[];
11
- readonly consents: readonly HouseGaChoice[];
12
- readonly tabs: readonly HouseTab[];
13
- address(): string;
14
- open(address: string): void;
15
- publish(status: HouseStatus): void;
16
- attachRuntime(runtime: HouseMessagePort): void;
17
- }
18
- export interface HouseBridgeTestHostOptions {
19
- readonly address?: string;
20
- readonly status?: HouseStatus;
21
- readonly images?: readonly (HouseImageCopy & {
22
- readonly room: string;
23
- })[];
24
- readonly sources?: readonly {
25
- readonly top: HouseSourceTop;
26
- readonly download: HouseSourceDownload;
27
- }[];
28
- readonly screenshots?: Readonly<Record<string, Blob>>;
29
- }
30
- export declare function createHouseBridgeTestHost(options?: HouseBridgeTestHostOptions): HouseBridgeTestHost;
@@ -1,159 +0,0 @@
1
- import { appAddressOf } from '../../src/apps/remainder.js';
2
- import { tabChange } from '../../src/apps/tab.js';
3
- import { HOUSE_ADDRESS_MAX_CHARS } from './wire.js';
4
- const HOST_ORIGIN = 'https://house.invalid';
5
- function sameOriginPath(value) {
6
- if (typeof value !== 'string')
7
- return null;
8
- if (value.charAt(0) !== '/' || value.slice(0, 2) === '//')
9
- return null;
10
- if (value.length > HOUSE_ADDRESS_MAX_CHARS)
11
- return null;
12
- const target = new URL(value, HOST_ORIGIN);
13
- return target.origin === HOST_ORIGIN ? `${target.pathname}${target.search}${target.hash}` : null;
14
- }
15
- export function createHouseBridgeTestHost(options = {}) {
16
- const navigations = [];
17
- const ceremonies = [];
18
- const consents = [];
19
- const tabs = [];
20
- const listeners = new Set();
21
- const queued = [];
22
- let started = false;
23
- let address = appAddressOf('/', options.address ?? '/');
24
- let status = options.status ?? null;
25
- function deliver(data, ports = []) {
26
- const event = { data, ports };
27
- if (!started) {
28
- queued.push(event);
29
- return;
30
- }
31
- queueMicrotask(() => {
32
- for (const listener of listeners)
33
- listener(event);
34
- });
35
- }
36
- function arrive(target, navigated) {
37
- address = target;
38
- deliver({ house: 'address', address, navigated });
39
- }
40
- function navigate(mode, asked) {
41
- const target = sameOriginPath(asked);
42
- if (target === null) {
43
- deliver({
44
- house: 'navigate-refused',
45
- address: typeof asked === 'string' ? asked : null,
46
- });
47
- return;
48
- }
49
- navigations.push({ mode: mode === 'replace' ? 'replace' : 'push', address: target });
50
- arrive(target, true);
51
- }
52
- function ceremony(path) {
53
- const target = sameOriginPath(path);
54
- if (target === null) {
55
- deliver({ house: 'ceremony-refused', path: typeof path === 'string' ? path : null });
56
- return;
57
- }
58
- ceremonies.push(target);
59
- }
60
- function publish(next) {
61
- status = next;
62
- deliver({ house: 'status', status });
63
- }
64
- function consent(choice) {
65
- if (status === null || !status.ga.measures)
66
- return;
67
- if (choice !== 'accept' && choice !== 'decline')
68
- return;
69
- consents.push(choice);
70
- publish({ ...status, ga: { measures: true, choice } });
71
- }
72
- function receive(message) {
73
- const data = message;
74
- if (data === null || typeof data !== 'object')
75
- return;
76
- if (data.house === 'navigate') {
77
- navigate(data.mode, data.address);
78
- }
79
- else if (data.house === 'ceremony') {
80
- ceremony(data.path);
81
- }
82
- else if (data.house === 'install') {
83
- deliver({ house: 'install', id: data.id, outcome: 'manual' });
84
- }
85
- else if (data.house === 'tab') {
86
- const change = tabChange(data);
87
- if (change !== null)
88
- tabs.push(change);
89
- }
90
- else if (data.house === 'ga/consent') {
91
- consent(data.choice);
92
- }
93
- else if (data.house === 'source-top' || data.house === 'source-download') {
94
- const asked = data;
95
- const source = options.sources?.find((entry) => entry.top.app_ref === asked.app);
96
- const answer = source === undefined ? null : data.house === 'source-top' ? source.top : source.download;
97
- deliver({ house: 'source', id: asked.id, answer });
98
- }
99
- else if (data.house === 'attachment-read') {
100
- const asked = data;
101
- const image = options.images?.find((entry) => entry.room === asked.room && entry.attachment === asked.attachment);
102
- deliver({
103
- house: 'attachment',
104
- id: asked.id,
105
- image: image === undefined
106
- ? null
107
- : {
108
- attachment: image.attachment,
109
- copy: image.copy,
110
- name: image.name,
111
- media_type: image.media_type,
112
- },
113
- });
114
- }
115
- else if (data.house === 'screenshot-read') {
116
- const asked = data;
117
- const held = typeof asked.app === 'string' ? options.screenshots?.[asked.app] : undefined;
118
- deliver({ house: 'screenshot', id: asked.id, image: held ?? null });
119
- }
120
- }
121
- const port = {
122
- postMessage(message) {
123
- receive(message);
124
- },
125
- addEventListener(_type, listener) {
126
- listeners.add(listener);
127
- },
128
- start() {
129
- started = true;
130
- const held = queued.splice(0);
131
- queueMicrotask(() => {
132
- for (const event of held) {
133
- for (const listener of listeners)
134
- listener(event);
135
- }
136
- });
137
- },
138
- };
139
- if (status !== null)
140
- deliver({ house: 'status', status });
141
- deliver({ house: 'address', address, navigated: false });
142
- return {
143
- port: Promise.resolve(port),
144
- navigations,
145
- ceremonies,
146
- consents,
147
- tabs,
148
- address() {
149
- return address;
150
- },
151
- open(asked) {
152
- arrive(appAddressOf('/', asked), false);
153
- },
154
- publish,
155
- attachRuntime(runtime) {
156
- deliver({ house: 'runtime' }, [runtime]);
157
- },
158
- };
159
- }