@tinvar/react 0.0.0-stage → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +168 -4
  3. package/src/action-server.js +41 -0
  4. package/src/action.d.ts +68 -0
  5. package/src/action.js +167 -0
  6. package/src/bootstrap.js +153 -0
  7. package/src/channel.d.ts +46 -0
  8. package/src/channel.js +132 -0
  9. package/src/client-modules.d.ts +8 -0
  10. package/src/client-modules.js +88 -0
  11. package/src/client.d.ts +36 -0
  12. package/src/client.js +42 -0
  13. package/src/compose-route.d.ts +33 -0
  14. package/src/compose-route.js +107 -0
  15. package/src/csrf.d.ts +11 -0
  16. package/src/csrf.js +49 -0
  17. package/src/deferred-boundary.d.ts +61 -0
  18. package/src/deferred-boundary.js +646 -0
  19. package/src/deferred-groups.d.ts +24 -0
  20. package/src/deferred-groups.js +198 -0
  21. package/src/deferred-revalidation.d.ts +37 -0
  22. package/src/deferred-revalidation.js +108 -0
  23. package/src/deferred-triggers.d.ts +26 -0
  24. package/src/deferred-triggers.js +172 -0
  25. package/src/deferred.d.ts +24 -0
  26. package/src/deferred.jsx +100 -0
  27. package/src/document-load.js +95 -0
  28. package/src/endpoint.d.ts +87 -0
  29. package/src/endpoint.js +304 -0
  30. package/src/form-data.js +219 -0
  31. package/src/form.d.ts +177 -0
  32. package/src/form.js +615 -0
  33. package/src/forms.d.ts +4 -0
  34. package/src/forms.js +10 -0
  35. package/src/fragment-client.d.ts +70 -0
  36. package/src/fragment-client.js +510 -0
  37. package/src/head.d.ts +85 -0
  38. package/src/head.js +360 -0
  39. package/src/inbox-view.d.ts +17 -0
  40. package/src/inbox-view.js +105 -0
  41. package/src/inbox.d.ts +108 -0
  42. package/src/inbox.js +293 -0
  43. package/src/index.combined.d.ts +31 -0
  44. package/src/index.d.ts +5 -0
  45. package/src/index.js +29 -0
  46. package/src/index.server.d.ts +4 -0
  47. package/src/index.server.js +8 -0
  48. package/src/infinite-scroll.d.ts +108 -0
  49. package/src/infinite-scroll.js +715 -0
  50. package/src/layout-props.d.ts +30 -0
  51. package/src/layout-props.js +143 -0
  52. package/src/link-click.d.ts +3 -0
  53. package/src/link-click.js +68 -0
  54. package/src/link.d.ts +63 -0
  55. package/src/link.js +274 -0
  56. package/src/live.d.ts +65 -0
  57. package/src/live.js +977 -0
  58. package/src/merge-slot.d.ts +41 -0
  59. package/src/merge-slot.js +253 -0
  60. package/src/merge.d.ts +111 -0
  61. package/src/merge.js +149 -0
  62. package/src/once-slot.d.ts +14 -0
  63. package/src/once-slot.js +75 -0
  64. package/src/once-store.d.ts +26 -0
  65. package/src/once-store.js +211 -0
  66. package/src/once.d.ts +44 -0
  67. package/src/once.js +65 -0
  68. package/src/optimistic.d.ts +14 -0
  69. package/src/optimistic.js +159 -0
  70. package/src/optional.d.ts +10 -0
  71. package/src/optional.js +31 -0
  72. package/src/page-root.d.ts +56 -0
  73. package/src/page-root.js +455 -0
  74. package/src/payload.d.ts +15 -0
  75. package/src/payload.js +101 -0
  76. package/src/poll.d.ts +55 -0
  77. package/src/poll.js +224 -0
  78. package/src/precognition.js +190 -0
  79. package/src/prefetch-cache.d.ts +46 -0
  80. package/src/prefetch-cache.js +256 -0
  81. package/src/progress.d.ts +38 -0
  82. package/src/progress.js +243 -0
  83. package/src/remember-store.d.ts +50 -0
  84. package/src/remember-store.js +486 -0
  85. package/src/rescue.d.ts +10 -0
  86. package/src/rescue.js +30 -0
  87. package/src/resumable.js +822 -0
  88. package/src/resume-store.js +364 -0
  89. package/src/route.d.ts +50 -0
  90. package/src/route.js +227 -0
  91. package/src/router-hooks.js +234 -0
  92. package/src/router.d.ts +586 -0
  93. package/src/router.js +3555 -0
  94. package/src/segment.d.ts +36 -0
  95. package/src/segment.js +116 -0
  96. package/src/server.d.ts +71 -0
  97. package/src/server.js +242 -0
  98. package/src/submit.js +333 -0
  99. package/src/telemetry.d.ts +71 -0
  100. package/src/telemetry.js +851 -0
  101. package/src/testing.d.ts +89 -0
  102. package/src/testing.js +634 -0
  103. package/src/transfer.d.ts +48 -0
  104. package/src/transfer.js +135 -0
  105. package/src/transport.js +390 -0
  106. package/src/upload.d.ts +342 -0
  107. package/src/upload.js +1240 -0
  108. package/src/use-channel.d.ts +21 -0
  109. package/src/use-channel.js +51 -0
  110. package/src/use-form.d.ts +86 -0
  111. package/src/use-form.js +432 -0
  112. package/src/use-http.d.ts +56 -0
  113. package/src/use-http.js +248 -0
  114. package/src/use-inbox.d.ts +41 -0
  115. package/src/use-inbox.js +201 -0
  116. package/src/use-remember.d.ts +10 -0
  117. package/src/use-remember.js +132 -0
  118. package/src/when-mounted.d.ts +15 -0
  119. package/src/when-mounted.js +27 -0
  120. 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
+ }