@tinvar/react 0.0.0-stage → 0.4.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/LICENSE +21 -0
- package/package.json +168 -4
- package/src/action-server.js +41 -0
- package/src/action.d.ts +68 -0
- package/src/action.js +167 -0
- package/src/bootstrap.js +153 -0
- package/src/channel.d.ts +46 -0
- package/src/channel.js +132 -0
- package/src/client-modules.d.ts +8 -0
- package/src/client-modules.js +88 -0
- package/src/client.d.ts +36 -0
- package/src/client.js +42 -0
- package/src/compose-route.d.ts +33 -0
- package/src/compose-route.js +107 -0
- package/src/csrf.d.ts +11 -0
- package/src/csrf.js +49 -0
- package/src/deferred-boundary.d.ts +61 -0
- package/src/deferred-boundary.js +646 -0
- package/src/deferred-groups.d.ts +24 -0
- package/src/deferred-groups.js +198 -0
- package/src/deferred-revalidation.d.ts +37 -0
- package/src/deferred-revalidation.js +108 -0
- package/src/deferred-triggers.d.ts +26 -0
- package/src/deferred-triggers.js +172 -0
- package/src/deferred.d.ts +24 -0
- package/src/deferred.jsx +100 -0
- package/src/document-load.js +95 -0
- package/src/endpoint.d.ts +87 -0
- package/src/endpoint.js +304 -0
- package/src/form-data.js +219 -0
- package/src/form.d.ts +177 -0
- package/src/form.js +615 -0
- package/src/forms.d.ts +4 -0
- package/src/forms.js +10 -0
- package/src/fragment-client.d.ts +70 -0
- package/src/fragment-client.js +510 -0
- package/src/head.d.ts +85 -0
- package/src/head.js +360 -0
- package/src/inbox-view.d.ts +17 -0
- package/src/inbox-view.js +105 -0
- package/src/inbox.d.ts +108 -0
- package/src/inbox.js +293 -0
- package/src/index.combined.d.ts +31 -0
- package/src/index.d.ts +5 -0
- package/src/index.js +29 -0
- package/src/index.server.d.ts +4 -0
- package/src/index.server.js +8 -0
- package/src/infinite-scroll.d.ts +108 -0
- package/src/infinite-scroll.js +715 -0
- package/src/layout-props.d.ts +30 -0
- package/src/layout-props.js +143 -0
- package/src/link-click.d.ts +3 -0
- package/src/link-click.js +68 -0
- package/src/link.d.ts +63 -0
- package/src/link.js +274 -0
- package/src/live.d.ts +65 -0
- package/src/live.js +977 -0
- package/src/merge-slot.d.ts +41 -0
- package/src/merge-slot.js +253 -0
- package/src/merge.d.ts +111 -0
- package/src/merge.js +149 -0
- package/src/once-slot.d.ts +14 -0
- package/src/once-slot.js +75 -0
- package/src/once-store.d.ts +26 -0
- package/src/once-store.js +211 -0
- package/src/once.d.ts +44 -0
- package/src/once.js +65 -0
- package/src/optimistic.d.ts +14 -0
- package/src/optimistic.js +159 -0
- package/src/optional.d.ts +10 -0
- package/src/optional.js +31 -0
- package/src/page-root.d.ts +56 -0
- package/src/page-root.js +455 -0
- package/src/payload.d.ts +15 -0
- package/src/payload.js +101 -0
- package/src/poll.d.ts +55 -0
- package/src/poll.js +224 -0
- package/src/precognition.js +190 -0
- package/src/prefetch-cache.d.ts +46 -0
- package/src/prefetch-cache.js +256 -0
- package/src/progress.d.ts +38 -0
- package/src/progress.js +243 -0
- package/src/remember-store.d.ts +50 -0
- package/src/remember-store.js +486 -0
- package/src/rescue.d.ts +10 -0
- package/src/rescue.js +30 -0
- package/src/resumable.js +822 -0
- package/src/resume-store.js +364 -0
- package/src/route.d.ts +50 -0
- package/src/route.js +227 -0
- package/src/router-hooks.js +234 -0
- package/src/router.d.ts +586 -0
- package/src/router.js +3555 -0
- package/src/segment.d.ts +36 -0
- package/src/segment.js +116 -0
- package/src/server.d.ts +71 -0
- package/src/server.js +242 -0
- package/src/submit.js +333 -0
- package/src/telemetry.d.ts +71 -0
- package/src/telemetry.js +851 -0
- package/src/testing.d.ts +89 -0
- package/src/testing.js +634 -0
- package/src/transfer.d.ts +48 -0
- package/src/transfer.js +135 -0
- package/src/transport.js +390 -0
- package/src/upload.d.ts +342 -0
- package/src/upload.js +1240 -0
- package/src/use-channel.d.ts +21 -0
- package/src/use-channel.js +51 -0
- package/src/use-form.d.ts +86 -0
- package/src/use-form.js +432 -0
- package/src/use-http.d.ts +56 -0
- package/src/use-http.js +248 -0
- package/src/use-inbox.d.ts +41 -0
- package/src/use-inbox.js +201 -0
- package/src/use-remember.d.ts +10 -0
- package/src/use-remember.js +132 -0
- package/src/when-mounted.d.ts +15 -0
- package/src/when-mounted.js +27 -0
- package/README.md +0 -3
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// What a form does to the page after an action, through the router.
|
|
2
|
+
//
|
|
3
|
+
// An action's answer changes the page: a redirect to follow, a re-rendered
|
|
4
|
+
// payload to mount, prefetch entries to drop, a flash to announce. The
|
|
5
|
+
// router owns all of it -- the page, history, the prefetch cache, its
|
|
6
|
+
// events -- so a form asks it, through its public API:
|
|
7
|
+
//
|
|
8
|
+
// router.flushAll() every prefetched page, after an action
|
|
9
|
+
// that went through; see
|
|
10
|
+
// flushAfterAction()
|
|
11
|
+
// router.applyPayload(payload, o) mounts a re-rendered payload as the page
|
|
12
|
+
// that is up: no request, no history, no
|
|
13
|
+
// scroll
|
|
14
|
+
// router.flash(flash, detail) the router's `flash` event, which is
|
|
15
|
+
// also a native `tinvar:flash` event
|
|
16
|
+
// router.announce(name, detail) the router's `actionStart`, `progress`
|
|
17
|
+
// and `actionSettle` events; see
|
|
18
|
+
// announceAction()
|
|
19
|
+
// router.revoke() a 401: everything held for the session,
|
|
20
|
+
// dropped, before the page is loaded
|
|
21
|
+
//
|
|
22
|
+
// The router is optional throughout. A form rendered without one -- no
|
|
23
|
+
// RouterProvider, a server render, a test -- still posts: a redirect is a
|
|
24
|
+
// full load, there is no prefetch cache to flush, and a flash or an action
|
|
25
|
+
// event goes out as the native event alone.
|
|
26
|
+
|
|
27
|
+
import { revokeSession } from './router.js';
|
|
28
|
+
import { markDocumentLoad, reloadUnlessLoading } from './document-load.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {Pick<import('./router.js').Router,
|
|
32
|
+
* 'visit' | 'reload' | 'routeTokens' | 'flushAll' | 'applyPayload' | 'flash' | 'revoke'
|
|
33
|
+
* > & Partial<Pick<import('./router.js').Router, 'flushInvalidated' | 'announce'>> | null | undefined} RouterLike
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** The router's tokens for the pages it has mounted, which an action re-renders from. */
|
|
37
|
+
export function routeTokens(/** @type {RouterLike} */ router) {
|
|
38
|
+
try {
|
|
39
|
+
const tokens = router?.routeTokens();
|
|
40
|
+
return Array.isArray(tokens) ? tokens.filter((t) => typeof t === 'string') : [];
|
|
41
|
+
} catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Forgets every prefetched page once an action has gone through.
|
|
48
|
+
*
|
|
49
|
+
* Not only those its `X-Invalidated-Tags` names: an action can sign in, sign
|
|
50
|
+
* out or switch tenant, and a session rotated in a cookie script cannot read
|
|
51
|
+
* leaves nothing on the page to tell it by. Every page prefetched before it
|
|
52
|
+
* may have been rendered for the session that has just ended -- the next
|
|
53
|
+
* visit asks the server again.
|
|
54
|
+
*
|
|
55
|
+
* And by those it names, for what is kept beyond the prefetch cache: the
|
|
56
|
+
* content of a #[Once] field kept under one of the tags is no longer
|
|
57
|
+
* claimed, so the next page sends it fresh (ADR 0020). Only by its tags:
|
|
58
|
+
* whose session it was is the server's to check, on every request.
|
|
59
|
+
*
|
|
60
|
+
* @param {RouterLike} router
|
|
61
|
+
* @param {Headers} [headers] the action's response's
|
|
62
|
+
*/
|
|
63
|
+
export function flushAfterAction(router, headers) {
|
|
64
|
+
router?.flushAll();
|
|
65
|
+
if (headers !== undefined) router?.flushInvalidated?.(headers);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Follows an action's redirect: a client visit when there is a router, so
|
|
70
|
+
* the next page's layout reads the new session without a full load.
|
|
71
|
+
*
|
|
72
|
+
* Fresh, never from the prefetch cache: the page it leads to was most
|
|
73
|
+
* likely prefetched before the action, under whatever session the action
|
|
74
|
+
* may have just replaced.
|
|
75
|
+
*
|
|
76
|
+
* @param {RouterLike} router
|
|
77
|
+
* @param {string} href
|
|
78
|
+
* @param {Record<string, unknown>} options the visit options a form was given
|
|
79
|
+
* @param {(url: string) => void} hardNavigate
|
|
80
|
+
*/
|
|
81
|
+
export async function followRedirect(router, href, options, hardNavigate) {
|
|
82
|
+
if (router) {
|
|
83
|
+
await router.visit(href, { ...options, fresh: true });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
hardNavigate(href);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Puts the server's re-render of the page on screen.
|
|
91
|
+
*
|
|
92
|
+
* "The response carries a fresh RSC payload for affected mounted routes."
|
|
93
|
+
* The router sends one token, for the page it has up, so there is at most one
|
|
94
|
+
* payload, and it is this page's. An action that revalidated routes and sent
|
|
95
|
+
* no payload -- the page's pipeline denied, its token was refused, the server
|
|
96
|
+
* re-renders nothing -- means "reload that route yourself", and the router's
|
|
97
|
+
* reload does.
|
|
98
|
+
*
|
|
99
|
+
* Only while the page the action was sent from is still the one up, by the
|
|
100
|
+
* route token it was sent with. An action takes time, and a link followed
|
|
101
|
+
* meanwhile puts another page up: the re-render of the page left, mounted
|
|
102
|
+
* over it, showed the old page under the new one's URL, and a reload
|
|
103
|
+
* reloaded a page the action never touched.
|
|
104
|
+
*
|
|
105
|
+
* With the re-render's live token, when the server sent one under the same
|
|
106
|
+
* key (`live`, ADR 0048): what the page listens for from then on, handed on
|
|
107
|
+
* in the router's `success` event as a reload's is. Null is a token too --
|
|
108
|
+
* the re-render read nothing to listen for. No key at all leaves the page
|
|
109
|
+
* listening as it was.
|
|
110
|
+
*
|
|
111
|
+
* @param {RouterLike} router
|
|
112
|
+
* @param {Record<string, string> | null | undefined} payloads
|
|
113
|
+
* @param {string[]} revalidated
|
|
114
|
+
* @param {Record<string, unknown>} options
|
|
115
|
+
* @param {string | null} [sentFrom] the route token the action was sent with
|
|
116
|
+
* @param {unknown} [live] the action's `live`: page key to live token
|
|
117
|
+
* @returns {Promise<boolean>} whether the page on screen was re-rendered
|
|
118
|
+
*/
|
|
119
|
+
export async function applyPayloads(router, payloads, revalidated, options, sentFrom, live) {
|
|
120
|
+
if (!router) return false;
|
|
121
|
+
|
|
122
|
+
const entry = Object.entries(payloads ?? {}).find(([, p]) => typeof p === 'string');
|
|
123
|
+
if (entry !== undefined) {
|
|
124
|
+
const [key, payload] = entry;
|
|
125
|
+
/** @type {Record<string, unknown>} */
|
|
126
|
+
const opts = sentFrom === undefined ? { ...options } : { ...options, routeToken: sentFrom };
|
|
127
|
+
if (live !== null && typeof live === 'object' && !Array.isArray(live) && Object.hasOwn(live, key)) {
|
|
128
|
+
const token = /** @type {Record<string, unknown>} */ (live)[key];
|
|
129
|
+
opts.liveToken = typeof token === 'string' ? token : null;
|
|
130
|
+
}
|
|
131
|
+
return router.applyPayload(payload, opts);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (revalidated.length > 0) {
|
|
135
|
+
if (sentFrom !== undefined && (routeTokens(router)[0] ?? null) !== sentFrom) return false;
|
|
136
|
+
// Refused, as a payload is, while the user's own visit is in flight.
|
|
137
|
+
return (
|
|
138
|
+
(await router.reload(sentFrom === undefined ? options : { ...options, routeToken: sentFrom })) !== false
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The session is over: the server answered 401. "Session revocation, which
|
|
147
|
+
* the server signals with a 401", is cleared "with a full page load"
|
|
148
|
+
* (tinvar-spec.md, "Client-held data") -- here as on a navigation, and not
|
|
149
|
+
* the caller's to cancel. The load alone was not the clearing: what
|
|
150
|
+
* useRemember() and useForm() kept is in the tab's storage, which outlives
|
|
151
|
+
* the document, and the reloaded page read it back under the same session
|
|
152
|
+
* hash.
|
|
153
|
+
*
|
|
154
|
+
* What the session held goes at once; the load leaves a document load
|
|
155
|
+
* already begun to arrive. An action that ended the session -- a sign-out,
|
|
156
|
+
* a reset -- sent the browser on with one, and a form, a call or an upload
|
|
157
|
+
* refused meanwhile for the session it ended reloaded the page being left
|
|
158
|
+
* over it, and landed the browser back there (document-load.js).
|
|
159
|
+
*
|
|
160
|
+
* @param {RouterLike} router
|
|
161
|
+
* @param {(url: string) => void} hardNavigate
|
|
162
|
+
*/
|
|
163
|
+
export function endSession(router, hardNavigate) {
|
|
164
|
+
revokeSession(router);
|
|
165
|
+
reloadUnlessLoading(hardNavigate);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* A full page load, for what the router cannot do in place: an external
|
|
170
|
+
* location, a revoked session.
|
|
171
|
+
*
|
|
172
|
+
* @param {string} url
|
|
173
|
+
*/
|
|
174
|
+
export function assignLocation(url) {
|
|
175
|
+
// http(s) on any origin, and nothing else: a javascript: URL from a
|
|
176
|
+
// response header would run.
|
|
177
|
+
let target;
|
|
178
|
+
try {
|
|
179
|
+
target = new URL(url, globalThis.location.href);
|
|
180
|
+
} catch {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (target.protocol !== 'http:' && target.protocol !== 'https:') return;
|
|
184
|
+
markDocumentLoad(target.href);
|
|
185
|
+
globalThis.location.assign(target.href);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Tells the page an action flashed something: through the router, so
|
|
190
|
+
* `router.on('flash')` hears it as well as anything listening for the
|
|
191
|
+
* native `tinvar:flash` event -- or, without a router, as that event alone.
|
|
192
|
+
*
|
|
193
|
+
* @param {RouterLike} router
|
|
194
|
+
* @param {Record<string, unknown>} flash
|
|
195
|
+
* @param {string} action
|
|
196
|
+
*/
|
|
197
|
+
export function announceFlash(router, flash, action) {
|
|
198
|
+
if (Object.keys(flash).length === 0) return;
|
|
199
|
+
if (router) {
|
|
200
|
+
router.flash(flash, { action });
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (typeof document === 'undefined') return;
|
|
204
|
+
document.dispatchEvent(new CustomEvent('tinvar:flash', { detail: { flash, action } }));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Tells the page about an action call: tinvar-spec.md's RSC events
|
|
209
|
+
* `actionStart` and `actionSettle`, and `progress` as its files upload --
|
|
210
|
+
* Inertia's `progress` event, which is a form's upload, since here a form
|
|
211
|
+
* posts to an action and a visit is a GET with nothing to upload. Through
|
|
212
|
+
* the router, so `router.on()` hears them as well as anything listening for
|
|
213
|
+
* the native `tinvar:<name>` events -- or, without one, as those alone.
|
|
214
|
+
*
|
|
215
|
+
* Never a reason for the call to fail: a listener's exception is reported
|
|
216
|
+
* by the router, and anything the router itself throws is reported here.
|
|
217
|
+
*
|
|
218
|
+
* @param {RouterLike} router
|
|
219
|
+
* @param {'actionStart' | 'progress' | 'actionSettle'} name
|
|
220
|
+
* @param {Record<string, unknown>} detail
|
|
221
|
+
*/
|
|
222
|
+
export function announceAction(router, name, detail) {
|
|
223
|
+
if (typeof router?.announce === 'function') {
|
|
224
|
+
try {
|
|
225
|
+
router.announce(name, detail);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (typeof globalThis.reportError === 'function') globalThis.reportError(error);
|
|
228
|
+
else console.error(error);
|
|
229
|
+
}
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (typeof document === 'undefined') return;
|
|
233
|
+
document.dispatchEvent(new CustomEvent(`tinvar:${name}`, { detail }));
|
|
234
|
+
}
|