gesso-framework 0.4.2 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,125 @@
1
1
  # gesso-framework
2
2
 
3
+ ## 0.5.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 77195a4: Two more semantics properties, for a field that opens a list of suggestions. `controls` is a relation, like `activeDescendant`: the node this one shows or changes, such as the list a combobox's field has open, held on the record as that node's id and written by the mirror as `aria-controls` naming its element. `autocomplete` (`'list' | 'inline' | 'both'`, the new `UiAutocomplete`) says what a field offers as it's typed into, written as `aria-autocomplete`. The editing proxy writes both while a field has focus, so `EditingMirrorTarget.describe` takes the controlled element's DOM id after the active descendant's. `Combobox` uses them: its field controls the list while it's open, and its autocomplete is `list`.
8
+ - 6b71716: An overlay can open beside a part of its anchor: `anchorRect` on an overlay entry (and on `useOverlay`'s options), and the layout property of the same name, is a rectangle in the anchor's own coordinates that the entry is placed against, with the same flip and shift, and that it follows through scrolling and layout as it follows the anchor. It can be an Observable, so it moves without the entry opening again. `EditingService.caretRectOf(node, offset)` (and `UiEditingController.caretRectOf`) answers for a character other than the caret's, so a list opened by `@` sits under the `@` as the name is typed, and goes to the next line with it when it wraps. A point opened beside the caret stayed behind when the page scrolled.
9
+ - 581cf89: An open overlay now follows the theme of the place it was declared. The overlay layer read the theme, text style and content colour once, as the entry opened, so a dialog or menu open when the system turned dark, or when a theme the person chose arrived from another worker a moment after they opened it, stayed in the old theme over a page in the new one until it closed. Underneath, a modifier's `host.environment(key, of?)` and `host.onEnvironment(listener, of?)` can read and follow another node's environment, and an environment provided by a node whose own environment changed in the same frame is rebuilt in that frame.
10
+ - Updated dependencies [77195a4]
11
+ - Updated dependencies [6b71716]
12
+ - Updated dependencies [581cf89]
13
+ - Updated dependencies [13c096f]
14
+ - Updated dependencies [db7040b]
15
+ - gesso-core@0.5.1
16
+
17
+ ## 0.5.0
18
+
19
+ ### Minor Changes
20
+
21
+ - 5a27b40: `activeDescendant` names the node that's active while another keeps focus, such as the highlighted option of a combobox whose field holds the caret, or the cell a grid's cursor is on. The semantics record carries it as the node's id, and the accessibility mirror writes `aria-activedescendant` from it, both on the node's element and on the editing proxy while a field has focus. Every mirrored element now has a DOM id for it to point at. The proxy also says `aria-expanded` for a field whose record is expanded or collapsed.
22
+ - f265910: An AI agent can drive a web application while it runs in development. `gesso-vite-plugin` serves MCP at `/__gesso/mcp` on the dev server and prints the `claude mcp add` line to connect; the agent then sees every channel the open page can reach, the ones its render worker feeds and the ones its application and channel workers serve, and its commands change the page as a click would. Messages travel down the HMR socket to the page, which answers them against the render worker, which asks each worker behind it over a `gesso:agent` port. A command marked `@confirm` is put to the person with the browser's dialog first. The endpoint refuses requests from browser pages, the newest open tab answers, and `agent: false` turns it off. A build carries none of it.
23
+
24
+ `gesso-framework/agent` gains what the bridge is made of: `serveAgentPort`, `remoteSurface` and `combineSurfaces` for a surface across threads, `connectDevAgent` for the page's half, and `AgentSurfaceLike` for a surface whose answers are promises, which `handleMcpMessage` and `mcpHandler` now accept. `WorkerApp.openRenderPort(key)` opens a port to the render worker. The scaffolded `AGENTS.md` says how to connect.
25
+
26
+ - d36a2fa: `gesso-framework/agent` hands an application to an AI agent. `agentSurface(channels)` takes the same `{ token, source }` registrations an application already serves and offers each channel as a resource holding its view, a read-only `<channel>_view` tool, and a `<channel>_<command>` tool per command that sends it and returns the view once it has settled. Tool descriptions, input schemas and hints come from the schema `gesso-vite-plugin` writes from the contract's JSDoc; arguments that do not fit are refused with a sentence naming the field, `@hidden` commands are not offered, and `@confirm` commands are sent only once the `confirm` option says the person approved.
27
+
28
+ `mcpHandler(surface)` serves it over MCP's Streamable HTTP transport as a `fetch` handler, ready for `Bun.serve`, refusing unknown browser origins and optionally requiring a bearer token. `handleMcpMessage` is the same server without the transport, for stdio or a relay.
29
+
30
+ - c38f97e: A single-thread app can be driven by an AI agent too. `createSyncApp(...)` builders now answer `openRenderPort('gesso:agent')` from the page, with the channels fed there, whatever their channel workers serve, and the screen tools, so the dev server endpoint and WebMCP work exactly as they do for `createApp`. `useWebMcp(true | false | { confirm })` is the builder's form of `createApp({ webmcp })`. In a dev server, `gesso-vite-plugin` now hands a `createSyncApp` builder to the agent bridge and turns WebMCP on unless the app's own `useWebMcp(false)` says otherwise. The screen tools run a pending frame after each action on the main thread as they do in the render worker, so a background tab still reports what changed. `serveApplicationAgent` in `gesso-framework/agent` is the assembly both configurations share.
31
+ - b90ecb2: An agent can operate the interface, not only the channels. Beside the channel tools, the page now offers `ui_snapshot`, the screen as an outline of what a screen reader announces with a short ref per control, and `ui_press`, `ui_type`, `ui_focus` and `ui_key`, which act on a control named by ref or by role and name and answer with the outline afterwards. They go through the accessibility mirror's own path, so a press is a click, a value is a keyboard edit, a disabled control refuses, and a focus trap holds. Available in the dev server endpoint and through WebMCP. `GessoRuntime.focusedNodeId()` reports which node holds focus.
32
+
33
+ The dev bridge also announces the page again whenever its HMR socket reconnects, so a restarted dev server no longer tells an agent that no page is open while one is.
34
+
35
+ - 96f4bdc: A channel can describe itself. `describeChannel(token, schema)` attaches a JSON Schema of the channel's view and of each command, and `channelSchema(token)` reads it back, so anything that meets an application only at run time can ask a channel what it holds and what its commands take: an AI agent being handed the channel as tools, a devtools panel, a test that drives an app by its commands.
36
+
37
+ `gesso-vite-plugin` writes the schema for you. It reads each contract with TypeScript 7's checker and takes the descriptions from the JSDoc you already wrote: on the token, on each view key, on each command, and `@param` for its parameters. Four tags annotate a command for an agent: `@destructive`, `@idempotent`, `@confirm` and `@hidden`. A value that cannot cross a channel, such as a `Date`, a `Map` or an untyped `[]`, is reported as a build warning naming its path. The plugin needs `typescript` 7 or later installed, says so once if it is not, and `channelSchemas: false` turns the whole thing off.
38
+
39
+ - 88d93b3: A command may carry bytes. An `ArrayBuffer` or a typed array in a command's parameters is no longer reported as unable to cross a channel, because a command's argument is structured-cloned and bytes clone as themselves; a file can be sent as its bytes, with no base64 pass on the render thread. The schema describes such a field as a base64 string tagged `x-gesso-binary` with the type it becomes, and the agent surface decodes an agent's base64 back into that type before the command is sent. Bytes in a view key are still a warning, since a view is diffed.
40
+ - dc7f199: `ShellService.copyText` now returns a promise of whether the text reached the clipboard, so an application that says "Copied" after a command or a shortcut can say so only when it's true. Callers that ignore it are unaffected. The `clipboard` request carries an `id`, the shell answers it with a `clipboardResult` message, `GessoRuntime.settleClipboard` settles it, and `writeClipboard` resolves `false` when both the async clipboard and the `execCommand` fallback refuse. A runtime with no shell answers `false` at once.
41
+ - 8fb3607: A `current` semantic state, mirrored as `aria-current`, for the current item of a set such as the navigation link to the open page. `selected` was the only way to say it, and on a link or a button a screen reader ignores `aria-selected`, so which page was open went unsaid.
42
+
43
+ `Pagination` marks the page you're on `current` instead of `selected`.
44
+
45
+ - 53b4c46: A copy can put HTML on the clipboard beside the text. An editing group's new `copyHtml(start, end)` gives it, for a selection across fields or inside one field of the group, and `EditingState.html` carries it to the shell, whose copy and cut set `text/html` as well as `text/plain`. A rich editor's copy into a document or an email keeps its formatting. What a group makes of a selection is kept until the selection or its text changes, so `copyText` and `copyHtml` are no longer asked every frame.
46
+ - f0ade22: Editables can select as one. Set `editingGroup` on a container and a selection can start in one field and end in another: arrows move between fields at their edges (up and down keep the column), Shift extends across them, a drag or a Shift and press reaches into other fields, and select all takes the whole group. Every field in the range draws its part. Typing, deleting, Enter, paste and cut over such a selection go to the group's `onEdit` with both ends, since only the application knows how its blocks join, and copy and cut take the whole selection, as the group's `copyText` if it gives one.
47
+ - 29a36ac: `EditingService.select(anchor, focus)` sets a selection from code, in one field or across the fields of an editing group, and focuses the field its focus end is in. An editor needs it to leave a selection selected after a command over it, since its fields can only select their own text.
48
+ - fac08c0: Focus from code can leave the page where it is, as `element.focus({ preventScroll: true })` does: `autoFocus({ preventScroll: true })`, `FocusService.focus(node, { preventScroll: true })` and `UiFocusManager.focus(node, source, { preventScroll: true })`. For focus placed for a screen reader, such as a page's content region focused as it opens, which a reveal scrolled to a few pixels short of its own top. The options reach `onFocusChange` listeners as a third argument, and a key pressed later that makes the focus visible doesn't scroll to it either. The default is unchanged.
49
+ - 979053a: A layout listener that changes layout is painted on the same frame. `breakpoint`, `sizeContainer` (and so `Responsive`), `autoFocus` and anything else on `host.onLayout` hear a box after layout, and what they wrote used to be laid out on the next frame, so a page whose breakpoint gave it wide padding was drawn with its narrow padding first and jumped. The runtime now lays out again before it paints, as a browser does after a `ResizeObserver` callback: only what the listeners dirtied, telling only the listeners whose boxes then changed, until they write nothing more that lays out. The loop is bounded at 8 layout passes a frame; past it the frame paints what it has and a warning says so once. A frame whose listeners write nothing that lays out runs one pass. A scroll into view asked for while the listeners run (an `autoFocus` revealing its node) waits until the boxes are final. `FrameMetrics.layoutPasses` counts the passes, and `measured` and `relayoutRoots` count every pass. A geometry `sharedElement` morph lands on the frame of the change instead of being covered by a transform for one frame. `UiScheduler.recollect`, `UiFrame.merged` and `DirtyNodeSet.anyFlags` are what the runtime builds this from.
50
+ - 99538fa: A `multiselectable` semantic state, mirrored as `aria-multiselectable`. A list whose rows are selected as a set had no way to say so, and Chrome took the option under its active descendant to be the selected one: a screen reader announced a row as selected that wasn't.
51
+ - 28f5b72: `createApp({ pageKeys: true })` says the application is the page: a key pressed while nothing on the page has focus goes to the app, and the canvas takes focus. Keys only reached the app through its canvas, so a page that loads with focus on its body ignored every shortcut until the first click. The templates `create-gesso-app` writes turn it on; an app embedded in a larger page leaves it off.
52
+ - b7c9514: A paste carries the clipboard's HTML along with its text. The shell read only the plain text, so a copy from a web page or a document arrived without its headings, lists and links. `UiBeforeInputEvent`, `UiPasteEvent` and an editing group's edit now have `html` (null when the clipboard had none); the field still inserts the plain text, and an editor that keeps structure can cancel that and convert the HTML. `fireEvent.paste` takes the HTML as a second argument.
53
+ - 68b01e0: `ScrollService.scrollIntoView(node, padding?)` scrolls the containers above a node until it's in view, for a component whose highlight moves without focus: a combobox walking its list while the caret stays in the field, a grid's cursor. Focus moved from the keyboard already did this; a highlight had no way to.
54
+ - 62883e0: `ShellService.contrast` reports the platform's contrast preference: `high` while the person has asked for more contrast (`prefers-contrast: more`) or turned on forced colours (Windows' contrast themes, which a canvas doesn't get from the browser), `standard` otherwise. Reported once at start and on every change, by both the worker and the single-thread shell. `withContrast(theme, contrast)` is the theme that answers it.
55
+ - f02740f: A tooltip opens for focus the keyboard can see and not for the focus a press gives, so clicking a button no longer leaves its tooltip over whatever the click opened. A tooltip whose element is removed closes with it, even while the component that rendered the element stays, as when a Run button turns into Cancel. The `Tooltip` component now follows keyboard focus anywhere inside its wrapper, which it could not before because a focus event does not bubble. `FocusService.focusVisible` says whether the focus held is focus the keyboard can see.
56
+ - cf3b16a: `createApp({ webmcp: true })` offers an application's channels to an AI agent in the browser through WebMCP. Once the app mounts, every tool the agent surface offers, a view tool per channel and a tool per command, is registered with `document.modelContext.registerTool` (or the older `navigator.modelContext`), and removed when the app is disposed. View tools carry `readOnlyHint`, `@destructive` commands carry `consequentialHint`, a call answers with the view it left or rejects with the sentence that says why, and a `@confirm` command is put to the person with `window.confirm` unless `webmcp: { confirm }` supplies the application's own dialog. In a browser without WebMCP nothing is registered and nothing fails. The code loads on demand, so the shell is no bigger for an app that does not ask.
57
+
58
+ `gesso-framework/agent` adds `registerWebMcpTools`, `connectWebMcp`, `pageModelContext` and `confirmInWindow`. `gesso-vite-plugin` turns `webmcp` on in a dev server; the app's own setting still decides.
59
+
60
+ ### Patch Changes
61
+
62
+ - 8c1b8ed: `FrameMetrics.measured` and `relayoutRoots` are 0 for a frame that ran no layout. They used to repeat the last layout pass's numbers, so a caret blink or a scroll looked like a full re-measure to every profiler and proof panel that reads them.
63
+ - 2bfedcd: A component no longer receives the same input value again when its parent re-renders. A prop built as a new Observable in the parent's render re-subscribed and replayed its current value, which re-ran every binding derived from that input even though nothing had changed. In a 2,868-block editor, inserting one block re-measured 12,912 nodes; it now re-measures 488. A changed value, or a new object, still arrives as before.
64
+ - 1dfb6c2: The accessibility mirror can no longer be scrolled by the browser. A browser scrolls even an `overflow: hidden` box to bring something into view, as Tab focus, a screen reader or an automation tool does, and a region whose content reached past its box was left scrolled. Every element in it was then described tens of pixels from where it is drawn, so activating one by position activated its neighbour. The mirror now uses `overflow: clip`, which cannot be scrolled.
65
+ - fb2a6d8: A precision device's wheel steps are paced over frames. A trackpad sends on its own clock, so a frame got one, two or three of its steps and a steady flick moved unevenly; the runtime now moves each frame by the rate the steps have been arriving at, never more than a frame behind, and applies the rest when the input stops. `UiWheelController` takes a `pace` option and an `advance()` a host calls once a frame; the runtime turns it on.
66
+ - 0bef08b: A precision device's scroll is predicted to the frame rather than paced behind it. Each frame puts the page where the input will have reached when the frame is shown, from the steps' velocity and their timestamps, which the shells now pass with each wheel event; the page stays as even as pacing made it without trailing the hand by a frame. `UiWheelController.wheel` takes the event's time, and `advance` the frame's.
67
+ - af33f45: `formatUrl` leaves `,` `:` `@` and `/` unencoded in a query, so a list of values reads as written (`?status=todo,done`, not `?status=todo%2Cdone`). What would change how the query parses (`&`, `=`, `+`, `#`) is still encoded, and `parseUrl` reads both forms the same.
68
+ - 93d580b: `router.params(route)`, `router.observeParams(route)` and `router.isActive(route)` now recognise a route by its path as well as by identity. Under Vite's dev server, a route table that imports its screens, with screens that import the table, could load twice after a hot edit. The screens then held route objects the router had never seen, and every param read `null`, so a page for one team silently showed the default team.
69
+ - 5d67836: The runtime keeps the accessibility tree as records by id plus each record's children in order, and works out a record's index only when it sends that record or the tree is asked for. A structural change used to renumber every record after it and rebuild the whole tree's order, a pass over the whole document on every Enter in a long editor. An Enter in a 5,000-line document now spends about 3 ms on semantics where it spent 7 to 10. The structural rebuild goes through `rewalkSemantics`, and `SemanticsMemory` is what each walk leaves for the next.
70
+ - acad77f: Finding the accessibility boxes that moved after a layout now walks only what is on screen. It used to work out every mirrored node's box to learn whether it was visible, which cost a keystroke in a 5,000-line document 4 to 24 ms. Subtrees whose bounds are off screen are passed over whole.
71
+ - 444371c: A change in the shape of the tree no longer rebuilds the whole accessibility tree, and no longer sends every later sibling an update. The tree is rebuilt from the nearest record above the change, and anything inside it that nothing touched is taken back as it was: renumbered if it moved, never described again, and a transparent subtree (a block of a long document) taken back as a run without being walked. Updates that only renumber a record whose siblings kept their order are no longer sent, since a mirror that applies removals and adds in order already has it in place: inserting a paragraph in a 5,000-line document sent 4,266 patches to the main thread, and now sends 2. `diffSemantics` gains a companion, `dropIndexShifts`.
72
+ - 5b59d13: Every wheel step between two frames counts. Each was added to the offset the last layout settled on, so when a device sent faster than the display drew, as a trackpad does, each step overwrote the one before and a flick moved about half as far as it should, unevenly. A scroll now starts from where the container is going, clamped to its range.
73
+ - Updated dependencies [5a27b40]
74
+ - Updated dependencies [8d25c04]
75
+ - Updated dependencies [20ac739]
76
+ - Updated dependencies [303e85a]
77
+ - Updated dependencies [0f02fc2]
78
+ - Updated dependencies [8fb3607]
79
+ - Updated dependencies [4450c5c]
80
+ - Updated dependencies [53b4c46]
81
+ - Updated dependencies [d356006]
82
+ - Updated dependencies [101ea8a]
83
+ - Updated dependencies [839011e]
84
+ - Updated dependencies [f0ade22]
85
+ - Updated dependencies [29a36ac]
86
+ - Updated dependencies [7753bdc]
87
+ - Updated dependencies [47aba08]
88
+ - Updated dependencies [fac08c0]
89
+ - Updated dependencies [5b3508d]
90
+ - Updated dependencies [fe0c1e0]
91
+ - Updated dependencies [2ce079c]
92
+ - Updated dependencies [b502e0e]
93
+ - Updated dependencies [be3e274]
94
+ - Updated dependencies [979053a]
95
+ - Updated dependencies [b2dddbc]
96
+ - Updated dependencies [ab0c1a6]
97
+ - Updated dependencies [80a3577]
98
+ - Updated dependencies [47aba08]
99
+ - Updated dependencies [99538fa]
100
+ - Updated dependencies [fb2a6d8]
101
+ - Updated dependencies [94a9f13]
102
+ - Updated dependencies [427ce99]
103
+ - Updated dependencies [b7c9514]
104
+ - Updated dependencies [d3ab865]
105
+ - Updated dependencies [0bef08b]
106
+ - Updated dependencies [aa33728]
107
+ - Updated dependencies [6d51def]
108
+ - Updated dependencies [8c4f475]
109
+ - Updated dependencies [1d61bae]
110
+ - Updated dependencies [5d67836]
111
+ - Updated dependencies [444371c]
112
+ - Updated dependencies [d617d34]
113
+ - Updated dependencies [6f03f61]
114
+ - Updated dependencies [2c572e3]
115
+ - Updated dependencies [84fe6d5]
116
+ - Updated dependencies [e73a5fc]
117
+ - Updated dependencies [1de054a]
118
+ - Updated dependencies [9341d31]
119
+ - Updated dependencies [2e3e56d]
120
+ - Updated dependencies [db1a6a1]
121
+ - gesso-core@0.5.0
122
+
3
123
  ## 0.4.2
4
124
 
5
125
  ### Patch Changes
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # gesso-framework
2
2
 
3
+ SwiftUI and Compose, for the web: declarative components for native-grade apps, with the whole interface drawn from a worker so it stays fast however hard your code works.
4
+
3
5
  Components, cells, the frame runtime, channels, routing and the worker barrier, over [`gesso-core`](https://github.com/kevinpbaker/gesso/tree/main/packages/core).
4
6
 
5
7
  ```bash
@@ -0,0 +1,247 @@
1
+ //#region src/channel/ChannelToken.d.ts
2
+ /**
3
+ * The barrier between the application and the view, declared once.
4
+ *
5
+ * A token is a name, a shape and an initial value — and no
6
+ * implementation at all. Both threads import it, which is the point:
7
+ * the module holding it has nothing in it to bundle, so an app's api
8
+ * client and domain logic never reach the render worker the way a
9
+ * shared `Store` class dragged them there.
10
+ *
11
+ * What the framework knows about a channel ends here. It diffs plain
12
+ * data and ships patches; where the observables came from — a single
13
+ * subject, or an api → repository → domain → view-model stack — is the
14
+ * application's business and the framework cannot tell the difference.
15
+ *
16
+ * export interface CatalogView {
17
+ * products: ProductRow[];
18
+ * status: 'loading' | 'ready' | 'error';
19
+ * }
20
+ * export interface CatalogCommands {
21
+ * addToCart(id: string): void;
22
+ * }
23
+ * export const Catalog = channel<CatalogView, CatalogCommands>('catalog', {
24
+ * products: [],
25
+ * status: 'loading'
26
+ * });
27
+ */
28
+ /**
29
+ * A command a component can send back across the barrier.
30
+ *
31
+ * The arguments are what cross: each is structured-cloned onto the
32
+ * owning thread, so they must be plain data. There may be as many as
33
+ * the command needs, which is why `move(from, to)` is written the way
34
+ * anyone would write it rather than as `move({ from, to })`; a command
35
+ * used to carry one payload and the second argument was dropped on the
36
+ * floor with a warning.
37
+ *
38
+ * Commands return nothing: the effect comes back as a patch, never as
39
+ * a return value, since there is no synchronous answer to be had
40
+ * across a thread.
41
+ */
42
+ type Command = (...args: never[]) => void;
43
+ /**
44
+ * The internal view of a command set: a bag of callables by name.
45
+ *
46
+ * Public signatures constrain to `object`, not to this. An application
47
+ * declares its commands as an ordinary interface —
48
+ * `interface CatalogCommands { addToCart(id: string): void }` — and an
49
+ * interface has no index signature, so it does not satisfy a
50
+ * `Record<string, …>` constraint however well it fits in spirit.
51
+ * Constraining to `object` accepts what people actually write; this
52
+ * alias is what the implementation casts to when it looks a command up
53
+ * by name.
54
+ */
55
+ type CommandMap = Record<string, Command>;
56
+ /** The declared barrier for one area of an application. */
57
+ interface ChannelToken<View extends object, Commands extends object = Record<string, never>> {
58
+ readonly name: string;
59
+ /**
60
+ * What every view key holds before the first patch arrives.
61
+ *
62
+ * Required rather than optional, so the render thread never observes
63
+ * `undefined` for a declared key. A channel that is genuinely still
64
+ * loading says so in its own shape — a `status` field — rather than
65
+ * leaving the view to infer it from an absence.
66
+ */
67
+ readonly initial: View;
68
+ /** Phantom, carrying the command types to `send`. Never read. */
69
+ readonly commands?: Commands;
70
+ }
71
+ /**
72
+ * Declares a channel.
73
+ *
74
+ * The name identifies it across the thread boundary and must be
75
+ * stable; unlike a class name it survives minification, which is why
76
+ * it is written out rather than derived.
77
+ */
78
+ declare function channel<View extends object, Commands extends object = Record<string, never>>(name: string, initial: View): ChannelToken<View, Commands>;
79
+ /** A channel written as one object: the view with its values, and the commands. */
80
+ interface ChannelSpec<View extends object, Commands extends object> {
81
+ /**
82
+ * Every view key with the value it holds before the first patch.
83
+ *
84
+ * The type of the channel's view is the type of this object, so the
85
+ * keys and their initial values are written once instead of in an
86
+ * interface and again in a literal that has to agree with it.
87
+ */
88
+ readonly view: View;
89
+ /**
90
+ * The commands, as a type rather than as implementations.
91
+ *
92
+ * Written `{} as { addToCart(id: string, quantity: number): void }`:
93
+ * the handlers live on the thread that owns the data and are passed
94
+ * to `provide`, so what belongs in the token is only their shape.
95
+ */
96
+ readonly commands?: Commands;
97
+ }
98
+ /**
99
+ * Declares a channel from one object.
100
+ *
101
+ * export const Catalog = defineChannel('catalog', {
102
+ * view: {
103
+ * products: [] as readonly ProductRow[],
104
+ * status: 'loading' as ShelfStatus
105
+ * },
106
+ * commands: {} as {
107
+ * addToCart(id: string, quantity: number): void;
108
+ * move(from: number, to: number): void;
109
+ * }
110
+ * });
111
+ *
112
+ * export type CatalogView = ViewOf<typeof Catalog>;
113
+ *
114
+ * The same token `channel()` returns, declared once instead of three
115
+ * times. `channel<View, Commands>(name, initial)` wrote the view as an
116
+ * interface, then as an initial literal that had to agree with it, and
117
+ * a key added to one and forgotten in the other was a type error in a
118
+ * third file. Here the object is the type.
119
+ *
120
+ * A field whose initial value is narrower than the type it holds is
121
+ * given the type it holds: `[]` is `never[]` and `'loading'` is
122
+ * `string` unless it is said, which is what the `as` clauses above are
123
+ * for. `ViewOf` and `CommandsOf` name the resulting types wherever the
124
+ * application used to name its own interface.
125
+ *
126
+ * `channel()` is not deprecated and keeps working exactly as it did.
127
+ * An application with interfaces it wants to keep, because they are
128
+ * shared with something else or because the initial values are built
129
+ * elsewhere, has nothing to migrate.
130
+ */
131
+ declare function defineChannel<View extends object, Commands extends object = Record<string, never>>(name: string, spec: ChannelSpec<View, Commands>): ChannelToken<View, Commands>;
132
+ /** The view type of a channel token, for an application that wants to name it. */
133
+ type ViewOf<T> = T extends ChannelToken<infer View, object> ? View : never;
134
+ /** The command type of a channel token. */
135
+ type CommandsOf<T> = T extends ChannelToken<object, infer Commands> ? Commands : never;
136
+ /**
137
+ * The keys a channel publishes.
138
+ *
139
+ * Structural in its parameter rather than generic over the token, so
140
+ * it does not have to agree with any particular command type to read
141
+ * what is only ever the initial value's shape.
142
+ */
143
+ declare function viewKeys(token: {
144
+ initial: object;
145
+ }): string[];
146
+ //#endregion
147
+ //#region src/channel/StorePatch.d.ts
148
+ type PatchPath = readonly (string | number)[];
149
+ /**
150
+ * A change to one projection, as sent from a data worker to a replica.
151
+ *
152
+ * Paths are relative to the projection's root value, so a patch is
153
+ * self-contained: the replica never needs the previous value to apply
154
+ * one, only the value it already holds.
155
+ */
156
+ type Patch = {
157
+ op: 'set';
158
+ projection: string;
159
+ path: PatchPath;
160
+ value: unknown;
161
+ } | {
162
+ op: 'delete';
163
+ projection: string;
164
+ path: PatchPath;
165
+ } | {
166
+ op: 'splice';
167
+ projection: string;
168
+ path: PatchPath;
169
+ index: number;
170
+ deleteCount: number;
171
+ items: readonly unknown[];
172
+ };
173
+ /**
174
+ * Describes how to turn `previous` into `current` for one projection.
175
+ *
176
+ * Returns an empty list when nothing changed, which is the common case
177
+ * and the reason this exists: the point of a projection is that most
178
+ * state changes do not alter it, and the ones that do usually alter a
179
+ * small part.
180
+ */
181
+ declare function diffProjection(projection: string, previous: unknown, current: unknown): Patch[];
182
+ /**
183
+ * Applies patches to a projection value, sharing structure with the
184
+ * original everywhere the patch did not reach.
185
+ *
186
+ * Nothing handed in is mutated: bindings hold onto emitted values, so a
187
+ * replica that edited in place would change data a component already
188
+ * rendered.
189
+ *
190
+ * **Each container on a patched path is copied once per batch, not once
191
+ * per patch.** A batch of N patches into one K-key object used to cost
192
+ * N × K: every patch spread the whole object again to change one key.
193
+ * A snapshot keyed by id is exactly that shape — gessologic published
194
+ * `Record<netId, 0 | 1>` for ten thousand nets, about nine hundred
195
+ * patches a publish, and the render worker's patch phase fell minutes
196
+ * behind a 60 Hz stream it could never catch. So the batch remembers
197
+ * the containers it has copied, and writes into those in place: they
198
+ * are its own, made during this call and seen by nobody yet. Values
199
+ * that arrived inside a patch are never written into, because they are
200
+ * the patch's — a devtools log replays the same patch objects again.
201
+ */
202
+ declare function applyPatches(root: unknown, patches: readonly Patch[]): unknown;
203
+ declare function applyPatch(root: unknown, patch: Patch): unknown;
204
+ //#endregion
205
+ //#region src/channel/ChannelProtocol.d.ts
206
+ /**
207
+ * A two-way channel endpoint. `MessagePort` satisfies it, as does a
208
+ * test double, so nothing in the channel layer depends on Worker.
209
+ */
210
+ interface ChannelPort {
211
+ postMessage(message: unknown): void;
212
+ onmessage: ((event: {
213
+ data: unknown;
214
+ }) => void) | null;
215
+ }
216
+ /**
217
+ * Render thread → the thread that owns the channel.
218
+ *
219
+ * A command's first argument stayed `payload` when commands learned to
220
+ * take more than one, and the rest travel beside it. That is not
221
+ * tidiness: an older view talking to a newer application worker sends
222
+ * no `rest`, which reads as the one-argument call it is, and an older
223
+ * application worker ignores the field, which is exactly what it did
224
+ * before the field existed.
225
+ */
226
+ type ChannelClientMessage = {
227
+ type: 'channel:sync';
228
+ } | {
229
+ type: 'channel:command';
230
+ command: string;
231
+ payload: unknown;
232
+ rest?: unknown[];
233
+ };
234
+ /** The owning thread → render thread. */
235
+ type ChannelHostMessage = {
236
+ type: 'channel:patch';
237
+ patches: Patch[];
238
+ } | {
239
+ type: 'channel:error';
240
+ message: string;
241
+ stack?: string;
242
+ };
243
+ declare function isChannelClientMessage(value: unknown): value is ChannelClientMessage;
244
+ declare function isChannelHostMessage(value: unknown): value is ChannelHostMessage;
245
+ //#endregion
246
+ export { channel as _, isChannelHostMessage as a, applyPatch as c, ChannelSpec as d, ChannelToken as f, ViewOf as g, CommandsOf as h, isChannelClientMessage as i, applyPatches as l, CommandMap as m, ChannelHostMessage as n, Patch as o, Command as p, ChannelPort as r, PatchPath as s, ChannelClientMessage as t, diffProjection as u, defineChannel as v, viewKeys as y };
247
+ //# sourceMappingURL=ChannelProtocol-ByNoHujM.d.ts.map
@@ -1,3 +1,4 @@
1
+ import { f as ChannelToken, o as Patch, r as ChannelPort } from "./ChannelProtocol-ByNoHujM.js";
1
2
  import { BehaviorSubject, Observable, Subject, Subscription } from "rxjs";
2
3
  import { LayoutBox, Reactive, UiChild, UiModifier } from "gesso-core";
3
4
  //#region src/Component.d.ts
@@ -257,251 +258,6 @@ declare class BoundsCell extends InternalState<LayoutBox> {
257
258
  */
258
259
  declare function bounds(label?: string): BoundsCell;
259
260
  //#endregion
260
- //#region src/channel/ChannelToken.d.ts
261
- /**
262
- * The barrier between the application and the view, declared once.
263
- *
264
- * A token is a name, a shape and an initial value — and no
265
- * implementation at all. Both threads import it, which is the point:
266
- * the module holding it has nothing in it to bundle, so an app's api
267
- * client and domain logic never reach the render worker the way a
268
- * shared `Store` class dragged them there.
269
- *
270
- * What the framework knows about a channel ends here. It diffs plain
271
- * data and ships patches; where the observables came from — a single
272
- * subject, or an api → repository → domain → view-model stack — is the
273
- * application's business and the framework cannot tell the difference.
274
- *
275
- * export interface CatalogView {
276
- * products: ProductRow[];
277
- * status: 'loading' | 'ready' | 'error';
278
- * }
279
- * export interface CatalogCommands {
280
- * addToCart(id: string): void;
281
- * }
282
- * export const Catalog = channel<CatalogView, CatalogCommands>('catalog', {
283
- * products: [],
284
- * status: 'loading'
285
- * });
286
- */
287
- /**
288
- * A command a component can send back across the barrier.
289
- *
290
- * The arguments are what cross: each is structured-cloned onto the
291
- * owning thread, so they must be plain data. There may be as many as
292
- * the command needs, which is why `move(from, to)` is written the way
293
- * anyone would write it rather than as `move({ from, to })`; a command
294
- * used to carry one payload and the second argument was dropped on the
295
- * floor with a warning.
296
- *
297
- * Commands return nothing: the effect comes back as a patch, never as
298
- * a return value, since there is no synchronous answer to be had
299
- * across a thread.
300
- */
301
- type Command = (...args: never[]) => void;
302
- /**
303
- * The internal view of a command set: a bag of callables by name.
304
- *
305
- * Public signatures constrain to `object`, not to this. An application
306
- * declares its commands as an ordinary interface —
307
- * `interface CatalogCommands { addToCart(id: string): void }` — and an
308
- * interface has no index signature, so it does not satisfy a
309
- * `Record<string, …>` constraint however well it fits in spirit.
310
- * Constraining to `object` accepts what people actually write; this
311
- * alias is what the implementation casts to when it looks a command up
312
- * by name.
313
- */
314
- type CommandMap = Record<string, Command>;
315
- /** The declared barrier for one area of an application. */
316
- interface ChannelToken<View extends object, Commands extends object = Record<string, never>> {
317
- readonly name: string;
318
- /**
319
- * What every view key holds before the first patch arrives.
320
- *
321
- * Required rather than optional, so the render thread never observes
322
- * `undefined` for a declared key. A channel that is genuinely still
323
- * loading says so in its own shape — a `status` field — rather than
324
- * leaving the view to infer it from an absence.
325
- */
326
- readonly initial: View;
327
- /** Phantom, carrying the command types to `send`. Never read. */
328
- readonly commands?: Commands;
329
- }
330
- /**
331
- * Declares a channel.
332
- *
333
- * The name identifies it across the thread boundary and must be
334
- * stable; unlike a class name it survives minification, which is why
335
- * it is written out rather than derived.
336
- */
337
- declare function channel<View extends object, Commands extends object = Record<string, never>>(name: string, initial: View): ChannelToken<View, Commands>;
338
- /** A channel written as one object: the view with its values, and the commands. */
339
- interface ChannelSpec<View extends object, Commands extends object> {
340
- /**
341
- * Every view key with the value it holds before the first patch.
342
- *
343
- * The type of the channel's view is the type of this object, so the
344
- * keys and their initial values are written once instead of in an
345
- * interface and again in a literal that has to agree with it.
346
- */
347
- readonly view: View;
348
- /**
349
- * The commands, as a type rather than as implementations.
350
- *
351
- * Written `{} as { addToCart(id: string, quantity: number): void }`:
352
- * the handlers live on the thread that owns the data and are passed
353
- * to `provide`, so what belongs in the token is only their shape.
354
- */
355
- readonly commands?: Commands;
356
- }
357
- /**
358
- * Declares a channel from one object.
359
- *
360
- * export const Catalog = defineChannel('catalog', {
361
- * view: {
362
- * products: [] as readonly ProductRow[],
363
- * status: 'loading' as ShelfStatus
364
- * },
365
- * commands: {} as {
366
- * addToCart(id: string, quantity: number): void;
367
- * move(from: number, to: number): void;
368
- * }
369
- * });
370
- *
371
- * export type CatalogView = ViewOf<typeof Catalog>;
372
- *
373
- * The same token `channel()` returns, declared once instead of three
374
- * times. `channel<View, Commands>(name, initial)` wrote the view as an
375
- * interface, then as an initial literal that had to agree with it, and
376
- * a key added to one and forgotten in the other was a type error in a
377
- * third file. Here the object is the type.
378
- *
379
- * A field whose initial value is narrower than the type it holds is
380
- * given the type it holds: `[]` is `never[]` and `'loading'` is
381
- * `string` unless it is said, which is what the `as` clauses above are
382
- * for. `ViewOf` and `CommandsOf` name the resulting types wherever the
383
- * application used to name its own interface.
384
- *
385
- * `channel()` is not deprecated and keeps working exactly as it did.
386
- * An application with interfaces it wants to keep, because they are
387
- * shared with something else or because the initial values are built
388
- * elsewhere, has nothing to migrate.
389
- */
390
- declare function defineChannel<View extends object, Commands extends object = Record<string, never>>(name: string, spec: ChannelSpec<View, Commands>): ChannelToken<View, Commands>;
391
- /** The view type of a channel token, for an application that wants to name it. */
392
- type ViewOf<T> = T extends ChannelToken<infer View, object> ? View : never;
393
- /** The command type of a channel token. */
394
- type CommandsOf<T> = T extends ChannelToken<object, infer Commands> ? Commands : never;
395
- /**
396
- * The keys a channel publishes.
397
- *
398
- * Structural in its parameter rather than generic over the token, so
399
- * it does not have to agree with any particular command type to read
400
- * what is only ever the initial value's shape.
401
- */
402
- declare function viewKeys(token: {
403
- initial: object;
404
- }): string[];
405
- //#endregion
406
- //#region src/channel/StorePatch.d.ts
407
- type PatchPath = readonly (string | number)[];
408
- /**
409
- * A change to one projection, as sent from a data worker to a replica.
410
- *
411
- * Paths are relative to the projection's root value, so a patch is
412
- * self-contained: the replica never needs the previous value to apply
413
- * one, only the value it already holds.
414
- */
415
- type Patch = {
416
- op: 'set';
417
- projection: string;
418
- path: PatchPath;
419
- value: unknown;
420
- } | {
421
- op: 'delete';
422
- projection: string;
423
- path: PatchPath;
424
- } | {
425
- op: 'splice';
426
- projection: string;
427
- path: PatchPath;
428
- index: number;
429
- deleteCount: number;
430
- items: readonly unknown[];
431
- };
432
- /**
433
- * Describes how to turn `previous` into `current` for one projection.
434
- *
435
- * Returns an empty list when nothing changed, which is the common case
436
- * and the reason this exists: the point of a projection is that most
437
- * state changes do not alter it, and the ones that do usually alter a
438
- * small part.
439
- */
440
- declare function diffProjection(projection: string, previous: unknown, current: unknown): Patch[];
441
- /**
442
- * Applies patches to a projection value, sharing structure with the
443
- * original everywhere the patch did not reach.
444
- *
445
- * Nothing handed in is mutated: bindings hold onto emitted values, so a
446
- * replica that edited in place would change data a component already
447
- * rendered.
448
- *
449
- * **Each container on a patched path is copied once per batch, not once
450
- * per patch.** A batch of N patches into one K-key object used to cost
451
- * N × K: every patch spread the whole object again to change one key.
452
- * A snapshot keyed by id is exactly that shape — gessologic published
453
- * `Record<netId, 0 | 1>` for ten thousand nets, about nine hundred
454
- * patches a publish, and the render worker's patch phase fell minutes
455
- * behind a 60 Hz stream it could never catch. So the batch remembers
456
- * the containers it has copied, and writes into those in place: they
457
- * are its own, made during this call and seen by nobody yet. Values
458
- * that arrived inside a patch are never written into, because they are
459
- * the patch's — a devtools log replays the same patch objects again.
460
- */
461
- declare function applyPatches(root: unknown, patches: readonly Patch[]): unknown;
462
- declare function applyPatch(root: unknown, patch: Patch): unknown;
463
- //#endregion
464
- //#region src/channel/ChannelProtocol.d.ts
465
- /**
466
- * A two-way channel endpoint. `MessagePort` satisfies it, as does a
467
- * test double, so nothing in the channel layer depends on Worker.
468
- */
469
- interface ChannelPort {
470
- postMessage(message: unknown): void;
471
- onmessage: ((event: {
472
- data: unknown;
473
- }) => void) | null;
474
- }
475
- /**
476
- * Render thread → the thread that owns the channel.
477
- *
478
- * A command's first argument stayed `payload` when commands learned to
479
- * take more than one, and the rest travel beside it. That is not
480
- * tidiness: an older view talking to a newer application worker sends
481
- * no `rest`, which reads as the one-argument call it is, and an older
482
- * application worker ignores the field, which is exactly what it did
483
- * before the field existed.
484
- */
485
- type ChannelClientMessage = {
486
- type: 'channel:sync';
487
- } | {
488
- type: 'channel:command';
489
- command: string;
490
- payload: unknown;
491
- rest?: unknown[];
492
- };
493
- /** The owning thread → render thread. */
494
- type ChannelHostMessage = {
495
- type: 'channel:patch';
496
- patches: Patch[];
497
- } | {
498
- type: 'channel:error';
499
- message: string;
500
- stack?: string;
501
- };
502
- declare function isChannelClientMessage(value: unknown): value is ChannelClientMessage;
503
- declare function isChannelHostMessage(value: unknown): value is ChannelHostMessage;
504
- //#endregion
505
261
  //#region src/channel/ChannelReplica.d.ts
506
262
  /**
507
263
  * The render thread's end of a channel.
@@ -725,5 +481,5 @@ type ComponentArgs<C> = RequiredKeys<ComponentProps<C>> extends never ? [inputs?
725
481
  */
726
482
  declare function isClassComponent(component: ComponentType): component is ClassComponent;
727
483
  //#endregion
728
- export { bounds as A, output as B, CommandMap as C, defineChannel as D, channel as E, OutputTarget as F, internalState as H, ReadableCell as I, input as L, EmitValue as M, InputCell as N, viewKeys as O, OutputCell as P, into as R, Command as S, ViewOf as T, Component as U, InternalState as V, applyPatch as _, ComponentType as a, ChannelSpec as b, isClassComponent as c, ChannelHostMessage as d, ChannelPort as f, PatchPath as g, Patch as h, ComponentProps as i, EmitArgs as j, BoundsCell as k, ChannelReplica as l, isChannelHostMessage as m, ComponentArgs as n, FunctionComponent as o, isChannelClientMessage as p, ComponentContext as r, Inputs as s, ClassComponent as t, ChannelClientMessage as u, applyPatches as v, CommandsOf as w, ChannelToken as x, diffProjection as y, isOutputTarget as z };
729
- //# sourceMappingURL=FunctionComponent-CgwLKE5d.d.ts.map
484
+ export { internalState as C, InternalState as S, ReadableCell as _, ComponentType as a, isOutputTarget as b, isClassComponent as c, bounds as d, EmitArgs as f, OutputTarget as g, OutputCell as h, ComponentProps as i, ChannelReplica as l, InputCell as m, ComponentArgs as n, FunctionComponent as o, EmitValue as p, ComponentContext as r, Inputs as s, ClassComponent as t, BoundsCell as u, input as v, Component as w, output as x, into as y };
485
+ //# sourceMappingURL=FunctionComponent-fYMdePtH.d.ts.map