@timber-js/app 0.2.0-alpha.211 → 0.2.0-alpha.213
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/agent-skill.md +10 -5
- package/dist/_chunks/{actions-Rjk4htmA.js → actions-CEootpB1.js} +49 -12
- package/dist/_chunks/actions-CEootpB1.js.map +1 -0
- package/dist/_chunks/{als-registry-DaxkVjt5.js → als-registry-BZqHCtq-.js} +2 -4
- package/dist/_chunks/als-registry-BZqHCtq-.js.map +1 -0
- package/dist/_chunks/{cache-api-DGdYfNJn.js → cache-api-LA3sBpUS.js} +5 -5
- package/dist/_chunks/{cache-api-DGdYfNJn.js.map → cache-api-LA3sBpUS.js.map} +1 -1
- package/dist/_chunks/{chains-DGX9zmg9.js → chains-BfoPFraI.js} +2 -2
- package/dist/_chunks/{chains-DGX9zmg9.js.map → chains-BfoPFraI.js.map} +1 -1
- package/dist/_chunks/{classify-QwG5rxKI.js → classify-BT66U83D.js} +2 -2
- package/dist/_chunks/{classify-QwG5rxKI.js.map → classify-BT66U83D.js.map} +1 -1
- package/dist/_chunks/{cli-check-ajNY3B2e.js → cli-check-BfQ54-UJ.js} +3 -3
- package/dist/_chunks/{cli-check-ajNY3B2e.js.map → cli-check-BfQ54-UJ.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-D2eI8jEg.js → cli-schema-sync-czh2dsLs.js} +2 -2
- package/dist/_chunks/{cli-schema-sync-D2eI8jEg.js.map → cli-schema-sync-czh2dsLs.js.map} +1 -1
- package/dist/_chunks/{client-dep-entries-2HCF09no.js → client-dep-entries-CQwpb8dI.js} +2 -2
- package/dist/_chunks/{client-dep-entries-2HCF09no.js.map → client-dep-entries-CQwpb8dI.js.map} +1 -1
- package/dist/_chunks/{convention-lint-DLmhGsRS.js → convention-lint-BEVW4EID.js} +3 -3
- package/dist/_chunks/{convention-lint-DLmhGsRS.js.map → convention-lint-BEVW4EID.js.map} +1 -1
- package/dist/_chunks/{dev-server-TFpEwm3H.js → dev-server-FKxptbnI.js} +2 -2
- package/dist/_chunks/{dev-server-TFpEwm3H.js.map → dev-server-FKxptbnI.js.map} +1 -1
- package/dist/_chunks/{error-boundary-DsNScGRM.js → error-boundary-9g_Lb2na.js} +3 -3
- package/dist/_chunks/{error-boundary-DsNScGRM.js.map → error-boundary-9g_Lb2na.js.map} +1 -1
- package/dist/_chunks/{json-lossy-check-CVuRs2hG.js → json-lossy-check-C8zBY2uZ.js} +2 -2
- package/dist/_chunks/{json-lossy-check-CVuRs2hG.js.map → json-lossy-check-C8zBY2uZ.js.map} +1 -1
- package/dist/_chunks/{live-graph-cNuWMYQI.js → live-graph-C_4v-fHv.js} +4 -4
- package/dist/_chunks/{live-graph-cNuWMYQI.js.map → live-graph-C_4v-fHv.js.map} +1 -1
- package/dist/_chunks/{logger-BP0LN6vP.js → logger-CbLdcy-W.js} +2 -2
- package/dist/_chunks/{logger-BP0LN6vP.js.map → logger-CbLdcy-W.js.map} +1 -1
- package/dist/_chunks/{poison-scan-CfQ3unZR.js → poison-scan-C92liMAr.js} +2 -2
- package/dist/_chunks/{poison-scan-CfQ3unZR.js.map → poison-scan-C92liMAr.js.map} +1 -1
- package/dist/_chunks/{scanner-DmqdxzbW.js → scanner-CQt12vE2.js} +2 -2
- package/dist/_chunks/{scanner-DmqdxzbW.js.map → scanner-CQt12vE2.js.map} +1 -1
- package/dist/_chunks/{sizeof-BM1409x2.js → sizeof-QPE5nd3u.js} +2 -2
- package/dist/_chunks/{sizeof-BM1409x2.js.map → sizeof-QPE5nd3u.js.map} +1 -1
- package/dist/_chunks/{walkers-Czu2jXFq.js → walkers-DAT4avhZ.js} +3 -3
- package/dist/_chunks/{walkers-Czu2jXFq.js.map → walkers-DAT4avhZ.js.map} +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +1 -1
- package/dist/analyze/crawl-entry.js +3 -3
- package/dist/analyze/graph-command.js +2 -2
- package/dist/cache/index.js +2 -2
- package/dist/cache/stores/memory.js +1 -1
- package/dist/cdn/workers-cache-purge.js +1 -1
- package/dist/cli.js +3 -3
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/action-queue.d.ts +1 -0
- package/dist/client/browser-entry/action-queue.d.ts.map +1 -1
- package/dist/client/browser-entry/form-state.d.ts +22 -0
- package/dist/client/browser-entry/form-state.d.ts.map +1 -0
- package/dist/client/browser-entry/hydrate.d.ts +9 -1
- package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
- package/dist/client/browser-entry/index.d.ts +2 -0
- package/dist/client/browser-entry/index.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -1
- package/dist/client/form.d.ts +17 -64
- package/dist/client/form.d.ts.map +1 -1
- package/dist/client/index.d.ts +3 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +111 -51
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.js +33 -32
- package/dist/client/internal.js.map +1 -1
- package/dist/client/navigation-api.d.ts +25 -63
- package/dist/client/navigation-api.d.ts.map +1 -1
- package/dist/client/navigation-transition.d.ts +11 -2
- package/dist/client/navigation-transition.d.ts.map +1 -1
- package/dist/client/router-effects.d.ts +7 -3
- package/dist/client/router-effects.d.ts.map +1 -1
- package/dist/client/router-lifecycle.d.ts +17 -11
- package/dist/client/router-lifecycle.d.ts.map +1 -1
- package/dist/client/router-pipeline.d.ts +3 -2
- package/dist/client/router-pipeline.d.ts.map +1 -1
- package/dist/client/router-types.d.ts +24 -44
- package/dist/client/router-types.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/use-form-field.d.ts +39 -0
- package/dist/client/use-form-field.d.ts.map +1 -0
- package/dist/config-types.d.ts +2 -1
- package/dist/config-types.d.ts.map +1 -1
- package/dist/index.js +6 -7
- package/dist/index.js.map +1 -1
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/rsc-runtime/rsc.d.ts +1 -1
- package/dist/rsc-runtime/rsc.d.ts.map +1 -1
- package/dist/server/action-client.d.ts +20 -7
- package/dist/server/action-client.d.ts.map +1 -1
- package/dist/server/action-handler.d.ts +27 -8
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +22 -2
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/client-error-message.d.ts +12 -0
- package/dist/server/client-error-message.d.ts.map +1 -0
- package/dist/server/flight-scripts.d.ts +9 -0
- package/dist/server/flight-scripts.d.ts.map +1 -1
- package/dist/server/form-data.d.ts +13 -4
- package/dist/server/form-data.d.ts.map +1 -1
- package/dist/server/form-state-flight.d.ts +32 -0
- package/dist/server/form-state-flight.d.ts.map +1 -0
- package/dist/server/index.d.ts +0 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +41 -60
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.js +10 -6
- package/dist/server/internal.js.map +1 -1
- package/dist/server/logger.d.ts +1 -0
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts +20 -6
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/request-context.d.ts +27 -2
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/rsc-entry/action-dispatcher.d.ts +6 -5
- package/dist/server/rsc-entry/action-dispatcher.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts +2 -0
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts +6 -0
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/ssr-bridge-types.d.ts +10 -0
- package/dist/server/ssr-bridge-types.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/ssr-form-state.d.ts +30 -0
- package/dist/server/ssr-form-state.d.ts.map +1 -0
- package/dist/server/ssr-render.d.ts +3 -0
- package/dist/server/ssr-render.d.ts.map +1 -1
- package/dist/shared/form-state-flight.d.ts +36 -0
- package/dist/shared/form-state-flight.d.ts.map +1 -0
- package/docs/api/31-api-client.mdx +31 -3
- package/docs/api/34-api-config.mdx +1 -1
- package/docs/learn/08-forms-and-actions.mdx +116 -36
- package/package.json +1 -1
- package/src/client/browser-entry/action-dispatch.ts +104 -24
- package/src/client/browser-entry/action-queue.ts +1 -1
- package/src/client/browser-entry/form-state.ts +48 -0
- package/src/client/browser-entry/hydrate.ts +10 -1
- package/src/client/browser-entry/index.ts +25 -7
- package/src/client/browser-entry/router-init.ts +7 -20
- package/src/client/form.tsx +33 -98
- package/src/client/index.ts +3 -2
- package/src/client/navigation-api.ts +47 -173
- package/src/client/navigation-transition.ts +15 -4
- package/src/client/router-effects.ts +8 -4
- package/src/client/router-lifecycle.ts +40 -20
- package/src/client/router-pipeline.ts +10 -12
- package/src/client/router-types.ts +24 -48
- package/src/client/router.ts +49 -56
- package/src/client/use-form-field.ts +132 -0
- package/src/config-types.ts +2 -1
- package/src/plugins/shims.ts +0 -2
- package/src/rsc-runtime/rsc.ts +3 -0
- package/src/rsc-runtime/vendor-types.d.ts +14 -0
- package/src/server/action-client.ts +77 -64
- package/src/server/action-handler.ts +133 -95
- package/src/server/als-registry.ts +23 -9
- package/src/server/client-error-message.ts +18 -0
- package/src/server/flight-scripts.ts +13 -0
- package/src/server/form-data.ts +62 -10
- package/src/server/form-state-flight.ts +67 -0
- package/src/server/index.ts +0 -4
- package/src/server/logger.ts +6 -1
- package/src/server/pipeline.ts +27 -8
- package/src/server/request-context.ts +39 -2
- package/src/server/route-element-builder.ts +12 -1
- package/src/server/rsc-entry/action-dispatcher.ts +40 -34
- package/src/server/rsc-entry/error-renderer.ts +2 -1
- package/src/server/rsc-entry/index.ts +1 -0
- package/src/server/rsc-entry/render-route.ts +16 -0
- package/src/server/rsc-entry/rsc-stream.ts +20 -11
- package/src/server/rsc-entry/ssr-renderer.ts +11 -0
- package/src/server/ssr-bridge-types.ts +10 -0
- package/src/server/ssr-entry.ts +16 -2
- package/src/server/ssr-form-state.ts +58 -0
- package/src/server/ssr-render.ts +5 -0
- package/src/shared/form-state-flight.ts +74 -0
- package/dist/_chunks/actions-Rjk4htmA.js.map +0 -1
- package/dist/_chunks/als-registry-DaxkVjt5.js.map +0 -1
- package/dist/server/form-flash.d.ts +0 -78
- package/dist/server/form-flash.d.ts.map +0 -1
- package/src/server/form-flash.ts +0 -89
|
@@ -35,23 +35,13 @@ export interface NavigationEpoch {
|
|
|
35
35
|
readonly idle: boolean;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/** The subset of `RouterDeps` the navigation lifecycle needs. */
|
|
39
|
-
export interface NavigationLifecycleDeps {
|
|
40
|
-
/**
|
|
41
|
-
* Signal that a router-initiated navigation has completed. Resolves the
|
|
42
|
-
* deferred promise that ties the browser's native loading state to the
|
|
43
|
-
* navigation lifecycle.
|
|
44
|
-
*/
|
|
45
|
-
completeRouterNavigation?: () => void;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
38
|
export interface NavigationLifecycle {
|
|
49
39
|
/** The owner that holds the router right now, or null when idle. */
|
|
50
40
|
currentOwner: () => RenderOwner | null;
|
|
51
41
|
/**
|
|
52
42
|
* Take ownership of the router, superseding whatever held it. Returns
|
|
53
43
|
* a fresh `RenderOwner` for the new navigation. See the implementation
|
|
54
|
-
* for the
|
|
44
|
+
* for the two parts of superseding.
|
|
55
45
|
*/
|
|
56
46
|
createNavOwner: (
|
|
57
47
|
kind: 'navigation' | 'revalidation',
|
|
@@ -99,6 +89,21 @@ export interface NavigationLifecycle {
|
|
|
99
89
|
* replaces an earlier one (TIM-1474).
|
|
100
90
|
*/
|
|
101
91
|
runWhenIdle: (task: () => void) => void;
|
|
92
|
+
/**
|
|
93
|
+
* Subscribe to the router becoming idle (pending false AND commit done):
|
|
94
|
+
* `listener` runs on every such transition until unsubscribed. Unlike
|
|
95
|
+
* `runWhenIdle`, any number of listeners may wait.
|
|
96
|
+
*/
|
|
97
|
+
onIdle: (listener: () => void) => () => void;
|
|
98
|
+
/**
|
|
99
|
+
* Subscribe to React committing a navigation's tree — the tree on screen
|
|
100
|
+
* has changed, whether or not its stream has finished decoding (so the
|
|
101
|
+
* router may still be pending). Runs on every commit until unsubscribed,
|
|
102
|
+
* with the committing navigation's `epoch().seq`: a tree handed to React
|
|
103
|
+
* before a later navigation started can still commit after it, and the
|
|
104
|
+
* sequence says which one it was.
|
|
105
|
+
*/
|
|
106
|
+
onTreeCommit: (listener: (seq: number) => void) => () => void;
|
|
102
107
|
/**
|
|
103
108
|
* Settle any handed-off-but-uncommitted owners as superseded, then flush
|
|
104
109
|
* the idle task if the router is now idle. Called by `syncShallowSearch`
|
|
@@ -119,7 +124,7 @@ function isAbortError(error: unknown): boolean {
|
|
|
119
124
|
return false;
|
|
120
125
|
}
|
|
121
126
|
|
|
122
|
-
export function createNavigationLifecycle(
|
|
127
|
+
export function createNavigationLifecycle(): NavigationLifecycle {
|
|
123
128
|
// The single ownership slot. One navigation at a time.
|
|
124
129
|
let current: RenderOwner | null = null;
|
|
125
130
|
|
|
@@ -135,6 +140,10 @@ export function createNavigationLifecycle(deps: NavigationLifecycleDeps): Naviga
|
|
|
135
140
|
|
|
136
141
|
let navigationSeq = 0;
|
|
137
142
|
let idleTask: (() => void) | null = null;
|
|
143
|
+
const idleListeners = new Set<() => void>();
|
|
144
|
+
const treeCommitListeners = new Set<(seq: number) => void>();
|
|
145
|
+
// The navigationSeq each owner ran under, for tree-commit listeners.
|
|
146
|
+
const ownerSeq = new WeakMap<RenderOwner, number>();
|
|
138
147
|
|
|
139
148
|
/**
|
|
140
149
|
* Whether a handed-off tree has not yet committed. Derived from the slot:
|
|
@@ -167,17 +176,13 @@ export function createNavigationLifecycle(deps: NavigationLifecycleDeps): Naviga
|
|
|
167
176
|
* in-flight navigation. Optionally links to an external signal (e.g.,
|
|
168
177
|
* from the Navigation API's NavigateEvent.signal).
|
|
169
178
|
*
|
|
170
|
-
* Superseding is one operation with
|
|
179
|
+
* Superseding is one operation with two parts:
|
|
171
180
|
* 1. Abort the previous navigation's fetch — UNLESS its tree is the one on
|
|
172
181
|
* screen, in which case tearing the stream down would error the page the
|
|
173
182
|
* user is currently looking at. See `RenderOwner.handedOff`.
|
|
174
183
|
* 2. Settle the previous owner as 'superseded' so its transition detects
|
|
175
184
|
* it lost and never hands a stale tree to React. This replaces both
|
|
176
185
|
* `supersedeNavigationTransitions()` and the transition counter bump.
|
|
177
|
-
* 3. Resolve its Navigation API deferred — the superseded navigation's
|
|
178
|
-
* finally block is staleness-guarded (see TIM-1034) and no longer
|
|
179
|
-
* cleans up after itself, so the browser's native loading state for
|
|
180
|
-
* the dead navigation is cleared here.
|
|
181
186
|
*/
|
|
182
187
|
function createNavOwner(
|
|
183
188
|
kind: 'navigation' | 'revalidation',
|
|
@@ -186,7 +191,6 @@ export function createNavigationLifecycle(deps: NavigationLifecycleDeps): Naviga
|
|
|
186
191
|
if (current) {
|
|
187
192
|
abortUnlessHandedOff(current);
|
|
188
193
|
current.settle('superseded');
|
|
189
|
-
deps.completeRouterNavigation?.();
|
|
190
194
|
}
|
|
191
195
|
// A handed-off owner whose runNavigation has already finished but whose
|
|
192
196
|
// tree hasn't committed yet. Settle it so its onCommit fires.
|
|
@@ -196,6 +200,7 @@ export function createNavigationLifecycle(deps: NavigationLifecycleDeps): Naviga
|
|
|
196
200
|
}
|
|
197
201
|
navigationSeq += 1;
|
|
198
202
|
const owner = createRenderOwner(kind);
|
|
203
|
+
ownerSeq.set(owner, navigationSeq);
|
|
199
204
|
current = owner;
|
|
200
205
|
|
|
201
206
|
// If an external signal is provided (e.g., Navigation API),
|
|
@@ -264,14 +269,16 @@ export function createNavigationLifecycle(deps: NavigationLifecycleDeps): Naviga
|
|
|
264
269
|
if (current === owner) {
|
|
265
270
|
current = null;
|
|
266
271
|
setPending(false);
|
|
267
|
-
deps.completeRouterNavigation?.();
|
|
268
272
|
flushIdleTask();
|
|
269
273
|
}
|
|
270
274
|
}
|
|
271
275
|
}
|
|
272
276
|
|
|
277
|
+
// Every place the router may have just become idle calls this.
|
|
273
278
|
function flushIdleTask(): void {
|
|
274
|
-
if (routerPhase.phase !== 'idle' || hasUncommittedNav()
|
|
279
|
+
if (routerPhase.phase !== 'idle' || hasUncommittedNav()) return;
|
|
280
|
+
for (const listener of [...idleListeners]) listener();
|
|
281
|
+
if (!idleTask) return;
|
|
275
282
|
const task = idleTask;
|
|
276
283
|
idleTask = null;
|
|
277
284
|
task();
|
|
@@ -307,10 +314,13 @@ export function createNavigationLifecycle(deps: NavigationLifecycleDeps): Naviga
|
|
|
307
314
|
if (pendingCommit === owner) {
|
|
308
315
|
pendingCommit = null;
|
|
309
316
|
}
|
|
317
|
+
const seq = ownerSeq.get(owner) ?? navigationSeq;
|
|
318
|
+
for (const listener of [...treeCommitListeners]) listener(seq);
|
|
310
319
|
flushIdleTask();
|
|
311
320
|
},
|
|
312
321
|
|
|
313
322
|
placeRevalidationOwner(owner: RenderOwner): void {
|
|
323
|
+
ownerSeq.set(owner, navigationSeq);
|
|
314
324
|
current = owner;
|
|
315
325
|
},
|
|
316
326
|
|
|
@@ -340,6 +350,16 @@ export function createNavigationLifecycle(deps: NavigationLifecycleDeps): Naviga
|
|
|
340
350
|
idleTask = task;
|
|
341
351
|
},
|
|
342
352
|
|
|
353
|
+
onIdle(listener) {
|
|
354
|
+
idleListeners.add(listener);
|
|
355
|
+
return () => idleListeners.delete(listener);
|
|
356
|
+
},
|
|
357
|
+
|
|
358
|
+
onTreeCommit(listener) {
|
|
359
|
+
treeCommitListeners.add(listener);
|
|
360
|
+
return () => treeCommitListeners.delete(listener);
|
|
361
|
+
},
|
|
362
|
+
|
|
343
363
|
settleHandoffs(): void {
|
|
344
364
|
if (pendingCommit) {
|
|
345
365
|
pendingCommit.settle('superseded');
|
|
@@ -55,7 +55,6 @@ export interface NavigationFetchOptions {
|
|
|
55
55
|
replace: boolean;
|
|
56
56
|
commitUrl?: string;
|
|
57
57
|
signal?: AbortSignal;
|
|
58
|
-
skipHistory?: boolean;
|
|
59
58
|
departingUrl?: string;
|
|
60
59
|
}
|
|
61
60
|
|
|
@@ -85,7 +84,9 @@ export interface NavigationPipeline {
|
|
|
85
84
|
types: readonly string[],
|
|
86
85
|
perform: () => Promise<NavigationPayload>,
|
|
87
86
|
/** See `NavigationOptions.onCommit`. */
|
|
88
|
-
onCommit?: (outcome: CommitOutcome) => void
|
|
87
|
+
onCommit?: (outcome: CommitOutcome) => void,
|
|
88
|
+
/** See `NavigationOptions.onHandOff`. */
|
|
89
|
+
onHandOff?: () => void
|
|
89
90
|
) => Promise<void>;
|
|
90
91
|
}
|
|
91
92
|
|
|
@@ -187,7 +188,8 @@ export function createNavigationPipeline({
|
|
|
187
188
|
owner: RenderOwner,
|
|
188
189
|
types: readonly string[],
|
|
189
190
|
perform: () => Promise<NavigationPayload>,
|
|
190
|
-
onCommit?: (outcome: CommitOutcome) => void
|
|
191
|
+
onCommit?: (outcome: CommitOutcome) => void,
|
|
192
|
+
onHandOff?: () => void
|
|
191
193
|
): Promise<void> {
|
|
192
194
|
// Record that THIS navigation's payload has reached React, at the moment
|
|
193
195
|
// the tree is built and handed back for `navigateTransition` to give to
|
|
@@ -261,7 +263,8 @@ export function createNavigationPipeline({
|
|
|
261
263
|
commit: commitAndForget(result.commit),
|
|
262
264
|
};
|
|
263
265
|
},
|
|
264
|
-
onCommit
|
|
266
|
+
onCommit,
|
|
267
|
+
onHandOff
|
|
265
268
|
);
|
|
266
269
|
}
|
|
267
270
|
|
|
@@ -388,9 +391,9 @@ export function createNavigationPipeline({
|
|
|
388
391
|
result = await fetchRscPayload(url, deps, freshStateTree, currentUrl, options.signal);
|
|
389
392
|
}
|
|
390
393
|
|
|
391
|
-
// Update the browser history
|
|
392
|
-
//
|
|
393
|
-
// The committed URL keeps the #fragment (commitUrl) even though the
|
|
394
|
+
// Update the browser history. This is the only place a push/replace
|
|
395
|
+
// navigation moves the address bar, with or without the Navigation API
|
|
396
|
+
// (its listener leaves same-document pushState alone). The committed URL keeps the #fragment (commitUrl) even though the
|
|
394
397
|
// fetch/history-stack URL is hash-less (TIM-1035).
|
|
395
398
|
//
|
|
396
399
|
// Deferred with the rest of the commit: the address bar is navigation
|
|
@@ -400,17 +403,12 @@ export function createNavigationPipeline({
|
|
|
400
403
|
// response that won the race anyway re-pushed the abandoned URL
|
|
401
404
|
// (TIM-1022's failure mode, made unconditional by TIM-1301).
|
|
402
405
|
const commitHistoryUrl = (): void => {
|
|
403
|
-
if (options.skipHistory) return;
|
|
404
406
|
const commitUrl = options.commitUrl ?? url;
|
|
405
|
-
// Set the router-navigating flag so the Navigation API's navigate
|
|
406
|
-
// listener doesn't double-intercept this pushState/replaceState.
|
|
407
|
-
deps.setRouterNavigating?.(true);
|
|
408
407
|
if (options.replace) {
|
|
409
408
|
deps.replaceState({ timber: true, scrollY: 0 }, '', commitUrl);
|
|
410
409
|
} else {
|
|
411
410
|
deps.pushState({ timber: true, scrollY: 0 }, '', commitUrl);
|
|
412
411
|
}
|
|
413
|
-
deps.setRouterNavigating?.(false);
|
|
414
412
|
};
|
|
415
413
|
|
|
416
414
|
const params = await result.params;
|
|
@@ -90,24 +90,14 @@ export interface NavigationOptions {
|
|
|
90
90
|
*/
|
|
91
91
|
onCommit?: (outcome: CommitOutcome) => void;
|
|
92
92
|
/**
|
|
93
|
-
* @internal
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
93
|
+
* @internal Runs once, synchronously after this navigation's tree is handed
|
|
94
|
+
* to React (its transition scheduled) and before React can commit it. Not
|
|
95
|
+
* at all if the navigation is superseded or fails first; the returned
|
|
96
|
+
* promise settles then. A server action that redirects settles its
|
|
97
|
+
* `useActionState` here, so React commits the action's result and the
|
|
98
|
+
* destination together (TIM-1573).
|
|
97
99
|
*/
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* @internal Skip pushState/replaceState — the Navigation API has already
|
|
101
|
-
* updated the URL via event.intercept(). Used for external navigations
|
|
102
|
-
* intercepted by the navigate event handler.
|
|
103
|
-
*/
|
|
104
|
-
_skipHistory?: boolean;
|
|
105
|
-
/**
|
|
106
|
-
* @internal The URL the user is navigating FROM, captured before the
|
|
107
|
-
* Navigation API commits the destination. Sent as X-Timber-URL for
|
|
108
|
-
* slot skip comparison on the server (TIM-1232).
|
|
109
|
-
*/
|
|
110
|
-
_departingUrl?: string;
|
|
100
|
+
onHandOff?: () => void;
|
|
111
101
|
}
|
|
112
102
|
|
|
113
103
|
/**
|
|
@@ -174,37 +164,17 @@ export interface RouterDeps {
|
|
|
174
164
|
) => unknown
|
|
175
165
|
) => Promise<TransitionResult<unknown>>,
|
|
176
166
|
/** See `NavigationOptions.onCommit`; forwarded to `navigateTransition`. */
|
|
177
|
-
onCommit?: (outcome: CommitOutcome) => void
|
|
167
|
+
onCommit?: (outcome: CommitOutcome) => void,
|
|
168
|
+
/** See `NavigationOptions.onHandOff`; forwarded to `navigateTransition`. */
|
|
169
|
+
onHandOff?: () => void
|
|
178
170
|
) => Promise<void>;
|
|
179
171
|
|
|
180
|
-
/**
|
|
181
|
-
* Whether the Navigation API is active and handling traversals.
|
|
182
|
-
* When true, the popstate handler is a no-op — the Navigation API's
|
|
183
|
-
* navigate event covers back/forward button presses.
|
|
184
|
-
*/
|
|
185
|
-
navigationApiActive?: boolean;
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Called around pushState/replaceState to set a flag that prevents
|
|
189
|
-
* the Navigation API's navigate listener from double-handling
|
|
190
|
-
* router-initiated navigations.
|
|
191
|
-
*/
|
|
192
|
-
setRouterNavigating?: (value: boolean) => void;
|
|
193
|
-
|
|
194
172
|
/**
|
|
195
173
|
* Save scroll position via the Navigation API's per-entry state.
|
|
196
174
|
* When provided, used instead of history.replaceState for scroll storage.
|
|
197
175
|
*/
|
|
198
176
|
saveNavigationEntryScroll?: (scrollY: number) => void;
|
|
199
177
|
|
|
200
|
-
/**
|
|
201
|
-
* Signal that a router-initiated navigation has completed. Resolves the
|
|
202
|
-
* deferred promise that ties the browser's native loading state to the
|
|
203
|
-
* navigation lifecycle. Called in the finally block of navigate/refresh,
|
|
204
|
-
* the same place `setPending(false)` clears the TopLoader.
|
|
205
|
-
*/
|
|
206
|
-
completeRouterNavigation?: () => void;
|
|
207
|
-
|
|
208
178
|
/**
|
|
209
179
|
* Get the current unwrapped RSC payload element. Used for partial
|
|
210
180
|
* navigation: the router re-wraps the same element with new
|
|
@@ -212,14 +182,6 @@ export interface RouterDeps {
|
|
|
212
182
|
*/
|
|
213
183
|
_getCurrentPayload?: () => unknown;
|
|
214
184
|
|
|
215
|
-
/**
|
|
216
|
-
* Initiate a navigation via the Navigation API (`navigation.navigate()`).
|
|
217
|
-
* Fires the navigate event BEFORE committing the URL, allowing Chrome
|
|
218
|
-
* to show its native loading indicator. Falls back to pushState when
|
|
219
|
-
* unavailable.
|
|
220
|
-
*/
|
|
221
|
-
navigationNavigate?: (url: string, replace: boolean) => void;
|
|
222
|
-
|
|
223
185
|
/**
|
|
224
186
|
* Scroll the element matching a URL #fragment into view. Returns true
|
|
225
187
|
* when a matching element was found and scrolled. When absent or false,
|
|
@@ -301,6 +263,20 @@ export interface RouterInstance {
|
|
|
301
263
|
* router-idle are true (TIM-1474).
|
|
302
264
|
*/
|
|
303
265
|
runWhenIdle(task: () => void): void;
|
|
266
|
+
/**
|
|
267
|
+
* Subscribe to the router becoming idle — no navigation pending and no
|
|
268
|
+
* handed-off tree waiting to commit. Runs on every such transition until
|
|
269
|
+
* the returned function unsubscribes; any number of listeners may wait.
|
|
270
|
+
*/
|
|
271
|
+
onIdle(listener: () => void): () => void;
|
|
272
|
+
/**
|
|
273
|
+
* Subscribe to React committing a navigation's tree: the page on screen
|
|
274
|
+
* has changed, even if its stream is still decoding and the router is
|
|
275
|
+
* still pending. Runs on every commit until unsubscribed, with the
|
|
276
|
+
* committing navigation's `epoch().seq` — a tree handed to React before a
|
|
277
|
+
* later navigation started can still commit after it.
|
|
278
|
+
*/
|
|
279
|
+
onTreeCommit(listener: (seq: number) => void): () => void;
|
|
304
280
|
/**
|
|
305
281
|
* Settle any handed-off-but-uncommitted owners as superseded, then flush
|
|
306
282
|
* idle tasks. Called by `syncShallowSearch` when a shallow URL update
|
package/src/client/router.ts
CHANGED
|
@@ -59,7 +59,7 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
59
59
|
|
|
60
60
|
// Ownership of the router: who is navigating, whose fetch may be cut, and
|
|
61
61
|
// the pending store TopLoader subscribes to. See router-lifecycle.ts.
|
|
62
|
-
const lifecycle = createNavigationLifecycle(
|
|
62
|
+
const lifecycle = createNavigationLifecycle();
|
|
63
63
|
const {
|
|
64
64
|
currentOwner,
|
|
65
65
|
createNavOwner,
|
|
@@ -102,14 +102,13 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
102
102
|
currentOwner,
|
|
103
103
|
leaveSpaIfOwned,
|
|
104
104
|
// Hoisted — `navigate` is a function declaration below.
|
|
105
|
-
navigate: (url, types) =>
|
|
105
|
+
navigate: (url, types, onHandOff) =>
|
|
106
|
+
navigate(url, { replace: true, _renderTypes: types, onHandOff }),
|
|
106
107
|
});
|
|
107
108
|
|
|
108
109
|
async function navigate(url: string, options: NavigationOptions = {}): Promise<void> {
|
|
109
110
|
const scroll = options.scroll !== false;
|
|
110
111
|
const replace = options.replace === true;
|
|
111
|
-
const externalSignal = options._signal as AbortSignal | undefined;
|
|
112
|
-
const skipHistory = options._skipHistory === true;
|
|
113
112
|
// A push, a replace, and a server action redirect all move the user
|
|
114
113
|
// forward; the caller's own types ride beside it. A redirect this router
|
|
115
114
|
// follows keeps the types of the render it interrupted instead.
|
|
@@ -127,11 +126,9 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
127
126
|
const hash = hashIndex === -1 ? '' : url.slice(hashIndex);
|
|
128
127
|
const fetchUrl = hashIndex === -1 ? url : url.slice(0, hashIndex);
|
|
129
128
|
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
// for non-Navigation-API navigations (TIM-1232).
|
|
134
|
-
const departingUrl = options._departingUrl ?? deps.getCurrentUrl();
|
|
129
|
+
// The address bar moves only when the destination commits, so it still
|
|
130
|
+
// names the page being left (TIM-1232).
|
|
131
|
+
const departingUrl = deps.getCurrentUrl();
|
|
135
132
|
|
|
136
133
|
// Capture the departing page's scroll position for scroll={false} preservation.
|
|
137
134
|
const currentScrollY = deps.getScrollY();
|
|
@@ -155,57 +152,51 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
155
152
|
await leaveSpaSuperseding(url, departingUrl);
|
|
156
153
|
}
|
|
157
154
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
155
|
+
await runNavigation(url, async (owner) => {
|
|
156
|
+
try {
|
|
157
|
+
await renderViaTransition(
|
|
158
|
+
fetchUrl,
|
|
159
|
+
owner,
|
|
160
|
+
types,
|
|
161
|
+
() =>
|
|
162
|
+
performNavigationFetch(fetchUrl, {
|
|
163
|
+
replace,
|
|
164
|
+
commitUrl: url,
|
|
165
|
+
signal: owner.fetchAbort.signal,
|
|
166
|
+
departingUrl,
|
|
167
|
+
}),
|
|
168
|
+
options.onCommit,
|
|
169
|
+
options.onHandOff
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Scroll-to-top on forward navigation, scroll to the #fragment target
|
|
173
|
+
// when the URL has one, or restore captured position for scroll={false}.
|
|
174
|
+
if (scroll && hash) {
|
|
175
|
+
scrollToHashAfterPaint(hash);
|
|
176
|
+
} else {
|
|
177
|
+
restoreScrollAfterPaint(scroll ? 0 : currentScrollY);
|
|
172
178
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
179
|
+
} catch (error) {
|
|
180
|
+
// `url` is where the user asked to go and `departingUrl` is where
|
|
181
|
+
// they were: a failure here finishes the click as a full document
|
|
182
|
+
// load of the destination, fragment included (TIM-1234). Reloading
|
|
183
|
+
// instead would rebuild the page they were *leaving* and discard
|
|
184
|
+
// the click (TIM-1275).
|
|
185
|
+
if (
|
|
186
|
+
await recoverFromNavigationError(
|
|
187
|
+
error,
|
|
177
188
|
owner,
|
|
189
|
+
url,
|
|
190
|
+
departingUrl,
|
|
178
191
|
types,
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
signal: owner.fetchAbort.signal,
|
|
184
|
-
skipHistory: effectiveSkipHistory,
|
|
185
|
-
departingUrl,
|
|
186
|
-
}),
|
|
187
|
-
options.onCommit
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
// Scroll-to-top on forward navigation, scroll to the #fragment target
|
|
191
|
-
// when the URL has one, or restore captured position for scroll={false}.
|
|
192
|
-
if (scroll && hash) {
|
|
193
|
-
scrollToHashAfterPaint(hash);
|
|
194
|
-
} else {
|
|
195
|
-
restoreScrollAfterPaint(scroll ? 0 : currentScrollY);
|
|
196
|
-
}
|
|
197
|
-
} catch (error) {
|
|
198
|
-
// `url` is where the user asked to go and `departingUrl` is where
|
|
199
|
-
// they were: a failure here finishes the click as a full document
|
|
200
|
-
// load of the destination, fragment included (TIM-1234). Reloading
|
|
201
|
-
// instead would rebuild the page they were *leaving* and discard
|
|
202
|
-
// the click (TIM-1275).
|
|
203
|
-
if (await recoverFromNavigationError(error, owner, url, departingUrl, types)) return;
|
|
204
|
-
throw error;
|
|
192
|
+
options.onHandOff
|
|
193
|
+
)
|
|
194
|
+
) {
|
|
195
|
+
return;
|
|
205
196
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
);
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
});
|
|
209
200
|
}
|
|
210
201
|
|
|
211
202
|
/**
|
|
@@ -523,6 +514,8 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
523
514
|
return outcome === 'committed';
|
|
524
515
|
},
|
|
525
516
|
runWhenIdle: (task: () => void) => lifecycle.runWhenIdle(task),
|
|
517
|
+
onIdle: (listener) => lifecycle.onIdle(listener),
|
|
518
|
+
onTreeCommit: (listener) => lifecycle.onTreeCommit(listener),
|
|
526
519
|
settleHandoffs: () => lifecycle.settleHandoffs(),
|
|
527
520
|
suppressSegmentReuse(): void {
|
|
528
521
|
// TIM-1466: uses invalidateReuse() instead of clear() to preserve
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useFormField — state for a form field that needs JavaScript (a combobox, a
|
|
3
|
+
* chip list, a row list), made to behave like a native uncontrolled input.
|
|
4
|
+
*
|
|
5
|
+
* React resets a form after its action settles, so a native field with a
|
|
6
|
+
* `defaultValue` lands on whatever default the page now renders: the action's
|
|
7
|
+
* `submittedValues`, or fresh page data after a redirect. State held in
|
|
8
|
+
* `useState` misses that reset and keeps showing the pre-submit edit. This
|
|
9
|
+
* hook shows `defaultValue` until the user edits, and drops the edit when the
|
|
10
|
+
* form resets, so it lands where the native fields do.
|
|
11
|
+
*
|
|
12
|
+
* See design/08-forms-and-actions.md §"The Form Model".
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
useCallback,
|
|
17
|
+
useEffect,
|
|
18
|
+
useLayoutEffect,
|
|
19
|
+
useRef,
|
|
20
|
+
useState,
|
|
21
|
+
type Dispatch,
|
|
22
|
+
type RefCallback,
|
|
23
|
+
type SetStateAction,
|
|
24
|
+
} from 'react';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* State for a field that follows its form's reset.
|
|
28
|
+
*
|
|
29
|
+
* Returns `[value, setValue, ref]`. Attach `ref` to any element inside the
|
|
30
|
+
* `<form>`, and render the value into the form data yourself (usually a
|
|
31
|
+
* hidden input). `value` is `defaultValue` until `setValue` is called, and is
|
|
32
|
+
* `defaultValue` again after the form resets, unless the form cancels its
|
|
33
|
+
* `reset` event. An edit dispatches a bubbling `change` event from the `ref`
|
|
34
|
+
* element after it commits, so a native `change` listener on the form sees it
|
|
35
|
+
* as it sees a native input's (React's synthetic `onChange` reports only
|
|
36
|
+
* inputs, selects and textareas). A reset and a new default fire none, as
|
|
37
|
+
* they fire none for a native input.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```tsx
|
|
41
|
+
* // draft: the form's defaults, read from result?.submittedValues ?? pageData
|
|
42
|
+
* const [tags, setTags, ref] = useFormField(draft.tags);
|
|
43
|
+
* <div ref={ref}>
|
|
44
|
+
* <input type="hidden" name="tags" value={tags.join(',')} />
|
|
45
|
+
* <TagPicker value={tags} onChange={setTags} />
|
|
46
|
+
* </div>
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export function useFormField<T>(
|
|
50
|
+
defaultValue: T
|
|
51
|
+
): [value: T, setValue: Dispatch<SetStateAction<T>>, ref: RefCallback<HTMLElement>] {
|
|
52
|
+
// `null` means "not edited": the field shows its default. A box, so an edit
|
|
53
|
+
// back to a value equal to the default still counts as an edit.
|
|
54
|
+
const [edit, setEdit] = useState<{ value: T } | null>(null);
|
|
55
|
+
const value = edit ? edit.value : defaultValue;
|
|
56
|
+
|
|
57
|
+
// The latest default, for an updater called from an old closure. Written
|
|
58
|
+
// after commit, never during render.
|
|
59
|
+
const defaultRef = useRef(defaultValue);
|
|
60
|
+
useLayoutEffect(() => {
|
|
61
|
+
defaultRef.current = defaultValue;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Resets the form has fired whose outcome this field has not applied yet.
|
|
65
|
+
// A reset's outcome is known only once its dispatch is over: our listener
|
|
66
|
+
// runs on the form before React's delegated `onReset`, which may cancel
|
|
67
|
+
// it. `takeResets` applies every reset whose dispatch has ended, in order:
|
|
68
|
+
// if any of them was not cancelled, the field was reset. Both the deferred
|
|
69
|
+
// clear and `setValue` call it, so writes follow a native input's order:
|
|
70
|
+
// - after `form.reset()` returns, the reset has happened and a write lands
|
|
71
|
+
// on top of it (the write takes the reset, and the clear finds nothing);
|
|
72
|
+
// - during the reset's dispatch (an `onReset` handler), the reset is still
|
|
73
|
+
// pending: the write applies to the current value, and the clear then
|
|
74
|
+
// erases it unless the reset is cancelled, as a reset overwrites a
|
|
75
|
+
// native input written in its handler.
|
|
76
|
+
// A list, not one slot: `reset(); reset()` where only the second is
|
|
77
|
+
// cancelled still resets, as it does the native fields.
|
|
78
|
+
const pendingResets = useRef<Event[]>([]);
|
|
79
|
+
const takeResets = useCallback((): boolean => {
|
|
80
|
+
// eventPhase is NONE once dispatch is over.
|
|
81
|
+
const over = pendingResets.current.filter((e) => e.eventPhase === Event.NONE);
|
|
82
|
+
if (over.length === 0) return false;
|
|
83
|
+
pendingResets.current = pendingResets.current.filter((e) => !over.includes(e));
|
|
84
|
+
return over.some((e) => !e.defaultPrevented);
|
|
85
|
+
}, []);
|
|
86
|
+
|
|
87
|
+
const setValue = useCallback<Dispatch<SetStateAction<T>>>(
|
|
88
|
+
(next) => {
|
|
89
|
+
const wasReset = takeResets();
|
|
90
|
+
setEdit((latest) => {
|
|
91
|
+
const current = wasReset ? null : latest;
|
|
92
|
+
const base = current ? current.value : defaultRef.current;
|
|
93
|
+
const value = next instanceof Function ? next(base) : next;
|
|
94
|
+
// Unchanged: keep the box, so nothing re-renders and no `change`
|
|
95
|
+
// fires, as a native input fires none for a value it already has.
|
|
96
|
+
return Object.is(value, base) ? current : { value };
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
[takeResets]
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const element = useRef<HTMLElement | null>(null);
|
|
103
|
+
const ref = useCallback<RefCallback<HTMLElement>>(
|
|
104
|
+
(el) => {
|
|
105
|
+
element.current = el;
|
|
106
|
+
const form = el?.closest('form');
|
|
107
|
+
if (!form) return;
|
|
108
|
+
const onReset = (event: Event) => {
|
|
109
|
+
pendingResets.current.push(event);
|
|
110
|
+
// Dispatch is over by the time a microtask runs.
|
|
111
|
+
queueMicrotask(() => {
|
|
112
|
+
if (takeResets()) setEdit(null);
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
form.addEventListener('reset', onReset);
|
|
116
|
+
return () => {
|
|
117
|
+
element.current = null;
|
|
118
|
+
pendingResets.current = [];
|
|
119
|
+
form.removeEventListener('reset', onReset);
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
[takeResets]
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// Every `setValue` stores a new box, and nothing else does except a reset
|
|
126
|
+
// (which stores null), so a new non-null box is exactly "the user edited".
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (edit) element.current?.dispatchEvent(new Event('change', { bubbles: true }));
|
|
129
|
+
}, [edit]);
|
|
130
|
+
|
|
131
|
+
return [value, setValue, ref];
|
|
132
|
+
}
|
package/src/config-types.ts
CHANGED
|
@@ -56,7 +56,8 @@ export interface TimberUserConfig {
|
|
|
56
56
|
forms?: {
|
|
57
57
|
/**
|
|
58
58
|
* Strip sensitive fields (passwords, tokens, CVV, SSN, etc.) from the
|
|
59
|
-
* `submittedValues` echoed back
|
|
59
|
+
* `submittedValues` echoed back when an action fails (a validation
|
|
60
|
+
* failure, or a `serverError` from a form submission).
|
|
60
61
|
*
|
|
61
62
|
* Applied to both the with-JS (`createActionClient`) and no-JS (form POST
|
|
62
63
|
* re-render) paths. Safe-by-default: the built-in deny-list is active
|
package/src/plugins/shims.ts
CHANGED
package/src/rsc-runtime/rsc.ts
CHANGED
|
@@ -16,6 +16,9 @@ export {
|
|
|
16
16
|
loadServerAction,
|
|
17
17
|
decodeReply,
|
|
18
18
|
decodeAction,
|
|
19
|
+
// No-JS action result → the `useActionState` hook that submitted it
|
|
20
|
+
// (server/action-handler.ts), as Next.js does in its action handler.
|
|
21
|
+
decodeFormState,
|
|
19
22
|
encryptActionBoundArgs,
|
|
20
23
|
decryptActionBoundArgs,
|
|
21
24
|
// Flight client running INSIDE the RSC environment — decodes a cached
|
|
@@ -25,3 +25,17 @@ declare module '@vitejs/plugin-rsc/vendor/react-server-dom/server.edge' {
|
|
|
25
25
|
options?: { temporaryReferences?: unknown; arraySizeLimit?: number }
|
|
26
26
|
): Promise<unknown[]>;
|
|
27
27
|
}
|
|
28
|
+
|
|
29
|
+
// src decodes through plugin-rsc's wrappers; tests decode a Flight response
|
|
30
|
+
// with the vendored client directly, as the browser's createFromFetch does.
|
|
31
|
+
declare module '@vitejs/plugin-rsc/vendor/react-server-dom/client.edge' {
|
|
32
|
+
export function createFromReadableStream(
|
|
33
|
+
stream: ReadableStream<Uint8Array>,
|
|
34
|
+
options: {
|
|
35
|
+
serverConsumerManifest: unknown;
|
|
36
|
+
nonce?: string;
|
|
37
|
+
temporaryReferences?: unknown;
|
|
38
|
+
encodeFormAction?: unknown;
|
|
39
|
+
}
|
|
40
|
+
): PromiseLike<unknown>;
|
|
41
|
+
}
|