gesso-framework 0.6.29 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -0
- package/dist/{FunctionComponent-CHtojpp7.d.ts → FunctionComponent-Czkex8fp.d.ts} +358 -114
- package/dist/{serveChannels-BlEag7WU.js → WorkerPorts-m_hLRoPj.js} +8 -240
- package/dist/WorkerPorts-m_hLRoPj.js.map +1 -0
- package/dist/agent/index.d.ts +53 -2
- package/dist/agent/index.js +5 -4
- package/dist/agent/index.js.map +1 -1
- package/dist/app-De3z58x7.js +638 -0
- package/dist/app-De3z58x7.js.map +1 -0
- package/dist/computed-_cYcdczA.js +577 -0
- package/dist/computed-_cYcdczA.js.map +1 -0
- package/dist/createComponent-DJads88q.js +47 -0
- package/dist/createComponent-DJads88q.js.map +1 -0
- package/dist/{index-DZbSb8IN.d.ts → index-BENndtaH.d.ts} +222 -96
- package/dist/index.d.ts +4 -5
- package/dist/index.js +440 -6645
- package/dist/index.js.map +1 -1
- package/dist/jsx/jsx-runtime.d.ts +8 -5
- package/dist/jsx/jsx-runtime.js +15 -6
- package/dist/jsx/jsx-runtime.js.map +1 -1
- package/dist/{persisted-fUzIU8FZ.js → persisted-8meJmsOh.js} +4 -576
- package/dist/persisted-8meJmsOh.js.map +1 -0
- package/dist/remote/index.d.ts +2 -2
- package/dist/remote/index.js +2 -1
- package/dist/remote/index.js.map +1 -1
- package/dist/{remote-BoB8zh7z.js → remote-B4bjdTzg.js} +19 -4
- package/dist/remote-B4bjdTzg.js.map +1 -0
- package/dist/render-cydQ10Nd.js +6795 -0
- package/dist/render-cydQ10Nd.js.map +1 -0
- package/dist/serveChannels-DReCmRxO.js +241 -0
- package/dist/serveChannels-DReCmRxO.js.map +1 -0
- package/dist/{createComponent-3n1ZSql8.js → show-D0MevHaV.js} +2 -46
- package/dist/show-D0MevHaV.js.map +1 -0
- package/dist/{ui-CHtNccR8.d.ts → tools-BFBwuinE.d.ts} +351 -8
- package/dist/{webmcp-CdDq2nzS.js → webmcp-D7IlA0fu.js} +3 -3
- package/dist/webmcp-D7IlA0fu.js.map +1 -0
- package/dist/worker/index.d.ts +3 -4
- package/dist/worker/index.js +5 -3
- package/dist/worker/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/ChannelProtocol-ByNoHujM.d.ts +0 -247
- package/dist/app-Yig59Ygf.js +0 -326
- package/dist/app-Yig59Ygf.js.map +0 -1
- package/dist/createComponent-3n1ZSql8.js.map +0 -1
- package/dist/persisted-fUzIU8FZ.js.map +0 -1
- package/dist/remote-BoB8zh7z.js.map +0 -1
- package/dist/serveChannels-BlEag7WU.js.map +0 -1
- package/dist/webmcp-CdDq2nzS.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persisted-8meJmsOh.js","names":[],"sources":["../src/select.ts","../src/resource.ts","../src/mutate.ts","../src/debounce.ts","../src/channel/pick.ts","../src/storage/StorageAdapter.ts","../src/undo/UndoStack.ts","../src/undo/undoable.ts","../src/storage/OpfsStorage.ts","../src/storage/IndexedDbStorage.ts","../src/storage/persisted.ts"],"sourcesContent":["import type { Observable } from 'rxjs';\n\nimport { computed, type ComputedCell } from './computed';\nimport type { Equality } from './derive';\n\nexport interface SelectOptions<T> {\n /**\n * How a projected value is judged unchanged; `structural` by default,\n * because a projection nearly always builds a value rather than\n * passing one through, and a rebuilt equal one is not a change.\n */\n readonly equal?: Equality<T>;\n /** What to call the cell in a warning. */\n readonly label?: string;\n}\n\n/**\n * One field of a cell, or one projection of it, as a cell.\n *\n * const title = select(inputs.track, 'title');\n * const names = select(inputs.track, entry => entry?.tags ?? []);\n *\n * Reading four fields of one input costs four of these rather than four\n * `pipe(map(...), distinctUntilChanged(...))`, which is what the two\n * applications wrote by hand often enough to invent their own `field()`\n * and `text()` helpers for it.\n *\n * The source may be any Observable, not only a cell: `select` is a\n * `computed` over one source, and reads it the way a computed reads\n * anything. The comparison is structural by default, since the reason\n * for nearly every hand-written comparator is exactly this: a\n * projection that rebuilds an equal array or object each time should\n * not re-bind everything reading it.\n *\n * There is no `inputs.track.title` proxy. A cell is an Observable, so\n * its own members (`value`, `pipe`, `subscribe`, `source`) would shadow\n * the fields of anything projected through it, and a data type that\n * happens to have a field called `value` would read as the cell's\n * current value instead. `select` names the field explicitly and cannot\n * collide.\n */\nexport function select<T extends object, K extends keyof T>(\n source: Observable<T>,\n key: K,\n options?: SelectOptions<T[K]>\n): ComputedCell<T[K]>;\nexport function select<T, R>(\n source: Observable<T>,\n project: (value: T) => R,\n options?: SelectOptions<R>\n): ComputedCell<R>;\nexport function select(\n source: Observable<unknown>,\n keyOrProject: PropertyKey | ((value: unknown) => unknown),\n options: SelectOptions<unknown> = {}\n): ComputedCell<unknown> {\n const project =\n typeof keyOrProject === 'function'\n ? keyOrProject\n : // A stream that has not spoken yet has no value to read a field\n // off, and neither has a source that is legitimately null. Both\n // answer `undefined` rather than throwing on the first frame.\n (value: unknown) =>\n value === null || value === undefined ? undefined : (value as Record<PropertyKey, unknown>)[keyOrProject];\n return computed(read => project(read(source)), {\n equal: options.equal ?? 'structural',\n ...(options.label === undefined ? {} : { label: options.label })\n });\n}\n","import { Subscription, type Observable } from 'rxjs';\n\nimport type { ComputedCell } from './computed';\nimport type { ReadableCell } from './Input';\nimport { internalState, type InternalState } from './InternalState';\nimport { select } from './select';\n\n/**\n * What a request has come to, in the five words every screen needs.\n *\n * Chosen once so screens stop inventing them. The tree had three\n * enums saying nearly the same thing in different words\n * (`'loading' | 'ready' | 'empty' | 'failed'`,\n * `'idle' | 'loading' | 'ready' | 'missing' | 'failed'`,\n * `'idle' | 'searching' | 'done' | 'failed'`), which meant a screen\n * reading two of them had two vocabularies for one idea.\n *\n * - `idle`: nothing has been asked for. The key is `null`.\n * - `loading`: a request is out and there is nothing to show yet.\n * - `ready`: there is a value. A refresh may still be in the air, and\n * that is deliberately not a separate status: what is on screen is\n * real, and saying \"loading\" over it would be a lie.\n * - `missing`: the answer was that there is no such thing. A fetch\n * says so by resolving `null`.\n * - `failed`: the request could not be answered at all.\n *\n * `missing` and `failed` are reported only when there is nothing to\n * fall back on. A refresh that fails over a value already held leaves\n * it on screen and stays `ready`, because a stale answer is stale and\n * not wrong, and an empty page would be worse. The error is still\n * there to read.\n *\n * What is *not* here is `empty`. A list that loaded and has no rows is\n * `ready` with an empty array, which is a judgement about the value\n * rather than about the request, and the screen that cares makes it.\n */\nexport type ResourceStatus = 'idle' | 'loading' | 'ready' | 'missing' | 'failed';\n\n/**\n * A resource as one record, so it can be a channel view key.\n *\n * Plain data throughout: the error is its message rather than the\n * `Error`, because a message is what a screen shows and what survives\n * the barrier, and an `Error` is neither.\n */\nexport interface ResourceState<T> {\n readonly status: ResourceStatus;\n readonly value: T | null;\n readonly error: string | null;\n}\n\nexport interface ResourceOptions<K, T> {\n /**\n * What is already known for this key, shown at once while the\n * request runs behind it.\n *\n * This is the store-first rule in one function: a hit is on screen\n * synchronously and marked `ready` even though a request is on its\n * way, so a page opened twice is instant the second time and correct\n * a moment later. Answering `null` or `undefined` means nothing is\n * held, and the resource says `loading`.\n */\n readonly peek?: (key: K) => T | null | undefined;\n /** What to call this resource in a warning; optional. */\n readonly label?: string;\n}\n\n/**\n * A request, keyed, so a stale answer cannot win.\n *\n * The key says what to fetch, and every value the key source emits is\n * a request. An answer is published only if its request is still the\n * current one, which is the generation counter every screen that loads\n * anything was writing by hand, and the reason opening a page, going\n * back and opening another before the first answers does not end with\n * the first answer on screen.\n *\n * private readonly ref = internalState<PageRef | null>(null);\n * readonly page = resource(this.ref, ref => api.trackPage(ref));\n *\n * show(ref: PageRef | null): Promise<void> {\n * this.ref.value = ref;\n * return this.page.settled;\n * }\n *\n * A `null` key is \"nothing is being asked for\": the status is `idle`,\n * the value is `null`, and no fetch runs. That is what a screen showing\n * nothing yet actually means, and it saves every caller a branch.\n *\n * Unlike `computed`, a resource is eager: it follows its key from the\n * moment it is made, because a request is an effect and an effect that\n * waits for a subscriber is a request that never happens. It takes one\n * subscription to the key source for its whole life, however many\n * requests run through it, and `dispose()` gives that back.\n *\n * It is a helper and not a data layer. Nothing in the framework\n * requires one, a channel is reached exactly as it was, and an\n * application that would rather write its own is writing against the\n * same barrier this is written against.\n */\nexport class Resource<K, T> {\n /** The one cell everything else here is a projection of. */\n private readonly cell: InternalState<ResourceState<T>>;\n /** Bumped per request; an answer from an older one is dropped. */\n private requests = 0;\n private asked: K | null = null;\n private settling: Promise<void> = Promise.resolve();\n private readonly following: Subscription;\n\n /** Status, value and error as one record: what a channel view key takes. */\n readonly state: ReadableCell<ResourceState<T>>;\n readonly status: ComputedCell<ResourceStatus>;\n /** What is loaded, or `null` while there is nothing to show. */\n readonly value: ComputedCell<T | null>;\n /** Why the last request failed, as its message; `null` when it did not. */\n readonly error: ComputedCell<string | null>;\n\n constructor(\n key: Observable<K | null | undefined>,\n private readonly fetch: (key: K) => Promise<T | null>,\n private readonly options: ResourceOptions<K, T> = {}\n ) {\n this.cell = internalState<ResourceState<T>>({ status: 'idle', value: null, error: null }, options.label);\n this.state = this.cell;\n this.status = select(this.cell, 'status', named(options.label, 'status'));\n // Compared by identity rather than structurally: a fetch builds a\n // new value every time, and walking a page-sized object to\n // discover that is exactly the cost the differ already pays once.\n this.value = select(this.cell, 'value', { ...named(options.label, 'value'), equal: 'reference' });\n this.error = select(this.cell, 'error', named(options.label, 'error'));\n this.following = key.subscribe(next => this.request(next ?? null));\n }\n\n /** What is being asked for, for a caller that needs to guard on it. */\n get requested(): K | null {\n return this.asked;\n }\n\n /**\n * The request in the air, as a promise that resolves when it settles.\n *\n * Already resolved when nothing is in flight, so a caller that sets\n * the key and returns this reads as an ordinary async method. Each\n * request keeps its own promise, so a dropped one still resolves for\n * whoever is awaiting it; it simply changes nothing on the way.\n */\n get settled(): Promise<void> {\n return this.settling;\n }\n\n /**\n * Asks again for the same key, keeping what is on screen.\n *\n * This is the retry button. It does not clear the value the way a\n * new key does, because a person pressing retry is asking for the\n * thing they can already see to be brought up to date, and blanking\n * it first would be a worse answer than the stale one.\n */\n retry(): Promise<void> {\n if (this.asked === null) {\n return this.settling;\n }\n const generation = ++this.requests;\n const held = this.cell.value;\n if (held.value === null && held.status !== 'loading') {\n this.write({ status: 'loading', value: null, error: null });\n }\n this.settling = this.run(this.asked, generation);\n return this.settling;\n }\n\n /**\n * Replaces what is loaded, for a change made here rather than\n * fetched.\n *\n * A page of comments appended to the answer already held, an\n * optimistic edit: the resource holds the value, so something has to\n * be able to write it. It does not touch the request in flight, so a\n * refresh that lands afterwards still wins, which is what it should\n * do: it is the newer truth.\n */\n set(value: T): void {\n this.write({ status: 'ready', value, error: null });\n }\n\n /** Gives back the subscription to the key source. */\n dispose(): void {\n this.following.unsubscribe();\n }\n\n private request(key: K | null): void {\n this.asked = key;\n const generation = ++this.requests;\n if (key === null) {\n this.write({ status: 'idle', value: null, error: null });\n this.settling = Promise.resolve();\n return;\n }\n const held = this.options.peek?.(key) ?? null;\n this.write({ status: held === null ? 'loading' : 'ready', value: held, error: null });\n this.settling = this.run(key, generation);\n }\n\n private run(key: K, generation: number): Promise<void> {\n return this.fetch(key).then(\n answer => this.answered(generation, answer),\n (error: unknown) => this.refused(generation, error)\n );\n }\n\n private answered(generation: number, answer: T | null): void {\n if (generation !== this.requests) {\n return;\n }\n if (answer === null) {\n const held = this.cell.value.value;\n this.write({ status: held === null ? 'missing' : 'ready', value: held, error: null });\n return;\n }\n this.write({ status: 'ready', value: answer, error: null });\n }\n\n private refused(generation: number, error: unknown): void {\n if (generation !== this.requests) {\n return;\n }\n const held = this.cell.value.value;\n this.write({ status: held === null ? 'failed' : 'ready', value: held, error: messageOf(error) });\n }\n\n private write(next: ResourceState<T>): void {\n this.cell.value = next;\n }\n}\n\n/**\n * A keyed request with a status, a value, an error and a retry.\n *\n * const page = resource(ref, key => api.page(key), { peek: key => store.get(key) });\n * <Show when={computed(() => page.status.value === 'loading')}>{() => <Spinner />}</Show>\n *\n * See `Resource` for what each status means and when a stale value is\n * kept. The key is an Observable so that setting a cell is what asks\n * for something: `computed` and `internalState` both work, and so does\n * a channel view key or a router match.\n */\nexport function resource<K, T>(\n key: Observable<K | null | undefined>,\n fetch: (key: K) => Promise<T | null>,\n options: ResourceOptions<K, T> = {}\n): Resource<K, T> {\n return new Resource(key, fetch, options);\n}\n\nfunction named(base: string | undefined, part: string): { label?: string } {\n return base === undefined ? {} : { label: `${base}.${part}` };\n}\n\nfunction messageOf(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n","import { equalityOf, type Equality } from './derive';\nimport type { ReadableCell } from './Input';\nimport { internalState, type InternalState } from './InternalState';\n\nexport interface MutateOptions<T> {\n /**\n * How the rollback decides the cell still holds what was applied;\n * `structural` by default, because an optimistic change nearly\n * always builds a new list or a new record rather than passing one\n * through, so identity would never match and nothing would ever roll\n * back.\n */\n readonly equal?: Equality<T>;\n /** What to call the in-flight count in a warning; optional. */\n readonly label?: string;\n}\n\n/** A change that is made locally at once and confirmed afterwards. */\nexport interface Mutation<A> {\n /**\n * How many writes are in the air, for a saving indicator.\n *\n * A count rather than a flag, because two presses in quick\n * succession are two writes and a flag cleared by the first would\n * say the second had finished.\n */\n readonly pending: ReadableCell<number>;\n /**\n * Applies the change, commits it, and puts it back if the commit\n * refuses. Answers whether it stuck.\n */\n run(argument: A): Promise<boolean>;\n}\n\n/**\n * An optimistic change to a cell, with a rollback that does not fight\n * the person.\n *\n * private readonly favourites = internalState<readonly string[]>([]);\n * private readonly like = mutate(this.favourites, toggled, id => api.favourite(id));\n *\n * toggle(id: string): void {\n * void this.like.run(id);\n * }\n *\n * Three things happen and the order is the whole point. `apply` runs\n * first and writes the cell, so the screen changes on the press rather\n * than a round trip later. `commit` then does the real write. If it\n * rejects, or resolves `false`, the cell goes back to what it held\n * before.\n *\n * **The rollback is guarded.** It happens only while the cell still\n * holds exactly what `apply` wrote. Without that, a slow rejection\n * would fight a fast second press and the cell would end up saying the\n * opposite of the last thing anyone did, which is the guard every\n * optimistic screen writes by hand and half of them get wrong.\n *\n * The cell is named first, and not because it was first written\n * `mutate(apply, commit)`: the cell is what makes the guard possible.\n * A mutation handed only two functions can undo its own change but\n * cannot tell whether undoing it is still the right thing to do.\n *\n * A helper and not a data layer: nothing in the framework requires\n * one, and an application that would rather write the four lines out\n * is writing the same four lines this does.\n */\nexport function mutate<T, A = void>(\n cell: InternalState<T>,\n apply: (current: T, argument: A) => T,\n commit: (argument: A, applied: T) => Promise<unknown>,\n options: MutateOptions<T> = {}\n): Mutation<A> {\n const equal = equalityOf<T>(options.equal ?? 'structural');\n const inFlight = internalState(0, options.label);\n\n const revert = (before: T, applied: T): void => {\n if (!equal(cell.value, applied)) {\n // Changed again since, by whoever is using the screen. Their\n // change is newer than this answer and wins.\n return;\n }\n cell.value = before;\n };\n\n return {\n pending: inFlight,\n async run(argument: A): Promise<boolean> {\n const before = cell.value;\n const applied = apply(before, argument);\n cell.value = applied;\n inFlight.value = inFlight.value + 1;\n try {\n // `false` and a rejection mean the same thing: the write did\n // not happen. Anything else, `undefined` included, is a\n // commit that went through.\n if ((await commit(argument, applied)) === false) {\n revert(before, applied);\n return false;\n }\n return true;\n } catch {\n revert(before, applied);\n return false;\n } finally {\n inFlight.value = Math.max(0, inFlight.value - 1);\n }\n }\n };\n}\n","import {\n debounceTime,\n Observable,\n Subject,\n Subscription,\n tap,\n throttleTime,\n type MonoTypeOperatorFunction\n} from 'rxjs';\n\nimport { trackRead, type ReadableCell } from './Input';\n\n/**\n * A cell that lets its source through on a timer.\n *\n * The two operators below are the same machinery with a different\n * gate, and both are cells rather than plain streams on purpose: a\n * `computed` reads a cell with `.value` and follows it, so a debounced\n * search term composes with everything else in the dialect instead of\n * being the one value in a screen that has to be piped.\n *\n * It holds the last value the gate let through, which is what `.value`\n * answers while something is following it. With nothing following\n * there is no timer running to hold anything back, so `.value` reads\n * the source directly, which is the honest answer rather than a value\n * frozen at whatever moment the last follower left.\n *\n * A value equal to the one it already holds is not a change and is not\n * emitted, the same rule `computed` follows. Without it the first pass\n * of the gate after a subscription would repeat the value the\n * subscriber had just been handed.\n *\n * One subscription upstream however many followers it has, given back\n * when the last of them leaves, on the same terms as `ComputedCell`.\n */\nclass TimedCell<T> extends Observable<T> implements ReadableCell<T> {\n private current!: T;\n private hasCurrent = false;\n private followers = 0;\n private seeding = false;\n private upstream: Subscription | null = null;\n private readonly changes = new Subject<T>();\n\n constructor(\n private readonly stream: Observable<T>,\n private readonly gate: MonoTypeOperatorFunction<T>\n ) {\n super(subscriber => {\n this.followers++;\n if (this.upstream === null) {\n this.attach();\n }\n subscriber.next(this.current);\n const following = this.changes.subscribe(subscriber);\n return () => {\n following.unsubscribe();\n this.followers--;\n if (this.followers === 0) {\n this.upstream?.unsubscribe();\n this.upstream = null;\n }\n };\n });\n }\n\n get value(): T {\n trackRead(this);\n if (this.upstream === null) {\n const asking = this.stream.subscribe(value => {\n this.current = value;\n this.hasCurrent = true;\n });\n asking.unsubscribe();\n }\n return this.current;\n }\n\n private attach(): void {\n // Seeds the current value from whatever the source already holds,\n // so a cell behind a gate has a value on the first frame rather\n // than after the first delay. Only during the subscribe itself: a\n // value arriving later is a change, and absorbing it here would\n // let it past the gate without telling anybody.\n this.seeding = true;\n this.upstream = this.stream\n .pipe(\n tap(value => {\n if (this.seeding && !this.hasCurrent) {\n this.current = value;\n this.hasCurrent = true;\n }\n }),\n this.gate\n )\n .subscribe(value => {\n if (this.hasCurrent && Object.is(this.current, value)) {\n return;\n }\n this.current = value;\n this.hasCurrent = true;\n this.changes.next(value);\n });\n this.seeding = false;\n }\n}\n\n/**\n * A cell that follows its source once it has stopped moving.\n *\n * const query = internalState('');\n * const term = debounced(query, 200);\n * const results = computed(() => index.search(term.value));\n *\n * Nothing is emitted while values keep arriving; `ms` after the last\n * one, the last one is. This is what a search field wants and what\n * every search field in the tree was reaching the router without: a\n * keystroke is not a question, and a pause is.\n *\n * The value the source already held is there immediately, so a screen\n * built from this draws on the first frame rather than `ms` later.\n */\nexport function debounced<T>(source: Observable<T>, ms: number): ReadableCell<T> {\n return new TimedCell(source, debounceTime<T>(ms));\n}\n\n/**\n * A cell that follows its source at most once every `ms`.\n *\n * const position = throttled(scrollOffset, 100);\n *\n * The first value goes straight through and the last of a burst\n * follows at the end of the window, so a value that arrives while the\n * window is open is late rather than lost. That pairing is what makes\n * this usable for a position or a progress reading, where the\n * beginning and the end of a movement both matter and the middle does\n * not.\n *\n * Use this for something that is continuously true, and `debounced`\n * for something a person has finished saying.\n */\nexport function throttled<T>(source: Observable<T>, ms: number): ReadableCell<T> {\n return new TimedCell(source, throttleTime<T>(ms, undefined, { leading: true, trailing: true }));\n}\n","import { distinctUntilChanged, map, type Observable } from 'rxjs';\n\n/**\n * One key of a view model, as its own Observable, emitting only when\n * that key's value changes.\n *\n * `provide` and `serveChannels` want one Observable per view key, so\n * the differ can patch each key on its own, while a view model is most\n * naturally one Observable of one object. This is the seam between the\n * two, and every application worker was about to write it.\n */\nexport function pick<T, K extends keyof T>(source: Observable<T>, key: K): Observable<T[K]> {\n return source.pipe(\n map(value => value[key]),\n distinctUntilChanged()\n );\n}\n\n/**\n * Every key of a view model as its own Observable: the `view` a channel\n * source wants, from the one Observable a view model has.\n *\n * serveChannels([{ token: Queue, source: { view: pickKeys(queue.view, QUEUE_KEYS), commands } }])\n */\nexport function pickKeys<T extends object, K extends keyof T>(\n source: Observable<T>,\n keys: readonly K[]\n): { readonly [P in K]: Observable<T[P]> } {\n const out: Partial<{ [P in K]: Observable<T[P]> }> = {};\n for (const key of keys) {\n out[key] = pick(source, key);\n }\n return out as { readonly [P in K]: Observable<T[P]> };\n}\n","/**\n * What a store did with a request.\n *\n * Four words rather than an exception, because three of them are\n * ordinary outcomes an application routes around rather than faults it\n * reports. A browser that refuses storage in a private window, and a\n * quota that has run out, are both things a running application has to\n * carry on through, and making every caller wrap a `try` around a read\n * to discover which one happened is how storage code ends up assuming\n * success.\n *\n * - `ok`: the store answered. For a read that includes \"there is no\n * such record\", which is `value: null` and not a failure.\n * - `denied`: the platform will not let this origin store anything.\n * No OPFS in this browser, a private window that refuses IndexedDB,\n * `localStorage` blocked by a site setting. Permanent for the\n * session: nothing an application does will change the answer, so\n * asking again is wasted work.\n * - `full`: the quota is spent. Temporary, and worth trying again\n * after something has been given back, which is why it is not\n * `denied`.\n * - `failed`: anything else. A half-written file, a corrupt database,\n * a transaction that aborted for a reason the platform did not\n * explain.\n */\nexport type StorageOutcome = 'ok' | 'denied' | 'full' | 'failed';\n\n/** What came back from a read. */\nexport interface StorageRead {\n readonly outcome: StorageOutcome;\n /** The record, or null when there is none or the read did not answer. */\n readonly value: string | null;\n /** Why it did not answer, as a message; null when it did. */\n readonly error: string | null;\n}\n\n/**\n * Somewhere an application's state survives being closed.\n *\n * Four methods over text keyed by a string, which is the shape all\n * three implementations can actually keep. Bytes are deliberately not\n * in it: `localStorage` cannot hold them, so an interface that\n * promised them would be one the shell route could not implement, and\n * an application storing pictures wants a store of its own with an\n * eviction policy rather than this. the\n * artwork cache is that other thing.\n *\n * Nothing rejects. Every method answers with an outcome, and the\n * per-failure behaviour is the implementation's to document; the three\n * in this package all follow `classifyStorageError`.\n *\n * It is a shape and not a service. Nothing in the framework holds one,\n * `persisted` takes whichever one it is given, and an application with\n * its own store implements four methods rather than adopting anything.\n */\nexport interface StorageAdapter {\n /** Reads one record. */\n read(key: string): Promise<StorageRead>;\n /** Writes one record, replacing whatever was there. */\n write(key: string, value: string): Promise<StorageOutcome>;\n /** Removes one record. Removing what is not there is `ok`. */\n remove(key: string): Promise<StorageOutcome>;\n /** Every key this store holds, for an application that evicts its own. */\n keys(): Promise<readonly string[]>;\n}\n\n/**\n * Which of the four outcomes a thrown platform error is.\n *\n * The names are the ones the storage APIs actually throw.\n * `QuotaExceededError` is the DOM's word for full, and every browser\n * uses it for OPFS, IndexedDB and `localStorage` alike. `SecurityError`\n * and `NotAllowedError` are what a blocked origin gets. A missing API\n * (no `navigator.storage`, no `indexedDB`) is a `TypeError` here and\n * is `denied` for the same reason: nothing the application does will\n * produce a store.\n */\nexport function classifyStorageError(error: unknown): StorageOutcome {\n const name = error instanceof Error ? error.name : '';\n if (name === 'QuotaExceededError' || name === 'NS_ERROR_DOM_QUOTA_REACHED') {\n return 'full';\n }\n if (name === 'SecurityError' || name === 'NotAllowedError' || name === 'TypeError') {\n return 'denied';\n }\n return 'failed';\n}\n\n/** A thrown value as the message a screen could show. */\nexport function storageErrorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/** A read that could not answer, as one record. */\nexport function storageReadFailure(error: unknown): StorageRead {\n return { outcome: classifyStorageError(error), value: null, error: storageErrorMessage(error) };\n}\n\n/** A read that answered, whether or not it found anything. */\nexport function storageReadValue(value: string | null): StorageRead {\n return { outcome: 'ok', value, error: null };\n}\n\n/**\n * The same contract in memory, for specs and for a platform with no\n * store at all.\n *\n * Not a fallback anything installs on its own. An application that\n * would rather run with an unremembered session than fail says so by\n * passing one of these; one that would rather tell the person its\n * settings will not be kept reads the `denied` outcome and says so.\n * Choosing between those two on an application's behalf is exactly the\n * kind of decision the thread model keeps out of the framework.\n */\nexport class MemoryStorage implements StorageAdapter {\n private readonly records = new Map<string, string>();\n /** Written by a spec that wants to see what a full store does. */\n full = false;\n\n read(key: string): Promise<StorageRead> {\n return Promise.resolve(storageReadValue(this.records.get(key) ?? null));\n }\n\n write(key: string, value: string): Promise<StorageOutcome> {\n if (this.full) {\n return Promise.resolve('full');\n }\n this.records.set(key, value);\n return Promise.resolve('ok');\n }\n\n remove(key: string): Promise<StorageOutcome> {\n this.records.delete(key);\n return Promise.resolve('ok');\n }\n\n keys(): Promise<readonly string[]> {\n return Promise.resolve([...this.records.keys()]);\n }\n}\n","import { computed, type ComputedCell } from '../computed';\nimport { internalState, type InternalState } from '../InternalState';\nimport { select } from '../select';\n\n/** One undoable change, as the two functions that make and unmake it. */\nexport interface UndoTransaction {\n /**\n * What a menu item or a toast calls this change: \"Move Sunset\",\n * \"Remove Sunset from the queue\".\n *\n * A phrase rather than a sentence, because the words around it are\n * the caller's: an application writes \"Undo \" in front of it.\n */\n readonly label: string;\n /** Puts the world back the way it was before `redo` ran. */\n readonly undo: () => void;\n /** Makes the change again, from wherever `undo` left things. */\n readonly redo: () => void;\n /**\n * Merges this into the entry before it when the two keys match.\n *\n * A drag reports a crossing per row and a typed name reports a\n * keystroke, and neither is a change a person means to undo one step\n * at a time. Entries pushed one after another under the same key\n * become one entry: **the first one's `undo` and the last one's\n * `redo`**, with everything between them dropped.\n *\n * That merge rule is what makes the two functions above worth\n * writing as absolute moves rather than as deltas. `undo` should say\n * where the thing goes, not how far back it goes, because after\n * three more coalesced steps a delta is wrong and a position is\n * still right. See `endRun` for where a run stops.\n */\n readonly coalesce?: string;\n}\n\nexport interface UndoStackOptions {\n /**\n * How many entries are kept, oldest dropped first. Default 100.\n *\n * A bound rather than a growing list, for the reason\n * `EditableTextModel` has one: an application that is used for an\n * afternoon would otherwise hold every intermediate state of that\n * afternoon, and nobody undoes a hundred steps.\n */\n readonly limit?: number;\n /** What to call the stack in a devtools reading; optional. */\n readonly label?: string;\n}\n\n/** The two labels, as one record so a menu reads them in one go. */\ninterface UndoLabels {\n readonly undo: string | null;\n readonly redo: string | null;\n}\n\nconst NOTHING: UndoLabels = { undo: null, redo: null };\n\n/**\n * The application's undo, as a stack of named transactions.\n *\n * A component registers what it did and how to unmake it, and a menu,\n * a button or a keyboard shortcut drives the stack. Nothing here\n * listens to anything or knows what an application's state is: the two\n * functions in a transaction are the whole of the coupling, which is\n * why this can sit on either thread and why it does not become a data\n * layer.\n *\n * const undo = ctx.inject(UndoStack);\n *\n * queue.send.remove(at);\n * undo.push({\n * label: `Remove ${track.title}`,\n * undo: () => queue.send.addToQueue(track.id),\n * redo: () => queue.send.remove(at)\n * });\n *\n * ## How this relates to the undo inside a text field\n *\n * `EditableTextModel` has had its own undo since text became editable,\n * and the two are deliberately separate. A field's undo is a stack of\n * **snapshots of one string**, private to the field, and it has to be:\n * the model is the only thing that knows where the caret was, which\n * run of typing coalesces with which, and what an IME composition is\n * doing. This one is a stack of **inverse operations** over whatever\n * an application's state happens to be, and it cannot see inside a\n * field at all.\n *\n * They meet at one key press, and the rule there is that the focused\n * field wins: `registerUndoShortcuts` skips Mod+Z while something is\n * being typed into, so undo in a field undoes typing and undo\n * everywhere else undoes the application's last change. Merging the\n * two stacks would mean a keystroke and a queue reorder sharing one\n * history, which is not what either of them means.\n *\n * ## Reentrancy\n *\n * A push while an undo or a redo is running is dropped. Without it an\n * application whose edit path records itself would record the undo as\n * a new edit and the stack would never empty. Write the inverse\n * functions to call the state directly rather than through the same\n * path that records, and the guard never fires.\n */\nexport class UndoStack {\n private readonly entries: UndoTransaction[] = [];\n private readonly undone: UndoTransaction[] = [];\n private readonly limit: number;\n /** True while `undo()` or `redo()` is running one of the functions. */\n private running = false;\n /** Set by `endRun`, so the next push starts a new entry whatever its key. */\n private sealed = false;\n\n private readonly labels: InternalState<UndoLabels>;\n\n /** What undoing would undo, or null when there is nothing to undo. */\n readonly undoLabel: ComputedCell<string | null>;\n /** What redoing would redo, or null when there is nothing to redo. */\n readonly redoLabel: ComputedCell<string | null>;\n /** Whether there is anything to undo, for a menu item's `disabled`. */\n readonly canUndo: ComputedCell<boolean>;\n readonly canRedo: ComputedCell<boolean>;\n\n constructor(options: UndoStackOptions = {}) {\n this.limit = Math.max(1, options.limit ?? 100);\n this.labels = internalState<UndoLabels>(NOTHING, options.label);\n this.undoLabel = select(this.labels, 'undo');\n this.redoLabel = select(this.labels, 'redo');\n this.canUndo = computed(() => this.undoLabel.value !== null);\n this.canRedo = computed(() => this.redoLabel.value !== null);\n }\n\n /** How many entries are held, for a spec or a budget. */\n get size(): number {\n return this.entries.length;\n }\n\n /** How many redos are waiting, for a spec or a budget. */\n get redoSize(): number {\n return this.undone.length;\n }\n\n /**\n * Records a change that has already been made.\n *\n * Pushing is what discards the redo branch: making a change after\n * undoing two is the person choosing the other future, and keeping\n * the abandoned one would mean redoing into a state that no longer\n * follows from what is on screen.\n */\n push(transaction: UndoTransaction): void {\n if (this.running) {\n return;\n }\n this.undone.length = 0;\n const previous = this.entries[this.entries.length - 1];\n const sealed = this.sealed;\n this.sealed = false;\n if (\n !sealed &&\n previous !== undefined &&\n transaction.coalesce !== undefined &&\n previous.coalesce === transaction.coalesce\n ) {\n // The run's own undo, kept from its first step, and its newest\n // redo. Every step in between is released here, which is the\n // whole of what stops a drag across sixty rows retaining sixty\n // closures and the sixty values they close over.\n this.entries[this.entries.length - 1] = {\n label: transaction.label,\n undo: previous.undo,\n redo: transaction.redo,\n coalesce: transaction.coalesce\n };\n this.publish();\n return;\n }\n this.entries.push(transaction);\n while (this.entries.length > this.limit) {\n this.entries.shift();\n }\n this.publish();\n }\n\n /**\n * Groups everything pushed inside `body` into one entry.\n *\n * For a change an application makes as several calls and a person\n * made as one press: undoing runs the group's undos in reverse, and\n * redoing runs its redos in order. Unlike `coalesce`, a group keeps\n * every step, because a group is written down as a group rather than\n * discovered from a run of similar pushes.\n */\n transact<R>(label: string, body: () => R): R {\n if (this.running) {\n return body();\n }\n const outer = this.entries.length;\n const result = body();\n const collected = this.entries.splice(outer);\n if (collected.length > 0) {\n this.entries.push({\n label,\n undo: () => {\n for (let at = collected.length - 1; at >= 0; at--) {\n collected[at]!.undo();\n }\n },\n redo: () => {\n for (const step of collected) {\n step.redo();\n }\n }\n });\n this.sealed = true;\n this.publish();\n }\n return result;\n }\n\n /**\n * Ends the current run, so the next push starts its own entry.\n *\n * The twin of `EditableTextModel.endTypingRun`. A drag calls it when\n * the pointer comes up: without it, dragging a row, letting go, and\n * dragging the same row again would coalesce into one entry, and one\n * undo would put the row back where it was two gestures ago.\n */\n endRun(): void {\n this.sealed = true;\n }\n\n /** Undoes the last change. False when there was nothing to undo. */\n undo(): boolean {\n const entry = this.entries.pop();\n if (entry === undefined) {\n return false;\n }\n this.run(entry.undo);\n this.undone.push(entry);\n this.sealed = true;\n this.publish();\n return true;\n }\n\n /** Redoes the last undone change. False when there was nothing to redo. */\n redo(): boolean {\n const entry = this.undone.pop();\n if (entry === undefined) {\n return false;\n }\n this.run(entry.redo);\n this.entries.push(entry);\n this.sealed = true;\n this.publish();\n return true;\n }\n\n /**\n * Forgets everything, in both directions.\n *\n * What an application calls when the thing the entries refer to is\n * gone: a queue emptied, a document closed, a signed-out account's\n * library replaced. `EditableTextModel.setText` does the same for\n * the same reason, and the reason is that an inverse function whose\n * subject no longer exists is not an undo, it is a surprise.\n */\n clear(): void {\n this.entries.length = 0;\n this.undone.length = 0;\n this.sealed = false;\n this.publish();\n }\n\n private run(action: () => void): void {\n this.running = true;\n try {\n action();\n } finally {\n this.running = false;\n }\n }\n\n private publish(): void {\n const next: UndoLabels = {\n undo: this.entries[this.entries.length - 1]?.label ?? null,\n redo: this.undone[this.undone.length - 1]?.label ?? null\n };\n // Structural comparison rather than a write per push: coalescing a\n // drag rewrites the same label sixty times, and a menu bound to\n // these should redraw when the words change and not before.\n if (next.undo !== this.labels.value.undo || next.redo !== this.labels.value.redo) {\n this.labels.value = next;\n }\n }\n}\n","import type { Mutation } from '../mutate';\nimport type { UndoStack } from './UndoStack';\n\nexport interface UndoableOptions<A> {\n /** What the entry is called, from the argument that made it. */\n readonly label: string | ((argument: A) => string);\n /** The coalescing key, from the argument; see `UndoTransaction.coalesce`. */\n readonly coalesce?: string | ((argument: A) => string);\n}\n\n/**\n * Puts a `mutate` on an undo stack, by way of its own inverse.\n *\n * private readonly liked = internalState<readonly string[]>([]);\n * private readonly like = mutate(this.liked, toggled, id => api.favourite(id));\n * readonly toggleLike = undoable(undo, this.like, id => id, { label: () => 'Like' });\n *\n * The whole of it is that undoing an optimistic change is another\n * optimistic change. Nothing here writes the cell behind the\n * mutation's back, which is the thing that would go wrong if an undo\n * stack held values rather than operations: it would restore a value\n * the server has not been told about, and the next rollback would\n * fight it.\n *\n * `invert` answers the argument that undoes this one. For a toggle\n * that is the same argument again, which is why the example above\n * looks like it does nothing.\n *\n * **A refused write is not on the stack.** `mutate` already puts the\n * cell back when a commit rejects or resolves `false`, so the change\n * did not happen and there is nothing to undo; pushing it would give a\n * person an undo that undoes something they never saw.\n *\n * The undo and the redo call `mutation.run` rather than this wrapper,\n * so running them records nothing and the stack's reentrancy guard\n * never has to fire.\n */\nexport function undoable<A>(\n stack: UndoStack,\n mutation: Mutation<A>,\n invert: (argument: A) => A,\n options: UndoableOptions<A>\n): (argument: A) => Promise<boolean> {\n const { label, coalesce } = options;\n return async (argument: A): Promise<boolean> => {\n if (!(await mutation.run(argument))) {\n return false;\n }\n stack.push({\n label: typeof label === 'function' ? label(argument) : label,\n undo: () => void mutation.run(invert(argument)),\n redo: () => void mutation.run(argument),\n ...(coalesce === undefined ? {} : { coalesce: typeof coalesce === 'function' ? coalesce(argument) : coalesce })\n });\n return true;\n };\n}\n","import {\n classifyStorageError,\n storageReadFailure,\n storageReadValue,\n type StorageAdapter,\n type StorageOutcome,\n type StorageRead\n} from './StorageAdapter';\n\n/**\n * As much of the Origin Private File System as a store needs.\n *\n * Written out rather than taken from `lib.dom`, for two reasons. The\n * workspace's `lib` is `ES2023` and `DOM`, which has the handles but\n * not the async iteration `values()` needs, and a structural type is\n * what lets a spec hand this a directory of its own instead of\n * standing up a file system to test four methods.\n */\nexport interface OpfsDirectory {\n getFileHandle(name: string, options?: { create?: boolean }): Promise<OpfsFileHandle>;\n getDirectoryHandle(name: string, options?: { create?: boolean }): Promise<OpfsDirectory>;\n removeEntry(name: string): Promise<void>;\n keys(): AsyncIterable<string>;\n}\n\nexport interface OpfsFileHandle {\n getFile(): Promise<{ text(): Promise<string> }>;\n createWritable(): Promise<OpfsWritable>;\n}\n\nexport interface OpfsWritable {\n write(data: string): Promise<void>;\n close(): Promise<void>;\n}\n\nexport interface OpfsStorageOptions {\n /**\n * The folder under the origin's private root, so two stores in one\n * application do not share a namespace. Default `'gesso'`.\n */\n readonly directory?: string;\n /** The root, for a spec. Default `navigator.storage.getDirectory()`. */\n readonly root?: () => Promise<OpfsDirectory>;\n}\n\n/**\n * A store in the origin's private file system.\n *\n * The right default for an application's own state. It is reachable\n * from a worker, which `localStorage` is not, so the thread that owns\n * the state is the thread that writes it and nothing has to cross the\n * barrier to be remembered. It is asynchronous throughout, so nothing\n * it does blocks a frame. And it is per-origin and invisible to the\n * person, which is the right place for a queue or a draft and the\n * wrong place for anything they should be able to find and delete.\n *\n * One file per key, named by the key with the characters a file system\n * would refuse escaped, so a key is recoverable from a listing and a\n * key containing a slash cannot reach out of the folder.\n *\n * What happens on each failure:\n *\n * - **The platform has no OPFS**, or the browser refuses it (a private\n * window, a blocked origin): every method answers `denied` and the\n * error message says which. The store is not usable this session and\n * `persisted` stops writing to it after the first denial.\n * - **The quota is spent**: the write answers `full`. Nothing is\n * rolled back, because the value the application holds is the real\n * one and only the remembering failed.\n * - **There is no such record**: the read answers `ok` with `null`.\n * Not having been written yet is the ordinary first run, not a\n * failure.\n * - **Anything else**: `failed`, with the platform's message. A file\n * whose *contents* are not what this version writes is a different\n * thing and is `persisted`'s to judge, which it does by discarding\n * it, the same call `Tokens.ts` makes.\n */\nexport class OpfsStorage implements StorageAdapter {\n private readonly folder: string;\n private readonly rootOf: () => Promise<OpfsDirectory>;\n /** The folder, once opened. Reused, because opening it is a round trip. */\n private opening: Promise<OpfsDirectory> | null = null;\n\n constructor(options: OpfsStorageOptions = {}) {\n this.folder = options.directory ?? 'gesso';\n this.rootOf = options.root ?? defaultRoot;\n }\n\n async read(key: string): Promise<StorageRead> {\n try {\n const directory = await this.open();\n const handle = await directory.getFileHandle(fileFor(key));\n return storageReadValue(await (await handle.getFile()).text());\n } catch (error) {\n // A file that is not there is the ordinary answer to \"is\n // anything remembered\", and the platform says so by throwing\n // `NotFoundError`. It is not a failure and it is not `denied`.\n if (error instanceof Error && error.name === 'NotFoundError') {\n return storageReadValue(null);\n }\n return storageReadFailure(error);\n }\n }\n\n async write(key: string, value: string): Promise<StorageOutcome> {\n try {\n const directory = await this.open();\n const handle = await directory.getFileHandle(fileFor(key), { create: true });\n const writable = await handle.createWritable();\n // Written and closed in one go rather than kept open: a handle\n // held across frames is a handle a reload can leave locked.\n await writable.write(value);\n await writable.close();\n return 'ok';\n } catch (error) {\n return classifyStorageError(error);\n }\n }\n\n async remove(key: string): Promise<StorageOutcome> {\n try {\n await (await this.open()).removeEntry(fileFor(key));\n return 'ok';\n } catch (error) {\n if (error instanceof Error && error.name === 'NotFoundError') {\n return 'ok';\n }\n return classifyStorageError(error);\n }\n }\n\n async keys(): Promise<readonly string[]> {\n try {\n const found: string[] = [];\n for await (const name of (await this.open()).keys()) {\n found.push(keyFor(name));\n }\n return found;\n } catch {\n // A listing that cannot be taken is an empty one. The caller is\n // an application evicting its own records, and evicting nothing\n // is the safe answer to not knowing what is there.\n return [];\n }\n }\n\n /**\n * The folder, opened once and kept.\n *\n * A failed open is *not* kept. A rejected promise left in `opening`\n * would answer every later call with the same rejection, so one\n * refusal at start-up would be a store that never worked again even\n * after the person granted storage or made room. Kept when it\n * succeeds, dropped when it does not, which is one line and the\n * difference between a cache and a poison.\n */\n private open(): Promise<OpfsDirectory> {\n if (this.opening === null) {\n const opening = this.rootOf().then(root => root.getDirectoryHandle(this.folder, { create: true }));\n this.opening = opening;\n opening.catch(() => {\n if (this.opening === opening) {\n this.opening = null;\n }\n });\n }\n return this.opening;\n }\n}\n\nfunction defaultRoot(): Promise<OpfsDirectory> {\n const storage = (globalThis as { navigator?: { storage?: { getDirectory?: () => Promise<OpfsDirectory> } } })\n .navigator?.storage;\n if (storage?.getDirectory === undefined) {\n // A `TypeError`, which `classifyStorageError` reads as `denied`:\n // there is no store here and there will not be one.\n return Promise.reject(new TypeError('This environment has no Origin Private File System.'));\n }\n return storage.getDirectory();\n}\n\n/**\n * A key as a file name.\n *\n * `encodeURIComponent` and not a hash, so a listing of the folder in\n * devtools reads as the keys the application wrote. It escapes the\n * slash and the dot, which is what stops a key reaching a directory it\n * was not given.\n */\nfunction fileFor(key: string): string {\n return `${encodeURIComponent(key)}.json`;\n}\n\nfunction keyFor(name: string): string {\n return decodeURIComponent(name.replace(/\\.json$/, ''));\n}\n","import {\n classifyStorageError,\n storageReadFailure,\n storageReadValue,\n type StorageAdapter,\n type StorageOutcome,\n type StorageRead\n} from './StorageAdapter';\n\nexport interface IndexedDbStorageOptions {\n /** The database. Default `'gesso'`. */\n readonly database?: string;\n /** The object store inside it. Default `'records'`. */\n readonly store?: string;\n /** The factory, for a spec. Default `globalThis.indexedDB`. */\n readonly factory?: IDBFactory;\n}\n\n/**\n * A store in IndexedDB.\n *\n * Beside OPFS rather than instead of it, because the two fail in\n * different places and an application picks by which failure it\n * minds. IndexedDB is reachable from every thread, survives longer\n * under a browser's own eviction, and is what a Safari that has\n * disabled OPFS still has; OPFS is faster for one large record and\n * simpler to inspect. Neither is a default the framework picks: an\n * application names the one it wants.\n *\n * One object store of strings keyed by string, which is the shape\n * `StorageAdapter` describes and no more. Indexes, versions past the\n * first, and cursors over ranges are what an application builds when\n * it has outgrown a key-value store, and at that point it is writing\n * against IndexedDB rather than against this.\n *\n * What happens on each failure:\n *\n * - **No `indexedDB`, or an origin that may not open one** (a private\n * window in some browsers, a blocked third-party context): every\n * method answers `denied`, and the open is retried next time rather\n * than cached, because a `denied` can be lifted by a site setting\n * mid-session.\n * - **The quota is spent**: the write answers `full`. IndexedDB\n * reports this on the transaction rather than on the request, which\n * is why the write waits for `oncomplete` and not for\n * `onsuccess`: a put that succeeded into a transaction that then\n * aborted has not been written, and answering `ok` for it would be\n * the adapter assuming success.\n * - **A version change from another tab**: the connection is closed\n * and dropped, so the next call opens a fresh one. Answering\n * `failed` and holding a dead connection would make every later\n * call fail too.\n * - **Anything else**: `failed`, with the platform's message.\n */\nexport class IndexedDbStorage implements StorageAdapter {\n private readonly database: string;\n private readonly store: string;\n private readonly factory: IDBFactory | undefined;\n private connecting: Promise<IDBDatabase> | null = null;\n\n constructor(options: IndexedDbStorageOptions = {}) {\n this.database = options.database ?? 'gesso';\n this.store = options.store ?? 'records';\n this.factory = options.factory ?? (globalThis as { indexedDB?: IDBFactory }).indexedDB;\n }\n\n async read(key: string): Promise<StorageRead> {\n try {\n const value = await this.transact('readonly', store => store.get(key));\n return storageReadValue(typeof value === 'string' ? value : null);\n } catch (error) {\n return storageReadFailure(error);\n }\n }\n\n async write(key: string, value: string): Promise<StorageOutcome> {\n return this.outcomeOf(() => this.transact('readwrite', store => store.put(value, key)));\n }\n\n async remove(key: string): Promise<StorageOutcome> {\n return this.outcomeOf(() => this.transact('readwrite', store => store.delete(key)));\n }\n\n async keys(): Promise<readonly string[]> {\n try {\n const found = await this.transact('readonly', store => store.getAllKeys());\n return Array.isArray(found) ? found.filter((key): key is string => typeof key === 'string') : [];\n } catch {\n return [];\n }\n }\n\n /** Lets go of the connection, for a spec or an application shutting down. */\n close(): void {\n const connecting = this.connecting;\n this.connecting = null;\n void connecting?.then(\n database => database.close(),\n () => undefined\n );\n }\n\n private async outcomeOf(work: () => Promise<unknown>): Promise<StorageOutcome> {\n try {\n await work();\n return 'ok';\n } catch (error) {\n const outcome = classifyStorageError(error);\n if (outcome === 'denied') {\n this.connecting = null;\n }\n return outcome;\n }\n }\n\n /**\n * Runs one request inside one transaction and answers its result.\n *\n * A write resolves on the transaction completing rather than on the\n * request succeeding, because those are two different claims: the\n * second says the put was accepted, and only the first says it\n * reached the disk.\n */\n private async transact<T>(mode: IDBTransactionMode, run: (store: IDBObjectStore) => IDBRequest<T>): Promise<T> {\n const database = await this.connect();\n return new Promise<T>((resolve, reject) => {\n let answer: T;\n const transaction = database.transaction(this.store, mode);\n const request = run(transaction.objectStore(this.store));\n request.onsuccess = () => {\n answer = request.result;\n };\n request.onerror = () => reject(request.error ?? new Error('The request failed.'));\n transaction.oncomplete = () => resolve(answer);\n transaction.onabort = () => reject(transaction.error ?? new Error('The transaction was aborted.'));\n });\n }\n\n private connect(): Promise<IDBDatabase> {\n this.connecting ??= this.open();\n return this.connecting;\n }\n\n private open(): Promise<IDBDatabase> {\n const factory = this.factory;\n if (factory === undefined) {\n // A `TypeError`, which `classifyStorageError` reads as `denied`.\n return Promise.reject(new TypeError('This environment has no IndexedDB.'));\n }\n return new Promise<IDBDatabase>((resolve, reject) => {\n let request: IDBOpenDBRequest;\n try {\n request = factory.open(this.database, 1);\n } catch (error) {\n reject(error);\n return;\n }\n request.onupgradeneeded = () => {\n if (!request.result.objectStoreNames.contains(this.store)) {\n request.result.createObjectStore(this.store);\n }\n };\n request.onsuccess = () => {\n // Another tab asking for a newer version cannot proceed while\n // this connection is open, so it is given up rather than held.\n request.result.onversionchange = () => this.close();\n resolve(request.result);\n };\n request.onerror = () => reject(request.error ?? new Error('The database could not be opened.'));\n request.onblocked = () => reject(new Error('The database is open in another tab at a different version.'));\n });\n }\n}\n","import { of, skip, type Subscription } from 'rxjs';\n\nimport { debounced } from '../debounce';\nimport type { ReadableCell } from '../Input';\nimport { internalState, type InternalState } from '../InternalState';\nimport { resource, type Resource, type ResourceStatus } from '../resource';\nimport type { StorageAdapter } from './StorageAdapter';\n\nexport interface PersistedOptions<T> {\n /**\n * What the value is before anything has been read, and what it goes\n * back to when nothing was stored.\n *\n * Required, and that is the whole answer to the race between\n * hydration and the first frame: a screen reading this on the frame\n * it mounts gets the default, with `status` saying `loading`, and\n * gets the remembered value a moment later if there was one. There\n * is no fourth state to draw and no null to guard against.\n */\n readonly initial: T;\n /**\n * Reads a record back, refusing one this version does not\n * understand.\n *\n * Answer `null` and the record is treated as not being there, which\n * is what a record written by an older build should be: the cost is\n * one default, and the alternative is state holding `undefined`\n * where a field belongs. `Tokens.isStoredTokens` in Segue is the\n * same check written by hand.\n *\n * Without one the parsed JSON is trusted as-is, which is fine while\n * the shape has only ever had one version and is not fine\n * afterwards.\n */\n readonly revive?: (raw: unknown) => T | null;\n /**\n * How long a change waits for the next one before being written, in\n * milliseconds. Default 250.\n *\n * A `debounced` cell rather than a timer of its own, so this is the\n * same gate a search field uses. It is what makes a value changed\n * sixty times a second cost one write rather than sixty: see\n * `persisted.budget.spec.ts`, which asserts exactly that.\n */\n readonly settle?: number;\n /** What to call this in a devtools reading; optional. */\n readonly label?: string;\n}\n\nconst MESSAGES: Readonly<Record<string, string>> = {\n denied: 'This browser will not let the application store anything.',\n full: 'There is no room left to store this.',\n failed: 'The store could not be written to.'\n};\n\n/**\n * A value that survives the application being closed.\n *\n * Hydration is a `resource`, which is not a detail: reading from a\n * disk is a keyed request that can be slow, can answer \"there is\n * nothing\", and can fail, which is the same set of outcomes a request\n * over the network has. So the statuses here are `ResourceStatus`\n * itself rather than a fourth enum saying the same five things in\n * different words.\n *\n * readonly draft = persisted(new OpfsStorage(), 'draft', { initial: '' });\n *\n * // on a screen\n * <TextInput value={draft.value} onChange={text => draft.set(text)} />\n *\n * ## What a screen sees before hydration finishes\n *\n * The default, and `status` reading `loading`. Nothing waits, nothing\n * is null, and no screen has a shape it only has for the first eighty\n * milliseconds. When the read lands the value changes like any other\n * cell change, and a screen that wants to say \"restoring\" reads\n * `status`.\n *\n * The one race that needs a rule is a person changing the value before\n * the disk has answered, which is not rare: a queue is a press away\n * and OPFS is a round trip away. **What they did wins.** A hydration\n * answer is applied only if nothing has been `set` since, on the same\n * reasoning as `mutate`'s guarded rollback: an answer that was\n * overtaken is stale, and putting it on screen would undo something\n * the person just did.\n *\n * ## What happens when storing fails\n *\n * - `denied`: nothing is written this session and nothing is tried\n * again, because the answer will not change. `status` is `failed`\n * and `saveError` says so once rather than on every keystroke.\n * - `full`: the write failed and the value in memory is kept. Nothing\n * is rolled back: the change is real and only the remembering of it\n * failed. The next change is still attempted, because a quota can be\n * given back.\n * - `failed`: the same as `full`, and for the same reason.\n * - A record that parses but is not the shape `revive` accepts is\n * treated as `missing`: the application starts from its default\n * rather than showing an error about a file the person cannot see.\n *\n * ## It is a helper\n *\n * Nothing in the framework holds one, and nothing is reachable only\n * through it. A channel is served from plain Observables as it always\n * was, and an application that would rather read and write a store\n * itself is writing against the same `StorageAdapter` this is written\n * against. The thread model declined to own an application's data\n * architecture, and remembering a value is not the exception to that.\n */\nexport class PersistedState<T> {\n private readonly held: InternalState<T>;\n private readonly saves: InternalState<number>;\n private readonly failure: InternalState<string | null>;\n private readonly record: Resource<string, T>;\n private readonly writing: Subscription;\n /** True once the application has written a value of its own. */\n private touched = false;\n /** Set when the store said `denied`, which is permanent for the session. */\n private refused = false;\n /** The text last known to be on disk, so hydration does not write itself back. */\n private stored: string | null = null;\n /** What `forget` goes back to. */\n private readonly initial: T;\n\n /** Where the first read got to; the same five words `resource` uses. */\n readonly status: ReadableCell<ResourceStatus>;\n /** Why the read did not answer, as a message; null when it did. */\n readonly error: ReadableCell<string | null>;\n /** What is remembered: the default until the read lands. */\n readonly value: ReadableCell<T>;\n /** Writes in the air, for a saving indicator. A count, as `mutate.pending` is. */\n readonly saving: ReadableCell<number>;\n /** Why the last write did not happen, as a message; null when it did. */\n readonly saveError: ReadableCell<string | null>;\n /** Resolves when the first read has settled, whatever it found. */\n readonly hydrated: Promise<void>;\n\n constructor(\n private readonly adapter: StorageAdapter,\n private readonly key: string,\n options: PersistedOptions<T>\n ) {\n const label = options.label;\n this.initial = options.initial;\n this.held = internalState<T>(options.initial, label);\n this.saves = internalState(0, label === undefined ? undefined : `${label}.saving`);\n this.failure = internalState<string | null>(null, label === undefined ? undefined : `${label}.saveError`);\n this.value = this.held;\n this.saving = this.saves;\n this.saveError = this.failure;\n // A constant key, because the record being read is named once. The\n // resource is eager, so constructing this is what starts the read,\n // which is what \"hydration on start\" means: nothing has to\n // remember to call a `load()`.\n this.record = resource(\n of(key),\n () => this.load(options.revive),\n label === undefined ? {} : { label: `${label}.hydration` }\n );\n this.status = this.record.status;\n this.error = this.record.error;\n this.hydrated = this.record.settled.then(() => this.apply());\n this.writing = debounced(this.held, options.settle ?? 250)\n // The gate hands a new follower the value the cell already has,\n // which here is the default nobody asked to be written. Only\n // what comes after it is a change.\n .pipe(skip(1))\n .subscribe(value => void this.flush(value));\n }\n\n /** What is remembered right now, for code that is not subscribing. */\n get current(): T {\n return this.held.value;\n }\n\n /** Remembers a new value. The write follows once the changes stop. */\n set(value: T): void {\n this.touched = true;\n this.held.value = value;\n }\n\n /**\n * Writes what is held now, without waiting for the gate.\n *\n * For the moment an application knows it is about to lose the thread:\n * a `visibilitychange`, a route away from an editor, a sign-out.\n */\n save(): Promise<void> {\n return this.flush(this.held.value);\n }\n\n /**\n * Forgets the record and goes back to the default.\n *\n * Both halves, because a stored value removed while the cell still\n * holds it would be written straight back by the next change.\n */\n async forget(): Promise<void> {\n this.stored = null;\n this.touched = true;\n this.held.value = this.initial;\n await this.adapter.remove(this.key);\n }\n\n /** Gives back the write subscription and the resource's. */\n dispose(): void {\n this.writing.unsubscribe();\n this.record.dispose();\n }\n\n private async load(revive: ((raw: unknown) => T | null) | undefined): Promise<T | null> {\n const read = await this.adapter.read(this.key);\n if (read.outcome !== 'ok') {\n if (read.outcome === 'denied') {\n this.refused = true;\n }\n throw new Error(read.error ?? MESSAGES[read.outcome] ?? 'The store could not be read.');\n }\n if (read.value === null) {\n return null;\n }\n this.stored = read.value;\n let parsed: unknown;\n try {\n parsed = JSON.parse(read.value);\n } catch {\n // Half-written, or written by something else entirely. Treated\n // as nothing stored rather than as a failure: there is nothing a\n // person can do about it and a default is a working application.\n return null;\n }\n return revive === undefined ? (parsed as T) : revive(parsed);\n }\n\n private apply(): void {\n const found = this.record.value.value;\n if (found === null || this.touched) {\n // Nothing stored, or the person got there first. Theirs is the\n // newer truth and it stays.\n return;\n }\n this.held.value = found;\n }\n\n private async flush(value: T): Promise<void> {\n if (this.refused) {\n return;\n }\n let text: string;\n try {\n text = JSON.stringify(value);\n } catch (error) {\n this.failure.value = error instanceof Error ? error.message : String(error);\n return;\n }\n if (text === this.stored) {\n // Hydration writing what it just read, or a change that came\n // back round to where it started. Either way there is nothing\n // to say to the disk.\n return;\n }\n this.saves.value = this.saves.value + 1;\n try {\n const outcome = await this.adapter.write(this.key, text);\n if (outcome === 'ok') {\n this.stored = text;\n this.failure.value = null;\n return;\n }\n if (outcome === 'denied') {\n this.refused = true;\n }\n this.failure.value = MESSAGES[outcome] ?? 'The store could not be written to.';\n } finally {\n this.saves.value = Math.max(0, this.saves.value - 1);\n }\n }\n}\n\n/**\n * A value read from a store on start and written back as it changes.\n *\n * const settings = persisted(new IndexedDbStorage(), 'settings', {\n * initial: DEFAULTS,\n * revive: raw => (isSettings(raw) ? raw : null)\n * });\n *\n * See `PersistedState` for what a screen sees before the read lands\n * and what each kind of storage failure does.\n */\nexport function persisted<T>(adapter: StorageAdapter, key: string, options: PersistedOptions<T>): PersistedState<T> {\n return new PersistedState(adapter, key, options);\n}\n"],"mappings":"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"}
|
package/dist/remote/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { B as ServedChannel } from "../tools-BFBwuinE.js";
|
|
2
|
+
import { Dt as AppLogicEndpoint } from "../index-BENndtaH.js";
|
|
3
3
|
//#region src/remote/frames.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* The whole wire format, which is deliberately smaller than the
|
package/dist/remote/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { i as
|
|
1
|
+
import { i as isPortHandshake, n as isHubMessage } from "../WorkerPorts-m_hLRoPj.js";
|
|
2
|
+
import { n as serveChannels } from "../serveChannels-DReCmRxO.js";
|
|
2
3
|
//#region src/remote/frames.ts
|
|
3
4
|
/**
|
|
4
5
|
* The whole wire format, which is deliberately smaller than the
|
package/dist/remote/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/remote/frames.ts","../../src/remote/bridge.ts","../../src/remote/host.ts"],"sourcesContent":["/**\n * The whole wire format, which is deliberately smaller than the\n * channel protocol it carries.\n *\n * Three frames in each direction. `open` says a named channel wants a\n * stream, `data` carries one channel message, `close` ends a stream.\n * Nothing here knows what a channel is, what a patch is, or what a\n * command is: a frame's `body` is a string this module produced by\n * serializing a value it never looked inside. That restraint is the\n * rule the bridge keeps, and it is what keeps the page's main thread\n * a transport rather than a router.\n */\n\n/**\n * How much of a serialized message goes in one frame.\n *\n * Electrobun's transport fails above roughly 8 MiB in a single\n * message, and it fails badly: the main process throws while draining\n * and the sender sees only a timeout, so the real error is in a log\n * nobody is reading. A megabyte leaves eight times the headroom and\n * costs nothing at the measured rates.\n */\nexport const DEFAULT_CHUNK_BYTES = 1_048_576;\n\nexport type GessoFrame =\n | { readonly kind: 'open'; readonly stream: number; readonly name: string }\n | {\n readonly kind: 'data';\n readonly stream: number;\n readonly body: string;\n /** Absent unless the message was split; then 0-based. */\n readonly part?: number;\n /** Absent unless the message was split; then how many parts to expect. */\n readonly parts?: number;\n }\n | { readonly kind: 'close'; readonly stream: number }\n /**\n * The adapter's own traffic, which is not a channel: the appearance\n * the platform is in, a url the application wants opened outside the\n * window, and one of its own routes it wants opened in a new window.\n * It carries a name and a serialized payload for\n * the same reason a `data` frame carries a body, and it is a\n * separate kind so that nothing has to reserve a stream number.\n */\n | { readonly kind: 'control'; readonly name: string; readonly body: string };\n\nexport function isGessoFrame(value: unknown): value is GessoFrame {\n const kind = (value as { kind?: unknown } | null)?.kind;\n return kind === 'open' || kind === 'data' || kind === 'close' || kind === 'control';\n}\n\n/**\n * A `SharedArrayBuffer` is memory two threads of one page read at once,\n * and another process cannot read it. JSON would quietly turn it into\n * `{}`, so it is refused here, by name.\n */\nfunction refuseSharedMemory(this: unknown, key: string, value: unknown): unknown {\n if (typeof SharedArrayBuffer !== 'undefined' && value instanceof SharedArrayBuffer) {\n throw new Error(\n `A channel message holds a SharedArrayBuffer${key === '' ? '' : ` at '${key}'`}, which cannot cross to another process. ` +\n 'Shared memory works between the workers of one page; across a process boundary send the bytes as data.'\n );\n }\n return value;\n}\n\n/** Wraps one control message. Never split: these are small by construction. */\nexport function frameControl(name: string, payload: unknown): GessoFrame {\n return { kind: 'control', name, body: JSON.stringify(payload ?? null) ?? 'null' };\n}\n\n/**\n * Serializes one channel message into the frames that carry it.\n *\n * JSON rather than structured clone, because the transport underneath\n * is JSON either way: `Electroview.createTransport` stringifies every\n * message before it encrypts it. That means `undefined` inside a value\n * does not survive, which is true of this transport with or without\n * this module, and which a view key cannot rely on anyway.\n */\nexport function frameData(stream: number, value: unknown, chunkBytes = DEFAULT_CHUNK_BYTES): GessoFrame[] {\n const body = JSON.stringify(value, refuseSharedMemory);\n if (body === undefined) {\n throw new Error(\n `A channel message for stream ${stream} could not be serialized. Only plain data crosses a channel; see requirePlainData.`\n );\n }\n if (body.length <= chunkBytes) {\n return [{ kind: 'data', stream, body }];\n }\n const parts = Math.ceil(body.length / chunkBytes);\n const frames: GessoFrame[] = [];\n for (let part = 0; part < parts; part++) {\n frames.push({\n kind: 'data',\n stream,\n body: body.slice(part * chunkBytes, (part + 1) * chunkBytes),\n part,\n parts\n });\n }\n return frames;\n}\n\n/**\n * Puts split messages back together.\n *\n * The transport delivers in order (`Electroview` dispatches through a\n * promise tail that preserves frame order), so a part that arrives out\n * of turn is a bug rather than a race, and it says so instead of\n * quietly assembling a corrupt message.\n */\nexport class FrameAssembler {\n private readonly partial = new Map<number, { parts: number; chunks: string[] }>();\n\n /**\n * Returns the value a `data` frame completes, or `undefined` while\n * more parts are still to come.\n */\n take(frame: GessoFrame & { kind: 'data' }): unknown {\n if (frame.parts === undefined) {\n return JSON.parse(frame.body);\n }\n const held = this.partial.get(frame.stream) ?? { parts: frame.parts, chunks: [] };\n const expected = held.chunks.length;\n if (frame.part !== expected) {\n this.partial.delete(frame.stream);\n throw new Error(\n `Stream ${frame.stream} received part ${String(frame.part)} when part ${expected} was next. ` +\n 'Frames are expected in order; a gap means the transport reordered or dropped one.'\n );\n }\n held.chunks.push(frame.body);\n if (held.chunks.length < held.parts) {\n this.partial.set(frame.stream, held);\n return undefined;\n }\n this.partial.delete(frame.stream);\n return JSON.parse(held.chunks.join(''));\n }\n\n /** Drops anything half-received for a stream that has closed. */\n forget(stream: number): void {\n this.partial.delete(stream);\n }\n}\n","/**\n * The page's half of a remote channel bridge.\n *\n * A Gesso render worker opens a named port per channel and expects the\n * application layer at the other end of it. When the application layer\n * lives in another process (a desktop app's main process, a server on\n * the person's machine) it is reachable only from the page's main\n * thread, so this stands in its place: it accepts the handshakes the\n * render worker sends and pumps each one as a numbered stream of frames\n * over whatever the page has to the other process. Electrobun's RPC is\n * one such thing; a WebSocket is another.\n *\n * It is a transport and nothing else. It serializes a message it never\n * inspects, and it holds no channel, no token and no patch. Anything\n * that needs to understand a payload to route it belongs on the other\n * side of the bridge, and if that ever changes here, the wrong thing\n * is happening on the thread that must stay free for input.\n */\nimport type { AppLogicEndpoint } from '../app/worker/WorkerApp';\nimport { isHubMessage, isPortHandshake } from '../worker/WorkerPorts';\n\nimport { DEFAULT_CHUNK_BYTES, FrameAssembler, frameControl, frameData, type GessoFrame } from './frames';\n\nexport interface RemoteBridgeOptions {\n /**\n * Sends one frame to the other process: `view.rpc.send.<name>` in\n * an Electrobun window, `socket.send(JSON.stringify(frame))` over a\n * WebSocket. A function rather than the transport itself, so nothing\n * here depends on one, and it can be specified without any.\n */\n send: (frame: GessoFrame) => void;\n /** Overrides `DEFAULT_CHUNK_BYTES`. Only a test should need to. */\n chunkBytes?: number;\n /**\n * The appearance the platform is in, as the other process reports it.\n *\n * Wire it to `app.setColorScheme`. It exists because\n * `prefers-color-scheme` is not to be trusted in every webview: on\n * WebKitGTK it reported light on a desktop that was in dark mode,\n * and a shell that believes it is a browser gets the appearance\n * wrong there. A browser tab can leave it out.\n */\n onColorScheme?: (scheme: 'light' | 'dark') => void;\n}\n\nexport interface RemoteBridge {\n /**\n * Hand this to the shell as its application layer:\n *\n * createApp({ renderWorker: …, appLogicWorker: bridge.endpoint })\n *\n * The shell wires it exactly as it wires a worker it was handed, and\n * never closes it.\n */\n readonly endpoint: AppLogicEndpoint;\n /** Call with each frame that arrives from the other process. */\n receive(frame: GessoFrame): void;\n /**\n * Hands a url to the other process to open outside the page.\n *\n * Pass it as `onOpenUrl` to the shell: `window.open` in a webview\n * opens another webview or nothing at all, and a link in a desktop\n * application belongs in the person's browser.\n */\n openUrl(url: string): void;\n /**\n * Asks the other process to open this application at one of its own\n * urls somewhere new: a new window, in a desktop application.\n *\n * Pass it as `onOpenRoute` to the shell. It is what a Cmd-click on an\n * in-app link means in a desktop application: a window has no tabs,\n * and a `window.open` here would open a bare webview the application\n * does not serve. `createDesktopApp` opens the window, at that route,\n * unless the application said otherwise.\n */\n openRoute(url: string): void;\n /** Closes every stream and stops pumping. */\n dispose(): void;\n}\n\nexport function createRemoteBridge(options: RemoteBridgeOptions): RemoteBridge {\n const chunkBytes = options.chunkBytes ?? DEFAULT_CHUNK_BYTES;\n const assembler = new FrameAssembler();\n /** The render worker's port for each stream this side opened. */\n const ports = new Map<number, MessagePort>();\n let hub: MessagePort | null = null;\n let nextStream = 1;\n let disposed = false;\n\n const openStream = (name: string, port: MessagePort): void => {\n const stream = nextStream++;\n ports.set(stream, port);\n port.onmessage = event => {\n for (const frame of frameData(stream, event.data, chunkBytes)) {\n options.send(frame);\n }\n };\n port.start?.();\n options.send({ kind: 'open', stream, name });\n };\n\n const endpoint: AppLogicEndpoint = {\n postMessage(message: unknown, transfer?: Transferable[]): void {\n if (disposed) {\n return;\n }\n if (isHubMessage(message)) {\n const port = transfer?.[0] as MessagePort | undefined;\n if (port === undefined) {\n throw new Error('The shell sent a hub message with no port attached.');\n }\n hub = port;\n hub.onmessage = event => {\n if (!isPortHandshake(event.data)) {\n return;\n }\n const handshake = event.ports?.[0];\n if (handshake === undefined) {\n throw new Error(`Port handshake for '${event.data.key}' arrived with no port attached.`);\n }\n openStream(event.data.key, handshake);\n };\n hub.start?.();\n return;\n }\n // Everything else the shell sends an application worker is about\n // a worker in this page: console forwarding, today. The\n // application process's console is its own, and reaching it is\n // E1.2's business rather than the transport's.\n },\n addEventListener(): void {\n // The shell listens here for console entries from the\n // application worker. There is no worker, and nothing on the\n // other side of the bridge speaks that protocol yet, so a\n // listener would never be called and is not kept.\n },\n removeEventListener(): void {}\n };\n\n return {\n endpoint,\n openUrl(url: string): void {\n if (!disposed) {\n options.send(frameControl('openUrl', { url }));\n }\n },\n openRoute(url: string): void {\n if (!disposed) {\n options.send(frameControl('openRoute', { url }));\n }\n },\n receive(frame: GessoFrame): void {\n if (disposed) {\n return;\n }\n if (frame.kind === 'control') {\n if (frame.name === 'colorScheme') {\n const payload = JSON.parse(frame.body) as { scheme?: 'light' | 'dark' };\n if (payload.scheme !== undefined) {\n options.onColorScheme?.(payload.scheme);\n }\n }\n // An unknown control name is ignored rather than thrown on: the\n // other process may be newer than the page, which is ordinary\n // during a hot reload.\n return;\n }\n if (frame.kind === 'close') {\n assembler.forget(frame.stream);\n ports.get(frame.stream)?.close();\n ports.delete(frame.stream);\n return;\n }\n if (frame.kind !== 'data') {\n // `open` is this side's word. One arriving from the main\n // process means the two ends disagree about who starts a\n // stream, which is worth hearing about rather than ignoring.\n throw new Error(`The other process opened stream ${frame.stream}, which only the page may do.`);\n }\n const value = assembler.take(frame);\n if (value === undefined) {\n return;\n }\n const port = ports.get(frame.stream);\n if (port === undefined) {\n // A frame for a stream this side has closed. Ordinary during\n // teardown, because the far end may already have sent.\n return;\n }\n port.postMessage(value);\n },\n dispose(): void {\n disposed = true;\n for (const [stream, port] of ports) {\n options.send({ kind: 'close', stream });\n port.close();\n }\n ports.clear();\n if (hub !== null) {\n hub.onmessage = null;\n hub.close();\n hub = null;\n }\n }\n };\n}\n","/**\n * The other process's half of a remote channel bridge.\n *\n * `serveChannels` publishes an application's channels over a handshake\n * that arrives on a worker's global scope with a `MessagePort`\n * attached. No process boundary outside a worker can carry a port, so\n * this synthesises both: an `open` frame becomes the handshake, and\n * the port it hands over writes back as frames on the same stream.\n *\n * Nothing in `serveChannels`, `provide` or a view model changes for\n * this. That is the point of the seam: the application layer does not\n * learn it is talking to a page in another process, whether that page\n * is a desktop window or a browser tab on a WebSocket.\n */\nimport { serveChannels, type ServedChannel } from '../channel/serveChannels';\nimport type { PortHost } from '../worker/WorkerPorts';\n\nimport { DEFAULT_CHUNK_BYTES, FrameAssembler, frameControl, frameData, type GessoFrame } from './frames';\n\nexport interface RemoteChannelHostOptions {\n /**\n * Sends one frame to the page: `window.webview.rpc.send.<name>` for\n * an Electrobun window, `socket.send(JSON.stringify(frame))` for a\n * WebSocket. One host per page: each keeps its own streams, and\n * `provide` already keeps a separate record of what each client has\n * seen, so two pages agree without anything here arranging it.\n */\n send: (frame: GessoFrame) => void;\n /** Overrides `DEFAULT_CHUNK_BYTES`. Only a test should need to. */\n chunkBytes?: number;\n /**\n * A url the page asked to have opened outside itself.\n *\n * `Utils.openExternal(url)` is what an Electrobun application passes\n * here. It is not called for the application: opening something is\n * an act, and which urls an application is willing to hand to the\n * operating system is the application's decision.\n */\n onOpenUrl?: (url: string) => void;\n /**\n * One of the application's own urls the page asked to have opened\n * somewhere new: a Cmd-click on an in-app link, sent by the page\n * bridge's `openRoute`.\n *\n * Not called for the application either. `createDesktopApp` answers\n * it by opening a window at that route; a host built on this alone\n * decides for itself.\n */\n onOpenRoute?: (url: string) => void;\n}\n\nexport interface RemoteChannelHost {\n /** Call with each frame that arrives from the page. */\n receive(frame: GessoFrame): void;\n /** Tells the page which appearance the platform is in. */\n setColorScheme(scheme: 'light' | 'dark'): void;\n /** Stops serving and disposes every channel this host provided. */\n dispose(): void;\n}\n\n/**\n * Serves an application's channels to one page in another process.\n *\n * const host = serveRemoteChannels(\n * [{ token: Catalogue, source: { view: { … }, commands: { … } } }],\n * { send: frame => socket.send(JSON.stringify(frame)) }\n * );\n * // and, for each message from that page:\n * host.receive(JSON.parse(message));\n */\nexport function serveRemoteChannels(\n channels: readonly ServedChannel[],\n options: RemoteChannelHostOptions\n): RemoteChannelHost {\n const chunkBytes = options.chunkBytes ?? DEFAULT_CHUNK_BYTES;\n const assembler = new FrameAssembler();\n /** The synthetic port each stream is served through. */\n const ports = new Map<number, StreamPort>();\n\n // `servePorts` installs its chain on this and answers handshakes\n // delivered through `onmessage`. It is the whole of the shim: a\n // worker's global scope, as far as anything downstream can tell.\n const host: PortHost = { onmessage: null };\n const stop = serveChannels(channels, host);\n\n return {\n setColorScheme(scheme: 'light' | 'dark'): void {\n options.send(frameControl('colorScheme', { scheme }));\n },\n receive(frame: GessoFrame): void {\n if (frame.kind === 'control') {\n if (frame.name === 'openUrl' || frame.name === 'openRoute') {\n const payload = JSON.parse(frame.body) as { url?: string };\n if (typeof payload.url === 'string') {\n (frame.name === 'openUrl' ? options.onOpenUrl : options.onOpenRoute)?.(payload.url);\n }\n }\n return;\n }\n if (frame.kind === 'open') {\n const port = new StreamPort(frame.stream, options.send, chunkBytes);\n ports.set(frame.stream, port);\n host.onmessage?.({\n data: { type: 'gesso:port', key: frame.name },\n // A `StreamPort` is a `MessagePort` in the two ways anything\n // downstream uses one, and in no others. The cast is the\n // seam; widening `PortHost` to admit a structural port would\n // widen it for every worker as well.\n ports: [port as unknown as MessagePort]\n });\n return;\n }\n if (frame.kind === 'close') {\n assembler.forget(frame.stream);\n ports.delete(frame.stream);\n return;\n }\n const value = assembler.take(frame);\n if (value === undefined) {\n return;\n }\n const port = ports.get(frame.stream);\n if (port === undefined) {\n throw new Error(\n `A frame arrived for stream ${frame.stream}, which was never opened. The page and this process disagree about what is running.`\n );\n }\n port.deliver(value);\n },\n dispose(): void {\n for (const stream of ports.keys()) {\n options.send({ kind: 'close', stream });\n }\n ports.clear();\n stop();\n host.onmessage = null;\n }\n };\n}\n\n/**\n * One channel's port, as the application layer sees it.\n *\n * `provide` sets `onmessage` and calls `postMessage`, and that is the\n * entire surface it uses, which is why a channel can be served over\n * something that is not a `MessagePort` at all.\n */\nclass StreamPort {\n onmessage: ((event: { data: unknown }) => void) | null = null;\n\n constructor(\n private readonly stream: number,\n private readonly send: (frame: GessoFrame) => void,\n private readonly chunkBytes: number\n ) {}\n\n postMessage(value: unknown): void {\n for (const frame of frameData(this.stream, value, this.chunkBytes)) {\n this.send(frame);\n }\n }\n\n deliver(value: unknown): void {\n this.onmessage?.({ data: value });\n }\n\n /** `provide` closes a port it is done with; there is nothing to close. */\n close(): void {}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/remote/frames.ts","../../src/remote/bridge.ts","../../src/remote/host.ts"],"sourcesContent":["/**\n * The whole wire format, which is deliberately smaller than the\n * channel protocol it carries.\n *\n * Three frames in each direction. `open` says a named channel wants a\n * stream, `data` carries one channel message, `close` ends a stream.\n * Nothing here knows what a channel is, what a patch is, or what a\n * command is: a frame's `body` is a string this module produced by\n * serializing a value it never looked inside. That restraint is the\n * rule the bridge keeps, and it is what keeps the page's main thread\n * a transport rather than a router.\n */\n\n/**\n * How much of a serialized message goes in one frame.\n *\n * Electrobun's transport fails above roughly 8 MiB in a single\n * message, and it fails badly: the main process throws while draining\n * and the sender sees only a timeout, so the real error is in a log\n * nobody is reading. A megabyte leaves eight times the headroom and\n * costs nothing at the measured rates.\n */\nexport const DEFAULT_CHUNK_BYTES = 1_048_576;\n\nexport type GessoFrame =\n | { readonly kind: 'open'; readonly stream: number; readonly name: string }\n | {\n readonly kind: 'data';\n readonly stream: number;\n readonly body: string;\n /** Absent unless the message was split; then 0-based. */\n readonly part?: number;\n /** Absent unless the message was split; then how many parts to expect. */\n readonly parts?: number;\n }\n | { readonly kind: 'close'; readonly stream: number }\n /**\n * The adapter's own traffic, which is not a channel: the appearance\n * the platform is in, a url the application wants opened outside the\n * window, and one of its own routes it wants opened in a new window.\n * It carries a name and a serialized payload for\n * the same reason a `data` frame carries a body, and it is a\n * separate kind so that nothing has to reserve a stream number.\n */\n | { readonly kind: 'control'; readonly name: string; readonly body: string };\n\nexport function isGessoFrame(value: unknown): value is GessoFrame {\n const kind = (value as { kind?: unknown } | null)?.kind;\n return kind === 'open' || kind === 'data' || kind === 'close' || kind === 'control';\n}\n\n/**\n * A `SharedArrayBuffer` is memory two threads of one page read at once,\n * and another process cannot read it. JSON would quietly turn it into\n * `{}`, so it is refused here, by name.\n */\nfunction refuseSharedMemory(this: unknown, key: string, value: unknown): unknown {\n if (typeof SharedArrayBuffer !== 'undefined' && value instanceof SharedArrayBuffer) {\n throw new Error(\n `A channel message holds a SharedArrayBuffer${key === '' ? '' : ` at '${key}'`}, which cannot cross to another process. ` +\n 'Shared memory works between the workers of one page; across a process boundary send the bytes as data.'\n );\n }\n return value;\n}\n\n/** Wraps one control message. Never split: these are small by construction. */\nexport function frameControl(name: string, payload: unknown): GessoFrame {\n return { kind: 'control', name, body: JSON.stringify(payload ?? null) ?? 'null' };\n}\n\n/**\n * Serializes one channel message into the frames that carry it.\n *\n * JSON rather than structured clone, because the transport underneath\n * is JSON either way: `Electroview.createTransport` stringifies every\n * message before it encrypts it. That means `undefined` inside a value\n * does not survive, which is true of this transport with or without\n * this module, and which a view key cannot rely on anyway.\n */\nexport function frameData(stream: number, value: unknown, chunkBytes = DEFAULT_CHUNK_BYTES): GessoFrame[] {\n const body = JSON.stringify(value, refuseSharedMemory);\n if (body === undefined) {\n throw new Error(\n `A channel message for stream ${stream} could not be serialized. Only plain data crosses a channel; see requirePlainData.`\n );\n }\n if (body.length <= chunkBytes) {\n return [{ kind: 'data', stream, body }];\n }\n const parts = Math.ceil(body.length / chunkBytes);\n const frames: GessoFrame[] = [];\n for (let part = 0; part < parts; part++) {\n frames.push({\n kind: 'data',\n stream,\n body: body.slice(part * chunkBytes, (part + 1) * chunkBytes),\n part,\n parts\n });\n }\n return frames;\n}\n\n/**\n * Puts split messages back together.\n *\n * The transport delivers in order (`Electroview` dispatches through a\n * promise tail that preserves frame order), so a part that arrives out\n * of turn is a bug rather than a race, and it says so instead of\n * quietly assembling a corrupt message.\n */\nexport class FrameAssembler {\n private readonly partial = new Map<number, { parts: number; chunks: string[] }>();\n\n /**\n * Returns the value a `data` frame completes, or `undefined` while\n * more parts are still to come.\n */\n take(frame: GessoFrame & { kind: 'data' }): unknown {\n if (frame.parts === undefined) {\n return JSON.parse(frame.body);\n }\n const held = this.partial.get(frame.stream) ?? { parts: frame.parts, chunks: [] };\n const expected = held.chunks.length;\n if (frame.part !== expected) {\n this.partial.delete(frame.stream);\n throw new Error(\n `Stream ${frame.stream} received part ${String(frame.part)} when part ${expected} was next. ` +\n 'Frames are expected in order; a gap means the transport reordered or dropped one.'\n );\n }\n held.chunks.push(frame.body);\n if (held.chunks.length < held.parts) {\n this.partial.set(frame.stream, held);\n return undefined;\n }\n this.partial.delete(frame.stream);\n return JSON.parse(held.chunks.join(''));\n }\n\n /** Drops anything half-received for a stream that has closed. */\n forget(stream: number): void {\n this.partial.delete(stream);\n }\n}\n","/**\n * The page's half of a remote channel bridge.\n *\n * A Gesso render worker opens a named port per channel and expects the\n * application layer at the other end of it. When the application layer\n * lives in another process (a desktop app's main process, a server on\n * the person's machine) it is reachable only from the page's main\n * thread, so this stands in its place: it accepts the handshakes the\n * render worker sends and pumps each one as a numbered stream of frames\n * over whatever the page has to the other process. Electrobun's RPC is\n * one such thing; a WebSocket is another.\n *\n * It is a transport and nothing else. It serializes a message it never\n * inspects, and it holds no channel, no token and no patch. Anything\n * that needs to understand a payload to route it belongs on the other\n * side of the bridge, and if that ever changes here, the wrong thing\n * is happening on the thread that must stay free for input.\n */\nimport type { AppLogicEndpoint } from '../app/worker/WorkerApp';\nimport { isHubMessage, isPortHandshake } from '../worker/WorkerPorts';\n\nimport { DEFAULT_CHUNK_BYTES, FrameAssembler, frameControl, frameData, type GessoFrame } from './frames';\n\nexport interface RemoteBridgeOptions {\n /**\n * Sends one frame to the other process: `view.rpc.send.<name>` in\n * an Electrobun window, `socket.send(JSON.stringify(frame))` over a\n * WebSocket. A function rather than the transport itself, so nothing\n * here depends on one, and it can be specified without any.\n */\n send: (frame: GessoFrame) => void;\n /** Overrides `DEFAULT_CHUNK_BYTES`. Only a test should need to. */\n chunkBytes?: number;\n /**\n * The appearance the platform is in, as the other process reports it.\n *\n * Wire it to `app.setColorScheme`. It exists because\n * `prefers-color-scheme` is not to be trusted in every webview: on\n * WebKitGTK it reported light on a desktop that was in dark mode,\n * and a shell that believes it is a browser gets the appearance\n * wrong there. A browser tab can leave it out.\n */\n onColorScheme?: (scheme: 'light' | 'dark') => void;\n}\n\nexport interface RemoteBridge {\n /**\n * Hand this to the shell as its application layer:\n *\n * createApp({ renderWorker: …, appLogicWorker: bridge.endpoint })\n *\n * The shell wires it exactly as it wires a worker it was handed, and\n * never closes it.\n */\n readonly endpoint: AppLogicEndpoint;\n /** Call with each frame that arrives from the other process. */\n receive(frame: GessoFrame): void;\n /**\n * Hands a url to the other process to open outside the page.\n *\n * Pass it as `onOpenUrl` to the shell: `window.open` in a webview\n * opens another webview or nothing at all, and a link in a desktop\n * application belongs in the person's browser.\n */\n openUrl(url: string): void;\n /**\n * Asks the other process to open this application at one of its own\n * urls somewhere new: a new window, in a desktop application.\n *\n * Pass it as `onOpenRoute` to the shell. It is what a Cmd-click on an\n * in-app link means in a desktop application: a window has no tabs,\n * and a `window.open` here would open a bare webview the application\n * does not serve. `createDesktopApp` opens the window, at that route,\n * unless the application said otherwise.\n */\n openRoute(url: string): void;\n /** Closes every stream and stops pumping. */\n dispose(): void;\n}\n\nexport function createRemoteBridge(options: RemoteBridgeOptions): RemoteBridge {\n const chunkBytes = options.chunkBytes ?? DEFAULT_CHUNK_BYTES;\n const assembler = new FrameAssembler();\n /** The render worker's port for each stream this side opened. */\n const ports = new Map<number, MessagePort>();\n let hub: MessagePort | null = null;\n let nextStream = 1;\n let disposed = false;\n\n const openStream = (name: string, port: MessagePort): void => {\n const stream = nextStream++;\n ports.set(stream, port);\n port.onmessage = event => {\n for (const frame of frameData(stream, event.data, chunkBytes)) {\n options.send(frame);\n }\n };\n port.start?.();\n options.send({ kind: 'open', stream, name });\n };\n\n const endpoint: AppLogicEndpoint = {\n postMessage(message: unknown, transfer?: Transferable[]): void {\n if (disposed) {\n return;\n }\n if (isHubMessage(message)) {\n const port = transfer?.[0] as MessagePort | undefined;\n if (port === undefined) {\n throw new Error('The shell sent a hub message with no port attached.');\n }\n hub = port;\n hub.onmessage = event => {\n if (!isPortHandshake(event.data)) {\n return;\n }\n const handshake = event.ports?.[0];\n if (handshake === undefined) {\n throw new Error(`Port handshake for '${event.data.key}' arrived with no port attached.`);\n }\n openStream(event.data.key, handshake);\n };\n hub.start?.();\n return;\n }\n // Everything else the shell sends an application worker is about\n // a worker in this page: console forwarding, today. The\n // application process's console is its own, and reaching it is\n // E1.2's business rather than the transport's.\n },\n addEventListener(): void {\n // The shell listens here for console entries from the\n // application worker. There is no worker, and nothing on the\n // other side of the bridge speaks that protocol yet, so a\n // listener would never be called and is not kept.\n },\n removeEventListener(): void {}\n };\n\n return {\n endpoint,\n openUrl(url: string): void {\n if (!disposed) {\n options.send(frameControl('openUrl', { url }));\n }\n },\n openRoute(url: string): void {\n if (!disposed) {\n options.send(frameControl('openRoute', { url }));\n }\n },\n receive(frame: GessoFrame): void {\n if (disposed) {\n return;\n }\n if (frame.kind === 'control') {\n if (frame.name === 'colorScheme') {\n const payload = JSON.parse(frame.body) as { scheme?: 'light' | 'dark' };\n if (payload.scheme !== undefined) {\n options.onColorScheme?.(payload.scheme);\n }\n }\n // An unknown control name is ignored rather than thrown on: the\n // other process may be newer than the page, which is ordinary\n // during a hot reload.\n return;\n }\n if (frame.kind === 'close') {\n assembler.forget(frame.stream);\n ports.get(frame.stream)?.close();\n ports.delete(frame.stream);\n return;\n }\n if (frame.kind !== 'data') {\n // `open` is this side's word. One arriving from the main\n // process means the two ends disagree about who starts a\n // stream, which is worth hearing about rather than ignoring.\n throw new Error(`The other process opened stream ${frame.stream}, which only the page may do.`);\n }\n const value = assembler.take(frame);\n if (value === undefined) {\n return;\n }\n const port = ports.get(frame.stream);\n if (port === undefined) {\n // A frame for a stream this side has closed. Ordinary during\n // teardown, because the far end may already have sent.\n return;\n }\n port.postMessage(value);\n },\n dispose(): void {\n disposed = true;\n for (const [stream, port] of ports) {\n options.send({ kind: 'close', stream });\n port.close();\n }\n ports.clear();\n if (hub !== null) {\n hub.onmessage = null;\n hub.close();\n hub = null;\n }\n }\n };\n}\n","/**\n * The other process's half of a remote channel bridge.\n *\n * `serveChannels` publishes an application's channels over a handshake\n * that arrives on a worker's global scope with a `MessagePort`\n * attached. No process boundary outside a worker can carry a port, so\n * this synthesises both: an `open` frame becomes the handshake, and\n * the port it hands over writes back as frames on the same stream.\n *\n * Nothing in `serveChannels`, `provide` or a view model changes for\n * this. That is the point of the seam: the application layer does not\n * learn it is talking to a page in another process, whether that page\n * is a desktop window or a browser tab on a WebSocket.\n */\nimport { serveChannels, type ServedChannel } from '../channel/serveChannels';\nimport type { PortHost } from '../worker/WorkerPorts';\n\nimport { DEFAULT_CHUNK_BYTES, FrameAssembler, frameControl, frameData, type GessoFrame } from './frames';\n\nexport interface RemoteChannelHostOptions {\n /**\n * Sends one frame to the page: `window.webview.rpc.send.<name>` for\n * an Electrobun window, `socket.send(JSON.stringify(frame))` for a\n * WebSocket. One host per page: each keeps its own streams, and\n * `provide` already keeps a separate record of what each client has\n * seen, so two pages agree without anything here arranging it.\n */\n send: (frame: GessoFrame) => void;\n /** Overrides `DEFAULT_CHUNK_BYTES`. Only a test should need to. */\n chunkBytes?: number;\n /**\n * A url the page asked to have opened outside itself.\n *\n * `Utils.openExternal(url)` is what an Electrobun application passes\n * here. It is not called for the application: opening something is\n * an act, and which urls an application is willing to hand to the\n * operating system is the application's decision.\n */\n onOpenUrl?: (url: string) => void;\n /**\n * One of the application's own urls the page asked to have opened\n * somewhere new: a Cmd-click on an in-app link, sent by the page\n * bridge's `openRoute`.\n *\n * Not called for the application either. `createDesktopApp` answers\n * it by opening a window at that route; a host built on this alone\n * decides for itself.\n */\n onOpenRoute?: (url: string) => void;\n}\n\nexport interface RemoteChannelHost {\n /** Call with each frame that arrives from the page. */\n receive(frame: GessoFrame): void;\n /** Tells the page which appearance the platform is in. */\n setColorScheme(scheme: 'light' | 'dark'): void;\n /** Stops serving and disposes every channel this host provided. */\n dispose(): void;\n}\n\n/**\n * Serves an application's channels to one page in another process.\n *\n * const host = serveRemoteChannels(\n * [{ token: Catalogue, source: { view: { … }, commands: { … } } }],\n * { send: frame => socket.send(JSON.stringify(frame)) }\n * );\n * // and, for each message from that page:\n * host.receive(JSON.parse(message));\n */\nexport function serveRemoteChannels(\n channels: readonly ServedChannel[],\n options: RemoteChannelHostOptions\n): RemoteChannelHost {\n const chunkBytes = options.chunkBytes ?? DEFAULT_CHUNK_BYTES;\n const assembler = new FrameAssembler();\n /** The synthetic port each stream is served through. */\n const ports = new Map<number, StreamPort>();\n\n // `servePorts` installs its chain on this and answers handshakes\n // delivered through `onmessage`. It is the whole of the shim: a\n // worker's global scope, as far as anything downstream can tell.\n const host: PortHost = { onmessage: null };\n const stop = serveChannels(channels, host);\n\n return {\n setColorScheme(scheme: 'light' | 'dark'): void {\n options.send(frameControl('colorScheme', { scheme }));\n },\n receive(frame: GessoFrame): void {\n if (frame.kind === 'control') {\n if (frame.name === 'openUrl' || frame.name === 'openRoute') {\n const payload = JSON.parse(frame.body) as { url?: string };\n if (typeof payload.url === 'string') {\n (frame.name === 'openUrl' ? options.onOpenUrl : options.onOpenRoute)?.(payload.url);\n }\n }\n return;\n }\n if (frame.kind === 'open') {\n const port = new StreamPort(frame.stream, options.send, chunkBytes);\n ports.set(frame.stream, port);\n host.onmessage?.({\n data: { type: 'gesso:port', key: frame.name },\n // A `StreamPort` is a `MessagePort` in the two ways anything\n // downstream uses one, and in no others. The cast is the\n // seam; widening `PortHost` to admit a structural port would\n // widen it for every worker as well.\n ports: [port as unknown as MessagePort]\n });\n return;\n }\n if (frame.kind === 'close') {\n assembler.forget(frame.stream);\n ports.delete(frame.stream);\n return;\n }\n const value = assembler.take(frame);\n if (value === undefined) {\n return;\n }\n const port = ports.get(frame.stream);\n if (port === undefined) {\n throw new Error(\n `A frame arrived for stream ${frame.stream}, which was never opened. The page and this process disagree about what is running.`\n );\n }\n port.deliver(value);\n },\n dispose(): void {\n for (const stream of ports.keys()) {\n options.send({ kind: 'close', stream });\n }\n ports.clear();\n stop();\n host.onmessage = null;\n }\n };\n}\n\n/**\n * One channel's port, as the application layer sees it.\n *\n * `provide` sets `onmessage` and calls `postMessage`, and that is the\n * entire surface it uses, which is why a channel can be served over\n * something that is not a `MessagePort` at all.\n */\nclass StreamPort {\n onmessage: ((event: { data: unknown }) => void) | null = null;\n\n constructor(\n private readonly stream: number,\n private readonly send: (frame: GessoFrame) => void,\n private readonly chunkBytes: number\n ) {}\n\n postMessage(value: unknown): void {\n for (const frame of frameData(this.stream, value, this.chunkBytes)) {\n this.send(frame);\n }\n }\n\n deliver(value: unknown): void {\n this.onmessage?.({ data: value });\n }\n\n /** `provide` closes a port it is done with; there is nothing to close. */\n close(): void {}\n}\n"],"mappings":"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"}
|
|
@@ -274,6 +274,12 @@ function describe(value) {
|
|
|
274
274
|
}
|
|
275
275
|
//#endregion
|
|
276
276
|
//#region src/agent/AgentSurface.ts
|
|
277
|
+
/** The `_meta` key a tool that waits on purpose says how long under, in milliseconds. */
|
|
278
|
+
const WAITS_UP_TO = "gesso/waitsUpToMs";
|
|
279
|
+
/** A result's text parts, joined: what a reader that shows no pictures shows. */
|
|
280
|
+
function resultText(result) {
|
|
281
|
+
return result.content.map((part) => part.type === "text" ? part.text : "").filter(Boolean).join("\n");
|
|
282
|
+
}
|
|
277
283
|
/** The longest tool name MCP clients accept. */
|
|
278
284
|
const MAX_NAME = 64;
|
|
279
285
|
function agentSurface(channels, options = {}) {
|
|
@@ -600,6 +606,9 @@ function answer(surface, request) {
|
|
|
600
606
|
*/
|
|
601
607
|
function remoteSurface(port, options = {}) {
|
|
602
608
|
const timeoutMs = options.timeoutMs ?? 2e3;
|
|
609
|
+
const callTimeoutMs = options.callTimeoutMs ?? 12e4;
|
|
610
|
+
/** How long each tool listed says it may wait, for the calls of it. */
|
|
611
|
+
const waits = /* @__PURE__ */ new Map();
|
|
603
612
|
let next = 1;
|
|
604
613
|
const pending = /* @__PURE__ */ new Map();
|
|
605
614
|
port.onmessage = (event) => {
|
|
@@ -643,7 +652,13 @@ function remoteSurface(port, options = {}) {
|
|
|
643
652
|
});
|
|
644
653
|
});
|
|
645
654
|
return {
|
|
646
|
-
tools: () => ask({ op: "tools" }, [])
|
|
655
|
+
tools: () => ask({ op: "tools" }, []).then((tools) => {
|
|
656
|
+
for (const tool of tools) {
|
|
657
|
+
const wait = tool._meta?.[WAITS_UP_TO];
|
|
658
|
+
if (typeof wait === "number") waits.set(tool.name, wait);
|
|
659
|
+
}
|
|
660
|
+
return tools;
|
|
661
|
+
}),
|
|
647
662
|
resources: () => ask({ op: "resources" }, []),
|
|
648
663
|
call: (name, args) => ask({
|
|
649
664
|
op: "call",
|
|
@@ -655,7 +670,7 @@ function remoteSurface(port, options = {}) {
|
|
|
655
670
|
text: `${name} did not answer in time.`
|
|
656
671
|
}],
|
|
657
672
|
isError: true
|
|
658
|
-
}, Math.max(timeoutMs,
|
|
673
|
+
}, Math.max(timeoutMs, callTimeoutMs, waits.has(name) ? waits.get(name) + 5e3 : 0)),
|
|
659
674
|
read: async (uri) => await ask({
|
|
660
675
|
op: "read",
|
|
661
676
|
uri
|
|
@@ -720,6 +735,6 @@ function combineSurfaces(surfaces) {
|
|
|
720
735
|
};
|
|
721
736
|
}
|
|
722
737
|
//#endregion
|
|
723
|
-
export {
|
|
738
|
+
export { WAITS_UP_TO as a, resultText as c, requirePlainData as d, channelSchema as f, serveAgentPort as i, validate as l, combineSurfaces as n, agentSurface as o, describeChannel as p, remoteSurface as r, resourceUri as s, AGENT_PORT as t, findUnplainPath as u };
|
|
724
739
|
|
|
725
|
-
//# sourceMappingURL=remote-
|
|
740
|
+
//# sourceMappingURL=remote-B4bjdTzg.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-B4bjdTzg.js","names":[],"sources":["../src/channel/ChannelSchema.ts","../src/channel/plainData.ts","../src/agent/binary.ts","../src/agent/validate.ts","../src/agent/AgentSurface.ts","../src/agent/remote.ts"],"sourcesContent":["/**\n * A channel described at run time: what its view holds and what its\n * commands take, as JSON Schema.\n *\n * A token's types are erased by the compiler, so a running application\n * cannot ask a channel what it accepts. That is no loss to a component,\n * which was typechecked against the token, and all of it to anything\n * that meets the application only at run time: an AI agent being\n * handed the channel as tools, a devtools panel, a test that drives an\n * app by its commands. The schema is the contract again, in the one\n * form those can read.\n *\n * `gesso-vite-plugin` writes it from the contract's own types and\n * JSDoc, so an application that uses the plugin describes its channels\n * by having written them. Without the plugin, `describeChannel` takes a\n * hand-written one.\n */\nimport type { ChannelToken } from './ChannelToken';\n\n/**\n * A JSON Schema (draft 2020-12) object.\n *\n * Deliberately loose: the framework stores and hands back schemas and\n * never interprets one, so a precise type would be a second JSON Schema\n * specification to keep in step with the real one.\n */\nexport type JsonSchema = { readonly [keyword: string]: unknown };\n\n/** One command, described. */\nexport interface CommandSchema {\n /** The command's JSDoc summary. */\n readonly description?: string;\n /**\n * The parameter names, in the order the command takes them.\n *\n * A command is called positionally, `move(from, to)`, while a tool\n * call names its arguments, so this is what maps one onto the other.\n */\n readonly parameters: readonly string[];\n /** The last parameter is a rest parameter, and its array is spread into the call. */\n readonly rest?: true;\n /** An object schema, one property per parameter, keyed by parameter name. */\n readonly input: JsonSchema;\n /** `@destructive`: what it does cannot be undone. */\n readonly destructive?: true;\n /** `@idempotent`: sending it twice changes nothing the first did not. */\n readonly idempotent?: true;\n /** `@confirm`: a person should approve it before anything else sends it. */\n readonly confirm?: true;\n /** `@hidden`: for the application's own components, not for an agent. */\n readonly hidden?: true;\n}\n\n/** A channel, described. */\nexport interface ChannelSchema {\n /** The token's JSDoc summary. */\n readonly description?: string;\n /** An object schema, one property per view key. */\n readonly view: JsonSchema;\n /**\n * `@hidden` view keys: the application's own plumbing, or bulk only a\n * screen can use — geometry, a clipboard, a file's text. An agent is\n * given the view without them, in its tools' results, its resource\n * and its output schema, so what it reads is what it can act on.\n */\n readonly hidden?: readonly string[];\n /**\n * `@hidden` on the token: the channel is the application's own\n * plumbing as a whole, the barrier between its threads, say, whose\n * commands are a screen's and whose view holds shared memory. An agent\n * is offered nothing of it: no view tool, no command, no resource. The\n * application reaches it as before.\n */\n readonly internal?: true;\n readonly commands: { readonly [name: string]: CommandSchema };\n}\n\n/**\n * The schemas, beside the tokens rather than on them.\n *\n * A token is a plain object both threads import and nothing should\n * grow on, and its interface is a public type that a schema field would\n * widen for every application, described or not. Keyed weakly, so a\n * token the application drops takes its schema with it.\n */\nconst schemas = new WeakMap<object, ChannelSchema>();\n\n/**\n * Attaches a schema to a token.\n *\n * `gesso-vite-plugin` calls this at the bottom of each contract module\n * it reads, so it runs once, when the module is first imported. Call it\n * yourself to describe a channel the plugin does not see. A second call\n * for the same token replaces the first, which is what a hot-replaced\n * contract module does.\n */\nexport function describeChannel(token: ChannelToken<object, object>, schema: ChannelSchema): void {\n schemas.set(token, schema);\n}\n\n/** The schema attached to a token, or undefined when nothing described it. */\nexport function channelSchema(token: ChannelToken<object, object>): ChannelSchema | undefined {\n return schemas.get(token);\n}\n","/**\n * Checks that a value can cross the barrier.\n *\n * `structurallyEqual` understands primitives, arrays and plain\n * objects, and falls back to reference equality for everything else —\n * which for a freshly built value reports \"changed\" every single time.\n * A view key holding a `Date`, a `Map` or a domain object therefore\n * re-emits on every unrelated update and dirties the subtree bound to\n * it, forever, while looking perfectly correct.\n *\n * That failure is invisible in a test and shows up as a vague slowness\n * much later, so `provide` checks each key's first emission and throws\n * naming the path. The view-model layer is where rich objects become\n * flat data; this is what makes that a rule rather than a convention.\n *\n * The check runs once per key, on the first value only. It is a\n * development guard against a design mistake, not a validator on the\n * hot path.\n *\n * One kind of object is allowed: a `SharedArrayBuffer`. Reference\n * equality is the right comparison for it, not a trap: it is a handle\n * on memory, two handles on the same memory are the same value, and\n * one built afresh is new memory. Posted to another thread it is not\n * copied; both threads read the same bytes. That is how a large\n * document is shared between the app worker and the render worker\n * rather than sent: the view carries the handle once, and what changes\n * after that is small plain data saying which version to read. It\n * cannot cross to another process (`remote/frames.ts` says so), and an\n * agent is shown its size rather than its bytes.\n */\n\nconst MAX_DEPTH = 100;\n\n/**\n * Returns the path to the first value that cannot cross, or null when\n * the whole tree is plain data.\n */\nexport function findUnplainPath(value: unknown, path: readonly (string | number)[] = []): string | null {\n if (path.length > MAX_DEPTH) {\n return format(path);\n }\n if (value === null) {\n return null;\n }\n const type = typeof value;\n if (type === 'string' || type === 'number' || type === 'boolean' || type === 'undefined') {\n return null;\n }\n if (type === 'function' || type === 'symbol' || type === 'bigint') {\n return format(path);\n }\n if (Array.isArray(value)) {\n for (let index = 0; index < value.length; index++) {\n const found = findUnplainPath(value[index], [...path, index]);\n if (found !== null) {\n return found;\n }\n }\n return null;\n }\n if (isSharedMemory(value)) {\n return null;\n }\n const prototype = Object.getPrototypeOf(value as object);\n if (prototype !== Object.prototype && prototype !== null) {\n return format(path);\n }\n for (const [key, member] of Object.entries(value as Record<string, unknown>)) {\n const found = findUnplainPath(member, [...path, key]);\n if (found !== null) {\n return found;\n }\n }\n return null;\n}\n\n/**\n * Throws when `value` cannot cross the barrier, naming the channel,\n * the key and the path within it.\n */\nexport function requirePlainData(channelName: string, key: string, value: unknown): void {\n const path = findUnplainPath(value);\n if (path === null) {\n return;\n }\n const where = path === '' ? `'${key}'` : `'${key}'${path}`;\n throw new Error(\n `Channel '${channelName}' published ${where}, which is not plain data. ` +\n `Only primitives, arrays, plain objects and SharedArrayBuffers cross the barrier: a Date, Map, Set, ` +\n `class instance or function compares by reference, so it would report a change on ` +\n `every update and rebuild the subtree bound to it. Flatten it in the view model.`\n );\n}\n\n/** A `SharedArrayBuffer`, where the platform has them (cross-origin isolated pages, and Node). */\nexport function isSharedMemory(value: unknown): value is SharedArrayBuffer {\n return typeof SharedArrayBuffer !== 'undefined' && value instanceof SharedArrayBuffer;\n}\n\n/**\n * The value with each `SharedArrayBuffer` in it replaced by a sentence\n * giving its size, for a reader that gets JSON: an agent, which would\n * otherwise be shown `{}`.\n */\nexport function describeSharedMemory(value: unknown): unknown {\n if (isSharedMemory(value)) {\n return `[shared memory, ${value.byteLength} bytes]`;\n }\n if (Array.isArray(value)) {\n return value.some(item => typeof item === 'object' && item !== null) ? value.map(describeSharedMemory) : value;\n }\n if (typeof value === 'object' && value !== null) {\n const entries = Object.entries(value as Record<string, unknown>);\n if (!entries.some(([, member]) => typeof member === 'object' && member !== null)) {\n return value;\n }\n return Object.fromEntries(entries.map(([key, member]) => [key, describeSharedMemory(member)]));\n }\n return value;\n}\n\nfunction format(path: readonly (string | number)[]): string {\n return path.map(step => (typeof step === 'number' ? `[${step}]` : `.${step}`)).join('');\n}\n","import type { JsonSchema } from '../channel/ChannelSchema';\n\n/**\n * Turns the base64 an agent wrote back into the bytes a command takes.\n *\n * A command may carry an `ArrayBuffer` or a typed array, which an agent\n * cannot write: it speaks JSON. `gesso-vite-plugin` describes such a\n * field as a base64 string tagged `x-gesso-binary` with the type the\n * command expects, the agent sends the string, and this walks the\n * arguments beside their schema and puts the bytes back before the\n * command is sent, so the application receives what its own components\n * would have sent it.\n *\n * Only what the schema tags is touched, and a value that is not a\n * string where bytes are expected is left for the command to see: the\n * validator has already said whether it was the right shape.\n */\nexport function decodeBinary(schema: JsonSchema | undefined, value: unknown, root: JsonSchema): unknown {\n if (schema === undefined) {\n return value;\n }\n const ref = schema.$ref;\n if (typeof ref === 'string') {\n const target = (root.$defs as Record<string, JsonSchema> | undefined)?.[ref.replace(/^#\\/\\$defs\\//, '')];\n return decodeBinary(target, value, root);\n }\n const kind = schema['x-gesso-binary'];\n if (typeof kind === 'string') {\n return typeof value === 'string' ? bytesOf(value, kind) : value;\n }\n if (Array.isArray(schema.anyOf)) {\n // A union holding bytes is nearly always `bytes | null`; the branch\n // that is bytes is the one a string can be.\n const binary = (schema.anyOf as JsonSchema[]).find(option => typeof option['x-gesso-binary'] === 'string');\n return binary !== undefined && typeof value === 'string' ? decodeBinary(binary, value, root) : value;\n }\n if (Array.isArray(value)) {\n const prefix = (schema.prefixItems as JsonSchema[] | undefined) ?? [];\n const items = schema.items as JsonSchema | boolean | undefined;\n return value.map((item, index) =>\n decodeBinary(index < prefix.length ? prefix[index] : typeof items === 'object' ? items : undefined, item, root)\n );\n }\n if (value !== null && typeof value === 'object') {\n const properties = (schema.properties as Record<string, JsonSchema> | undefined) ?? {};\n const out: Record<string, unknown> = {};\n for (const [key, field] of Object.entries(value)) {\n out[key] = decodeBinary(properties[key], field, root);\n }\n return out;\n }\n return value;\n}\n\nconst TYPED: Record<string, new (buffer: ArrayBuffer) => ArrayBufferView> = {\n Int8Array,\n Uint8Array,\n Uint8ClampedArray,\n Int16Array,\n Uint16Array,\n Int32Array,\n Uint32Array,\n Float32Array,\n Float64Array,\n BigInt64Array,\n BigUint64Array\n};\n\nfunction bytesOf(base64: string, kind: string): ArrayBuffer | ArrayBufferView {\n const text = atob(base64);\n const bytes = new Uint8Array(text.length);\n for (let index = 0; index < text.length; index++) {\n bytes[index] = text.charCodeAt(index);\n }\n if (kind === 'ArrayBuffer') {\n return bytes.buffer;\n }\n const Typed = TYPED[kind];\n return Typed === undefined || Typed === Uint8Array ? bytes : new Typed(bytes.buffer);\n}\n","import type { JsonSchema } from '../channel/ChannelSchema';\n\n/**\n * Checks a value against the JSON Schema a channel description uses.\n *\n * Not a general validator. It understands the keywords\n * `gesso-vite-plugin` writes (`type`, `enum`, `const`, `anyOf`,\n * `items`, `prefixItems`, `minItems`, `properties`, `required`,\n * `additionalProperties`, `not` and `$ref` into `$defs`), and passes\n * anything else. That is enough to stop the case that matters: an\n * agent sending `\"3\"` where a command takes a number, which the\n * command would accept, store, and fail on much later and somewhere\n * else.\n *\n * Returns the first problem as a sentence naming where it is, written\n * for the agent that sent the value so it can correct itself, or null.\n */\nexport function validate(schema: JsonSchema, value: unknown, root: JsonSchema = schema, at = 'input'): string | null {\n const ref = schema.$ref;\n if (typeof ref === 'string') {\n const name = ref.replace(/^#\\/\\$defs\\//, '');\n const target = (root.$defs as Record<string, JsonSchema> | undefined)?.[name];\n return target === undefined ? null : validate(target, value, root, at);\n }\n if (schema.not !== undefined && validate(schema.not as JsonSchema, value, root, at) === null) {\n return `${at} must not be given.`;\n }\n if ('const' in schema && value !== schema.const) {\n return `${at} must be ${JSON.stringify(schema.const)}.`;\n }\n if (Array.isArray(schema.enum) && !schema.enum.includes(value)) {\n return `${at} must be one of ${schema.enum.map(option => JSON.stringify(option)).join(', ')}.`;\n }\n if (Array.isArray(schema.anyOf)) {\n const options = schema.anyOf as JsonSchema[];\n if (options.every(option => validate(option, value, root, at) !== null)) {\n return `${at} does not match any of the ${options.length} shapes it may take.`;\n }\n return null;\n }\n const type = schema.type;\n if (typeof type === 'string' && !hasType(value, type)) {\n return `${at} must be ${article(type)}, not ${describe(value)}.`;\n }\n if (Array.isArray(value)) {\n return validateArray(schema, value, root, at);\n }\n if (type === 'object' && value !== null && typeof value === 'object') {\n return validateObject(schema, value as Record<string, unknown>, root, at);\n }\n return null;\n}\n\nfunction validateArray(schema: JsonSchema, value: readonly unknown[], root: JsonSchema, at: string): string | null {\n const prefix = (schema.prefixItems as JsonSchema[] | undefined) ?? [];\n if (typeof schema.minItems === 'number' && value.length < schema.minItems) {\n return `${at} must have at least ${schema.minItems} items.`;\n }\n for (let index = 0; index < value.length; index++) {\n const item = index < prefix.length ? prefix[index] : schema.items;\n if (item === false) {\n return `${at} must have at most ${prefix.length} items.`;\n }\n if (item !== undefined && item !== true) {\n const problem = validate(item as JsonSchema, value[index], root, `${at}[${index}]`);\n if (problem !== null) {\n return problem;\n }\n }\n }\n return null;\n}\n\nfunction validateObject(\n schema: JsonSchema,\n value: Record<string, unknown>,\n root: JsonSchema,\n at: string\n): string | null {\n const properties = (schema.properties as Record<string, JsonSchema> | undefined) ?? {};\n for (const key of (schema.required as string[] | undefined) ?? []) {\n if (value[key] === undefined) {\n return `${at}.${key} is required.`;\n }\n }\n for (const [key, property] of Object.entries(value)) {\n const declared = properties[key];\n if (declared !== undefined) {\n const problem = property === undefined ? null : validate(declared, property, root, `${at}.${key}`);\n if (problem !== null) {\n return problem;\n }\n continue;\n }\n const extra = schema.additionalProperties;\n if (extra === false) {\n const known = Object.keys(properties);\n return `${at}.${key} is not expected${known.length > 0 ? `; the fields are ${known.join(', ')}` : ''}.`;\n }\n if (extra !== undefined && extra !== true) {\n const problem = validate(extra as JsonSchema, property, root, `${at}.${key}`);\n if (problem !== null) {\n return problem;\n }\n }\n }\n return null;\n}\n\nfunction hasType(value: unknown, type: string): boolean {\n switch (type) {\n case 'string':\n return typeof value === 'string';\n case 'number':\n return typeof value === 'number' && Number.isFinite(value);\n case 'integer':\n return Number.isInteger(value);\n case 'boolean':\n return typeof value === 'boolean';\n case 'null':\n return value === null;\n case 'array':\n return Array.isArray(value);\n case 'object':\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n default:\n return true;\n }\n}\n\nfunction article(type: string): string {\n return /^[aeiou]/.test(type) ? `an ${type}` : `a ${type}`;\n}\n\nfunction describe(value: unknown): string {\n if (value === null) {\n return 'null';\n }\n if (Array.isArray(value)) {\n return 'an array';\n }\n return typeof value === 'object' ? 'an object' : `${typeof value} ${JSON.stringify(value)}`;\n}\n","import { describeSharedMemory } from '../channel/plainData';\nimport type { Subscription } from 'rxjs';\n\nimport { channelSchema, type CommandSchema, type JsonSchema } from '../channel/ChannelSchema';\nimport type { ServedChannel } from '../channel/serveChannels';\nimport { decodeBinary } from './binary';\nimport { validate } from './validate';\n\n/**\n * An application's channels, as an AI agent sees them.\n *\n * A channel is already the shape an agent wants. Its view is what the\n * application currently holds, as plain data, and its commands are\n * the things it can be asked to do, by name, with typed arguments. A\n * component reaches both through a replica; this reaches them from the\n * other side, through the same `{ token, source }` the application\n * already hands to `serveChannels` or `createDesktopApp`, so an agent\n * sends a command to exactly the handler a click does and sees its\n * effect in exactly the view a screen draws.\n *\n * For each channel it offers:\n *\n * - a resource, `gesso://<channel>/view`, holding the view;\n * - a tool, `<channel>_view`, returning the same thing, because more\n * agents call tools than read resources;\n * - a tool per command, `<channel>_<command>`, whose input schema is\n * the command's parameters by name. Calling it sends the command,\n * waits for the view to settle, and returns the view as it now is,\n * so the agent sees what its call did without a second round trip.\n *\n * The descriptions come from `channelSchema(token)`, which\n * `gesso-vite-plugin` writes from the contract's JSDoc. A command\n * marked `@hidden` is not offered, nor is a view key marked so, in\n * anything the agent reads, nor any of a channel whose token is; `@destructive` and `@idempotent`\n * become hints a client shows; `@confirm` means the person is asked,\n * through `confirm`, before the command is sent, and a surface given\n * no way to ask refuses it. A channel nobody described is still\n * offered, with its commands taking their arguments as a positional\n * list, and says so in its description.\n *\n * Nothing here knows about a transport. `mcpHandler` serves it over\n * MCP's HTTP transport; anything else can call `tools`, `call`,\n * `resources` and `read` directly.\n */\n\n/** A tool, in the shape MCP's `tools/list` returns. */\nexport interface AgentTool {\n readonly name: string;\n readonly title?: string;\n readonly description: string;\n readonly inputSchema: JsonSchema;\n readonly outputSchema?: JsonSchema;\n readonly annotations: {\n readonly title?: string;\n readonly readOnlyHint: boolean;\n readonly destructiveHint?: boolean;\n readonly idempotentHint?: boolean;\n readonly openWorldHint: false;\n };\n /**\n * MCP's own place for what is not in its schema. Gesso puts\n * `gesso/waitsUpToMs` here for a tool that waits on purpose (see\n * `AgentToolDefinition.waitsUpToMs`), so the bridges between the agent\n * and the thread give its calls that long.\n */\n readonly _meta?: Readonly<Record<string, unknown>>;\n}\n\n/** The `_meta` key a tool that waits on purpose says how long under, in milliseconds. */\nexport const WAITS_UP_TO = 'gesso/waitsUpToMs';\n\n/** A resource, in the shape MCP's `resources/list` returns. */\nexport interface AgentResource {\n readonly uri: string;\n readonly name: string;\n readonly description?: string;\n readonly mimeType: 'application/json';\n}\n\n/** One part of a tool's answer: text, or a picture, as `render_frame` returns. */\nexport type AgentContent =\n | { readonly type: 'text'; readonly text: string }\n | { readonly type: 'image'; readonly data: string; readonly mimeType: string };\n\n/** A result's text parts, joined: what a reader that shows no pictures shows. */\nexport function resultText(result: AgentToolResult): string {\n return result.content\n .map(part => (part.type === 'text' ? part.text : ''))\n .filter(Boolean)\n .join('\\n');\n}\n\n/** What a tool call returned, in the shape MCP's `tools/call` returns. */\nexport interface AgentToolResult {\n readonly content: readonly AgentContent[];\n readonly structuredContent?: Record<string, unknown>;\n readonly isError: boolean;\n}\n\n/** A command an agent wants to send that its contract says a person should approve. */\nexport interface AgentConfirmation {\n readonly channel: string;\n readonly command: string;\n readonly description?: string;\n /** The arguments, by parameter name. */\n readonly arguments: Readonly<Record<string, unknown>>;\n readonly destructive: boolean;\n}\n\nexport interface AgentSurfaceOptions {\n /**\n * Asks the person whether a `@confirm` command may be sent. Resolve\n * true to send it. Without this, such a command is refused, which is\n * the safe reading of a contract that asked for a person.\n */\n confirm?: (request: AgentConfirmation) => boolean | Promise<boolean>;\n /**\n * How long the view must be quiet after a command before the call\n * returns it, in milliseconds (default 50). A command whose effect is\n * synchronous settles at once; one that waits on a request settles\n * when the patch lands, or at `settleMs`. A command that changes\n * nothing in the view returns at `settleMs`.\n */\n quietMs?: number;\n /** The longest a call waits for the view to settle (default 1000). */\n settleMs?: number;\n}\n\n/**\n * Any surface an MCP server can speak for: one in this thread, or one\n * across a port whose every answer is a promise.\n */\nexport interface AgentSurfaceLike {\n tools(): readonly AgentTool[] | Promise<readonly AgentTool[]>;\n call(name: string, args: Readonly<Record<string, unknown>> | undefined): Promise<AgentToolResult>;\n resources(): readonly AgentResource[] | Promise<readonly AgentResource[]>;\n read(uri: string): Record<string, unknown> | undefined | Promise<Record<string, unknown> | undefined>;\n dispose?(): void;\n}\n\nexport interface AgentSurface extends AgentSurfaceLike {\n tools(): readonly AgentTool[];\n /** Calls a tool by name. Never throws: a failure is a result with `isError`, which an agent can read and correct. */\n call(name: string, args: Readonly<Record<string, unknown>> | undefined): Promise<AgentToolResult>;\n resources(): readonly AgentResource[];\n /** The current view of the channel a resource names, or undefined for a URI this surface does not hold. */\n read(uri: string): Record<string, unknown> | undefined;\n /** Stops following every view. */\n dispose(): void;\n}\n\n/** The longest tool name MCP clients accept. */\nconst MAX_NAME = 64;\n\ninterface Entry {\n readonly served: ServedChannel;\n readonly name: string;\n readonly description?: string;\n readonly viewSchema?: JsonSchema;\n readonly view: Record<string, unknown>;\n /** View keys kept from the agent. */\n readonly hidden: ReadonlySet<string>;\n readonly commands: ReadonlyMap<string, CommandSchema | null>;\n /**\n * Every change this channel's view has seen, and when the last was,\n * so a command can tell its own effect from what came before it.\n * Counted per channel: a call returns its own channel's view, and\n * another channel that never goes quiet (a clock, a status that\n * follows the person's typing) must not hold it up.\n */\n changes: number;\n lastChange: number;\n}\n\ntype Handler = (request: Readonly<Record<string, unknown>> | undefined) => Promise<AgentToolResult>;\n\nexport function agentSurface(channels: readonly ServedChannel[], options: AgentSurfaceOptions = {}): AgentSurface {\n const quietMs = options.quietMs ?? 50;\n const settleMs = options.settleMs ?? 1000;\n const subscriptions: Subscription[] = [];\n const entries: Entry[] = [];\n const tools: AgentTool[] = [];\n const handlers = new Map<string, Handler>();\n let following = false;\n\n /**\n * Subscribed on first use rather than at construction, so a surface\n * built at startup and never called costs the application nothing:\n * a view key may be a cold observable doing real work per subscriber.\n */\n const follow = () => {\n if (following) {\n return;\n }\n following = true;\n for (const entry of entries) {\n for (const [key, observable] of Object.entries(entry.served.source.view)) {\n subscriptions.push(\n observable.subscribe(value => {\n entry.view[key] = value;\n entry.lastChange = Date.now();\n entry.changes++;\n })\n );\n }\n }\n };\n\n /**\n * Waits for what a command sent when its channel's view had seen\n * `before` changes to reach that view.\n *\n * Quiet counts only once the view has changed since the command: a\n * command that answers after a request has changed nothing yet when\n * the first quiet window ends, and returning then would hand the\n * agent the view from before it. A command that changes nothing at\n * all waits out `settleMs`, which is the price of not being able to\n * tell it from one that is still working.\n */\n const settle = async (entry: Entry, before: number) => {\n const start = Date.now();\n for (;;) {\n await new Promise(resolve => setTimeout(resolve, quietMs));\n const now = Date.now();\n const changed = entry.changes > before;\n if ((changed && now - entry.lastChange >= quietMs) || now - start >= settleMs) {\n return;\n }\n }\n };\n\n const add = (tool: AgentTool, handler: Handler) => {\n if (handlers.has(tool.name)) {\n throw new Error(\n `Two tools would be called '${tool.name}'. A channel and command name pair has to be unique once joined ` +\n 'with an underscore, and a command cannot be called `view`.'\n );\n }\n tools.push(tool);\n handlers.set(tool.name, handler);\n };\n\n for (const served of channels) {\n const schema = channelSchema(served.token as never);\n if (schema?.internal === true) {\n continue;\n }\n const name = toolName(served.token.name);\n const commands = new Map<string, CommandSchema | null>();\n for (const command of Object.keys(served.source.commands ?? {})) {\n const described = schema?.commands[command];\n if (described?.hidden !== true) {\n commands.set(command, described ?? null);\n }\n }\n const entry: Entry = {\n served,\n name,\n description: schema?.description,\n viewSchema: schema === undefined ? undefined : withoutKeys(schema.view, schema.hidden ?? []),\n view: { ...(served.token.initial as Record<string, unknown>) },\n hidden: new Set(schema?.hidden ?? []),\n commands,\n changes: 0,\n lastChange: 0\n };\n entries.push(entry);\n\n const about = entry.description === undefined ? '' : `\\n\\n${entry.description}`;\n add(\n {\n name: toolName(`${served.token.name}_view`),\n title: `Read ${served.token.name}`,\n description: `What the ${served.token.name} channel currently holds.${about}`,\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n ...(entry.viewSchema === undefined ? {} : { outputSchema: entry.viewSchema }),\n annotations: { readOnlyHint: true, openWorldHint: false }\n },\n async () => {\n follow();\n return viewResult(entry, `The ${served.token.name} view.`);\n }\n );\n\n for (const [command, described] of commands) {\n add(commandTool(entry, command, described), args => send(entry, command, described, args));\n }\n }\n\n const send = async (\n entry: Entry,\n command: string,\n described: CommandSchema | null,\n args: Readonly<Record<string, unknown>> | undefined\n ): Promise<AgentToolResult> => {\n follow();\n const channel = entry.served.token.name;\n const input = args ?? {};\n let positional: unknown[];\n if (described === null) {\n const list = input.arguments ?? [];\n if (!Array.isArray(list)) {\n return failure('arguments must be an array of the values the command takes, in order.');\n }\n positional = list;\n } else {\n const problem = validate(described.input, input);\n if (problem !== null) {\n return failure(problem);\n }\n // Bytes arrive as base64, which is all JSON can carry, and leave\n // as the typed array or buffer the command was written to take.\n const properties = (described.input.properties as Record<string, JsonSchema> | undefined) ?? {};\n positional = described.parameters.map(parameter =>\n decodeBinary(properties[parameter], input[parameter], described.input)\n );\n if (described.rest === true) {\n const spread = positional.pop();\n positional.push(...(Array.isArray(spread) ? spread : []));\n }\n while (positional.length > 0 && positional[positional.length - 1] === undefined) {\n positional.pop();\n }\n if (described.confirm === true) {\n if (options.confirm === undefined) {\n return failure(\n `${channel}.${command} asks for a person to approve it, and this application has given agents no way ` +\n 'to ask. Ask the person to do it themselves.'\n );\n }\n const approved = await options.confirm({\n channel,\n command,\n description: described.description,\n arguments: input,\n destructive: described.destructive === true\n });\n if (!approved) {\n return failure(`The person declined ${channel}.${command}. Do not send it again unless they ask.`);\n }\n }\n }\n const handler = entry.served.source.commands?.[command] as ((...values: unknown[]) => void) | undefined;\n if (handler === undefined) {\n return failure(`${channel} has no command ${command}.`);\n }\n const before = entry.changes;\n try {\n handler(...positional);\n } catch (error) {\n return failure(`${channel}.${command} failed: ${error instanceof Error ? error.message : String(error)}`);\n }\n await settle(entry, before);\n return viewResult(entry, `Sent ${command} to ${channel}. The view afterwards:`);\n };\n\n return {\n tools: () => tools,\n call: async (name, args) => {\n const handler = handlers.get(name);\n if (handler === undefined) {\n return failure(`There is no tool called ${name}. The tools are ${[...handlers.keys()].join(', ')}.`);\n }\n return handler(args);\n },\n resources: () =>\n entries.map(entry => ({\n uri: resourceUri(entry.served.token.name),\n name: entry.served.token.name,\n ...(entry.description === undefined ? {} : { description: entry.description }),\n mimeType: 'application/json' as const\n })),\n read: uri => {\n const entry = entries.find(candidate => resourceUri(candidate.served.token.name) === uri);\n if (entry === undefined) {\n return undefined;\n }\n follow();\n return shown(entry);\n },\n dispose: () => {\n for (const subscription of subscriptions) {\n subscription.unsubscribe();\n }\n subscriptions.length = 0;\n following = false;\n }\n };\n}\n\n/** The URI of a channel's view. */\nexport function resourceUri(channel: string): string {\n return `gesso://${encodeURIComponent(channel)}/view`;\n}\n\nfunction commandTool(entry: Entry, command: string, described: CommandSchema | null): AgentTool {\n const channel = entry.served.token.name;\n const destructive = described?.destructive === true;\n const lines = [described?.description ?? `Sends ${command} to the ${channel} channel.`];\n if (described === null) {\n lines.push(\n `This command was not described, so its arguments are a positional list. Read ${toolName(`${channel}_view`)} ` +\n 'first to see what the channel holds.'\n );\n }\n if (described?.confirm === true) {\n lines.push('The person is asked to approve this before it is sent.');\n }\n lines.push('Returns the view after the command has taken effect.');\n return {\n name: toolName(`${channel}_${command}`),\n title: `${channel}: ${command}`,\n description: lines.join('\\n\\n'),\n inputSchema:\n described?.input ??\n ({\n type: 'object',\n properties: { arguments: { type: 'array', description: 'The arguments, in order.' } },\n additionalProperties: false\n } satisfies JsonSchema),\n ...(entry.viewSchema === undefined ? {} : { outputSchema: entry.viewSchema }),\n annotations: {\n readOnlyHint: false,\n ...(destructive ? { destructiveHint: true } : { destructiveHint: false }),\n ...(described?.idempotent === true ? { idempotentHint: true } : {}),\n openWorldHint: false\n }\n };\n}\n\n/**\n * A tool name MCP clients accept: letters, digits, underscores and\n * dashes, at most 64 characters. A channel name is the application's\n * own string, so anything else in it becomes an underscore, and one\n * too long is an error at startup rather than a tool a client drops.\n */\nfunction toolName(raw: string): string {\n const name = raw.replace(/[^A-Za-z0-9_-]/g, '_');\n if (name.length > MAX_NAME) {\n throw new Error(`The tool name '${name}' is longer than ${MAX_NAME} characters. Shorten the channel's name.`);\n }\n return name;\n}\n\n/** The view as an agent is given it: without its hidden keys. */\nfunction shown(entry: Entry): Record<string, unknown> {\n const view: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(entry.view)) {\n // Shared memory is a handle the agent can do nothing with, and JSON\n // would show it as `{}`; it is described instead.\n if (!entry.hidden.has(key)) view[key] = describeSharedMemory(value);\n }\n return view;\n}\n\n/** An object schema without some of its properties. */\nfunction withoutKeys(schema: JsonSchema, keys: readonly string[]): JsonSchema {\n if (keys.length === 0) return schema;\n const properties = { ...(schema.properties as Record<string, JsonSchema> | undefined) };\n for (const key of keys) delete properties[key];\n const required = Array.isArray(schema.required)\n ? (schema.required as string[]).filter(key => !keys.includes(key))\n : undefined;\n return { ...schema, properties, ...(required === undefined ? {} : { required }) };\n}\n\nfunction viewResult(entry: Entry, lead: string): AgentToolResult {\n const view = shown(entry);\n return {\n // Compact: the same view is in structuredContent, and indenting a\n // large one doubles what the agent reads for nothing.\n content: [{ type: 'text', text: `${lead}\\n${JSON.stringify(view)}` }],\n structuredContent: view,\n isError: false\n };\n}\n\nfunction failure(text: string): AgentToolResult {\n return { content: [{ type: 'text', text }], isError: true };\n}\n","import {\n WAITS_UP_TO,\n type AgentConfirmation,\n type AgentResource,\n type AgentSurfaceLike,\n type AgentTool,\n type AgentToolResult\n} from './AgentSurface';\n\n/**\n * An agent surface across a thread.\n *\n * A web application's channels are served in workers, and an agent\n * reaches the page, so the surface has to cross from one to the other.\n * The worker that serves channels answers a `gesso:agent` port with its\n * own surface (`serveAgentPort`); the page holds the other end as a\n * surface of its own (`remoteSurface`); and a thread that knows several\n * such ports, the render worker with its channel workers behind it,\n * offers them as one (`combineSurfaces`).\n *\n * The four operations cross as they are. MCP itself is spoken only at\n * the end that faces the agent, so nothing in a worker parses JSON-RPC.\n */\n\n/** The port key a thread answers with its agent surface. */\nexport const AGENT_PORT = 'gesso:agent';\n\n/** What crosses an agent port, page to worker. */\nexport type AgentPortRequest =\n | { id: number; op: 'tools' }\n | { id: number; op: 'resources' }\n | { id: number; op: 'call'; name: string; args?: Readonly<Record<string, unknown>> }\n | { id: number; op: 'read'; uri: string };\n\n/** A request before it is given an id. */\ntype Unsent<T> = T extends unknown ? Omit<T, 'id'> : never;\n\n/** What crosses back. */\nexport type AgentPortResponse = { id: number; result: unknown } | { id: number; error: string };\n\n/**\n * The question going the other way: a command marked `@confirm` needs\n * a person, the person is at the far end, so the thread that serves\n * the channel asks down the port it was asked on and waits.\n */\nexport type AgentPortConfirm = { confirm: number; request: AgentConfirmation } | { confirm: number; approved: boolean };\n\n/** How a thread asks the person, wherever the person is. */\nexport type AgentConfirm = (request: AgentConfirmation) => boolean | Promise<boolean>;\n\n/** A `MessagePort`, or anything that posts and receives like one. */\ntype AgentPort = Pick<MessagePort, 'postMessage' | 'onmessage'>;\n\n/**\n * Answers a port with a surface.\n *\n * The surface is made on the first request rather than when the port\n * arrives: a port is opened by a page that may never ask anything, and\n * a surface subscribes to every view key once it is used. It is given\n * a `confirm` that asks the far end of this port, so a command marked\n * `@confirm` reaches the person wherever they are; the far end answers\n * no when it has no way to ask.\n */\nexport function serveAgentPort(port: AgentPort, makeSurface: (confirm: AgentConfirm) => AgentSurfaceLike): () => void {\n let surface: AgentSurfaceLike | undefined;\n let nextConfirm = 1;\n const asking = new Map<number, (approved: boolean) => void>();\n const confirm: AgentConfirm = request =>\n new Promise(resolve => {\n const id = nextConfirm++;\n asking.set(id, resolve);\n port.postMessage({ confirm: id, request } satisfies AgentPortConfirm);\n });\n port.onmessage = async event => {\n const data = event.data as AgentPortRequest | AgentPortConfirm;\n if ('confirm' in data && 'approved' in data) {\n asking.get(data.confirm)?.(data.approved);\n asking.delete(data.confirm);\n return;\n }\n const request = data as AgentPortRequest;\n if (typeof request?.id !== 'number') {\n return;\n }\n surface ??= makeSurface(confirm);\n try {\n port.postMessage({ id: request.id, result: await answer(surface, request) } satisfies AgentPortResponse);\n } catch (error) {\n port.postMessage({\n id: request.id,\n error: error instanceof Error ? error.message : String(error)\n } satisfies AgentPortResponse);\n }\n };\n return () => {\n port.onmessage = null;\n surface?.dispose?.();\n for (const resolve of asking.values()) {\n resolve(false);\n }\n asking.clear();\n };\n}\n\nfunction answer(surface: AgentSurfaceLike, request: AgentPortRequest): unknown {\n switch (request.op) {\n case 'tools':\n return surface.tools();\n case 'resources':\n return surface.resources();\n case 'call':\n return surface.call(request.name, request.args);\n case 'read':\n return surface.read(request.uri) ?? null;\n }\n}\n\n/**\n * The surface at the other end of a port.\n *\n * A request nobody answers within `timeoutMs` (default 2000) is taken\n * as a thread with nothing to offer: a worker that serves no channels\n * never installs the answering side, and an agent asking the whole\n * application should hear about the threads that do rather than wait\n * on one that does not.\n */\nexport function remoteSurface(\n port: AgentPort,\n options: { timeoutMs?: number; callTimeoutMs?: number; confirm?: AgentConfirm } = {}\n): AgentSurfaceLike {\n const timeoutMs = options.timeoutMs ?? 2000;\n const callTimeoutMs = options.callTimeoutMs ?? 120_000;\n /** How long each tool listed says it may wait, for the calls of it. */\n const waits = new Map<string, number>();\n let next = 1;\n const pending = new Map<number, { resolve: (value: unknown) => void; reject: (error: Error) => void }>();\n port.onmessage = event => {\n const question = event.data as AgentPortConfirm;\n if (question !== null && typeof question === 'object' && 'request' in question) {\n // Asked to put a command to the person. With nobody to ask, the\n // answer is no, which is what a contract asking for a person means.\n void Promise.resolve(options.confirm?.(question.request) ?? false).then(\n approved => port.postMessage({ confirm: question.confirm, approved } satisfies AgentPortConfirm),\n () => port.postMessage({ confirm: question.confirm, approved: false } satisfies AgentPortConfirm)\n );\n return;\n }\n const response = event.data as AgentPortResponse;\n const waiting = pending.get(response?.id);\n if (waiting === undefined) {\n return;\n }\n pending.delete(response.id);\n if ('error' in response) {\n waiting.reject(new Error(response.error));\n } else {\n waiting.resolve(response.result);\n }\n };\n const ask = <T>(request: Unsent<AgentPortRequest>, fallback: T, timeout = timeoutMs): Promise<T> =>\n new Promise<T>((resolve, reject) => {\n const id = next++;\n const timer = setTimeout(() => {\n pending.delete(id);\n resolve(fallback);\n }, timeout);\n pending.set(id, {\n resolve: value => {\n clearTimeout(timer);\n resolve(value as T);\n },\n reject: error => {\n clearTimeout(timer);\n reject(error);\n }\n });\n port.postMessage({ ...request, id });\n });\n return {\n tools: () =>\n ask<readonly AgentTool[]>({ op: 'tools' }, []).then(tools => {\n for (const tool of tools) {\n const wait = tool._meta?.[WAITS_UP_TO];\n if (typeof wait === 'number') {\n waits.set(tool.name, wait);\n }\n }\n return tools;\n }),\n resources: () => ask<readonly AgentResource[]>({ op: 'resources' }, []),\n // A command may wait on the person through `confirm`, so a call is\n // given far longer than a listing before it is abandoned; a tool\n // that waits on purpose, as long as it said, and a moment more.\n call: (name, args) =>\n ask<AgentToolResult>(\n { op: 'call', name, args },\n { content: [{ type: 'text', text: `${name} did not answer in time.` }], isError: true },\n Math.max(timeoutMs, callTimeoutMs, waits.has(name) ? waits.get(name)! + 5_000 : 0)\n ),\n read: async uri => (await ask<Record<string, unknown> | null>({ op: 'read', uri }, null)) ?? undefined\n };\n}\n\n/**\n * Several surfaces as one: tools and resources listed together, a call\n * or a read sent to whichever surface listed it. When two list the same\n * name, the first keeps it, as the order of the threads is the order\n * the application registered them in.\n */\nexport function combineSurfaces(surfaces: readonly AgentSurfaceLike[]): AgentSurfaceLike {\n const toolOwners = new Map<string, AgentSurfaceLike>();\n const resourceOwners = new Map<string, AgentSurfaceLike>();\n\n const tools = async () => {\n const lists = await Promise.all(surfaces.map(surface => surface.tools()));\n toolOwners.clear();\n const out: AgentTool[] = [];\n lists.forEach((list, index) => {\n for (const tool of list) {\n if (!toolOwners.has(tool.name)) {\n toolOwners.set(tool.name, surfaces[index]);\n out.push(tool);\n }\n }\n });\n return out;\n };\n\n const resources = async () => {\n const lists = await Promise.all(surfaces.map(surface => surface.resources()));\n resourceOwners.clear();\n const out: AgentResource[] = [];\n lists.forEach((list, index) => {\n for (const resource of list) {\n if (!resourceOwners.has(resource.uri)) {\n resourceOwners.set(resource.uri, surfaces[index]);\n out.push(resource);\n }\n }\n });\n return out;\n };\n\n return {\n tools,\n resources,\n call: async (name, args) => {\n if (!toolOwners.has(name)) {\n await tools();\n }\n const owner = toolOwners.get(name);\n if (owner === undefined) {\n const known = [...toolOwners.keys()].join(', ');\n return {\n content: [{ type: 'text', text: `There is no tool called ${name}. The tools are ${known}.` }],\n isError: true\n };\n }\n return owner.call(name, args);\n },\n read: async uri => {\n if (!resourceOwners.has(uri)) {\n await resources();\n }\n return resourceOwners.get(uri)?.read(uri);\n },\n dispose: () => {\n for (const surface of surfaces) {\n surface.dispose?.();\n }\n }\n };\n}\n"],"mappings":"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"}
|