@zerotal/flow 1.6.3 → 1.7.2
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 +181 -0
- package/api-surface.md +1125 -0
- package/package.json +7 -5
- package/src/Component.ts +13 -2
- package/src/browser/FlowBrowser.ts +7 -2
- package/src/client/bridge.ts +344 -80
- package/src/client/framePanes.ts +229 -0
- package/src/client/headless.ts +7 -1
- package/src/client/timeline.ts +12 -0
- package/src/components.ts +13 -2
- package/src/decorators.ts +104 -14
- package/src/dehydrate.ts +10 -4
- package/src/durable.ts +2 -1
- package/src/events.ts +3 -3
- package/src/index.ts +1 -0
- package/src/jsx-runtime.ts +13 -0
- package/src/observability.ts +28 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,187 @@ change.
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`@on` can name a channel per instance.** A listener's channel is read off the *class*, so
|
|
17
|
+
a channel containing a record id could not be written at all. The guide showed
|
|
18
|
+
`@on("echo-private:issues.${this.issueId},CommentPosted")`, which is not interpolation —
|
|
19
|
+
it is a plain string, and the eleven characters `${this.issueId}` reached the browser
|
|
20
|
+
intact, subscribed to a channel nobody broadcasts to, and delivered nothing. No error, no
|
|
21
|
+
warning, no subscription: a live feature with this bug looks exactly like a live feature
|
|
22
|
+
that was never built.
|
|
23
|
+
|
|
24
|
+
`@on` now also accepts a resolver, the same shape `@presence` and `@shared` have always
|
|
25
|
+
taken and are resolved with four lines away in the same file:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
@on((self) => `echo-private:issues.${self.issue.id},CommentPosted`)
|
|
29
|
+
async onCommentPosted(payload: { comment: Comment }) { … }
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
It is called with the component as the snapshot is built — after `onMount()`, so it can
|
|
33
|
+
read anything the component has loaded. A resolver that throws drops that one listener and
|
|
34
|
+
leaves the page rendering, because a page missing its live updates is degraded while a page
|
|
35
|
+
that 500s over an unnameable channel is gone. Static names are untouched.
|
|
36
|
+
|
|
37
|
+
Reach for the **narrowest** channel a reader is entitled to. A shared channel plus an
|
|
38
|
+
`if (payload.id !== this.id) return` in the handler looks equivalent and is not: the
|
|
39
|
+
broadcast still lands in every subscriber's browser, to be discarded after the fact.
|
|
40
|
+
|
|
41
|
+
- **`<Link down>` — prefetch on pointer-down.** The choice was between prefetching on hover
|
|
42
|
+
and not prefetching at all, and on a dense list neither is right. Hover prefetch is free
|
|
43
|
+
speed on a handful of stable links — a navigation rail, a breadcrumb — but the pointer
|
|
44
|
+
crosses every row between where it is and where it is going, so scrolling a hundred-row
|
|
45
|
+
table asks the server for a hundred pages nobody chose.
|
|
46
|
+
|
|
47
|
+
`down` fires on `pointerdown` with no dwell: once, on the link the reader has committed to,
|
|
48
|
+
and still ahead of the click by however long the button is held. `pointerdown` rather than
|
|
49
|
+
`mousedown` so a touch lands there too, and it is passive — the click that follows is
|
|
50
|
+
handled as usual and finds the page already cached. `flow:navigate.down` as an attribute;
|
|
51
|
+
both may be set.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- **`<FileUpload>` never stopped saying "Uploading… 100%".** A successful upload left the
|
|
56
|
+
dropzone reporting progress forever. The bytes had gone up, the signed reference had come
|
|
57
|
+
back, and the bound property had been set — the only thing that failed was the component
|
|
58
|
+
admitting it, which meant the feature read as broken while working perfectly.
|
|
59
|
+
|
|
60
|
+
The bridge dispatches `flow:upload-finish` on success and the guide documents that name.
|
|
61
|
+
The component listened for `flow:upload-done`, which appeared nowhere else in the codebase
|
|
62
|
+
and has never been dispatched by anything. So `uploading` was set true on start and had no
|
|
63
|
+
path back to false. The component's own test covered start, progress and error, and skipped
|
|
64
|
+
success — which is how a one-word mismatch survived. It now covers it.
|
|
65
|
+
|
|
66
|
+
- **Signing out in another tab left the first tab looking broken.** The server has always
|
|
67
|
+
handled this: `hydrate()` refuses a snapshot whose subject no longer matches the session, and
|
|
68
|
+
the provider answers with an error frame carrying `reload: true` and "Your session changed —
|
|
69
|
+
reload the page." The client never read the flag. It reloaded for a dev soft-refresh and for
|
|
70
|
+
the literal message `"Unknown component"`, then fell through to `console.error` — so the
|
|
71
|
+
instruction reached a console nobody had open.
|
|
72
|
+
|
|
73
|
+
What the reader saw was a button that did nothing. The page stayed on screen, still looking
|
|
74
|
+
signed in, the action silently rejected, and no way to discover otherwise but to reload by
|
|
75
|
+
hand. The frame is now honoured, guarded by the same one-shot that protects the other reload
|
|
76
|
+
paths, and the decision is a pure `_reloadReasonFor()` so it can be tested without a DOM.
|
|
77
|
+
Matching on the flag rather than the sentence, so rewording or translating the message cannot
|
|
78
|
+
turn the recovery off.
|
|
79
|
+
|
|
80
|
+
- **Sibling subclasses of one decorated base stole each other's fields.** Field decorators buffer
|
|
81
|
+
their registration and the buffer is matched to a class lazily, on the first read of that class's
|
|
82
|
+
registry. Matching took the longest run across **every** buffered group, so a class could claim —
|
|
83
|
+
and splice away — an entry belonging to a sibling whose group merely shared a name.
|
|
84
|
+
|
|
85
|
+
Given `NewPage { project }` and `EditPage { project, issue }` over a shared base, draining
|
|
86
|
+
`NewPage` first found `project` at the head of `EditPage`'s group, tied on length against its own
|
|
87
|
+
single entry, and — ties going to the newest group — took it. `EditPage` then drained to `issue`
|
|
88
|
+
alone; its `project` was never registered, route-param seeding skipped the field, and `render()`
|
|
89
|
+
died on `this.project.slug`. Whether the page worked depended on which sibling had been rendered
|
|
90
|
+
first in that process, which is what made it look flaky rather than broken.
|
|
91
|
+
|
|
92
|
+
Matching is now restricted to groups whose every name is a field the class declares, falling back
|
|
93
|
+
to the old behaviour when nothing is fully contained. A sibling's group is rejected by the names
|
|
94
|
+
it has that this class does not.
|
|
95
|
+
|
|
96
|
+
- **The real-time validation example did not compile.** `docs/flow/decorators.md` showed
|
|
97
|
+
`<input value={this.email} flow:model.live />`. That is the *emitted* directive, not the prop:
|
|
98
|
+
the `.` in an attribute name is a parse error in TSX (`TS1003`), so anyone copying it got a
|
|
99
|
+
build failure on a line the guide had just handed them. The prop is `live`; the note now says
|
|
100
|
+
so, and says why the compiled form cannot be written by hand.
|
|
101
|
+
|
|
102
|
+
### Documented
|
|
103
|
+
|
|
104
|
+
Four behaviours that were true but written down nowhere, each found by hitting it:
|
|
105
|
+
|
|
106
|
+
- **AOT compilation and `__()` are mutually exclusive** — a function call in a text child forces
|
|
107
|
+
the runtime fallback, so a translated app compiles no pages at all. Normally that is a speed
|
|
108
|
+
cost; under `cspSafe`, where every page must compile, it is a build failure. `performance.md`
|
|
109
|
+
now lists what blocks compilation and what to do about it.
|
|
110
|
+
- **`FlowTest` opens no request scope**, so `Auth.user()` and `Auth.attempt()` throw and no
|
|
111
|
+
action behind a sign-in can be driven without wrapping it. `testing.md` carries the wrapper.
|
|
112
|
+
- **A layout cannot set the document's `lang`** — Flow emits `<html lang="en">` and offers no
|
|
113
|
+
hook, so a localised app marks up its wrapper instead and the root element stays wrong.
|
|
114
|
+
- **`<Tabs>` selection is client-only and its strip is not themeable** — no bindable prop, so no
|
|
115
|
+
`?tab=`, no back button, and hardcoded gray/indigo that is invisible on a light surface.
|
|
116
|
+
- **Flow and `zerotal/view` components do not interoperate** — the two JSX runtimes produce
|
|
117
|
+
different element types (`HtmlNode` vs `SafeHtml`), so a view `FC` in a Flow page is `TS2786`.
|
|
118
|
+
- **`onSort` does not say which container took the drop** — the destination is encoded in which
|
|
119
|
+
method runs, so dragging between containers needs one action per container.
|
|
120
|
+
|
|
121
|
+
Also: `sortGroupId` is annotated as inert. It is declared, mapped to `flow:sort:group-id`, and
|
|
122
|
+
read by neither the runtime nor the compiler.
|
|
123
|
+
|
|
124
|
+
## [1.7.1] — 2026-08-16
|
|
125
|
+
|
|
126
|
+
### Changed
|
|
127
|
+
|
|
128
|
+
- **An action opens into its own tabs.** A frame's detail was a stack — Sent, then State,
|
|
129
|
+
then Server with the SQL buried under it. They are alternatives, not a sequence: you are
|
|
130
|
+
reading the arguments the click sent, _or_ the state it moved, _or_ the queries it ran. Now
|
|
131
|
+
a strip, matching the request rows: `Sent · State 2 · Server · Queries 3 · Logs 1`.
|
|
132
|
+
|
|
133
|
+
Queries and Logs are panes of their own, counted, so what an action cost the database is
|
|
134
|
+
one click from the frame rather than a list appended beneath a duration. Only the panes a
|
|
135
|
+
frame has appear, so the strip doubles as the summary, and which one you were reading is
|
|
136
|
+
kept as you step down the list — comparing the SQL of one action against the next should
|
|
137
|
+
not return you to the arguments each time.
|
|
138
|
+
|
|
139
|
+
- **One Flow tab in DevTools, and its rows open.** The time-travel timeline registered itself
|
|
140
|
+
as a tab called "Timeline", colliding with the panel's own Timeline tab, and sat apart from
|
|
141
|
+
the Flow tab even though the frames and the actions are the same clicks seen from the two
|
|
142
|
+
ends. They are now one tab, titled `Flow`.
|
|
143
|
+
|
|
144
|
+
Each frame expands to what that action actually did — the call and its arguments, the
|
|
145
|
+
fields it changed as `before → after`, and the server's own half: duration, status, client
|
|
146
|
+
IP, the queries it ran and the lines it logged, read off that action's devtools trace. A
|
|
147
|
+
frame that never left the browser says so rather than showing an empty server section.
|
|
148
|
+
Rewinding is unchanged: the row still time-travels, and the disclosure has its own hit area
|
|
149
|
+
so reading what happened does not also rewind the page you are reading it from.
|
|
150
|
+
|
|
151
|
+
Flow's action entries are recorded on the trace as before, now `hidden` so they no longer
|
|
152
|
+
claim a tab that repeated what the trace header already said.
|
|
153
|
+
|
|
154
|
+
### Fixed
|
|
155
|
+
|
|
156
|
+
- **The DevTools Flow tab shows Flow actions.** It could not show any, ever: the tab
|
|
157
|
+
reported "No flow activity during this request" on every page of an app whose every
|
|
158
|
+
interaction is a Flow action.
|
|
159
|
+
|
|
160
|
+
An action arrives over the WebSocket and runs against its own `HttpContext`, and the
|
|
161
|
+
bridge recorded its entry against that context — correctly. But DevTools builds a trace
|
|
162
|
+
from core's `RequestHandled` / `RequestFailed`, and a socket action fires neither, so
|
|
163
|
+
that context became no trace and everything buffered against it was dropped unread. Not
|
|
164
|
+
only the Flow entry: the queries an action ran, its logs, its N+1 warnings. For a Flow
|
|
165
|
+
app that is the entire interactive half of the application, invisible in the one tool
|
|
166
|
+
built to see it.
|
|
167
|
+
|
|
168
|
+
The bridge now finalises the action's context itself, through the new
|
|
169
|
+
`TraceSink.finalise` in `@zerotal/devtools`. An action appears in the request list
|
|
170
|
+
labelled `FLOW` rather than as a second `GET` of the page it ran on, carrying the same
|
|
171
|
+
queries, logs and timings any request does. Found by wiring DevTools into this repo's
|
|
172
|
+
own `apps/docs` and driving a real action in a browser.
|
|
173
|
+
|
|
174
|
+
## [1.7.0] — 2026-08-16
|
|
175
|
+
|
|
176
|
+
### Fixed
|
|
177
|
+
|
|
178
|
+
- **SPA navigation no longer orphans the outgoing page's snapshot.** The swap removed the
|
|
179
|
+
first `[id^="flow-state-"]` in document order, but a page-level snapshot is a direct child
|
|
180
|
+
of `<body>` while a child island's sits inside the body content — so on any page with an
|
|
181
|
+
island it removed the island's script and left the page's behind. Ids are random per
|
|
182
|
+
request, so the leftovers accumulated one per navigation for as long as the tab stayed
|
|
183
|
+
open. Now scoped to `body > script[id^="flow-state-"]`, and every match is removed, so a
|
|
184
|
+
session that has already leaked cleans itself up on the next navigation.
|
|
185
|
+
|
|
186
|
+
### Removed
|
|
187
|
+
|
|
188
|
+
- **`Component._skipMount`**, an `@internal` field that was written by `hydrate()` and
|
|
189
|
+
`restoreDurable()` and read by nothing. Mount-skipping is structural — the router branches
|
|
190
|
+
between `onHydrate()` and `onMount()` — and wiring the flag up would have been wrong:
|
|
191
|
+
`$refresh` and `$mount` deliberately re-run `onMount()` on a hydrated page. An inert flag
|
|
192
|
+
that reads as load-bearing is worse than none, so it is gone and the guarantee is pinned by
|
|
193
|
+
a test instead: mount runs exactly once across an initial render and any number of actions.
|
|
194
|
+
|
|
14
195
|
## [1.6.0] — 2026-08-15
|
|
15
196
|
|
|
16
197
|
### Added
|