@kestack/kabuki 0.0.0 → 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.
- package/README.md +156 -33
- package/dist/core/actor/actor.d.ts +24 -13
- package/dist/core/actor/actor.d.ts.map +1 -1
- package/dist/core/actor/actor.js +37 -26
- package/dist/core/actor/index.d.ts +2 -2
- package/dist/core/actor/index.d.ts.map +1 -1
- package/dist/core/actor/types.d.ts +91 -47
- package/dist/core/actor/types.d.ts.map +1 -1
- package/dist/core/history/history.d.ts +11 -3
- package/dist/core/history/history.d.ts.map +1 -1
- package/dist/core/history/history.js +106 -27
- package/dist/core/history/replay.d.ts +2 -0
- package/dist/core/history/replay.d.ts.map +1 -1
- package/dist/core/history/replay.js +1 -0
- package/dist/core/history/saved.d.ts +1 -1
- package/dist/core/history/saved.d.ts.map +1 -1
- package/dist/core/history/saved.js +13 -9
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/snapshot/invoke.d.ts +2 -3
- package/dist/core/snapshot/invoke.d.ts.map +1 -1
- package/dist/core/snapshot/invoke.js +42 -49
- package/dist/core/snapshot/restore.js +8 -7
- package/dist/core/snapshot/tree.d.ts +1 -1
- package/dist/core/snapshot/tree.d.ts.map +1 -1
- package/dist/core/snapshot/tree.js +1 -1
- package/dist/react/app.d.ts +30 -3
- package/dist/react/app.d.ts.map +1 -1
- package/dist/react/app.js +119 -40
- package/dist/react/drawn.d.ts +1 -1
- package/dist/react/drawn.d.ts.map +1 -1
- package/dist/react/drawn.js +2 -1
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +2 -0
- package/dist/react/lazy.d.ts +30 -0
- package/dist/react/lazy.d.ts.map +1 -0
- package/dist/react/lazy.js +56 -0
- package/dist/react/live.d.ts +7 -0
- package/dist/react/live.d.ts.map +1 -0
- package/dist/react/live.js +13 -0
- package/dist/react/view.d.ts +26 -6
- package/dist/react/view.d.ts.map +1 -1
- package/dist/react/view.js +17 -5
- package/dist/routing/adapter/adapter.d.ts +38 -0
- package/dist/routing/adapter/adapter.d.ts.map +1 -0
- package/dist/routing/adapter/adapter.js +10 -0
- package/dist/routing/adapter/browser/browser.d.ts +8 -0
- package/dist/routing/adapter/browser/browser.d.ts.map +1 -0
- package/dist/routing/adapter/browser/browser.js +33 -0
- package/dist/routing/adapter/browser/index.d.ts +2 -0
- package/dist/routing/adapter/browser/index.d.ts.map +1 -0
- package/dist/routing/adapter/browser/index.js +1 -0
- package/dist/routing/adapter/index.d.ts +3 -0
- package/dist/routing/adapter/index.d.ts.map +1 -0
- package/dist/routing/adapter/index.js +1 -0
- package/dist/routing/adapter/tanstack/index.d.ts +3 -0
- package/dist/routing/adapter/tanstack/index.d.ts.map +1 -0
- package/dist/routing/adapter/tanstack/index.js +1 -0
- package/dist/routing/adapter/tanstack/tanstack.d.ts +27 -0
- package/dist/routing/adapter/tanstack/tanstack.d.ts.map +1 -0
- package/dist/routing/adapter/tanstack/tanstack.js +38 -0
- package/dist/routing/capability/cache.d.ts +12 -0
- package/dist/routing/capability/cache.d.ts.map +1 -0
- package/dist/routing/capability/cache.js +170 -0
- package/dist/routing/capability/index.d.ts +6 -0
- package/dist/routing/capability/index.d.ts.map +1 -0
- package/dist/routing/capability/index.js +3 -0
- package/dist/routing/capability/loading.d.ts +82 -0
- package/dist/routing/capability/loading.d.ts.map +1 -0
- package/dist/routing/capability/loading.js +53 -0
- package/dist/routing/capability/navigation.d.ts +66 -0
- package/dist/routing/capability/navigation.d.ts.map +1 -0
- package/dist/routing/capability/navigation.js +65 -0
- package/dist/routing/index.d.ts +10 -0
- package/dist/routing/index.d.ts.map +1 -0
- package/dist/routing/index.js +4 -0
- package/dist/routing/page/index.d.ts +5 -0
- package/dist/routing/page/index.d.ts.map +1 -0
- package/dist/routing/page/index.js +2 -0
- package/dist/routing/page/page.d.ts +151 -0
- package/dist/routing/page/page.d.ts.map +1 -0
- package/dist/routing/page/page.js +121 -0
- package/dist/routing/page/status.d.ts +121 -0
- package/dist/routing/page/status.d.ts.map +1 -0
- package/dist/routing/page/status.js +69 -0
- package/dist/routing/route/index.d.ts +11 -0
- package/dist/routing/route/index.d.ts.map +1 -0
- package/dist/routing/route/index.js +6 -0
- package/dist/routing/route/loader.d.ts +98 -0
- package/dist/routing/route/loader.d.ts.map +1 -0
- package/dist/routing/route/loader.js +17 -0
- package/dist/routing/route/metadata.d.ts +44 -0
- package/dist/routing/route/metadata.d.ts.map +1 -0
- package/dist/routing/route/metadata.js +53 -0
- package/dist/routing/route/node.d.ts +162 -0
- package/dist/routing/route/node.d.ts.map +1 -0
- package/dist/routing/route/node.js +3 -0
- package/dist/routing/route/places.d.ts +15 -0
- package/dist/routing/route/places.d.ts.map +1 -0
- package/dist/routing/route/places.js +87 -0
- package/dist/routing/route/route.d.ts +53 -0
- package/dist/routing/route/route.d.ts.map +1 -0
- package/dist/routing/route/route.js +148 -0
- package/dist/routing/route/url.d.ts +13 -0
- package/dist/routing/route/url.d.ts.map +1 -0
- package/dist/routing/route/url.js +79 -0
- package/dist/routing/route/values.d.ts +12 -0
- package/dist/routing/route/values.d.ts.map +1 -0
- package/dist/routing/route/values.js +47 -0
- package/dist/routing/router/context.d.ts +123 -0
- package/dist/routing/router/context.d.ts.map +1 -0
- package/dist/routing/router/context.js +156 -0
- package/dist/routing/router/data.d.ts +117 -0
- package/dist/routing/router/data.d.ts.map +1 -0
- package/dist/routing/router/data.js +247 -0
- package/dist/routing/router/define.d.ts +52 -0
- package/dist/routing/router/define.d.ts.map +1 -0
- package/dist/routing/router/define.js +48 -0
- package/dist/routing/router/frame.d.ts +10 -0
- package/dist/routing/router/frame.d.ts.map +1 -0
- package/dist/routing/router/frame.js +10 -0
- package/dist/routing/router/index.d.ts +7 -0
- package/dist/routing/router/index.d.ts.map +1 -0
- package/dist/routing/router/index.js +3 -0
- package/dist/routing/router/router.d.ts +165 -0
- package/dist/routing/router/router.d.ts.map +1 -0
- package/dist/routing/router/router.js +801 -0
- package/dist/routing/routing.d.ts +72 -0
- package/dist/routing/routing.d.ts.map +1 -0
- package/dist/routing/routing.js +53 -0
- package/dist/routing/scroll/index.d.ts +4 -0
- package/dist/routing/scroll/index.d.ts.map +1 -0
- package/dist/routing/scroll/index.js +2 -0
- package/dist/routing/scroll/script.d.ts +6 -0
- package/dist/routing/scroll/script.d.ts.map +1 -0
- package/dist/routing/scroll/script.js +52 -0
- package/dist/routing/scroll/scroll.d.ts +75 -0
- package/dist/routing/scroll/scroll.d.ts.map +1 -0
- package/dist/routing/scroll/scroll.js +201 -0
- package/dist/routing/scroll/selector.d.ts +14 -0
- package/dist/routing/scroll/selector.d.ts.map +1 -0
- package/dist/routing/scroll/selector.js +40 -0
- package/dist/routing/scroll/session.d.ts +6 -0
- package/dist/routing/scroll/session.d.ts.map +1 -0
- package/dist/routing/scroll/session.js +23 -0
- package/dist/start/client/client.d.ts +11 -0
- package/dist/start/client/client.d.ts.map +1 -0
- package/dist/start/client/client.js +24 -0
- package/dist/start/client/index.d.ts +3 -0
- package/dist/start/client/index.d.ts.map +1 -0
- package/dist/start/client/index.js +2 -0
- package/dist/start/client/rpc.d.ts +14 -0
- package/dist/start/client/rpc.d.ts.map +1 -0
- package/dist/start/client/rpc.js +70 -0
- package/dist/start/common/app.d.ts +66 -0
- package/dist/start/common/app.d.ts.map +1 -0
- package/dist/start/common/app.js +23 -0
- package/dist/start/common/common.d.ts +26 -0
- package/dist/start/common/common.d.ts.map +1 -0
- package/dist/start/common/common.js +19 -0
- package/dist/start/common/document.d.ts +44 -0
- package/dist/start/common/document.d.ts.map +1 -0
- package/dist/start/common/document.js +58 -0
- package/dist/start/common/index.d.ts +9 -0
- package/dist/start/common/index.d.ts.map +1 -0
- package/dist/start/common/index.js +5 -0
- package/dist/start/common/page.d.ts +67 -0
- package/dist/start/common/page.d.ts.map +1 -0
- package/dist/start/common/page.js +29 -0
- package/dist/start/common/wire.d.ts +37 -0
- package/dist/start/common/wire.d.ts.map +1 -0
- package/dist/start/common/wire.js +73 -0
- package/dist/start/platform/cloudflare/cloudflare.d.ts +8 -0
- package/dist/start/platform/cloudflare/cloudflare.d.ts.map +1 -0
- package/dist/start/platform/cloudflare/cloudflare.js +31 -0
- package/dist/start/platform/cloudflare/index.d.ts +2 -0
- package/dist/start/platform/cloudflare/index.d.ts.map +1 -0
- package/dist/start/platform/cloudflare/index.js +1 -0
- package/dist/start/platform/index.d.ts +2 -0
- package/dist/start/platform/index.d.ts.map +1 -0
- package/dist/start/platform/index.js +1 -0
- package/dist/start/platform/platform.d.ts +31 -0
- package/dist/start/platform/platform.d.ts.map +1 -0
- package/dist/start/platform/platform.js +13 -0
- package/dist/start/server/cache.d.ts +4 -0
- package/dist/start/server/cache.d.ts.map +1 -0
- package/dist/start/server/cache.js +26 -0
- package/dist/start/server/draw.d.ts +10 -0
- package/dist/start/server/draw.d.ts.map +1 -0
- package/dist/start/server/draw.js +75 -0
- package/dist/start/server/index.d.ts +3 -0
- package/dist/start/server/index.d.ts.map +1 -0
- package/dist/start/server/index.js +2 -0
- package/dist/start/server/layers.d.ts +97 -0
- package/dist/start/server/layers.d.ts.map +1 -0
- package/dist/start/server/layers.js +37 -0
- package/dist/start/server/respond.d.ts +15 -0
- package/dist/start/server/respond.d.ts.map +1 -0
- package/dist/start/server/respond.js +35 -0
- package/dist/start/server/rpc.d.ts +11 -0
- package/dist/start/server/rpc.d.ts.map +1 -0
- package/dist/start/server/rpc.js +51 -0
- package/dist/start/server/server.d.ts +13 -0
- package/dist/start/server/server.d.ts.map +1 -0
- package/dist/start/server/server.js +138 -0
- package/dist/start/server/tiers.d.ts +14 -0
- package/dist/start/server/tiers.d.ts.map +1 -0
- package/dist/start/server/tiers.js +31 -0
- package/dist/start/vite/dev.d.ts +5 -0
- package/dist/start/vite/dev.d.ts.map +1 -0
- package/dist/start/vite/dev.js +56 -0
- package/dist/start/vite/entries.d.ts +27 -0
- package/dist/start/vite/entries.d.ts.map +1 -0
- package/dist/start/vite/entries.js +66 -0
- package/dist/start/vite/files.d.ts +22 -0
- package/dist/start/vite/files.d.ts.map +1 -0
- package/dist/start/vite/files.js +23 -0
- package/dist/start/vite/index.d.ts +2 -0
- package/dist/start/vite/index.d.ts.map +1 -0
- package/dist/start/vite/index.js +1 -0
- package/dist/start/vite/lazy.d.ts +4 -0
- package/dist/start/vite/lazy.d.ts.map +1 -0
- package/dist/start/vite/lazy.js +30 -0
- package/dist/start/vite/manifest.d.ts +18 -0
- package/dist/start/vite/manifest.d.ts.map +1 -0
- package/dist/start/vite/manifest.js +42 -0
- package/dist/start/vite/styles.d.ts +7 -0
- package/dist/start/vite/styles.d.ts.map +1 -0
- package/dist/start/vite/styles.js +58 -0
- package/dist/start/vite/vite.d.ts +11 -0
- package/dist/start/vite/vite.d.ts.map +1 -0
- package/dist/start/vite/vite.js +217 -0
- 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
|
+
};
|