@bgub/fig 0.1.0-alpha.0 → 0.1.0-alpha.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +517 -0
  2. package/README.md +50 -19
  3. package/dist/element-BVg1bKOz.d.ts +345 -0
  4. package/dist/element-DmbzNH0T.js +299 -0
  5. package/dist/element-DmbzNH0T.js.map +1 -0
  6. package/dist/hooks-B7BTQWiu.d.ts +158 -0
  7. package/dist/index.d.ts +3 -5
  8. package/dist/index.js +4 -4
  9. package/dist/internal.d.ts +215 -6
  10. package/dist/internal.js +15 -43
  11. package/dist/internal.js.map +1 -1
  12. package/dist/jsx-runtime.d.ts +1 -1
  13. package/dist/jsx-runtime.js +3 -3
  14. package/dist/jsx-runtime.js.map +1 -1
  15. package/dist/payload-format-KTNaSI4h.js +366 -0
  16. package/dist/payload-format-KTNaSI4h.js.map +1 -0
  17. package/dist/payload.d.ts +92 -0
  18. package/dist/payload.js +429 -0
  19. package/dist/payload.js.map +1 -0
  20. package/dist/{transition-DEqcImne.js → resource-DskjKeuM.js} +144 -83
  21. package/dist/resource-DskjKeuM.js.map +1 -0
  22. package/dist/{data-store-CRnNAT9-.js → transition-CC4kjjrU.js} +165 -54
  23. package/dist/transition-CC4kjjrU.js.map +1 -0
  24. package/package.json +5 -6
  25. package/dist/data-CHJh_xU9.d.ts +0 -79
  26. package/dist/data-h46EcMnE.js +0 -37
  27. package/dist/data-h46EcMnE.js.map +0 -1
  28. package/dist/data-store-CRnNAT9-.js.map +0 -1
  29. package/dist/data-store-EQOLQsW4.d.ts +0 -32
  30. package/dist/element-Bh_k9c3N.js +0 -179
  31. package/dist/element-Bh_k9c3N.js.map +0 -1
  32. package/dist/element-CCOOi4Ka.d.ts +0 -209
  33. package/dist/server.browser.d.ts +0 -9
  34. package/dist/server.browser.js +0 -8
  35. package/dist/server.browser.js.map +0 -1
  36. package/dist/server.d.ts +0 -9
  37. package/dist/server.js +0 -9
  38. package/dist/server.js.map +0 -1
  39. package/dist/transition-Cl6mAPcx.d.ts +0 -96
  40. package/dist/transition-DEqcImne.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,520 @@
1
+ ## @bgub/fig@0.1.0-alpha.2
2
+
3
+ ### Preload eligible host images during server rendering
4
+
5
+ HTML and Payload rendering now derive image preload assets from eager `<img>`
6
+ hosts. The hints share Fig's existing asset registry, dedupe against explicit
7
+ preloads, preserve responsive `srcset` and `sizes` selection, and arrive before
8
+ the content that depends on them.
9
+
10
+ The first ten distinct images receive early shell delivery without being
11
+ forced to high browser priority. Lazy, explicitly low-priority, data-URL,
12
+ `<picture>`, and `<noscript>` images remain untouched.
13
+
14
+ ### Keep empty children and document assets hydration-safe
15
+
16
+ Empty string children now normalize away on both the server and client, so a
17
+ rendered empty token cannot force hydration to recover the root.
18
+
19
+ When full-document hydration does recover, declarative stylesheets, titles,
20
+ and metadata now acquire against the replacement head instead of disappearing
21
+ with the server document.
22
+
23
+ ## @bgub/fig@0.1.0-alpha.1
24
+
25
+ ### Asset descriptors use native names and preserve native ordering
26
+
27
+ Client-inserted and host-rendered stylesheets now form precedence buckets in
28
+ the order each distinct precedence value is first discovered. A stylesheet
29
+ discovered later for an existing bucket is inserted before the following
30
+ bucket, keeping lazy and payload-delivered CSS in its intended cascade order.
31
+
32
+ Raw `<script>` elements now enter the asset registry only when explicitly
33
+ marked `async`. Non-async scripts retain their native document position and
34
+ execution semantics; explicit `script()` descriptors continue to support all
35
+ asset-delivery modes.
36
+
37
+ Asset descriptor options and serialized payload asset rows now use native HTML
38
+ attribute names: `crossorigin`, `fetchpriority`, and `http-equiv`. The previous
39
+ React-style `crossOrigin`, `fetchPriority`, and `httpEquiv` spellings are
40
+ removed.
41
+
42
+ Host resource resolution now receives the actual host parent and fixes
43
+ out-of-band placement once per fiber. SVG and MathML titles consequently stay
44
+ in their native namespace, while HTML titles carrying `itemprop` stay in-tree;
45
+ ordinary document titles continue to use the shared head registry.
46
+
47
+ ### Drop the `clientReferenceAssets` helper from the main entry
48
+
49
+ The runtime helper `clientReferenceAssets(reference)` (read a client
50
+ reference's declared assets, resolving thunks) had two homes; it is now
51
+ exported only from `@bgub/fig/internal`, where its consumers — the payload
52
+ serializer and framework manifest plumbing — already import it. Apps
53
+ declare assets with `clientReference({ assets })` and never call the
54
+ helper. The `ClientReferenceAssets` type stays on the main entry because
55
+ it appears in the public `ClientReferenceOptions.assets` signature.
56
+
57
+ ### Stable client-reference identity across decodes
58
+
59
+ `@bgub/fig/payload` exposes `createPayloadClientReferenceResolver(resolve)`:
60
+ a caller-owned stateful resolver passed as the `resolveClientReference`
61
+ decode option. With one, every resolvable client reference decodes to one
62
+ resolver-owned wrapper per reference id across all decodes sharing the
63
+ resolver — gated, ungated, or asynchronously resolved — so re-decoding a
64
+ payload updates islands in place instead of remounting them. Reveal gates
65
+ ride the decoded element instances rather than the wrapper: each decode
66
+ gates exactly its own content, so a newer decode's pending assets never
67
+ re-suspend an island already on screen, while its new island instances
68
+ still wait for the stylesheets they declared. The caller owns the
69
+ resolver's lifetime; under fast refresh no manual invalidation is needed
70
+ (hot edits remap the latched resolution through component families, and
71
+ unaccepted updates full-reload), while `delete`/`clear` cover manifest
72
+ swaps without a reload. With a plain resolve function, gated and async
73
+ references keep their per-decode wrapper identity, now with the same
74
+ per-element gating.
75
+
76
+ `@bgub/fig-dom`'s `payloadDataLoader` accepts the stateful resolver through
77
+ its existing `resolveClientReference` option.
78
+
79
+ Framework adapters can retain one resolver across refreshes and navigations,
80
+ preventing asset-gated islands from remounting on every re-decode.
81
+
82
+ ### Publish metadata only with its visible owner
83
+
84
+ Title and meta resources now travel through Payload as owner-bound
85
+ declarations and update the document only when their decoded tree commits.
86
+ Pending or superseded refreshes keep the previous metadata visible.
87
+
88
+ Streaming HTML now treats Suspense fallbacks as metadata owners and reconciles
89
+ the completed visible metadata snapshot in the boundary reveal operation.
90
+ Partial segments and failed or abandoned primary work cannot mutate the head.
91
+
92
+ The obsolete `onAssetError` option and its asset-diagnostic types are removed:
93
+ late metadata is delivered with its owner instead of being dropped.
94
+
95
+ ### Supersession abort waits for the successor's value
96
+
97
+ Refreshing a fulfilled data-resource entry no longer aborts the previous
98
+ generation's signal when the new load starts. Authority transfers when the
99
+ successor's value publishes: the visible stale value keeps streaming through
100
+ the refresh window (payload holes keep filling), subscribers re-render onto
101
+ the new tree in the same pass the old generation retires in, and a failed
102
+ refresh leaves the previous generation fully alive — stale value usable,
103
+ live holes included. Value-less pending loads still abort immediately when
104
+ superseded, and hydrate-over/eviction/disposal still end every generation.
105
+
106
+ This closes the gap where refreshing a serialized-component resource while
107
+ its holes were still streaming surfaced "Payload decode aborted." through
108
+ the nearest ErrorBoundary — cancellation now stays retirement, never a user
109
+ error, for plain consumers too.
110
+
111
+ ### DevTools show per-fiber data-resource dependencies
112
+
113
+ `@bgub/fig` data stores expose `inspectDataDependencyCanonicalKeys(owner)`,
114
+ a dev-only inspection read of the canonical keys an owner's committed
115
+ `readData` subscriptions point at (returns an empty array in prod builds).
116
+
117
+ `@bgub/fig-reconciler` devtools snapshots record those keys on every fiber
118
+ as `FigDevtoolsFiberSnapshot.dataResourceCanonicalKeys`. The field is
119
+ required, so external snapshot producers must supply it (empty array when
120
+ unknown).
121
+
122
+ The `@bgub/fig-devtools` panel filters the Data section by the selected
123
+ fiber's dependencies instead of always listing the entire store, and tree
124
+ rows show a green badge with the fiber's data-resource read count next to
125
+ the blue hook-count badge. Selecting
126
+ the root still lists every entry, including those with no committed
127
+ subscriber (unclaimed preloads, hydrated-but-unread rows). Refreshing
128
+ entries no longer render a redundant `Pending` row.
129
+
130
+ ### `ensureData`: the awaitable read for code outside render
131
+
132
+ `ensureData(resource, ...args)` resolves the value a key would render with:
133
+ the cached value when the entry has one (kicking the same background
134
+ revalidation a stale `readData` does), or the in-flight load's settlement on
135
+ a cache miss. It rejects with the error `readData` would throw, follows
136
+ superseding loads and server hydrations to the authoritative value, and never
137
+ subscribes — pair it with `readData` in the component, which claims the
138
+ settled entry within the preload retention window. An awaiting caller retains
139
+ the entry, so the unclaimed-preload eviction cannot abort a load out from
140
+ under an ensure.
141
+
142
+ Available as a free function (ambient store) and on the explicit handle
143
+ (`readDataStore()`, `root.data`). This is the delegation verb for external
144
+ routers: a route loader awaits `ensureData`, the component reads with
145
+ `readData`, and the data store stays the single cache.
146
+
147
+ ### Serialized components move to the data-resource model
148
+
149
+ Serialized trees are now ordinary data resources: servers return plain
150
+ Payload streams, and clients consume them through keyed `dataResource`
151
+ instances with `payloadDataLoader`. Refresh is `refreshData`, navigation can
152
+ select a new key, and back/forward navigations reuse cached entries. Commits
153
+ wait for the incoming Payload, island modules, and stylesheet gates.
154
+
155
+ Supporting API additions: `payloadDataLoader` accepts a `prepareAssets`
156
+ override (defaults to `insertAssetResources`), and `decodePayloadStream`
157
+ accepts an `onClientReference` observer for reference metadata.
158
+
159
+ ### Compose host behavior with mixins
160
+
161
+ Core now exports `createMixin()` and resolves render-time host behavior through
162
+ the `mix` prop. Mixins may contribute host props or nested mixins while keeping
163
+ the host type and subtree fixed. Explicit host props form the baseline; mixins
164
+ run in authoring order, and later results win.
165
+
166
+ DOM event listeners now use `mix={on(type, callback)}`. Migrate
167
+ `events={[on(type, callback)]}` to `mix={on(type, callback)}`; multiple or
168
+ conditional descriptors move into `mix={[...]}` and preserve positional
169
+ listener identity.
170
+
171
+ ### Keep `useId` stable through selective hydration
172
+
173
+ `useId` now follows one canonical server/hydration tree path through Suspense
174
+ and Activity. Dehydrated boundaries snapshot that path when they claim their
175
+ server marker, then restore it when hydration resumes, so client updates that
176
+ insert or move surrounding siblings cannot renumber ids already present in the
177
+ server HTML. Suspense's private Activity wrapper is transparent to the path.
178
+
179
+ Components mounted only on the client now receive ids from a separate
180
+ `fig-C-*` namespace. Those ids remain stable for the component lifetime and
181
+ cannot collide with ids reserved by server-rendered content that has not
182
+ hydrated yet.
183
+
184
+ ### `isValidElement` has a single home on the main entry
185
+
186
+ `isValidElement` was the one runtime export with two homes: the app-facing
187
+ main entry and `@bgub/fig/internal` (grouped with the other `$$typeof`
188
+ brand predicates). It is now exported only from `@bgub/fig`; the renderer
189
+ and server packages import it from there. The internal-only predicates
190
+ (`isSuspense`, `isPortal`, ...) are unchanged.
191
+
192
+ ### Expose the payload decoder on JSR
193
+
194
+ The JSR manifest now exports `@bgub/fig/payload`, matching the npm package
195
+ and making the browser-safe payload decoder available from both registries.
196
+
197
+ ### Payload exposes rendering and decoding, not its implementation
198
+
199
+ The payload packages now present two primary operations:
200
+ `renderToPayloadStream` on the server and `decodePayloadStream` in the
201
+ browser-safe core entry. Row/model types, value encoding, codec machinery,
202
+ content-type negotiation helpers, and framework document transports are
203
+ internal implementation details.
204
+
205
+ Client references use one `resolveClientReference(reference)` seam that may
206
+ return a component synchronously or asynchronously. It replaces the separate
207
+ load, resolve, and observation callbacks; reference metadata now includes its
208
+ stream-safe assets. The unused `load` field is also removed from
209
+ `clientReference(...)` declarations.
210
+
211
+ `payloadDataLoader` keeps only `request`, `resolveClientReference`, and the
212
+ optional `prepareAssets` override. Payload encoding is fixed internally rather
213
+ than exposed as a speculative custom-codec interface.
214
+
215
+ ### Generation-lifetime loader signals and `payloadDataLoader`
216
+
217
+ Data-resource loader signals now live as long as their load's generation,
218
+ not just the pending promise: the `{ signal }` a loader receives stays
219
+ unaborted after the value lands and aborts when the generation loses
220
+ authority — a newer load supersedes it, a server push hydrates over it, the
221
+ entry evicts, or the store is disposed. A rejected load's own signal aborts
222
+ on settlement, and `invalidateData` never aborts (marking stale does not
223
+ revoke authority). Loaders that stream into their value in the background —
224
+ payload decodes filling holes — tie that work to the signal; plain fetch
225
+ loaders are unaffected.
226
+
227
+ The load context also carries an internal, generation-guarded hydration
228
+ capability (symbol-keyed; read through `@bgub/fig/internal`) that hydrates
229
+ server-pushed `data` rows through the calling store only while the load is
230
+ authoritative, skipping rows that target the loading entry's own key.
231
+
232
+ New in `@bgub/fig-dom`: `payloadDataLoader({ request,
233
+ resolveClientReference?, prepareAssets? })` adapts a payload-stream
234
+ endpoint into an ordinary data-resource loader. It validates the response
235
+ (status, body, payload codec content-type; unusable bodies are cancelled),
236
+ wires `decodePayloadStream` to the generation-lifetime signal, hydrates
237
+ `data` rows through the store capability, inserts stream-discovered assets
238
+ with `insertAssetResources` (stylesheet gates delay only dependent reveal),
239
+ and resolves with the decoded root value — so `readData(postResource, slug)`
240
+ suspends like any read and returns renderable elements while streamed holes
241
+ keep filling in the background.
242
+
243
+ ### `@bgub/fig/payload`: the browser-safe payload home and `decodePayloadStream`
244
+
245
+ The payload decoder moved from `@bgub/fig-server/payload` to a new browser-safe
246
+ `@bgub/fig/payload` entry. Browser code no longer imports the server package to
247
+ decode; fig-server's serializer and the decoder share their private row/value
248
+ format through `@bgub/fig/internal`.
249
+
250
+ New client half: `decodePayloadStream(stream, options)` returns a live
251
+ `PayloadDecode` — `value` resolves when the root row decodes while outlined
252
+ holes keep streaming in, the never-rejecting `completion` reports
253
+ complete/failed/aborted, and `abort()` idempotently rejects unresolved holes
254
+ with an internal cancellation reason. Options wire data-row hydration
255
+ (`hydrate`), unified client-reference resolution, and asset preparation with
256
+ reveal gating (`prepareAssets`).
257
+
258
+ Internally, `assets` rows now carry an optional `for` — the row id whose
259
+ reveal depends on those assets, decided at serialization scope exit so a
260
+ suspended subtree's assets gate its outlined row rather than the enclosing
261
+ tree.
262
+
263
+ Dev-time enforcement: serialized components are render-only. During
264
+ `renderToPayloadStream`, `useState`, `useActionState`, `useTransition`,
265
+ `useStableEvent`, and the effect hooks now throw in development; the read
266
+ verbs, `useMemo`, `useId`, and `useSyncExternalStore`'s `getServerSnapshot`
267
+ path stay server-safe.
268
+
269
+ ### Deliver client-reference assets outside the resolution suspension
270
+
271
+ A decoded client reference's asset declarations now attach above its
272
+ module-load and reveal-gate suspension points. A server document render
273
+ that hits a cold reference module emits the island's stylesheet with the
274
+ segment containing the reference instead of with the late fill, so
275
+ first-request asset ordering matches warm requests.
276
+
277
+ ### Move `createPortalNode` to `@bgub/fig/internal`
278
+
279
+ `createPortalNode` is the cross-package seam renderers wrap in their
280
+ container-typed `createPortal`; apps never call it directly. It now lives
281
+ on the internal entry with the other renderer protocol exports instead of
282
+ the app-facing main entry. Portal-creating apps keep using
283
+ `createPortal(children, container, key?)` from `@bgub/fig-dom`; the
284
+ `FigPortal` type stays on the main entry because it appears in public
285
+ signatures.
286
+
287
+ ### Promise-shaped payload decoder
288
+
289
+ `decodePayloadStream` now returns `Promise<FigNode>` directly — the root
290
+ value promise — instead of a `PayloadDecode` handle. Cancellation is
291
+ signal-only (`options.signal`, unchanged); the redundant `abort()` method is
292
+ gone. The `completion` promise is replaced by an `onStreamDone(result)`
293
+ decode option, called exactly once when ingestion settles as `complete`,
294
+ `failed`, or `aborted` — post-root failures that strand no pending hole
295
+ remain observable there. The callback is never awaited and its exceptions
296
+ and rejections are swallowed, so an observer — sync or async — cannot break
297
+ decode teardown or leak an unhandled rejection. The
298
+ `PayloadDecode` interface and its non-thenable caveat are deleted;
299
+ `PayloadDecodeCompletion` remains as the callback's result type.
300
+
301
+ `@bgub/fig-dom`'s `payloadDataLoader` migrates internally; its public API is
302
+ unchanged.
303
+
304
+ Also considered and declined: narrowing `ResolveClientReference` to an
305
+ opaque id. The reference's `exportName`/`ssr`/`assets` mirror the `client`
306
+ row's wire fields and are all load-bearing in framework document pipelines;
307
+ the rationale is recorded in `docs/concepts/payload.md`.
308
+
309
+ ### Promise-valued children render through Suspense
310
+
311
+ `FigNode` now accepts promises of nodes. Promise children occupy distinct,
312
+ host-transparent child slots, suspend through the nearest `Suspense`, and route
313
+ rejections or invalid resolved children through normal error handling.
314
+
315
+ HTML rendering retains exact promise children as independent streaming tasks,
316
+ while Payload uses node-validated promise rows that decode to the same child
317
+ shape. Payload-rendered async components are invoked once and retain their
318
+ component-scoped assets on the outlined row.
319
+
320
+ ### Tighten public component, loader, asset, and bind signatures
321
+
322
+ `@bgub/fig` now names the shared data loader contract as
323
+ `DataResourceLoader`, constrains lazy loaders to components, and exposes
324
+ `ComponentProps` so lazy wrappers preserve the loaded component's props
325
+ without exposing its implementation statics. Client-reference SSR
326
+ implementations stay aligned with the reference's props. Stable-event typing
327
+ now models the trailing lifecycle signal separately from caller arguments.
328
+
329
+ `meta()` descriptors now require exactly one valid metadata identity:
330
+ `charset`, `name` plus `content`, `property` plus `content`, or `http-equiv`
331
+ plus `content`. Raw meta elements that do not satisfy the same shape remain
332
+ ordinary host elements instead of entering the asset registry.
333
+
334
+ `@bgub/fig-dom` payload requests now receive the standard
335
+ `DataResourceLoadContext`, portals retain their DOM container type in the
336
+ returned `FigPortal`, and bind callbacks must return `undefined`; cleanup is
337
+ exclusively driven by their `AbortSignal`.
338
+
339
+ ### Attribute rejected payload holes to their data resource
340
+
341
+ Payload hole errors are attributed to the authoritative owning data-resource
342
+ generation. Error boundaries receive `dataResourceKeys`, and
343
+ `invalidateDataError` retires the broken fulfilled value before retrying so a
344
+ remounted boundary suspends on fresh content instead of immediately catching
345
+ the same rejected hole. `decodePayloadStream` also exposes an observational
346
+ `onHoleError` callback.
347
+
348
+ The load context's hydration capability now shares the same authority window:
349
+ a still-visible generation's `data` rows keep hydrating through a superseding
350
+ refresh's window instead of being dropped the moment the refresh starts.
351
+
352
+ ### Remove the `FigText` type alias
353
+
354
+ `FigText` was a two-member alias (`string | number`) whose only use was
355
+ as a constituent of the `FigNode` union; no signature anywhere took it
356
+ by name. The union now spells out `string | number` directly, and the
357
+ alias is gone from both the main and internal entries. Code that
358
+ referenced `FigText` should use `string | number` (or `FigNode` where
359
+ the full children type is meant).
360
+
361
+ ### Remove the `h` alias for `createElement`
362
+
363
+ `@bgub/fig` no longer exports `createElement` under the second name `h`.
364
+ Every export has one home and one name; migrate by importing
365
+ `createElement` (or alias it locally: `const h = createElement`).
366
+
367
+ ### Use one data-resource API in every environment
368
+
369
+ `dataResource` now covers shared, browser, and server-only loaders without a
370
+ second API. Server-only loaders belong behind the framework's server module
371
+ boundary; browser code uses an explicit key-only resource when it needs the
372
+ same hydrated value.
373
+
374
+ The pass-through `serverDataResource` API, `@bgub/fig/server` entry point,
375
+ `figData` Vite transform, and generated browser resource stubs are removed.
376
+
377
+ ### The targeted-refresh protocol and payload consumer are gone
378
+
379
+ The serialized-components deletion gate passed across the demo e2e suites,
380
+ so the legacy architecture is removed rather than deprecated:
381
+
382
+ - `@bgub/fig-server/payload` no longer exports `createPayloadConsumer`,
383
+ `PayloadConsumer`, `PayloadBoundary`, `PAYLOAD_BOUNDARY_HEADER`,
384
+ `PayloadFetchError`, or the consumer fetch/ingestion seam, and
385
+ `renderToPayloadStream` drops its `refreshBoundary` option.
386
+ - The wire format loses the `refresh`/`refresh-error` rows and the
387
+ `$fig:"boundary"` model: the refresh unit is the data-resource key that
388
+ delivers the payload, so refreshing is requesting the same stream again.
389
+
390
+ Replacements, all already shipping: `decodePayloadStream` in
391
+ `@bgub/fig/payload` is the client half; fig-dom's `payloadDataLoader`
392
+ delivers a serialized tree as an ordinary data resource; the freshness
393
+ verbs (`refreshData`/`invalidateData`) are the refresh story; sub-tree
394
+ refresh granularity is finer resource keys.
395
+
396
+ ### Move HTML escaping helpers to a focused subpath
397
+
398
+ `escapeAttribute`, `escapeText`, `escapeScriptText`, and `escapeScriptJson` now export from
399
+ `@bgub/fig-server/html` instead of the main `@bgub/fig-server` entry.
400
+ The dedicated subpath keeps companion-markup helpers separate from server
401
+ render entry points while preserving their exact escaping behavior. The
402
+ TanStack Start adapter now consumes these helpers, Fig's internal data-store
403
+ brand predicate, and its own storage-context API instead of duplicating them.
404
+
405
+ ### Server-route navigations commit content in one pass
406
+
407
+ Navigating to a payload server route used to commit an empty slot for one
408
+ retry beat (stretched to the full animation length when a view transition
409
+ was running) before the content revealed, because three already-settled
410
+ promises still suspended on their first render read:
411
+
412
+ - payload element gates are now tracked at creation, so a gate that settles
413
+ before its first read resolves synchronously;
414
+ - client-reference module resolutions are tracked the same way;
415
+ - pre-commit asset preparation now reveals the island hydration gate,
416
+ so navigations mount real islands instead of paying a placeholder →
417
+ reveal follow-up commit.
418
+
419
+ The route swap now lands as a single commit containing the full decoded
420
+ content, so view transitions capture the destination page with its content
421
+ already present.
422
+
423
+ ### Resolved client references keep their identity across decodes
424
+
425
+ `decodePayloadStream` used to wrap every client reference in a fresh
426
+ per-decode component, so re-decoding a surrounding payload (refreshing a
427
+ server component that contains islands) remounted every island and dropped
428
+ its client state.
429
+
430
+ References that `resolveClientReference` resolves synchronously and that carry no asset
431
+ gate now decode to the resolved component itself. The element type is
432
+ identity-stable across decodes, so a refresh of the surrounding payload
433
+ updates islands in place and their state survives. Gated or
434
+ asynchronously resolved references still decode to per-decode wrappers.
435
+
436
+ ### Preserve framework-managed asset placement
437
+
438
+ Framework adapters can now keep externally managed head and body tags in their
439
+ declared positions without exposing a DOM prop. TanStack Router uses this for
440
+ route-managed links, styles, and scripts while continuing to map title and meta
441
+ entries through Fig asset resources. Full-document hydration now ignores the
442
+ doctype and one shared marker identifies every server-owned node without a
443
+ client fiber. Declarative asset lists also gain a client commit lifecycle, so
444
+ route titles and metadata update during navigation.
445
+
446
+ ### Add Payload routes to the TanStack Start adapter
447
+
448
+ `@bgub/fig-tanstack-start/payload` now exposes `payloadResource`, which compiles
449
+ an inline render callback into a private server function and Fig-owned route
450
+ data resource. Applications supply the cache key and component tree without
451
+ authoring transport plumbing. The shared declaration can stay in one
452
+ `.payload.tsx` module; its render callback and render-only imports are omitted
453
+ from the browser build. The initial SSR response is embedded into the document
454
+ and adopted without refetching; client navigation and refresh use the same
455
+ resource request path. Payload data rows
456
+ hydrate the shared store, asset resources are retained on their owning server
457
+ segments or inserted through the browser registry, and client references retain
458
+ their resolver-defined identity. `decodePayloadStream` and `payloadDataLoader`
459
+ now expose `retainAssets` for server document renderers that need this delivery
460
+ path.
461
+
462
+ `@bgub/fig-tanstack-start/server` exposes the lower-level
463
+ `renderPayloadResponse` used by the generated TanStack server function; it
464
+ defaults its render abort signal to the incoming request, so a disconnected
465
+ client cancels the Payload render. Shell
466
+ HTML streams while outlined Suspense holes settle, and the completed initial
467
+ responses are embedded before TanStack starts full-document hydration. The Vite
468
+ adapter also publishes assets imported only by server modules into the client
469
+ build output. Fig Router links also consume TanStack's `viewTransition`
470
+ navigation option without forwarding it as an invalid attribute to the rendered
471
+ anchor, and derive active state from the resolved route instead of an in-flight
472
+ location.
473
+
474
+ ### Add the TanStack Start runtime adapter
475
+
476
+ `createDataStore` now creates a root-neutral Fig store that route loaders can
477
+ populate before a renderer exists. Server and client renderers adopt that exact
478
+ store, preserving one cache while attaching their lifecycle and scheduling.
479
+
480
+ The new TanStack Start runtime uses the store for route loading, server
481
+ rendering, Fig-owned document serialization, client deserialization, and
482
+ hydration. Route-managed head and script output maps through the Router adapter,
483
+ including Fig asset resources. The end-to-end contract verifies no initial
484
+ client refetch and exactly one reload after invalidation.
485
+
486
+ ### Share the text-separator protocol constant; small DOM cleanups
487
+
488
+ The `<!--,-->` text-separator comment the server writes between adjacent text
489
+ fibers was hardcoded independently by the server renderer and fig-dom's
490
+ hydration cursor. The comment data now lives in `@bgub/fig/internal` as
491
+ `TEXT_SEPARATOR_DATA`, next to the other streaming protocol constants, so the
492
+ two sides cannot drift. No wire change — the emitted markup is identical.
493
+
494
+ fig-dom also drops an unused internal `rootFor` helper, routes style clearing
495
+ through the shared `isEmptyPropValue` predicate, and simplifies a redundant
496
+ branch in select-value syncing. No behavior change.
497
+
498
+ ### Add typed View Transition scopes and abortable lifecycle events
499
+
500
+ `transition` and the `useTransition` starter now accept explicit transition
501
+ types, which Fig carries with the updates that reach each root and forwards to
502
+ the browser without leaking into unrelated commits.
503
+
504
+ `ViewTransition` adds one `onTransition(event, signal)` lifecycle callback for
505
+ enter, exit, share, and update phases. Events expose all participating surfaces
506
+ and the commit's transition types. Fig DOM's optional View Transition entry can
507
+ resolve those opaque surfaces into group, image-pair, old, and new pseudo
508
+ handles for animation and inspection; the signal aborts when the native
509
+ transition finishes.
510
+
511
+ ### Bound stalled view-transition waits
512
+
513
+ Transition-eligible commits and annotated streaming reveals now wait at most 60
514
+ seconds for a previous browser View Transition. If its completion promise never
515
+ settles, Fig releases the document mutex and proceeds with the latest work
516
+ instead of parking it forever.
517
+
1
518
  ## @bgub/fig@0.1.0-alpha.0 (alpha)
2
519
 
3
520
  ### Initial alpha release
package/README.md CHANGED
@@ -32,7 +32,7 @@ function App() {
32
32
 
33
33
  return (
34
34
  <main>
35
- <button events={[on("click", () => setCount((value) => value + 1))]}>
35
+ <button mix={on("click", () => setCount((value) => value + 1))}>
36
36
  Count {count}
37
37
  </button>
38
38
  <Suspense fallback={<span>Loading</span>}>
@@ -52,6 +52,10 @@ createRoot(container).render(<App />);
52
52
 
53
53
  - Elements: `createElement`, `isValidElement`, `Fragment`, and the JSX
54
54
  runtime.
55
+ - Host behavior: `createMixin(type)` creates render-time prop composers for
56
+ intrinsic elements. A host accepts one descriptor or nested arrays through
57
+ `mix`; mixins may return props or more mixins but cannot replace `children`,
58
+ `key`, or `unsafeHTML`.
55
59
  - State and memoization: `useState`, `useDeferredValue`, `useMemo`, and
56
60
  `useCallback`.
57
61
  - Stable identifiers: `useId()` generates IDs that match server render and
@@ -91,7 +95,9 @@ createRoot(container).render(<App />);
91
95
  - `ErrorBoundary` catches render and Fig effect errors. Use `onError` for
92
96
  reporting and change the boundary key to reset sticky fallback state. Data
93
97
  resource load failures report failed keys on `info.dataResourceKeys`, so
94
- recovery flows can refresh or invalidate those keys before resetting.
98
+ recovery flows can refresh or invalidate those keys before resetting. This
99
+ includes a streamed payload hole that rejects after its owning entry has
100
+ already fulfilled.
95
101
  - `transition(callback)` and `useTransition()` mark updates that may preserve
96
102
  already-revealed Suspense content while new work is pending. Updates scheduled
97
103
  inside the callback run at transition priority, including updates after an
@@ -134,7 +140,8 @@ Use `@bgub/fig-dom` for browser rendering:
134
140
  - `hydrateRoot(container, children, options?)` hydrates server HTML and reports
135
141
  recoverable mismatches with `onRecoverableError`.
136
142
  - `createPortal(children, container, key?)` renders into external DOM targets.
137
- - DOM events use `events={[on("click", (event, signal) => ...)]}`.
143
+ - DOM events use `mix={on("click", (event, signal) => ...)}`; arrays compose
144
+ multiple or conditional mixins.
138
145
  - DOM node access uses `bind={(node, signal) => ...}`.
139
146
  - Raw trusted HTML uses `unsafeHTML="<p>trusted html</p>"`.
140
147
 
@@ -169,7 +176,7 @@ function Profile({ id }: { id: string }) {
169
176
  const user = readData(userResource, id);
170
177
 
171
178
  return (
172
- <button events={[on("click", () => void refreshData(userResource, id))]}>
179
+ <button mix={on("click", () => void refreshData(userResource, id))}>
173
180
  {user.name}
174
181
  </button>
175
182
  );
@@ -261,8 +268,24 @@ await save();
261
268
  data.run(() => refreshData(userResource, "one"));
262
269
  ```
263
270
 
264
- Client roots accept `dataPartition` and `initialData` options. The partition
265
- separates a store's internal keyspace without changing public resource keys.
271
+ Framework adapters can create and populate a root-neutral store before a
272
+ renderer exists, then let exactly one root adopt that same store:
273
+
274
+ ```ts
275
+ const dataStore = createDataStore();
276
+
277
+ await dataStore.ensureData(userResource, "one");
278
+
279
+ const root = createRoot(container, { dataStore });
280
+ root.data === dataStore; // true
281
+ ```
282
+
283
+ Adoption preserves entry identity and subscriptions. The adopting root owns
284
+ the store lifetime and disposes it when the root unmounts.
285
+
286
+ Client roots accept `dataStore`, `dataPartition`, and `initialData` options.
287
+ `dataStore` is exclusive with the other two options. The partition separates a
288
+ store's internal keyspace without changing public resource keys.
266
289
  Loaders receive only their resource arguments plus `{ signal }`; app services,
267
290
  request cookies, and auth state belong in the surrounding adapter or closure,
268
291
  not in the data store.
@@ -270,22 +293,30 @@ not in the data store.
270
293
  ### Server Values And Hydration
271
294
 
272
295
  Server renderers expose fulfilled data entries with `getData()`. Pass those
273
- entries to the client root as `initialData` to hydrate by key.
296
+ entries to the client root as `initialData` to hydrate by key, or use
297
+ `createDataStore()` when an adapter needs to populate and render through the
298
+ same live store.
274
299
 
275
300
  For server-only data, split the browser-safe key resource from the server
276
301
  loader:
277
302
 
278
303
  ```ts
304
+ // user-data.ts
279
305
  import { dataResource } from "@bgub/fig";
280
- import { serverDataResource } from "@bgub/fig/server";
281
306
 
282
307
  export const userKey = (id: string) => ["user", id];
283
308
 
284
309
  export const userResource = dataResource<[string], User>({
285
310
  key: userKey,
286
311
  });
312
+ ```
313
+
314
+ ```ts
315
+ // user-data.server.ts
316
+ import { dataResource } from "@bgub/fig";
317
+ import { userKey } from "./user-data.ts";
287
318
 
288
- export const userServerResource = serverDataResource({
319
+ export const userServerResource = dataResource({
289
320
  key: userKey,
290
321
  load: async (id, { signal }) => fetchUser(id, signal),
291
322
  });
@@ -297,18 +328,18 @@ resource has no client loader, `refreshData(userResource, id)` resolves with
297
328
  `{ status: "unsupported", reason: "no-client-loader" }`; revalidation needs a
298
329
  payload or framework refresh path.
299
330
 
300
- Browser bundles need the `figData` transform from `@bgub/fig-vite` so imports of
301
- `.server.ts(x)` modules become loader-free client stubs instead of bundling the
302
- server loader. Fig Start includes this transform automatically.
331
+ Place the loader-backed definition behind the host framework's server-only
332
+ module boundary. Browser code imports only the explicit loader-free resource;
333
+ Fig joins the two definitions by key during hydration without a bundler
334
+ transform. If only server-rendered code reads the value, the single
335
+ loader-backed definition is sufficient.
303
336
 
304
337
  Direct client refreshes of server data are a framework feature, not a core
305
- data one: an endpoint has to exist to serve them. Fig Start provides
306
- `remoteDataResource` (from `@bgub/fig-start/server`), whose browser import
307
- compiles into a plain `dataResource` with a loader that calls the framework
308
- data endpoint. Without a framework, define an isomorphic `dataResource` whose
309
- loader fetches an endpoint you own. Server route payload navigations do not
310
- make an extra data request either way — data read during the payload render
311
- streams in that same payload response.
338
+ data one: an endpoint has to exist to serve them. Define an isomorphic
339
+ `dataResource` whose loader calls the framework endpoint; with TanStack Start,
340
+ compose it with `createServerFn`. Server route payload navigations do not make
341
+ an extra data request either way data read during the payload render streams
342
+ in that same payload response.
312
343
 
313
344
  ### Activity, Errors, And DevTools
314
345