@connextar/house 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -1
- package/dist/house.css +66 -0
- package/dist/ux/errors.d.ts +26 -0
- package/dist/ux/errors.d.ts.map +1 -0
- package/dist/ux/errors.js +33 -0
- package/dist/ux/errors.js.map +1 -0
- package/dist/ux/feedback.d.ts +23 -0
- package/dist/ux/feedback.d.ts.map +1 -0
- package/dist/ux/feedback.js +22 -0
- package/dist/ux/feedback.js.map +1 -0
- package/dist/ux/index.d.ts +20 -0
- package/dist/ux/index.d.ts.map +1 -0
- package/dist/ux/index.js +20 -0
- package/dist/ux/index.js.map +1 -0
- package/dist/ux/navigation-index.d.ts +10 -0
- package/dist/ux/navigation-index.d.ts.map +1 -0
- package/dist/ux/navigation-index.js +10 -0
- package/dist/ux/navigation-index.js.map +1 -0
- package/dist/ux/navigation-progress.d.ts +53 -0
- package/dist/ux/navigation-progress.d.ts.map +1 -0
- package/dist/ux/navigation-progress.js +245 -0
- package/dist/ux/navigation-progress.js.map +1 -0
- package/dist/ux/optimistic.d.ts +139 -0
- package/dist/ux/optimistic.d.ts.map +1 -0
- package/dist/ux/optimistic.js +242 -0
- package/dist/ux/optimistic.js.map +1 -0
- package/dist/ux/use-action.d.ts +60 -0
- package/dist/ux/use-action.d.ts.map +1 -0
- package/dist/ux/use-action.js +100 -0
- package/dist/ux/use-action.js.map +1 -0
- package/dist/ux/use-optimistic-list.d.ts +69 -0
- package/dist/ux/use-optimistic-list.d.ts.map +1 -0
- package/dist/ux/use-optimistic-list.js +115 -0
- package/dist/ux/use-optimistic-list.js.map +1 -0
- package/dist/ux/use-optimistic-value.d.ts +40 -0
- package/dist/ux/use-optimistic-value.d.ts.map +1 -0
- package/dist/ux/use-optimistic-value.js +68 -0
- package/dist/ux/use-optimistic-value.js.map +1 -0
- package/package.json +10 -2
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { usePathname, useRouter, useSearchParams } from "next/navigation.js";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
/**
|
|
6
|
+
* Feedback for the other half of "nothing answers my click": moving between
|
|
7
|
+
* screens.
|
|
8
|
+
*
|
|
9
|
+
* Two signals, because they answer two different questions:
|
|
10
|
+
*
|
|
11
|
+
* - **A bar across the top of the page** — "something is happening". It waits
|
|
12
|
+
* a moment before appearing so a fast navigation does not flash it.
|
|
13
|
+
* - **A spinner on the thing that was clicked** — "*that* is what is
|
|
14
|
+
* happening". Without it, a slow route on a page full of links gives no clue
|
|
15
|
+
* which one took, which is when people click a second one.
|
|
16
|
+
*
|
|
17
|
+
* The second is set on the DOM node directly rather than through React state.
|
|
18
|
+
* The click can land on any link anywhere in the tree, including ones rendered
|
|
19
|
+
* by pages this component knows nothing about, and an attribute is the only
|
|
20
|
+
* thing they all have in common. `data-busy` is the same attribute the button
|
|
21
|
+
* primitive sets for an action, so one rule in the stylesheet covers both.
|
|
22
|
+
*/
|
|
23
|
+
/** Delay before the bar appears, so fast navigations do not flash it. */
|
|
24
|
+
const START_DELAY_MS = 140;
|
|
25
|
+
/** Cadence of the trickle that creeps the bar towards 90%. */
|
|
26
|
+
const TRICKLE_MS = 220;
|
|
27
|
+
/** Safety net: never leave the bar running forever if a navigation never commits. */
|
|
28
|
+
const MAX_PENDING_MS = 20_000;
|
|
29
|
+
/** How long the completed bar stays at 100% before fading out. */
|
|
30
|
+
const COMPLETE_MS = 260;
|
|
31
|
+
const START_EVENT = "cx:navigation-start";
|
|
32
|
+
/** The attribute every busy control carries, action or navigation alike. */
|
|
33
|
+
export const BUSY_ATTRIBUTE = "data-busy";
|
|
34
|
+
/** The control currently marked as the reason for a navigation. */
|
|
35
|
+
let marked = null;
|
|
36
|
+
function unmark() {
|
|
37
|
+
if (!marked)
|
|
38
|
+
return;
|
|
39
|
+
marked.removeAttribute(BUSY_ATTRIBUTE);
|
|
40
|
+
marked.removeAttribute("aria-busy");
|
|
41
|
+
marked = null;
|
|
42
|
+
}
|
|
43
|
+
function mark(element) {
|
|
44
|
+
if (marked === element)
|
|
45
|
+
return;
|
|
46
|
+
unmark();
|
|
47
|
+
if (!element)
|
|
48
|
+
return;
|
|
49
|
+
marked = element;
|
|
50
|
+
element.setAttribute(BUSY_ATTRIBUTE, "true");
|
|
51
|
+
element.setAttribute("aria-busy", "true");
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Show the navigation feedback for something this module cannot see on its
|
|
55
|
+
* own — a programmatic `router.push`. Pass the control that caused it and it
|
|
56
|
+
* gets the spinner too.
|
|
57
|
+
*/
|
|
58
|
+
export function startNavigation(element) {
|
|
59
|
+
if (typeof window === "undefined")
|
|
60
|
+
return;
|
|
61
|
+
mark(element ?? null);
|
|
62
|
+
window.dispatchEvent(new Event(START_EVENT));
|
|
63
|
+
}
|
|
64
|
+
/** Backwards-compatible alias for the original name. */
|
|
65
|
+
export const startRouteProgress = startNavigation;
|
|
66
|
+
/**
|
|
67
|
+
* `useRouter`, with the navigation feedback attached to `push` and `replace`.
|
|
68
|
+
*
|
|
69
|
+
* A click that runs `router.push(href)` is invisible to the watcher above —
|
|
70
|
+
* there is no anchor and no popstate — so those navigations were the ones with
|
|
71
|
+
* no feedback at all. The worst case is the one right after a save: the
|
|
72
|
+
* button's own spinner stops when the write lands, and then nothing happens
|
|
73
|
+
* for as long as the next screen takes to render.
|
|
74
|
+
*
|
|
75
|
+
* Swapping the import is the whole change; no call site moves. `refresh`,
|
|
76
|
+
* `back`, `forward` and `prefetch` pass straight through — a refresh re-renders
|
|
77
|
+
* the screen you are already on, which needs no bar.
|
|
78
|
+
*/
|
|
79
|
+
export function useAppRouter() {
|
|
80
|
+
const router = useRouter();
|
|
81
|
+
return React.useMemo(() => ({
|
|
82
|
+
...router,
|
|
83
|
+
// Forwarded by spread, not by naming the options: passing an explicit
|
|
84
|
+
// `undefined` is a different call from passing nothing.
|
|
85
|
+
push: (...args) => {
|
|
86
|
+
startNavigation();
|
|
87
|
+
router.push(...args);
|
|
88
|
+
},
|
|
89
|
+
replace: (...args) => {
|
|
90
|
+
startNavigation();
|
|
91
|
+
router.replace(...args);
|
|
92
|
+
},
|
|
93
|
+
}), [router]);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* `router.push` with the feedback attached, and the control that caused it
|
|
97
|
+
* marked as busy. For a button that navigates rather than saves.
|
|
98
|
+
*/
|
|
99
|
+
export function useNavigate() {
|
|
100
|
+
const router = useRouter();
|
|
101
|
+
return React.useCallback((href, options) => {
|
|
102
|
+
startNavigation(options?.element ?? null);
|
|
103
|
+
if (options?.replace)
|
|
104
|
+
router.replace(href);
|
|
105
|
+
else
|
|
106
|
+
router.push(href);
|
|
107
|
+
}, [router]);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* A click that means "take me there", rather than one the browser will handle
|
|
111
|
+
* itself — a new tab, a download, a context menu.
|
|
112
|
+
*
|
|
113
|
+
* Deliberately *not* a `defaultPrevented` check. This runs in the capture
|
|
114
|
+
* phase, and the reason is the whole point of this listener: Next's `Link`
|
|
115
|
+
* calls `preventDefault()` on its way to a client-side navigation, so by the
|
|
116
|
+
* time a bubble-phase listener sees the click, every in-app link looks
|
|
117
|
+
* cancelled. Guarding on `defaultPrevented` there means the bar fires for
|
|
118
|
+
* exactly the navigations that do not need it and stays silent for all the
|
|
119
|
+
* ones that do — which is what was happening here until this was measured in
|
|
120
|
+
* the browser.
|
|
121
|
+
*/
|
|
122
|
+
function isPlainLeftClick(event) {
|
|
123
|
+
return event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey;
|
|
124
|
+
}
|
|
125
|
+
function navigatesElsewhere(url) {
|
|
126
|
+
if (url.origin !== window.location.origin)
|
|
127
|
+
return false;
|
|
128
|
+
// API routes are downloads/exports, not page navigations.
|
|
129
|
+
if (url.pathname.startsWith("/api/"))
|
|
130
|
+
return false;
|
|
131
|
+
return url.pathname !== window.location.pathname || url.search !== window.location.search;
|
|
132
|
+
}
|
|
133
|
+
function NavigationProgressBar({ className }) {
|
|
134
|
+
const pathname = usePathname();
|
|
135
|
+
const search = useSearchParams().toString();
|
|
136
|
+
const [progress, setProgress] = React.useState(null);
|
|
137
|
+
const timers = React.useRef([]);
|
|
138
|
+
const active = React.useRef(false);
|
|
139
|
+
const clearTimers = React.useCallback(() => {
|
|
140
|
+
timers.current.forEach((id) => window.clearTimeout(id));
|
|
141
|
+
timers.current = [];
|
|
142
|
+
}, []);
|
|
143
|
+
const stop = React.useCallback(() => {
|
|
144
|
+
clearTimers();
|
|
145
|
+
active.current = false;
|
|
146
|
+
delete document.body.dataset.routePending;
|
|
147
|
+
unmark();
|
|
148
|
+
setProgress((value) => (value === null ? null : 100));
|
|
149
|
+
window.setTimeout(() => {
|
|
150
|
+
// A new navigation may have started in the meantime; leave its bar alone.
|
|
151
|
+
if (!active.current)
|
|
152
|
+
setProgress(null);
|
|
153
|
+
}, COMPLETE_MS);
|
|
154
|
+
}, [clearTimers]);
|
|
155
|
+
const start = React.useCallback(() => {
|
|
156
|
+
if (active.current)
|
|
157
|
+
return;
|
|
158
|
+
active.current = true;
|
|
159
|
+
document.body.dataset.routePending = "true";
|
|
160
|
+
const trickle = () => {
|
|
161
|
+
setProgress((value) => (value === null ? value : Math.min(value + Math.max(1, (90 - value) * 0.16), 90)));
|
|
162
|
+
timers.current.push(window.setTimeout(trickle, TRICKLE_MS));
|
|
163
|
+
};
|
|
164
|
+
timers.current.push(window.setTimeout(() => {
|
|
165
|
+
setProgress(8);
|
|
166
|
+
trickle();
|
|
167
|
+
}, START_DELAY_MS));
|
|
168
|
+
timers.current.push(window.setTimeout(stop, MAX_PENDING_MS));
|
|
169
|
+
}, [stop]);
|
|
170
|
+
// The navigation committed once the URL the router reports has changed.
|
|
171
|
+
const initialRender = React.useRef(true);
|
|
172
|
+
React.useEffect(() => {
|
|
173
|
+
if (initialRender.current) {
|
|
174
|
+
initialRender.current = false;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
stop();
|
|
178
|
+
}, [pathname, search, stop]);
|
|
179
|
+
React.useEffect(() => {
|
|
180
|
+
const onClick = (event) => {
|
|
181
|
+
if (!isPlainLeftClick(event))
|
|
182
|
+
return;
|
|
183
|
+
const target = event.target;
|
|
184
|
+
if (!(target instanceof Element))
|
|
185
|
+
return;
|
|
186
|
+
const anchor = target.closest("a");
|
|
187
|
+
if (!anchor?.href || anchor.hasAttribute("download"))
|
|
188
|
+
return;
|
|
189
|
+
if (anchor.target && anchor.target !== "_self")
|
|
190
|
+
return;
|
|
191
|
+
if (!navigatesElsewhere(new URL(anchor.href)))
|
|
192
|
+
return;
|
|
193
|
+
// Mark before `start`, which returns early when a navigation is already
|
|
194
|
+
// running: a second click during a slow load should still move the
|
|
195
|
+
// spinner to the link that was actually clicked.
|
|
196
|
+
mark(anchor);
|
|
197
|
+
start();
|
|
198
|
+
};
|
|
199
|
+
const onSubmit = (event) => {
|
|
200
|
+
if (event.defaultPrevented)
|
|
201
|
+
return;
|
|
202
|
+
const form = event.target;
|
|
203
|
+
if (!(form instanceof HTMLFormElement))
|
|
204
|
+
return;
|
|
205
|
+
if (form.method.toLowerCase() !== "get" || !form.action)
|
|
206
|
+
return;
|
|
207
|
+
if (new URL(form.action).origin !== window.location.origin)
|
|
208
|
+
return;
|
|
209
|
+
if (event.submitter)
|
|
210
|
+
mark(event.submitter);
|
|
211
|
+
start();
|
|
212
|
+
};
|
|
213
|
+
// Capture, so this runs before `Link` cancels the click (see above). The
|
|
214
|
+
// submit listener stays in the bubble phase on purpose: a form with an
|
|
215
|
+
// `onSubmit` that saves rather than navigates is the common case here, and
|
|
216
|
+
// `defaultPrevented` is exactly how to tell those apart.
|
|
217
|
+
document.addEventListener("click", onClick, true);
|
|
218
|
+
document.addEventListener("submit", onSubmit);
|
|
219
|
+
window.addEventListener("popstate", start);
|
|
220
|
+
window.addEventListener(START_EVENT, start);
|
|
221
|
+
return () => {
|
|
222
|
+
document.removeEventListener("click", onClick, true);
|
|
223
|
+
document.removeEventListener("submit", onSubmit);
|
|
224
|
+
window.removeEventListener("popstate", start);
|
|
225
|
+
window.removeEventListener(START_EVENT, start);
|
|
226
|
+
clearTimers();
|
|
227
|
+
unmark();
|
|
228
|
+
delete document.body.dataset.routePending;
|
|
229
|
+
};
|
|
230
|
+
}, [clearTimers, start]);
|
|
231
|
+
if (progress === null)
|
|
232
|
+
return null;
|
|
233
|
+
return (_jsx("div", { className: "pointer-events-none fixed inset-x-0 top-0 z-[100] h-0.5", role: "progressbar", "aria-label": "Loading page", "aria-busy": "true", children: _jsx("div", { className: className, style: { width: `${progress}%`, opacity: progress === 100 ? 0 : 1 } }) }));
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Mount once, above everything else — including anything that suspends, or the
|
|
237
|
+
* bar disappears for exactly the navigations it is there for.
|
|
238
|
+
*
|
|
239
|
+
* `className` styles the filled part of the bar, so the host app picks its own
|
|
240
|
+
* colour without this module knowing any of its tokens.
|
|
241
|
+
*/
|
|
242
|
+
export function NavigationProgress({ className }) {
|
|
243
|
+
return (_jsx(React.Suspense, { fallback: null, children: _jsx(NavigationProgressBar, { className: className }) }));
|
|
244
|
+
}
|
|
245
|
+
//# sourceMappingURL=navigation-progress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"navigation-progress.js","sourceRoot":"","sources":["../../src/ux/navigation-progress.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The patch queue behind the optimistic hooks — no React, no transport, no
|
|
3
|
+
* framework. All of the thinking is here so it can be tested as a function.
|
|
4
|
+
*
|
|
5
|
+
* ## The shape of the problem
|
|
6
|
+
*
|
|
7
|
+
* A screen shows rows the server sent. Someone ticks one. Three things then
|
|
8
|
+
* have to be true at once:
|
|
9
|
+
*
|
|
10
|
+
* 1. The tick shows **now**, before any request goes out.
|
|
11
|
+
* 2. If the write fails, the tick goes away again and the person is told.
|
|
12
|
+
* 3. When the server's own rows arrive, they win — including when they say
|
|
13
|
+
* something different from what was guessed.
|
|
14
|
+
*
|
|
15
|
+
* React's `useOptimistic` solves (1) and (2) for a server action, because the
|
|
16
|
+
* action *is* the transition and React knows when it ends. It does not solve
|
|
17
|
+
* (3) for an app that writes over `fetch` and then calls `router.refresh()`:
|
|
18
|
+
* `router.refresh()` is not awaitable, so the transition ends before the new
|
|
19
|
+
* rows land and the optimistic value snaps back to the stale one — a visible
|
|
20
|
+
* flash of the old answer on every single write. That is the reason this
|
|
21
|
+
* module exists rather than a wrapper around `useOptimistic`.
|
|
22
|
+
*
|
|
23
|
+
* ## How it works
|
|
24
|
+
*
|
|
25
|
+
* The server's rows are the *base*. Every optimistic change is a *patch* held
|
|
26
|
+
* beside the base, never merged into it. What the screen renders is the base
|
|
27
|
+
* with the patches applied, recomputed on each render.
|
|
28
|
+
*
|
|
29
|
+
* A patch is `pending` while its write is in flight, then either:
|
|
30
|
+
*
|
|
31
|
+
* - **dropped** — the write failed, so the base alone is the truth again; or
|
|
32
|
+
* - **settled** — the write landed. It keeps being applied, because the base
|
|
33
|
+
* is still the *old* rows until a refresh arrives, and taking the patch away
|
|
34
|
+
* at this point is precisely the flash we are avoiding.
|
|
35
|
+
*
|
|
36
|
+
* A settled patch is dropped when fresh rows arrive that account for it. That
|
|
37
|
+
* is the whole reconciliation rule, and it is deliberately evidence-based
|
|
38
|
+
* rather than timed: see `shouldDropSettled`.
|
|
39
|
+
*/
|
|
40
|
+
/** How a row is identified. Ids are strings here; numbers stringify fine. */
|
|
41
|
+
export type RowKey = string;
|
|
42
|
+
export type PatchKind = "insert" | "update" | "remove" | "move";
|
|
43
|
+
export interface Patch<T> {
|
|
44
|
+
readonly id: string;
|
|
45
|
+
readonly kind: PatchKind;
|
|
46
|
+
/**
|
|
47
|
+
* The row this patch concerns. For an insert this starts as a made-up key
|
|
48
|
+
* and becomes the server's own key once the write lands.
|
|
49
|
+
*/
|
|
50
|
+
readonly key: RowKey;
|
|
51
|
+
/** `insert`: the row to show. */
|
|
52
|
+
readonly row?: T;
|
|
53
|
+
/** `update`: the fields to lay over the row. */
|
|
54
|
+
readonly fields?: Partial<T>;
|
|
55
|
+
/** `insert`: which end of the list to show it at. */
|
|
56
|
+
readonly at?: "start" | "end";
|
|
57
|
+
/** `move`: the key this row now sits before; `null` means last. */
|
|
58
|
+
readonly before?: RowKey | null;
|
|
59
|
+
readonly settled: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* What the base said about `key` at the moment the write landed. Fresh rows
|
|
62
|
+
* are recognised by disagreeing with it — see `shouldDropSettled`.
|
|
63
|
+
*/
|
|
64
|
+
readonly witness?: T | null;
|
|
65
|
+
}
|
|
66
|
+
/** A patch id. Unique within a session; never stored or sent anywhere. */
|
|
67
|
+
export declare function nextPatchId(): string;
|
|
68
|
+
/**
|
|
69
|
+
* The rows to render: the server's, with every patch laid over them in the
|
|
70
|
+
* order they were made, so a later change to the same row wins.
|
|
71
|
+
*/
|
|
72
|
+
export declare function applyPatches<T>(base: readonly T[], patches: readonly Patch<T>[], keyOf: (row: T) => RowKey): readonly T[];
|
|
73
|
+
/** Shallow, own-keys equality. Enough to tell "the same row" from "new data". */
|
|
74
|
+
export declare function sameRow<T>(a: T | null | undefined, b: T | null | undefined): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Does the base already say what this patch says? A redundant patch has
|
|
77
|
+
* nothing left to show and can go.
|
|
78
|
+
*/
|
|
79
|
+
export declare function isRedundant<T>(patch: Patch<T>, base: readonly T[], keyOf: (row: T) => RowKey): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Whether a settled patch has been overtaken by fresh server rows.
|
|
82
|
+
*
|
|
83
|
+
* Two ways it can have been, and they cover the two endings the person cares
|
|
84
|
+
* about:
|
|
85
|
+
*
|
|
86
|
+
* - **The base agrees** — the refresh carried the change through. Dropping the
|
|
87
|
+
* patch changes nothing on screen, which is exactly the point.
|
|
88
|
+
* - **The base disagrees with what it said when the write landed** — new data
|
|
89
|
+
* for this row has arrived. It wins, whatever it says. This is the case
|
|
90
|
+
* where the server did something other than what was guessed (clamped a
|
|
91
|
+
* value, renamed on save, reordered), and the screen corrects itself.
|
|
92
|
+
*
|
|
93
|
+
* A base that has not changed at all leaves the patch applied, however long it
|
|
94
|
+
* has been there. There is no timeout on purpose: a screen that never refetches
|
|
95
|
+
* has no truth to fall back to, and snapping to stale rows after N seconds
|
|
96
|
+
* would be a bug that only shows up on slow connections.
|
|
97
|
+
*
|
|
98
|
+
* Only ever called when the base is a *different array* from the one the patch
|
|
99
|
+
* settled against, so a parent that rebuilds its rows on every render cannot
|
|
100
|
+
* shake patches loose.
|
|
101
|
+
*/
|
|
102
|
+
export declare function shouldDropSettled<T>(patch: Patch<T>, base: readonly T[], keyOf: (row: T) => RowKey): boolean;
|
|
103
|
+
/**
|
|
104
|
+
* The queue after fresh rows arrived: pending writes are left alone, settled
|
|
105
|
+
* ones are dropped once the rows account for them.
|
|
106
|
+
*/
|
|
107
|
+
export declare function reconcile<T>(patches: readonly Patch<T>[], base: readonly T[], keyOf: (row: T) => RowKey): readonly Patch<T>[];
|
|
108
|
+
/**
|
|
109
|
+
* Mark a landed write as settled, taking the server's own answer over the
|
|
110
|
+
* guess where it sent one — the correction in "confirm or correct" — and
|
|
111
|
+
* noting what the base said, so the next refresh can be recognised.
|
|
112
|
+
*
|
|
113
|
+
* What it takes from that answer is deliberately narrow. A write endpoint
|
|
114
|
+
* often returns a *thinner* record than the list endpoint does — no joined
|
|
115
|
+
* learner, no counts — because the caller did not need them. Laying that over
|
|
116
|
+
* the row wholesale would blank the columns beside the one that was edited,
|
|
117
|
+
* which looks exactly like data loss and is the reason this is a `pick` and a
|
|
118
|
+
* merge rather than an assignment.
|
|
119
|
+
*/
|
|
120
|
+
export declare function settlePatch<T>(patch: Patch<T>, serverRow: T | undefined, base: readonly T[], keyOf: (row: T) => RowKey): Patch<T>;
|
|
121
|
+
/**
|
|
122
|
+
* Drop what a write that has just landed supersedes: earlier *settled* changes
|
|
123
|
+
* to the same rows. Writes still in flight stay — each has a request that can
|
|
124
|
+
* still come back refused and needs rolling back on its own.
|
|
125
|
+
*
|
|
126
|
+
* This happens on landing, never on queueing. A change that succeeded is still
|
|
127
|
+
* true while a later one is being attempted, and clearing it early means a
|
|
128
|
+
* failed second write rolls the first one back with it.
|
|
129
|
+
*/
|
|
130
|
+
export declare function supersede<T>(patches: readonly Patch<T>[], keys: ReadonlySet<RowKey>): readonly Patch<T>[];
|
|
131
|
+
/**
|
|
132
|
+
* A stand-in key for a row that does not exist server-side yet. Distinctive on
|
|
133
|
+
* purpose: if one of these ever reaches a request body or a URL, the resulting
|
|
134
|
+
* 404 should say why at a glance.
|
|
135
|
+
*/
|
|
136
|
+
export declare function tempKey(prefix?: string): string;
|
|
137
|
+
/** Whether a key came from `tempKey` — a row that is still being created. */
|
|
138
|
+
export declare function isTempKey(key: RowKey): boolean;
|
|
139
|
+
//# sourceMappingURL=optimistic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"optimistic.d.ts","sourceRoot":"","sources":["../../src/ux/optimistic.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The patch queue behind the optimistic hooks — no React, no transport, no
|
|
3
|
+
* framework. All of the thinking is here so it can be tested as a function.
|
|
4
|
+
*
|
|
5
|
+
* ## The shape of the problem
|
|
6
|
+
*
|
|
7
|
+
* A screen shows rows the server sent. Someone ticks one. Three things then
|
|
8
|
+
* have to be true at once:
|
|
9
|
+
*
|
|
10
|
+
* 1. The tick shows **now**, before any request goes out.
|
|
11
|
+
* 2. If the write fails, the tick goes away again and the person is told.
|
|
12
|
+
* 3. When the server's own rows arrive, they win — including when they say
|
|
13
|
+
* something different from what was guessed.
|
|
14
|
+
*
|
|
15
|
+
* React's `useOptimistic` solves (1) and (2) for a server action, because the
|
|
16
|
+
* action *is* the transition and React knows when it ends. It does not solve
|
|
17
|
+
* (3) for an app that writes over `fetch` and then calls `router.refresh()`:
|
|
18
|
+
* `router.refresh()` is not awaitable, so the transition ends before the new
|
|
19
|
+
* rows land and the optimistic value snaps back to the stale one — a visible
|
|
20
|
+
* flash of the old answer on every single write. That is the reason this
|
|
21
|
+
* module exists rather than a wrapper around `useOptimistic`.
|
|
22
|
+
*
|
|
23
|
+
* ## How it works
|
|
24
|
+
*
|
|
25
|
+
* The server's rows are the *base*. Every optimistic change is a *patch* held
|
|
26
|
+
* beside the base, never merged into it. What the screen renders is the base
|
|
27
|
+
* with the patches applied, recomputed on each render.
|
|
28
|
+
*
|
|
29
|
+
* A patch is `pending` while its write is in flight, then either:
|
|
30
|
+
*
|
|
31
|
+
* - **dropped** — the write failed, so the base alone is the truth again; or
|
|
32
|
+
* - **settled** — the write landed. It keeps being applied, because the base
|
|
33
|
+
* is still the *old* rows until a refresh arrives, and taking the patch away
|
|
34
|
+
* at this point is precisely the flash we are avoiding.
|
|
35
|
+
*
|
|
36
|
+
* A settled patch is dropped when fresh rows arrive that account for it. That
|
|
37
|
+
* is the whole reconciliation rule, and it is deliberately evidence-based
|
|
38
|
+
* rather than timed: see `shouldDropSettled`.
|
|
39
|
+
*/
|
|
40
|
+
let counter = 0;
|
|
41
|
+
/** A patch id. Unique within a session; never stored or sent anywhere. */
|
|
42
|
+
export function nextPatchId() {
|
|
43
|
+
counter += 1;
|
|
44
|
+
return `p${counter}`;
|
|
45
|
+
}
|
|
46
|
+
function find(rows, key, keyOf) {
|
|
47
|
+
for (const row of rows)
|
|
48
|
+
if (keyOf(row) === key)
|
|
49
|
+
return row;
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The rows to render: the server's, with every patch laid over them in the
|
|
54
|
+
* order they were made, so a later change to the same row wins.
|
|
55
|
+
*/
|
|
56
|
+
export function applyPatches(base, patches, keyOf) {
|
|
57
|
+
if (patches.length === 0)
|
|
58
|
+
return base;
|
|
59
|
+
let rows = [...base];
|
|
60
|
+
for (const patch of patches) {
|
|
61
|
+
switch (patch.kind) {
|
|
62
|
+
case "insert": {
|
|
63
|
+
// The server's own copy has arrived under this key: show that one
|
|
64
|
+
// rather than two of the same thing.
|
|
65
|
+
if (patch.row === undefined || find(rows, patch.key, keyOf) !== null)
|
|
66
|
+
break;
|
|
67
|
+
if (patch.at === "start")
|
|
68
|
+
rows.unshift(patch.row);
|
|
69
|
+
else
|
|
70
|
+
rows.push(patch.row);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case "update": {
|
|
74
|
+
rows = rows.map((row) => (keyOf(row) === patch.key ? { ...row, ...patch.fields } : row));
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
case "remove": {
|
|
78
|
+
rows = rows.filter((row) => keyOf(row) !== patch.key);
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
case "move": {
|
|
82
|
+
const from = rows.findIndex((row) => keyOf(row) === patch.key);
|
|
83
|
+
if (from < 0)
|
|
84
|
+
break;
|
|
85
|
+
const moved = rows.splice(from, 1);
|
|
86
|
+
// `splice` above removed exactly one row at a known index, so this is
|
|
87
|
+
// never empty; the check is for the type, not for the case.
|
|
88
|
+
if (moved.length === 0)
|
|
89
|
+
break;
|
|
90
|
+
const to = patch.before == null ? -1 : rows.findIndex((other) => keyOf(other) === patch.before);
|
|
91
|
+
rows.splice(to < 0 ? rows.length : to, 0, ...moved);
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return rows;
|
|
97
|
+
}
|
|
98
|
+
/** The key that follows `key` in `rows`; `null` at the end. */
|
|
99
|
+
function neighbourAfter(rows, key, keyOf) {
|
|
100
|
+
const index = rows.findIndex((row) => keyOf(row) === key);
|
|
101
|
+
if (index < 0)
|
|
102
|
+
return null;
|
|
103
|
+
const next = rows[index + 1];
|
|
104
|
+
return next === undefined ? null : keyOf(next);
|
|
105
|
+
}
|
|
106
|
+
/** Shallow, own-keys equality. Enough to tell "the same row" from "new data". */
|
|
107
|
+
export function sameRow(a, b) {
|
|
108
|
+
if (a === b)
|
|
109
|
+
return true;
|
|
110
|
+
if (a == null || b == null)
|
|
111
|
+
return false;
|
|
112
|
+
if (typeof a !== "object" || typeof b !== "object")
|
|
113
|
+
return Object.is(a, b);
|
|
114
|
+
const left = a;
|
|
115
|
+
const right = b;
|
|
116
|
+
const keys = Object.keys(left);
|
|
117
|
+
if (keys.length !== Object.keys(right).length)
|
|
118
|
+
return false;
|
|
119
|
+
return keys.every((key) => Object.is(left[key], right[key]));
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Does the base already say what this patch says? A redundant patch has
|
|
123
|
+
* nothing left to show and can go.
|
|
124
|
+
*/
|
|
125
|
+
export function isRedundant(patch, base, keyOf) {
|
|
126
|
+
const current = find(base, patch.key, keyOf);
|
|
127
|
+
switch (patch.kind) {
|
|
128
|
+
case "insert":
|
|
129
|
+
return current !== null;
|
|
130
|
+
case "remove":
|
|
131
|
+
return current === null;
|
|
132
|
+
case "update": {
|
|
133
|
+
// A row that is no longer there cannot be waiting for an edit.
|
|
134
|
+
if (current === null)
|
|
135
|
+
return true;
|
|
136
|
+
const fields = (patch.fields ?? {});
|
|
137
|
+
const row = current;
|
|
138
|
+
return Object.keys(fields).every((key) => Object.is(row[key], fields[key]));
|
|
139
|
+
}
|
|
140
|
+
case "move":
|
|
141
|
+
return find(base, patch.key, keyOf) !== null && neighbourAfter(base, patch.key, keyOf) === patch.before;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Whether a settled patch has been overtaken by fresh server rows.
|
|
146
|
+
*
|
|
147
|
+
* Two ways it can have been, and they cover the two endings the person cares
|
|
148
|
+
* about:
|
|
149
|
+
*
|
|
150
|
+
* - **The base agrees** — the refresh carried the change through. Dropping the
|
|
151
|
+
* patch changes nothing on screen, which is exactly the point.
|
|
152
|
+
* - **The base disagrees with what it said when the write landed** — new data
|
|
153
|
+
* for this row has arrived. It wins, whatever it says. This is the case
|
|
154
|
+
* where the server did something other than what was guessed (clamped a
|
|
155
|
+
* value, renamed on save, reordered), and the screen corrects itself.
|
|
156
|
+
*
|
|
157
|
+
* A base that has not changed at all leaves the patch applied, however long it
|
|
158
|
+
* has been there. There is no timeout on purpose: a screen that never refetches
|
|
159
|
+
* has no truth to fall back to, and snapping to stale rows after N seconds
|
|
160
|
+
* would be a bug that only shows up on slow connections.
|
|
161
|
+
*
|
|
162
|
+
* Only ever called when the base is a *different array* from the one the patch
|
|
163
|
+
* settled against, so a parent that rebuilds its rows on every render cannot
|
|
164
|
+
* shake patches loose.
|
|
165
|
+
*/
|
|
166
|
+
export function shouldDropSettled(patch, base, keyOf) {
|
|
167
|
+
if (isRedundant(patch, base, keyOf))
|
|
168
|
+
return true;
|
|
169
|
+
return !sameRow(find(base, patch.key, keyOf), patch.witness ?? null);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The queue after fresh rows arrived: pending writes are left alone, settled
|
|
173
|
+
* ones are dropped once the rows account for them.
|
|
174
|
+
*/
|
|
175
|
+
export function reconcile(patches, base, keyOf) {
|
|
176
|
+
if (patches.length === 0)
|
|
177
|
+
return patches;
|
|
178
|
+
const kept = patches.filter((patch) => !patch.settled || !shouldDropSettled(patch, base, keyOf));
|
|
179
|
+
return kept.length === patches.length ? patches : kept;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Mark a landed write as settled, taking the server's own answer over the
|
|
183
|
+
* guess where it sent one — the correction in "confirm or correct" — and
|
|
184
|
+
* noting what the base said, so the next refresh can be recognised.
|
|
185
|
+
*
|
|
186
|
+
* What it takes from that answer is deliberately narrow. A write endpoint
|
|
187
|
+
* often returns a *thinner* record than the list endpoint does — no joined
|
|
188
|
+
* learner, no counts — because the caller did not need them. Laying that over
|
|
189
|
+
* the row wholesale would blank the columns beside the one that was edited,
|
|
190
|
+
* which looks exactly like data loss and is the reason this is a `pick` and a
|
|
191
|
+
* merge rather than an assignment.
|
|
192
|
+
*/
|
|
193
|
+
export function settlePatch(patch, serverRow, base, keyOf) {
|
|
194
|
+
const settled = { ...patch, settled: true, witness: find(base, patch.key, keyOf) };
|
|
195
|
+
if (serverRow === null || serverRow === undefined || typeof serverRow !== "object")
|
|
196
|
+
return settled;
|
|
197
|
+
if (patch.kind === "insert" && patch.row !== undefined) {
|
|
198
|
+
// The guess fills in whatever the create did not answer with; the key
|
|
199
|
+
// comes from the server, because that is the one thing only it knows.
|
|
200
|
+
const row = { ...patch.row, ...serverRow };
|
|
201
|
+
const key = keyOf(row);
|
|
202
|
+
return { ...settled, key, row, witness: find(base, key, keyOf) };
|
|
203
|
+
}
|
|
204
|
+
if (patch.kind === "update" && patch.fields) {
|
|
205
|
+
// Only the fields this write set, and only those the answer carried: a
|
|
206
|
+
// price rounded on save, a title trimmed, a status the server chose.
|
|
207
|
+
const answer = serverRow;
|
|
208
|
+
const corrected = { ...patch.fields };
|
|
209
|
+
for (const field of Object.keys(corrected)) {
|
|
210
|
+
if (field in answer)
|
|
211
|
+
corrected[field] = answer[field];
|
|
212
|
+
}
|
|
213
|
+
return { ...settled, fields: corrected };
|
|
214
|
+
}
|
|
215
|
+
return settled;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Drop what a write that has just landed supersedes: earlier *settled* changes
|
|
219
|
+
* to the same rows. Writes still in flight stay — each has a request that can
|
|
220
|
+
* still come back refused and needs rolling back on its own.
|
|
221
|
+
*
|
|
222
|
+
* This happens on landing, never on queueing. A change that succeeded is still
|
|
223
|
+
* true while a later one is being attempted, and clearing it early means a
|
|
224
|
+
* failed second write rolls the first one back with it.
|
|
225
|
+
*/
|
|
226
|
+
export function supersede(patches, keys) {
|
|
227
|
+
return patches.filter((patch) => !(patch.settled && keys.has(patch.key)));
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* A stand-in key for a row that does not exist server-side yet. Distinctive on
|
|
231
|
+
* purpose: if one of these ever reaches a request body or a URL, the resulting
|
|
232
|
+
* 404 should say why at a glance.
|
|
233
|
+
*/
|
|
234
|
+
export function tempKey(prefix = "tmp") {
|
|
235
|
+
counter += 1;
|
|
236
|
+
return `${prefix}:${counter}:${Date.now().toString(36)}`;
|
|
237
|
+
}
|
|
238
|
+
/** Whether a key came from `tempKey` — a row that is still being created. */
|
|
239
|
+
export function isTempKey(key) {
|
|
240
|
+
return key.startsWith("tmp:");
|
|
241
|
+
}
|
|
242
|
+
//# sourceMappingURL=optimistic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"optimistic.js","sourceRoot":"","sources":["../../src/ux/optimistic.ts"],"names":[],"mappings":"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"}
|