@kestack/kabuki 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/README.md +156 -33
  2. package/dist/core/actor/actor.d.ts +24 -13
  3. package/dist/core/actor/actor.d.ts.map +1 -1
  4. package/dist/core/actor/actor.js +37 -26
  5. package/dist/core/actor/index.d.ts +2 -2
  6. package/dist/core/actor/index.d.ts.map +1 -1
  7. package/dist/core/actor/types.d.ts +91 -47
  8. package/dist/core/actor/types.d.ts.map +1 -1
  9. package/dist/core/history/history.d.ts +11 -3
  10. package/dist/core/history/history.d.ts.map +1 -1
  11. package/dist/core/history/history.js +106 -27
  12. package/dist/core/history/replay.d.ts +2 -0
  13. package/dist/core/history/replay.d.ts.map +1 -1
  14. package/dist/core/history/replay.js +1 -0
  15. package/dist/core/history/saved.d.ts +1 -1
  16. package/dist/core/history/saved.d.ts.map +1 -1
  17. package/dist/core/history/saved.js +13 -9
  18. package/dist/core/index.d.ts +1 -1
  19. package/dist/core/index.d.ts.map +1 -1
  20. package/dist/core/snapshot/invoke.d.ts +2 -3
  21. package/dist/core/snapshot/invoke.d.ts.map +1 -1
  22. package/dist/core/snapshot/invoke.js +42 -49
  23. package/dist/core/snapshot/restore.js +8 -7
  24. package/dist/core/snapshot/tree.d.ts +1 -1
  25. package/dist/core/snapshot/tree.d.ts.map +1 -1
  26. package/dist/core/snapshot/tree.js +1 -1
  27. package/dist/react/app.d.ts +30 -3
  28. package/dist/react/app.d.ts.map +1 -1
  29. package/dist/react/app.js +119 -40
  30. package/dist/react/drawn.d.ts +1 -1
  31. package/dist/react/drawn.d.ts.map +1 -1
  32. package/dist/react/drawn.js +2 -1
  33. package/dist/react/index.d.ts +3 -0
  34. package/dist/react/index.d.ts.map +1 -1
  35. package/dist/react/index.js +2 -0
  36. package/dist/react/lazy.d.ts +30 -0
  37. package/dist/react/lazy.d.ts.map +1 -0
  38. package/dist/react/lazy.js +56 -0
  39. package/dist/react/live.d.ts +7 -0
  40. package/dist/react/live.d.ts.map +1 -0
  41. package/dist/react/live.js +13 -0
  42. package/dist/react/view.d.ts +26 -6
  43. package/dist/react/view.d.ts.map +1 -1
  44. package/dist/react/view.js +17 -5
  45. package/dist/routing/adapter/adapter.d.ts +38 -0
  46. package/dist/routing/adapter/adapter.d.ts.map +1 -0
  47. package/dist/routing/adapter/adapter.js +10 -0
  48. package/dist/routing/adapter/browser/browser.d.ts +8 -0
  49. package/dist/routing/adapter/browser/browser.d.ts.map +1 -0
  50. package/dist/routing/adapter/browser/browser.js +33 -0
  51. package/dist/routing/adapter/browser/index.d.ts +2 -0
  52. package/dist/routing/adapter/browser/index.d.ts.map +1 -0
  53. package/dist/routing/adapter/browser/index.js +1 -0
  54. package/dist/routing/adapter/index.d.ts +3 -0
  55. package/dist/routing/adapter/index.d.ts.map +1 -0
  56. package/dist/routing/adapter/index.js +1 -0
  57. package/dist/routing/adapter/tanstack/index.d.ts +3 -0
  58. package/dist/routing/adapter/tanstack/index.d.ts.map +1 -0
  59. package/dist/routing/adapter/tanstack/index.js +1 -0
  60. package/dist/routing/adapter/tanstack/tanstack.d.ts +27 -0
  61. package/dist/routing/adapter/tanstack/tanstack.d.ts.map +1 -0
  62. package/dist/routing/adapter/tanstack/tanstack.js +38 -0
  63. package/dist/routing/capability/cache.d.ts +12 -0
  64. package/dist/routing/capability/cache.d.ts.map +1 -0
  65. package/dist/routing/capability/cache.js +170 -0
  66. package/dist/routing/capability/index.d.ts +6 -0
  67. package/dist/routing/capability/index.d.ts.map +1 -0
  68. package/dist/routing/capability/index.js +3 -0
  69. package/dist/routing/capability/loading.d.ts +82 -0
  70. package/dist/routing/capability/loading.d.ts.map +1 -0
  71. package/dist/routing/capability/loading.js +53 -0
  72. package/dist/routing/capability/navigation.d.ts +66 -0
  73. package/dist/routing/capability/navigation.d.ts.map +1 -0
  74. package/dist/routing/capability/navigation.js +65 -0
  75. package/dist/routing/index.d.ts +10 -0
  76. package/dist/routing/index.d.ts.map +1 -0
  77. package/dist/routing/index.js +4 -0
  78. package/dist/routing/page/index.d.ts +5 -0
  79. package/dist/routing/page/index.d.ts.map +1 -0
  80. package/dist/routing/page/index.js +2 -0
  81. package/dist/routing/page/page.d.ts +151 -0
  82. package/dist/routing/page/page.d.ts.map +1 -0
  83. package/dist/routing/page/page.js +121 -0
  84. package/dist/routing/page/status.d.ts +121 -0
  85. package/dist/routing/page/status.d.ts.map +1 -0
  86. package/dist/routing/page/status.js +69 -0
  87. package/dist/routing/route/index.d.ts +11 -0
  88. package/dist/routing/route/index.d.ts.map +1 -0
  89. package/dist/routing/route/index.js +6 -0
  90. package/dist/routing/route/loader.d.ts +98 -0
  91. package/dist/routing/route/loader.d.ts.map +1 -0
  92. package/dist/routing/route/loader.js +17 -0
  93. package/dist/routing/route/metadata.d.ts +44 -0
  94. package/dist/routing/route/metadata.d.ts.map +1 -0
  95. package/dist/routing/route/metadata.js +53 -0
  96. package/dist/routing/route/node.d.ts +162 -0
  97. package/dist/routing/route/node.d.ts.map +1 -0
  98. package/dist/routing/route/node.js +3 -0
  99. package/dist/routing/route/places.d.ts +15 -0
  100. package/dist/routing/route/places.d.ts.map +1 -0
  101. package/dist/routing/route/places.js +87 -0
  102. package/dist/routing/route/route.d.ts +53 -0
  103. package/dist/routing/route/route.d.ts.map +1 -0
  104. package/dist/routing/route/route.js +148 -0
  105. package/dist/routing/route/url.d.ts +13 -0
  106. package/dist/routing/route/url.d.ts.map +1 -0
  107. package/dist/routing/route/url.js +79 -0
  108. package/dist/routing/route/values.d.ts +12 -0
  109. package/dist/routing/route/values.d.ts.map +1 -0
  110. package/dist/routing/route/values.js +47 -0
  111. package/dist/routing/router/context.d.ts +123 -0
  112. package/dist/routing/router/context.d.ts.map +1 -0
  113. package/dist/routing/router/context.js +156 -0
  114. package/dist/routing/router/data.d.ts +117 -0
  115. package/dist/routing/router/data.d.ts.map +1 -0
  116. package/dist/routing/router/data.js +247 -0
  117. package/dist/routing/router/define.d.ts +52 -0
  118. package/dist/routing/router/define.d.ts.map +1 -0
  119. package/dist/routing/router/define.js +48 -0
  120. package/dist/routing/router/frame.d.ts +10 -0
  121. package/dist/routing/router/frame.d.ts.map +1 -0
  122. package/dist/routing/router/frame.js +10 -0
  123. package/dist/routing/router/index.d.ts +7 -0
  124. package/dist/routing/router/index.d.ts.map +1 -0
  125. package/dist/routing/router/index.js +3 -0
  126. package/dist/routing/router/router.d.ts +165 -0
  127. package/dist/routing/router/router.d.ts.map +1 -0
  128. package/dist/routing/router/router.js +801 -0
  129. package/dist/routing/routing.d.ts +72 -0
  130. package/dist/routing/routing.d.ts.map +1 -0
  131. package/dist/routing/routing.js +53 -0
  132. package/dist/routing/scroll/index.d.ts +4 -0
  133. package/dist/routing/scroll/index.d.ts.map +1 -0
  134. package/dist/routing/scroll/index.js +2 -0
  135. package/dist/routing/scroll/script.d.ts +6 -0
  136. package/dist/routing/scroll/script.d.ts.map +1 -0
  137. package/dist/routing/scroll/script.js +52 -0
  138. package/dist/routing/scroll/scroll.d.ts +75 -0
  139. package/dist/routing/scroll/scroll.d.ts.map +1 -0
  140. package/dist/routing/scroll/scroll.js +201 -0
  141. package/dist/routing/scroll/selector.d.ts +14 -0
  142. package/dist/routing/scroll/selector.d.ts.map +1 -0
  143. package/dist/routing/scroll/selector.js +40 -0
  144. package/dist/routing/scroll/session.d.ts +6 -0
  145. package/dist/routing/scroll/session.d.ts.map +1 -0
  146. package/dist/routing/scroll/session.js +23 -0
  147. package/dist/start/client/client.d.ts +11 -0
  148. package/dist/start/client/client.d.ts.map +1 -0
  149. package/dist/start/client/client.js +24 -0
  150. package/dist/start/client/index.d.ts +3 -0
  151. package/dist/start/client/index.d.ts.map +1 -0
  152. package/dist/start/client/index.js +2 -0
  153. package/dist/start/client/rpc.d.ts +14 -0
  154. package/dist/start/client/rpc.d.ts.map +1 -0
  155. package/dist/start/client/rpc.js +70 -0
  156. package/dist/start/common/app.d.ts +66 -0
  157. package/dist/start/common/app.d.ts.map +1 -0
  158. package/dist/start/common/app.js +23 -0
  159. package/dist/start/common/common.d.ts +26 -0
  160. package/dist/start/common/common.d.ts.map +1 -0
  161. package/dist/start/common/common.js +19 -0
  162. package/dist/start/common/document.d.ts +44 -0
  163. package/dist/start/common/document.d.ts.map +1 -0
  164. package/dist/start/common/document.js +58 -0
  165. package/dist/start/common/index.d.ts +9 -0
  166. package/dist/start/common/index.d.ts.map +1 -0
  167. package/dist/start/common/index.js +5 -0
  168. package/dist/start/common/page.d.ts +67 -0
  169. package/dist/start/common/page.d.ts.map +1 -0
  170. package/dist/start/common/page.js +29 -0
  171. package/dist/start/common/wire.d.ts +37 -0
  172. package/dist/start/common/wire.d.ts.map +1 -0
  173. package/dist/start/common/wire.js +73 -0
  174. package/dist/start/platform/cloudflare/cloudflare.d.ts +8 -0
  175. package/dist/start/platform/cloudflare/cloudflare.d.ts.map +1 -0
  176. package/dist/start/platform/cloudflare/cloudflare.js +31 -0
  177. package/dist/start/platform/cloudflare/index.d.ts +2 -0
  178. package/dist/start/platform/cloudflare/index.d.ts.map +1 -0
  179. package/dist/start/platform/cloudflare/index.js +1 -0
  180. package/dist/start/platform/index.d.ts +2 -0
  181. package/dist/start/platform/index.d.ts.map +1 -0
  182. package/dist/start/platform/index.js +1 -0
  183. package/dist/start/platform/platform.d.ts +31 -0
  184. package/dist/start/platform/platform.d.ts.map +1 -0
  185. package/dist/start/platform/platform.js +13 -0
  186. package/dist/start/server/cache.d.ts +4 -0
  187. package/dist/start/server/cache.d.ts.map +1 -0
  188. package/dist/start/server/cache.js +26 -0
  189. package/dist/start/server/draw.d.ts +10 -0
  190. package/dist/start/server/draw.d.ts.map +1 -0
  191. package/dist/start/server/draw.js +75 -0
  192. package/dist/start/server/index.d.ts +3 -0
  193. package/dist/start/server/index.d.ts.map +1 -0
  194. package/dist/start/server/index.js +2 -0
  195. package/dist/start/server/layers.d.ts +97 -0
  196. package/dist/start/server/layers.d.ts.map +1 -0
  197. package/dist/start/server/layers.js +37 -0
  198. package/dist/start/server/respond.d.ts +15 -0
  199. package/dist/start/server/respond.d.ts.map +1 -0
  200. package/dist/start/server/respond.js +35 -0
  201. package/dist/start/server/rpc.d.ts +11 -0
  202. package/dist/start/server/rpc.d.ts.map +1 -0
  203. package/dist/start/server/rpc.js +51 -0
  204. package/dist/start/server/server.d.ts +13 -0
  205. package/dist/start/server/server.d.ts.map +1 -0
  206. package/dist/start/server/server.js +138 -0
  207. package/dist/start/server/tiers.d.ts +14 -0
  208. package/dist/start/server/tiers.d.ts.map +1 -0
  209. package/dist/start/server/tiers.js +31 -0
  210. package/dist/start/vite/dev.d.ts +5 -0
  211. package/dist/start/vite/dev.d.ts.map +1 -0
  212. package/dist/start/vite/dev.js +56 -0
  213. package/dist/start/vite/entries.d.ts +27 -0
  214. package/dist/start/vite/entries.d.ts.map +1 -0
  215. package/dist/start/vite/entries.js +66 -0
  216. package/dist/start/vite/files.d.ts +22 -0
  217. package/dist/start/vite/files.d.ts.map +1 -0
  218. package/dist/start/vite/files.js +23 -0
  219. package/dist/start/vite/index.d.ts +2 -0
  220. package/dist/start/vite/index.d.ts.map +1 -0
  221. package/dist/start/vite/index.js +1 -0
  222. package/dist/start/vite/lazy.d.ts +4 -0
  223. package/dist/start/vite/lazy.d.ts.map +1 -0
  224. package/dist/start/vite/lazy.js +30 -0
  225. package/dist/start/vite/manifest.d.ts +18 -0
  226. package/dist/start/vite/manifest.d.ts.map +1 -0
  227. package/dist/start/vite/manifest.js +42 -0
  228. package/dist/start/vite/styles.d.ts +7 -0
  229. package/dist/start/vite/styles.d.ts.map +1 -0
  230. package/dist/start/vite/styles.js +58 -0
  231. package/dist/start/vite/vite.d.ts +11 -0
  232. package/dist/start/vite/vite.d.ts.map +1 -0
  233. package/dist/start/vite/vite.js +217 -0
  234. package/package.json +53 -3
@@ -0,0 +1,801 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Clock, Effect, Layer, Option, Queue, Schema, Stream } from 'effect';
3
+ import { Actor, Command } from '../../core/index.js';
4
+ import { View, lazyOf } from '../../react/index.js';
5
+ import { Adapter, KEY } from '../adapter/index.js';
6
+ import { Loading, Navigation, moveOptions } from '../capability/index.js';
7
+ import { knownOf } from '../page/index.js';
8
+ import { Scrolling, keyOf, restores, scriptFor } from '../scroll/index.js';
9
+ import { linkOptions } from './context.js';
10
+ import { dueRuns, holdingAt, holdingSchema, loadPlace, placeMessages, readyOf, settle, shownOf, } from './data.js';
11
+ import { Frame } from './frame.js';
12
+ const HOW = ['load', 'push', 'replace', 'back', 'forward'];
13
+ /** How a pending move lands: `arrive` is a push the address already shows; `back` and `forward` go to an entry. */
14
+ const LANDS = ['push', 'replace', 'load', 'arrive', 'back', 'forward'];
15
+ /** Redirects followed in a row before a move gives up, as not found. */
16
+ const HOPS = 8;
17
+ /** What a landed move says about its scroll, of what its push or replace said. */
18
+ const scrollOf = (said) => ({
19
+ ...(said?.resetScroll === undefined ? {} : { resetScroll: said.resetScroll }),
20
+ ...(said?.hashScrollIntoView === undefined
21
+ ? {}
22
+ : { hashScrollIntoView: said.hashScrollIntoView }),
23
+ });
24
+ /** What a Pushed or Replaced Message said besides where it goes. */
25
+ const saidOf = ({ resetScroll, hash, hashScrollIntoView, }) => ({
26
+ ...(resetScroll === undefined ? {} : { resetScroll }),
27
+ ...(hash ? { hash } : {}),
28
+ ...(hashScrollIntoView === undefined ? {} : { hashScrollIntoView }),
29
+ });
30
+ /** An entry at `route`, with `hash` if there is one. */
31
+ const entryOf = (key, route, hash) => hash ? { key, route, hash } : { key, route };
32
+ /** Through the Adapter, if the Layer has one: in memory, nothing. */
33
+ const adapter = (use) => Effect.serviceOption(Adapter).pipe(Effect.map(Option.map(use)), Effect.asVoid);
34
+ const write = Command.make('write the address', {
35
+ args: Schema.Struct({
36
+ url: Schema.String,
37
+ key: Schema.String,
38
+ replace: Schema.Boolean,
39
+ }),
40
+ run: ({ url, key, replace }) => adapter((at) => (replace ? at.replace(url, key) : at.push(url, key))),
41
+ });
42
+ const go = Command.make('go', {
43
+ args: Schema.Struct({ by: Schema.Number }),
44
+ run: ({ by }) => adapter((at) => at.go(by)),
45
+ });
46
+ const leave = Command.make('leave', {
47
+ args: Schema.Struct({
48
+ url: Schema.String,
49
+ replace: Schema.optionalKey(Schema.Boolean),
50
+ }),
51
+ run: ({ url, replace }) => adapter((at) => at.leave(url, replace ? { replace } : undefined)),
52
+ });
53
+ const forget = Command.make('forget the Data cache', {
54
+ run: () => Effect.serviceOption(Loading).pipe(Effect.flatMap((found) => Option.isSome(found) && found.value.forget
55
+ ? found.value.forget
56
+ : Effect.void)),
57
+ });
58
+ /** The link a plain click landed on, unless the click asks for something else: a new tab, a download, a full load. */
59
+ const linkOf = (event) => {
60
+ if (event.defaultPrevented || event.button !== 0)
61
+ return undefined;
62
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
63
+ return undefined;
64
+ const link = event.target?.closest?.('a');
65
+ if (!(link instanceof HTMLAnchorElement) || !link.href)
66
+ return undefined;
67
+ if (link.hasAttribute('download'))
68
+ return undefined;
69
+ if (link.target && link.target !== '_self')
70
+ return undefined;
71
+ if (link.relList.contains('external'))
72
+ return undefined;
73
+ return link;
74
+ };
75
+ /**
76
+ * The router of an app: one Actor, above the build of the app's root page,
77
+ * drawn with the root page's View. It holds the history, as entries and
78
+ * where it is in them, and hands the root page the current Route. Every
79
+ * move is one Message to it: a link click, a Request on Navigation, the
80
+ * Adapter's Back and Forward. A move to places whose data is not there yet
81
+ * waits up to its delay while a Worker loads it through `Loading`, so a
82
+ * quick answer, or a copy the Data cache kept, shows no loading screen;
83
+ * after that the move lands and each new place loads its own. It holds the
84
+ * data of every place on screen, `data.ts`, and hands each page its own as
85
+ * `status`, down through the pages above it. It draws the app, catches
86
+ * plain clicks on links inside it, preloads for links, and writes the
87
+ * Metadata. With no Adapter in the Layer it routes in memory; with one, it
88
+ * writes each move to the address with a Command, and hears the address
89
+ * with a Worker.
90
+ */
91
+ export const app = (routes, view) => {
92
+ const { base, delay, scroll: scrolling } = routes.options;
93
+ const root = view.actor;
94
+ if (root.page !== routes.root)
95
+ throw new Error('[@kestack/kabuki/routing] the View handed to the Router does not draw a build of its root page');
96
+ // Named for devtools and errors, as every page is after its key.
97
+ if (root.name === 'Page') {
98
+ root.name = 'App';
99
+ root.definition.name = 'App';
100
+ }
101
+ const href = (route) => {
102
+ const path = routes.path(route);
103
+ return base && path === '/' ? base : `${base}${path}`;
104
+ };
105
+ /** A URL inside the base, without it; `undefined` for a URL outside. */
106
+ const inside = (url) => {
107
+ if (!base)
108
+ return url;
109
+ if (url === base)
110
+ return '/';
111
+ if (url.startsWith(`${base}/`))
112
+ return url.slice(base.length);
113
+ if (url.startsWith(`${base}?`))
114
+ return `/${url.slice(base.length)}`;
115
+ return undefined;
116
+ };
117
+ const same = (a, b) => routes.path(a) === routes.path(b);
118
+ /**
119
+ * Whether `url` is how `route` is written, give or take query params no
120
+ * page reads, like `?utm_source=`: such a URL is left in the address as
121
+ * it is, and every move the app makes writes its Route alone.
122
+ */
123
+ const shows = (url, route) => {
124
+ const written = routes.path(route);
125
+ if (url === written)
126
+ return true;
127
+ const [path, query = ''] = url.split('?');
128
+ const [want, wanted = ''] = written.split('?');
129
+ if (path !== want)
130
+ return false;
131
+ const reads = new URLSearchParams(wanted);
132
+ const read = new URLSearchParams(Array.from(new URLSearchParams(query)).filter(([name]) => reads.has(name)));
133
+ return read.toString() === reads.toString();
134
+ };
135
+ /** A URL inside the base, with it. */
136
+ const withBase = (url) => {
137
+ if (!base)
138
+ return url;
139
+ if (url === '/')
140
+ return base;
141
+ return url.startsWith('/?') ? `${base}${url.slice(1)}` : `${base}${url}`;
142
+ };
143
+ const start = () => routes.read('/').route;
144
+ const notFound = (route) => ({ _tag: 'NotFound', path: routes.path(route) });
145
+ const RouteSchema = routes.route;
146
+ /** What each place with data may start from, by dotted name, each with its own Schema. */
147
+ const KnownSchema = Schema.Struct(Object.fromEntries(routes.data.map(([name, data]) => [
148
+ name,
149
+ Schema.optionalKey(Schema.Union([
150
+ Schema.TaggedStruct('Ready', {
151
+ data,
152
+ stale: Schema.Boolean,
153
+ from: Schema.Literals(['server', 'browser', 'cache']),
154
+ at: Schema.Number,
155
+ took: Schema.NullOr(Schema.Number),
156
+ }),
157
+ Schema.TaggedStruct('Failed', {
158
+ reason: Schema.String,
159
+ notFound: Schema.Boolean,
160
+ }),
161
+ ])),
162
+ ])));
163
+ const actor = Actor.define('Router', {
164
+ input: Schema.UndefinedOr(Schema.Struct({
165
+ route: RouteSchema,
166
+ known: Schema.optionalKey(KnownSchema),
167
+ })),
168
+ model: Schema.Struct({
169
+ entries: Schema.Array(Schema.Struct({
170
+ key: Schema.String,
171
+ route: RouteSchema,
172
+ hash: Schema.optionalKey(Schema.String),
173
+ })),
174
+ at: Schema.Number,
175
+ session: Schema.String,
176
+ next: Schema.Number,
177
+ move: Schema.Struct({
178
+ from: Schema.NullOr(RouteSchema),
179
+ to: RouteSchema,
180
+ how: Schema.Literals(HOW),
181
+ resetScroll: moveOptions.resetScroll,
182
+ hashScrollIntoView: moveOptions.hashScrollIntoView,
183
+ }),
184
+ pending: Schema.NullOr(Schema.Struct({
185
+ id: Schema.Number,
186
+ to: RouteSchema,
187
+ how: Schema.Literals(LANDS),
188
+ wanted: Schema.Array(Schema.String),
189
+ hops: Schema.Number,
190
+ since: Schema.Number,
191
+ key: Schema.NullOr(Schema.String),
192
+ said: Schema.optionalKey(Schema.Struct(moveOptions)),
193
+ })),
194
+ holding: Schema.NullOr(RouteSchema),
195
+ loads: Schema.Number,
196
+ data: holdingSchema(routes),
197
+ reload: Schema.Number,
198
+ attached: Schema.Boolean,
199
+ loaded: Schema.Boolean,
200
+ }),
201
+ message: Schema.TaggedUnion({
202
+ Pushed: { route: RouteSchema, ...moveOptions },
203
+ Replaced: { route: RouteSchema, ...moveOptions },
204
+ Went: { by: Schema.Number },
205
+ WentBackTo: { route: RouteSchema },
206
+ Arrived: {
207
+ route: RouteSchema,
208
+ key: Schema.NullOr(Schema.String),
209
+ url: Schema.String,
210
+ hash: Schema.String,
211
+ },
212
+ Attached: { session: Schema.String },
213
+ Left: { url: Schema.String },
214
+ Waited: { id: Schema.Number },
215
+ Loaded: { id: Schema.Number, known: KnownSchema },
216
+ Redirected: { id: Schema.Number, route: RouteSchema },
217
+ Reloaded: {},
218
+ Preloading: { route: RouteSchema },
219
+ ...placeMessages(routes),
220
+ }),
221
+ });
222
+ const current = (model) => model.entries[model.at];
223
+ /** The Route drawn: the current entry's, or the one a held Back or Forward still draws. */
224
+ const shown = (model) => model.holding ?? current(model).route;
225
+ /** A key no entry has had, in this page load or any before it. */
226
+ const fresh = (model) => `${model.session}.${model.next}`;
227
+ /** Where an entry is in the address: its Route's URL, and its hash. */
228
+ const addressOf = (entry) => `${href(entry.route)}${entry.hash ? `#${entry.hash}` : ''}`;
229
+ const writing = (model, replace) => write({
230
+ url: addressOf(current(model)),
231
+ key: current(model).key,
232
+ replace,
233
+ });
234
+ /** The model with the data of the places on the Route it draws: each new place starts from what is known. */
235
+ const withData = (model, known) => ({
236
+ ...model,
237
+ data: holdingAt(routes, shown(model), model.data, known, model.reload),
238
+ });
239
+ /**
240
+ * A move landing, with what its new places start from. A Redirect or not
241
+ * found on the entry shown keeps the move that led to it, now to where it
242
+ * ends.
243
+ */
244
+ const land = (model, to, how, known, along = {}) => {
245
+ const out = landing(model, to, how, along);
246
+ return { ...out, model: withData(out.model, known) };
247
+ };
248
+ const landing = (model, to, how, { key = null, said } = {}) => {
249
+ const settled = { ...model, pending: null, holding: null };
250
+ const from = shown(model);
251
+ // Back and Forward are at their entry already: the move only lands.
252
+ if (how === 'back' || how === 'forward')
253
+ return { model: { ...settled, move: { from, to, how } } };
254
+ if (how === 'load') {
255
+ const changed = !same(to, from);
256
+ const next = {
257
+ ...settled,
258
+ entries: settled.entries.map((entry, at) => at === model.at ? entryOf(entry.key, to, entry.hash) : entry),
259
+ move: changed ? { ...model.move, to } : model.move,
260
+ };
261
+ return changed
262
+ ? { model: next, command: writing(next, true) }
263
+ : { model: next };
264
+ }
265
+ if (how === 'replace') {
266
+ // A new entry in the place of the one shown, with a key of its own:
267
+ // what was kept for the one before is not its.
268
+ const next = {
269
+ ...settled,
270
+ entries: settled.entries.map((entry, at) => at === model.at ? entryOf(fresh(model), to, said?.hash) : entry),
271
+ next: model.next + 1,
272
+ move: { from, to, how: 'replace', ...scrollOf(said) },
273
+ };
274
+ return { model: next, command: writing(next, true) };
275
+ }
276
+ // A URL the app did not write is an entry the address already holds,
277
+ // even at the Route shown: a redirect's is written over.
278
+ if (how !== 'arrive' &&
279
+ same(to, current(model).route) &&
280
+ (said?.hash ?? '') === (current(model).hash ?? ''))
281
+ return { model: settled };
282
+ const next = {
283
+ ...settled,
284
+ entries: [
285
+ ...model.entries.slice(0, model.at + 1),
286
+ entryOf(key ?? fresh(model), to, said?.hash),
287
+ ],
288
+ at: model.at + 1,
289
+ next: key === null ? model.next + 1 : model.next,
290
+ move: { from, to, how: 'push', ...scrollOf(said) },
291
+ };
292
+ return { model: next, command: writing(next, how === 'arrive') };
293
+ };
294
+ /**
295
+ * A move to `route`: it lands at once if every place it brings with data
296
+ * is already on screen; otherwise it waits, pending, while the `load`
297
+ * Worker loads the rest, at most `delay`.
298
+ */
299
+ const toward = (model, route, how, now, along = {}) => {
300
+ const drawn = new Set(routes.places(shown(model)).map((place) => place.key));
301
+ const wanted = routes
302
+ .places(route)
303
+ .filter((place) => place.input && !drawn.has(place.key))
304
+ .map((place) => place.name);
305
+ if (wanted.length === 0)
306
+ return land(model, route, how, {}, along);
307
+ const pending = {
308
+ id: model.loads,
309
+ to: route,
310
+ how,
311
+ wanted,
312
+ hops: along.hops ?? 0,
313
+ since: now,
314
+ key: along.key ?? null,
315
+ ...(along.said ? { said: along.said } : {}),
316
+ };
317
+ return { model: { ...model, pending, loads: model.loads + 1 } };
318
+ };
319
+ /**
320
+ * To entry `to` of the history, at `route`: the history moves at once, as
321
+ * the address does, and the Route shown stays drawn while the move is
322
+ * held, so anything that replaces the move starts from where the address is.
323
+ */
324
+ const reaching = (model, to, route, now) => toward({
325
+ ...model,
326
+ at: to,
327
+ entries: model.entries.map((entry, at) => at === to ? { key: entry.key, route } : entry),
328
+ holding: shown(model),
329
+ }, route, to < model.at ? 'back' : 'forward', now);
330
+ /** Back or Forward by `by` entries, the address too. */
331
+ const going = (model, by, now) => {
332
+ const to = Math.min(Math.max(model.at + by, 0), model.entries.length - 1);
333
+ if (to === model.at)
334
+ return {};
335
+ const moving = reaching(model, to, model.entries[to].route, now);
336
+ return {
337
+ ...moving,
338
+ command: [
339
+ ...[].concat(moving.command ?? []),
340
+ go({ by: to - model.at }),
341
+ ],
342
+ };
343
+ };
344
+ const firstModel = (from) => {
345
+ const route = from?.route ?? start();
346
+ return {
347
+ entries: [{ key: 'm.0', route }],
348
+ at: 0,
349
+ session: 'm',
350
+ next: 1,
351
+ move: { from: null, to: route, how: 'load' },
352
+ pending: null,
353
+ holding: null,
354
+ loads: 0,
355
+ data: holdingAt(routes, route, {}, from?.known ?? {}, 0),
356
+ reload: 0,
357
+ attached: false,
358
+ loaded: false,
359
+ };
360
+ };
361
+ /**
362
+ * Starts loading the code of the lazy View of every page on `route`, so it
363
+ * loads beside the page's data rather than after it. Code that fails to
364
+ * load is loaded again when it is drawn.
365
+ */
366
+ const preloadViews = (route) => Effect.sync(() => {
367
+ let build = root;
368
+ let value = route;
369
+ while (build && value) {
370
+ build = build.pages[value._tag];
371
+ if (build)
372
+ lazyOf(build)
373
+ ?.preload()
374
+ .catch(() => { });
375
+ value = value['page'];
376
+ }
377
+ });
378
+ /**
379
+ * Loads what the pending move wants, through `Loading`: with none, it
380
+ * lands without. The code of the lazy Views it will draw starts loading
381
+ * at the same time, so it is not a second wait after the data. A Redirect,
382
+ * the outermost first, moves on instead.
383
+ */
384
+ const load = (pending, send) => Effect.gen(function* () {
385
+ yield* preloadViews(pending.to);
386
+ const found = yield* Effect.serviceOption(Loading);
387
+ const { id } = pending;
388
+ if (Option.isNone(found))
389
+ return yield* send({ _tag: 'Loaded', id, known: {} });
390
+ const places = routes
391
+ .places(pending.to)
392
+ .filter((place) => pending.wanted.includes(place.name));
393
+ const answers = yield* found.value.load(places.map(({ name, params }) => ({ name, params })));
394
+ for (const { name } of places) {
395
+ const answer = answers[name];
396
+ if (answer?._tag === 'Redirect')
397
+ return yield* send(answer.href === undefined
398
+ ? { _tag: 'Redirected', id, route: answer.route }
399
+ : { _tag: 'RedirectedAway', href: answer.href, id });
400
+ }
401
+ const known = {};
402
+ const now = yield* Clock.currentTimeMillis;
403
+ for (const { name } of places) {
404
+ const each = answers[name] && knownOf(answers[name], now, 'browser');
405
+ if (each)
406
+ known[name] = each;
407
+ }
408
+ yield* send({ _tag: 'Loaded', id, known });
409
+ });
410
+ /** Warms the Data cache, and the code of the lazy Views, for a Route the app may move to. */
411
+ const preload = Command.make('preload', {
412
+ args: Schema.Struct({ route: RouteSchema }),
413
+ run: ({ route }) => Effect.gen(function* () {
414
+ yield* preloadViews(route);
415
+ const found = yield* Effect.serviceOption(Loading);
416
+ if (Option.isNone(found) || !found.value.preload)
417
+ return;
418
+ yield* found.value.preload(routes
419
+ .places(route)
420
+ .filter((place) => place.input)
421
+ .map(({ name, params }) => ({ name, params })));
422
+ }),
423
+ });
424
+ const reloading = (model) => ({
425
+ model: { ...model, reload: model.reload + 1 },
426
+ command: forget(),
427
+ });
428
+ const built = Actor.build(actor, {
429
+ init: (from) => ({
430
+ model: firstModel(from),
431
+ }),
432
+ children: {
433
+ page: {
434
+ actor: root,
435
+ props: ({ model }) => {
436
+ const route = shown(model);
437
+ const found = route._tag !== 'NotFound';
438
+ return {
439
+ params: {},
440
+ query: {},
441
+ route: {
442
+ name: '',
443
+ ...(found ? { page: route } : {}),
444
+ params: {},
445
+ notFound: found ? null : String(route['path']),
446
+ data: shownOf(routes, shown(model), model.data, model.reload),
447
+ },
448
+ };
449
+ },
450
+ },
451
+ },
452
+ provides: (self) => Layer.succeed(Navigation, {
453
+ push: (to, options) => self.send({
454
+ _tag: 'Pushed',
455
+ route: to,
456
+ ...options,
457
+ }),
458
+ replace: (to, options) => self.send({
459
+ _tag: 'Replaced',
460
+ route: to,
461
+ ...options,
462
+ }),
463
+ back: self.send({ _tag: 'Went', by: -1 }),
464
+ forward: self.send({ _tag: 'Went', by: 1 }),
465
+ backTo: (to) => self.send({ _tag: 'WentBackTo', route: to }),
466
+ reload: self.send({ _tag: 'Reloaded' }),
467
+ preload: (to) => self.send({ _tag: 'Preloading', route: to }),
468
+ }),
469
+ on: {
470
+ Pushed: (message, { model, at }) => {
471
+ const { route } = message;
472
+ const said = saidOf(message);
473
+ if (model.pending && same(route, model.pending.to))
474
+ return {};
475
+ // A link to the page shown loads it again: a failed page retries.
476
+ if (same(route, current(model).route) &&
477
+ (said.hash ?? '') === (current(model).hash ?? ''))
478
+ return reloading({ ...model, pending: null });
479
+ return toward(model, route, 'push', at, { said });
480
+ },
481
+ Replaced: (message, { model, at }) => toward(model, message.route, 'replace', at, { said: saidOf(message) }),
482
+ Went: ({ by }, { model, at }) => going(model, by, at),
483
+ WentBackTo: ({ route }, { model, at }) => {
484
+ const before = model.entries[model.at - 1];
485
+ if (before && same(before.route, route))
486
+ return going(model, -1, at);
487
+ return toward(model, route, 'replace', at);
488
+ },
489
+ Arrived: ({ route, key, url, hash, }, { model, at: now }) => {
490
+ // A URL that is not how the Route is written, a redirect's or one
491
+ // with a stray slash, is replaced by the one that is. On the entry
492
+ // the app opened at or holds, one that only has a query no page
493
+ // reads is left as it is.
494
+ const kept = shows(url, route);
495
+ const known = key === null
496
+ ? -1
497
+ : model.entries.findIndex((entry) => entry.key === key);
498
+ if (known >= 0) {
499
+ if (known === model.at && same(route, current(model).route))
500
+ return kept ? {} : { command: writing(model, true) };
501
+ const moving = reaching(model, known, route, now);
502
+ return kept
503
+ ? moving
504
+ : {
505
+ ...moving,
506
+ command: [
507
+ ...[].concat(moving.command ?? []),
508
+ write({
509
+ url: addressOf(entryOf(key, route, hash)),
510
+ key: key,
511
+ replace: true,
512
+ }),
513
+ ],
514
+ };
515
+ }
516
+ if (!model.loaded) {
517
+ // The URL the app opened at: this entry, with the key the address
518
+ // kept for it before a reload, else a new one.
519
+ const opened = {
520
+ ...model,
521
+ loaded: true,
522
+ entries: model.entries.map((entry, at) => at === model.at
523
+ ? entryOf(key ?? fresh(model), entry.route, hash)
524
+ : entry),
525
+ next: key === null ? model.next + 1 : model.next,
526
+ };
527
+ if (same(route, current(opened).route))
528
+ return {
529
+ model: opened,
530
+ command: kept
531
+ ? write({
532
+ url: `${withBase(url)}${hash ? `#${hash}` : ''}`,
533
+ key: current(opened).key,
534
+ replace: true,
535
+ })
536
+ : writing(opened, true),
537
+ };
538
+ return toward(opened, route, 'load', now);
539
+ }
540
+ // A URL the app did not write this page load: typed in, a link from
541
+ // the rest of the site, or an entry from before a reload, whose key
542
+ // it keeps.
543
+ return toward(model, route, 'arrive', now, {
544
+ key,
545
+ ...(hash ? { said: { hash } } : {}),
546
+ });
547
+ },
548
+ Attached: ({ session }, { model }) => ({
549
+ model: { ...model, attached: true, session },
550
+ }),
551
+ Left: ({ url }) => ({
552
+ command: leave({ url }),
553
+ }),
554
+ // Still loading after the delay: the move lands without the data it
555
+ // waits on, and each new place shows it is loading, then loads it.
556
+ Waited: ({ id }, { model }) => {
557
+ const pending = model.pending;
558
+ if (pending?.id !== id)
559
+ return {};
560
+ return land(model, pending.to, pending.how, {}, pending);
561
+ },
562
+ Loaded: ({ id, known, }, { model }) => {
563
+ const pending = model.pending;
564
+ if (pending?.id !== id)
565
+ return {};
566
+ return land(model, pending.to, pending.how, known, pending);
567
+ },
568
+ Redirected: ({ id, route }, { model, at }) => {
569
+ const pending = model.pending;
570
+ if (pending?.id !== id)
571
+ return {};
572
+ const settled = { ...model, pending: null };
573
+ const how = pending.how === 'back' || pending.how === 'forward'
574
+ ? 'replace'
575
+ : pending.how;
576
+ if (pending.hops < HOPS)
577
+ return toward(settled, route, how, at, { hops: pending.hops + 1 });
578
+ return land(settled, notFound(pending.to), how, {});
579
+ },
580
+ Reloaded: (_, { model }) => reloading(model),
581
+ Preloading: ({ route }, { model }) =>
582
+ // The page shown, or the move held, is loading it already.
583
+ same(route, shown(model)) ||
584
+ (model.pending !== null && same(route, model.pending.to))
585
+ ? {}
586
+ : { command: preload({ route }) },
587
+ PlaceLoaded: (message, { model }) => ({ model: { ...model, data: settle(model.data, message) } }),
588
+ PlaceFailed: (message, { model }) => ({ model: { ...model, data: settle(model.data, message) } }),
589
+ // A Loader's Redirect for a page on screen replaces the entry.
590
+ PlaceRedirected: ({ route }, { model, at }) => toward(model, route, 'replace', at),
591
+ // A Loader's Redirect out of the app: the move it ends, if any, is
592
+ // dropped, and the URL takes the entry's place. In memory, nothing is
593
+ // left.
594
+ RedirectedAway: ({ href, id }, { model }) => ({
595
+ model: id !== undefined && model.pending?.id === id
596
+ ? { ...model, pending: null }
597
+ : model,
598
+ command: leave({ url: href, replace: true }),
599
+ }),
600
+ },
601
+ workers: {
602
+ // Loads the data of each place on screen whose data is due.
603
+ place: {
604
+ each: ({ model }) => dueRuns(routes, shown(model), model.data, model.reload),
605
+ run: (self, run) => loadPlace(routes, run, {
606
+ send: self.send,
607
+ get: Effect.map(self.get, ({ model }) => ({
608
+ route: shown(model),
609
+ holding: model.data,
610
+ reload: model.reload,
611
+ })),
612
+ now: self.now,
613
+ }),
614
+ },
615
+ // Loads the data the pending move wants; a newer move stops it.
616
+ load: {
617
+ each: ({ model }) => model.pending ? [String(model.pending.id)] : [],
618
+ run: (self) => Effect.gen(function* () {
619
+ const { pending } = (yield* self.get).model;
620
+ if (pending)
621
+ yield* load(pending, self.send);
622
+ }),
623
+ },
624
+ // Lands a move still loading after the delay.
625
+ wait: {
626
+ each: ({ model }) => model.pending ? [String(model.pending.id)] : [],
627
+ run: (self) => Effect.gen(function* () {
628
+ const { pending } = (yield* self.get).model;
629
+ if (!pending)
630
+ return;
631
+ const now = yield* self.now;
632
+ yield* Effect.sleep(Math.max(0, pending.since + delay - now));
633
+ yield* self.send({ _tag: 'Waited', id: pending.id });
634
+ }),
635
+ },
636
+ // Keeps what the pages drawn first were handed, as a server's page
637
+ // hands them, in the Data cache: Back to them is at once, as to a
638
+ // page loaded in the browser.
639
+ keep: {
640
+ run: (self) => Effect.gen(function* () {
641
+ const found = yield* Effect.serviceOption(Loading);
642
+ if (Option.isNone(found) || !found.value.keep)
643
+ return;
644
+ const { model } = yield* self.get;
645
+ if (model.move.how !== 'load')
646
+ return;
647
+ for (const { name, params, input } of routes.places(shown(model))) {
648
+ const status = model.data[name]?.status;
649
+ // Arrived when the page says, so the copy goes stale when
650
+ // the page says it does.
651
+ if (input && status?._tag === 'Ready')
652
+ yield* found.value.keep({ name, params }, status.data, status.info.arrivedAt);
653
+ }
654
+ }),
655
+ },
656
+ // Hears the address, if an Adapter holds one: Back, Forward, a typed URL.
657
+ address: {
658
+ run: (self) => Effect.gen(function* () {
659
+ const found = yield* Effect.serviceOption(Adapter);
660
+ if (Option.isNone(found))
661
+ return;
662
+ const at = found.value;
663
+ if (!(yield* self.get).model.attached)
664
+ yield* self.send({
665
+ _tag: 'Attached',
666
+ session: Math.random().toString(36).slice(2, 10),
667
+ });
668
+ const changes = Stream.callback((queue) => Effect.acquireRelease(Effect.sync(() => {
669
+ Queue.offerUnsafe(queue, null);
670
+ return at.listen(() => void Queue.offerUnsafe(queue, null));
671
+ }), (stop) => Effect.sync(stop)));
672
+ yield* Stream.runForEach(changes, () => Effect.gen(function* () {
673
+ const url = inside(at.url());
674
+ if (url === undefined)
675
+ return;
676
+ const { model } = yield* self.get;
677
+ const entry = current(model);
678
+ const target = model.pending?.to;
679
+ // The Router's own write coming back, or the address of a move still loading: nothing moved.
680
+ if (at.key() === entry.key && shows(url, entry.route))
681
+ return;
682
+ if (target && url === routes.path(target))
683
+ return;
684
+ yield* self.send({
685
+ _tag: 'Arrived',
686
+ route: routes.read(url).route,
687
+ key: at.key(),
688
+ url,
689
+ hash: at.hash?.() ?? '',
690
+ });
691
+ }));
692
+ }),
693
+ },
694
+ },
695
+ });
696
+ const routerView = View.make(built, (self) => {
697
+ const model = self.model;
698
+ const send = self.send;
699
+ const onClick = (event) => {
700
+ const link = linkOf(event);
701
+ if (!link)
702
+ return;
703
+ const url = new URL(link.href);
704
+ if (url.origin !== location.origin)
705
+ return;
706
+ const target = `${url.pathname}${url.search}`;
707
+ // A link to a spot on this page is the browser's to scroll to.
708
+ if (url.hash && target === `${location.pathname}${location.search}`)
709
+ return;
710
+ const within = inside(target);
711
+ if (within === undefined) {
712
+ // Outside the app: the Adapter hands it on; in memory, the browser goes.
713
+ if (!model.attached)
714
+ return;
715
+ event.preventDefault();
716
+ send({ _tag: 'Left', url: target });
717
+ return;
718
+ }
719
+ event.preventDefault();
720
+ send({
721
+ _tag: 'Pushed',
722
+ route: routes.read(within).route,
723
+ ...linkOptions(link, url),
724
+ });
725
+ };
726
+ const drawn = shown(model);
727
+ // The Metadata follows the data of the pages drawn, not only the move.
728
+ const metadata = routes.metadata(drawn, readyOf(shownOf(routes, drawn, model.data, model.reload)));
729
+ return (_jsx(Frame, { drawing: {
730
+ href: href,
731
+ entries: model.entries,
732
+ at: model.at,
733
+ move: model.move,
734
+ pending: model.pending
735
+ ? {
736
+ to: model.pending.to,
737
+ how: model.pending.how === 'arrive' ? 'push' : model.pending.how,
738
+ }
739
+ : null,
740
+ metadata,
741
+ go: (by) => send({ _tag: 'Went', by }),
742
+ reload: () => send({ _tag: 'Reloaded' }),
743
+ preload: (route) => send({ _tag: 'Preloading', route: route }),
744
+ linkPreload: routes.options.preload,
745
+ }, attached: model.attached, root: self.root, onClick: onClick, children: _jsx(Scrolling, { options: scrolling, at: {
746
+ route: drawn,
747
+ path: routes.path(drawn),
748
+ key: current(model).key,
749
+ }, hash: current(model).hash, move: model.move, pending: model.pending !== null, attached: model.attached, loaded: model.loaded, children: (() => {
750
+ const RootView = view;
751
+ return (_jsx(RootView, { instance: self.children
752
+ .page }));
753
+ })() }) }));
754
+ });
755
+ /**
756
+ * The Router's Input at a URL, with what its places already start from:
757
+ * for `toReact`'s `input`, so an app on an Adapter is drawn at the right
758
+ * page from the first frame, and for a server, which draws a URL from what
759
+ * its Loaders gave. With no URL, the browser's, if there is one. Places
760
+ * that start from nothing load once the Runtime starts.
761
+ */
762
+ const startAt = (url, known) => {
763
+ const at = url ??
764
+ (typeof location === 'undefined'
765
+ ? undefined
766
+ : `${location.pathname}${location.search}`);
767
+ if (at === undefined)
768
+ return undefined;
769
+ const within = inside(at);
770
+ if (within === undefined)
771
+ return undefined;
772
+ const route = routes.read(within).route;
773
+ return known ? { route, known } : { route };
774
+ };
775
+ /** A key the Router never gives: what a custom key gives back when it keeps the entry's own. */
776
+ const ENTRY = '\u0000entry';
777
+ /**
778
+ * The script a server writes in a page at `route`, run before the app
779
+ * hydrates, that puts its scroll back from the tab's session storage, as
780
+ * TanStack Start's does; `null` where scroll restoration is off.
781
+ */
782
+ const restoreScroll = (route) => {
783
+ if (!restores(scrolling, route))
784
+ return null;
785
+ const key = keyOf(scrolling, {
786
+ route,
787
+ path: routes.path(route),
788
+ key: ENTRY,
789
+ });
790
+ return scriptFor(KEY, key === ENTRY ? undefined : key);
791
+ };
792
+ return {
793
+ actor: built,
794
+ view: routerView,
795
+ startAt,
796
+ href,
797
+ restoreScroll,
798
+ /** The Routes it serves: what Start reads a URL and serves the Loaders with. */
799
+ routes,
800
+ };
801
+ };