@sjcx/vorma 0.0.0-pre.1 → 0.0.0-pre.3
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/dist/build/assemble-artifacts.d.ts +1 -1
- package/dist/build/bundle-client.d.ts +1 -1
- package/dist/build/cli.js +31 -38
- package/dist/build/compile-application.d.ts +2 -2
- package/dist/build/development-artifacts.d.ts +2 -1
- package/dist/build/development-build.d.ts +3 -6
- package/dist/build/development-work.d.ts +2 -3
- package/dist/build/generate-application.d.ts +2 -2
- package/dist/build/index.d.ts +2 -3
- package/dist/build/index.js +1 -1
- package/dist/build/inspect-projects.d.ts +3 -3
- package/dist/build/load-config.d.ts +0 -2
- package/dist/build/publish-artifacts.d.ts +1 -4
- package/dist/build/render-client-transport.d.ts +1 -1
- package/dist/build/render-server-generated.d.ts +1 -12
- package/dist/build/render-types.d.ts +1 -1
- package/dist/build/resolve-config.d.ts +2 -4
- package/dist/build/source-layout.d.ts +1 -1
- package/dist/{build-DcP5_DUV.js → build-sHSh5zaX.js} +67 -282
- package/package.json +5 -36
- package/dist/build/compile-server.d.ts +0 -42
- package/dist/client/assembly.d.ts +0 -40
- package/dist/client/browser-http-client.d.ts +0 -10
- package/dist/client/client-loaders.d.ts +0 -93
- package/dist/client/commit.d.ts +0 -41
- package/dist/client/contracts.d.ts +0 -192
- package/dist/client/core.d.ts +0 -45
- package/dist/client/development.d.ts +0 -16
- package/dist/client/document-recovery.d.ts +0 -4
- package/dist/client/endpoints.d.ts +0 -54
- package/dist/client/errors.d.ts +0 -87
- package/dist/client/freshness.d.ts +0 -51
- package/dist/client/head.d.ts +0 -14
- package/dist/client/history.d.ts +0 -25
- package/dist/client/index.d.ts +0 -80
- package/dist/client/index.js +0 -150
- package/dist/client/links.d.ts +0 -48
- package/dist/client/materialize.d.ts +0 -21
- package/dist/client/modules.d.ts +0 -2
- package/dist/client/navigation.d.ts +0 -47
- package/dist/client/payload.d.ts +0 -54
- package/dist/client/prefetch.d.ts +0 -28
- package/dist/client/prepare.d.ts +0 -47
- package/dist/client/projections.d.ts +0 -26
- package/dist/client/queries.d.ts +0 -33
- package/dist/client/react/application.d.ts +0 -87
- package/dist/client/react/hooks.d.ts +0 -36
- package/dist/client/react/index.d.ts +0 -5
- package/dist/client/react/index.js +0 -3237
- package/dist/client/react/link.d.ts +0 -27
- package/dist/client/react/view.d.ts +0 -86
- package/dist/client/request.d.ts +0 -16
- package/dist/client/route-context.d.ts +0 -31
- package/dist/client/route-matching.d.ts +0 -22
- package/dist/client/route-synchronization.d.ts +0 -30
- package/dist/client/route-url.d.ts +0 -5
- package/dist/client/runtime.d.ts +0 -82
- package/dist/client/scroll.d.ts +0 -16
- package/dist/client/services.d.ts +0 -34
- package/dist/client/state.d.ts +0 -102
- package/dist/client/static-assets.d.ts +0 -3
- package/dist/client/stylesheets.d.ts +0 -7
- package/dist/client/transport-type-entries.d.ts +0 -2
- package/dist/client/types.d.ts +0 -152
- package/dist/client/view-definition.d.ts +0 -24
- package/dist/client/work-indicator.d.ts +0 -18
- package/dist/compile-Me8EOyMK.js +0 -870
- package/dist/development-B_rxuwY7.js +0 -128
- package/dist/hash-set-BPx3iXec.js +0 -141
- package/dist/href-Cqyax8Y0.js +0 -357
- package/dist/materialize-DZ7VSLFk.js +0 -739
- package/dist/metadata-BoPKo-G2.js +0 -32
- package/dist/primitives/tasks/index.d.ts +0 -1
- package/dist/primitives/tasks/index.js +0 -1
- package/dist/route-context-CtY_9U_W.js +0 -559
- package/dist/route-matching-D_uSG2P9.js +0 -33
- package/dist/search-parse-0f7oqVsG.js +0 -375
- package/dist/server/config/index.d.ts +0 -36
- package/dist/server/config/index.js +0 -30
- package/dist/server/development/metadata.d.ts +0 -11
- package/dist/server/document.d.ts +0 -37
- package/dist/server/form-data.d.ts +0 -21
- package/dist/server/graph/compile.d.ts +0 -118
- package/dist/server/graph/declarations.d.ts +0 -117
- package/dist/server/graph/literal-error-tag.d.ts +0 -14
- package/dist/server/graph/pattern-conflicts.d.ts +0 -36
- package/dist/server/graph/search-schema.d.ts +0 -13
- package/dist/server/graph/transport-schema.d.ts +0 -11
- package/dist/server/index.d.ts +0 -310
- package/dist/server/index.js +0 -2311
- package/dist/server/public-assets.d.ts +0 -18
- package/dist/server/runtime/application.d.ts +0 -15
- package/dist/server/runtime/assets.d.ts +0 -15
- package/dist/server/runtime/context.d.ts +0 -96
- package/dist/server/runtime/domain-errors.d.ts +0 -11
- package/dist/server/runtime/effects.d.ts +0 -83
- package/dist/server/runtime/execute.d.ts +0 -13
- package/dist/server/runtime/finalize.d.ts +0 -44
- package/dist/server/runtime/handler.d.ts +0 -106
- package/dist/server/runtime/head.d.ts +0 -22
- package/dist/server/runtime/payload.d.ts +0 -50
- package/dist/server/runtime/pipeline.d.ts +0 -27
- package/dist/server/runtime/plan.d.ts +0 -98
- package/dist/server/runtime/render-document.d.ts +0 -16
- package/dist/server/runtime/startup-error.d.ts +0 -11
- package/dist/server/runtime/transport-type-entries.d.ts +0 -2
- package/dist/server/runtime/vercel-deployment.d.ts +0 -2
- package/dist/server/url-pathname.d.ts +0 -13
- package/dist/static-assets-BrH3O0M8.js +0 -199
- package/dist/universal/generated-types.d.ts +0 -9
- package/dist/universal/href.d.ts +0 -17
- package/dist/universal/http-method.d.ts +0 -4
- package/dist/universal/json-transport.d.ts +0 -14
- package/dist/universal/null-prototype-record.d.ts +0 -7
- package/dist/universal/path-wire.d.ts +0 -71
- package/dist/universal/pattern-language.d.ts +0 -4
- package/dist/universal/protocol-schemas.d.ts +0 -99
- package/dist/universal/protocol.d.ts +0 -162
- package/dist/universal/search-grammar.d.ts +0 -7
- package/dist/universal/search-parse.d.ts +0 -22
- package/dist/universal/search-serialize.d.ts +0 -16
- package/dist/universal/search-shape.d.ts +0 -32
- package/dist/universal/static-assets.d.ts +0 -2
- package/dist/universal/transport-types/big-decimal.d.ts +0 -13
- package/dist/universal/transport-types/chunk.d.ts +0 -8
- package/dist/universal/transport-types/date-time-utc.d.ts +0 -13
- package/dist/universal/transport-types/date-time-zoned.d.ts +0 -13
- package/dist/universal/transport-types/duration.d.ts +0 -13
- package/dist/universal/transport-types/hash-map.d.ts +0 -8
- package/dist/universal/transport-types/hash-set.d.ts +0 -8
- package/dist/universal/transport-types/option.d.ts +0 -10
- package/dist/universal/transport-types/redacted.d.ts +0 -9
- package/dist/universal/value-transport.d.ts +0 -117
- package/dist/value-transport-Bst4Hk-n.js +0 -864
- /package/dist/{universal → build}/content-digest.d.ts +0 -0
|
@@ -1,3237 +0,0 @@
|
|
|
1
|
-
import { _ as DOCUMENT_PROTOCOL, b as HTTP_PROTOCOL, i as encodeTransportedValue, o as canonicalizeRequestPathForMatcher, r as decodeTransportedValue } from "../../value-transport-Bst4Hk-n.js";
|
|
2
|
-
import { A as routeStateAtom, B as RevalidationError, C as committedFrameAtom, D as projectRouteState, E as projectRouteError, F as HandlerError, H as getQueryRedirect, I as InputError, L as NavigationError, M as clientStateAtom, N as BuildMismatchError, P as CancelledByDedupeKeyError, R as PrefetchError, S as buildMismatchAtom, T as navigationStateAtom, V as TransportError, _ as CLIENT_TRANSPORT_TYPE_ENTRIES, a as readSynchronizationTarget, b as createLinkIntent, c as createWorkIndicator, d as ClientCoordinator, f as clientRuntime, g as toHref, h as universalRuntime, i as getRouteMatchAtom, j as workStateAtom, k as revalidationStateAtom, m as publishApplicationContexts, n as getClientLoaderDataAtom, o as runSynchronizationTarget, p as provideClientContext, r as getLoaderDataAtom, t as createRouteContext, u as CLIENT_UNAVAILABLE_ERROR, v as allowsInterception, x as getLinkStateAtomFor, y as attachPointerModalityTracking, z as QueryRedirected } from "../../route-context-CtY_9U_W.js";
|
|
3
|
-
import { h as encodeScalar, i as serializeSearchParams, t as buildHref } from "../../href-Cqyax8Y0.js";
|
|
4
|
-
import { i as readTransportedValue, n as readEndpointErrorEnvelope, r as readRedirectStatus, t as getStaticAssetDestination } from "../../static-assets-BrH3O0M8.js";
|
|
5
|
-
import { C as readDocumentHead, S as rememberScroll, _ as satisfyDemand, a as readDocumentStylesheets, b as readBrowserPosition, c as acknowledgeQuery, d as failAttempt, f as holdAttemptForRetry, g as retireDemand, h as releaseQuery, i as prepareStylesheets, l as canStartAttempt, m as registerDemand, n as materializeRoute, o as reloadDocument, p as makeRevalidationError, r as commitFrame, s as abandonAttempt, u as expectQueries, v as startAttempt, x as recallScroll, y as createEntryKey } from "../../materialize-DZ7VSLFk.js";
|
|
6
|
-
import { Brand, Cause, Context, Deferred, Duration, Effect, Exit, Fiber, FiberSet, Layer, Option, PubSub, RcMap, Result, Scope, Stream, identity } from "effect";
|
|
7
|
-
import { FetchHttpClient, Headers, HttpBody, HttpClient, HttpClientRequest } from "effect/unstable/http";
|
|
8
|
-
import { AsyncResult, Atom, AtomRegistry, Reactivity } from "effect/unstable/reactivity";
|
|
9
|
-
import { RegistryContext, scheduleTask, useAtomValue } from "@effect/atom-react";
|
|
10
|
-
import { createRoot } from "react-dom/client";
|
|
11
|
-
import { deepEquals } from "@vorma/primitives/json";
|
|
12
|
-
import { Component, createContext, memo, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
13
|
-
import { jsx } from "react/jsx-runtime";
|
|
14
|
-
//#region src/client/client-loaders.ts
|
|
15
|
-
const CONFIRMED_DYNAMIC_PATH_CAPACITY = 4096;
|
|
16
|
-
function toKnownMatch(route) {
|
|
17
|
-
return {
|
|
18
|
-
pattern: route.pattern,
|
|
19
|
-
params: route.params,
|
|
20
|
-
splatValues: route.splatValues,
|
|
21
|
-
input: route.searchInput
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
function createClientLoaderRegistry() {
|
|
25
|
-
/** By authored pattern. */
|
|
26
|
-
const facts = /* @__PURE__ */ new Map();
|
|
27
|
-
const confirmedPaths = /* @__PURE__ */ new Map();
|
|
28
|
-
let module = null;
|
|
29
|
-
/** Built from the registrations on first use after they change. */
|
|
30
|
-
let matcher = null;
|
|
31
|
-
function getMatcher() {
|
|
32
|
-
if (module === null) return null;
|
|
33
|
-
if (matcher === null) matcher = module.createLearnedMatcher([...facts.values()].map((own) => {
|
|
34
|
-
return {
|
|
35
|
-
pattern: own.compiled,
|
|
36
|
-
value: own
|
|
37
|
-
};
|
|
38
|
-
}));
|
|
39
|
-
return matcher;
|
|
40
|
-
}
|
|
41
|
-
function matchLearned(path) {
|
|
42
|
-
const learned = getMatcher();
|
|
43
|
-
if (learned === null) return null;
|
|
44
|
-
const chain = learned.matchPath(path);
|
|
45
|
-
if (chain === null) return null;
|
|
46
|
-
const proven = [];
|
|
47
|
-
for (const match of chain) {
|
|
48
|
-
const own = match.value;
|
|
49
|
-
proven.push({
|
|
50
|
-
pattern: own.pattern,
|
|
51
|
-
params: match.params,
|
|
52
|
-
splatValues: match.splatValues,
|
|
53
|
-
facts: own
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
return proven;
|
|
57
|
-
}
|
|
58
|
-
function selectStaticPrefix(chain) {
|
|
59
|
-
const end = chain.findIndex((match) => !match.facts.isStatic);
|
|
60
|
-
return end === -1 ? chain : chain.slice(0, end);
|
|
61
|
-
}
|
|
62
|
-
/** The chain the registry can prove for `path`: the confirmed chain while the learned patterns agree with it, else the static prefix of a learned match. */
|
|
63
|
-
function proveChain(path) {
|
|
64
|
-
const learned = matchLearned(path);
|
|
65
|
-
const confirmed = confirmedPaths.get(path);
|
|
66
|
-
if (confirmed !== void 0) {
|
|
67
|
-
if (learned === null || learned.length === confirmed.length && learned.every((match, index) => {
|
|
68
|
-
return confirmed[index]?.pattern === match.pattern;
|
|
69
|
-
})) {
|
|
70
|
-
confirmedPaths.delete(path);
|
|
71
|
-
confirmedPaths.set(path, confirmed);
|
|
72
|
-
return confirmed;
|
|
73
|
-
}
|
|
74
|
-
confirmedPaths.delete(path);
|
|
75
|
-
}
|
|
76
|
-
return learned === null ? [] : selectStaticPrefix(learned);
|
|
77
|
-
}
|
|
78
|
-
/** A position's search input parsed from the contract structure; null when parsing refuses it. */
|
|
79
|
-
function readSearchInput(input) {
|
|
80
|
-
const parsed = input.module.parseSearch({
|
|
81
|
-
document: input.search.document,
|
|
82
|
-
searchParams: input.searchParams
|
|
83
|
-
});
|
|
84
|
-
return Result.isFailure(parsed) ? null : { value: parsed.success };
|
|
85
|
-
}
|
|
86
|
-
function recordConfirmedPath(input) {
|
|
87
|
-
confirmedPaths.delete(input.path);
|
|
88
|
-
const firstDynamicIndex = input.matches.findIndex((match) => !match.facts.isStatic);
|
|
89
|
-
if (firstDynamicIndex === -1) return;
|
|
90
|
-
if (!input.matches.some((match, index) => {
|
|
91
|
-
return index >= firstDynamicIndex && match.facts.view?.clientLoader !== void 0;
|
|
92
|
-
})) return;
|
|
93
|
-
confirmedPaths.set(input.path, input.matches);
|
|
94
|
-
if (confirmedPaths.size <= CONFIRMED_DYNAMIC_PATH_CAPACITY) return;
|
|
95
|
-
for (const leastRecentlyUsedPath of confirmedPaths.keys()) {
|
|
96
|
-
confirmedPaths.delete(leastRecentlyUsedPath);
|
|
97
|
-
break;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
return {
|
|
101
|
-
replaceViewsFn: (replacements) => {
|
|
102
|
-
for (const own of facts.values()) if (own.view !== void 0 && replacements.has(own.view)) own.view = replacements.get(own.view);
|
|
103
|
-
},
|
|
104
|
-
recordConfirmedChain: (chain) => {
|
|
105
|
-
const proven = [];
|
|
106
|
-
for (const route of chain.routes) {
|
|
107
|
-
let own = facts.get(route.pattern);
|
|
108
|
-
if (own === void 0) {
|
|
109
|
-
own = {
|
|
110
|
-
pattern: route.pattern,
|
|
111
|
-
compiled: route.compiledPattern,
|
|
112
|
-
isStatic: route.compiledPattern.segments.every((segment) => {
|
|
113
|
-
return segment.kind === "static" || segment.kind === "index";
|
|
114
|
-
}),
|
|
115
|
-
search: route.searchContract === null ? null : { document: route.searchContract },
|
|
116
|
-
view: route.definition
|
|
117
|
-
};
|
|
118
|
-
facts.set(route.pattern, own);
|
|
119
|
-
matcher = null;
|
|
120
|
-
}
|
|
121
|
-
proven.push({
|
|
122
|
-
pattern: route.pattern,
|
|
123
|
-
params: route.params,
|
|
124
|
-
splatValues: route.splatValues,
|
|
125
|
-
facts: own
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
const path = canonicalizeRequestPathForMatcher(chain.url.pathname);
|
|
129
|
-
if (Result.isSuccess(path)) recordConfirmedPath({
|
|
130
|
-
path: path.success,
|
|
131
|
-
matches: proven
|
|
132
|
-
});
|
|
133
|
-
},
|
|
134
|
-
installMatching: (installed) => {
|
|
135
|
-
module = installed;
|
|
136
|
-
matcher = null;
|
|
137
|
-
},
|
|
138
|
-
findEligibleMatches: (url) => {
|
|
139
|
-
if (module === null) return [];
|
|
140
|
-
const path = canonicalizeRequestPathForMatcher(url.pathname);
|
|
141
|
-
if (Result.isFailure(path)) return [];
|
|
142
|
-
const eligible = [];
|
|
143
|
-
for (const match of proveChain(path.success)) {
|
|
144
|
-
let input;
|
|
145
|
-
if (match.facts.search !== null) {
|
|
146
|
-
const read = readSearchInput({
|
|
147
|
-
module,
|
|
148
|
-
search: match.facts.search,
|
|
149
|
-
searchParams: url.searchParams
|
|
150
|
-
});
|
|
151
|
-
if (read === null) break;
|
|
152
|
-
input = read.value;
|
|
153
|
-
}
|
|
154
|
-
eligible.push({
|
|
155
|
-
match: {
|
|
156
|
-
pattern: match.pattern,
|
|
157
|
-
params: match.params,
|
|
158
|
-
splatValues: match.splatValues,
|
|
159
|
-
input
|
|
160
|
-
},
|
|
161
|
-
view: match.facts.view
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
return eligible;
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
/** Forks one client loader into the current scope, joined to its candidate's server result. */
|
|
169
|
-
function startClientLoader(provideClientFn, input) {
|
|
170
|
-
const confirmed = Deferred.await(input.confirmation);
|
|
171
|
-
const context = {
|
|
172
|
-
trigger: input.trigger,
|
|
173
|
-
href: input.href,
|
|
174
|
-
historyState: input.historyState,
|
|
175
|
-
pattern: input.match.pattern,
|
|
176
|
-
params: input.match.params,
|
|
177
|
-
splatValues: input.match.splatValues,
|
|
178
|
-
input: input.match.input,
|
|
179
|
-
knownRouteMatches: input.known,
|
|
180
|
-
serverConfirmedRouteMatches: Effect.map(confirmed, (routes) => routes.map(toKnownMatch)),
|
|
181
|
-
loaderData: Effect.flatMap(confirmed, (c) => {
|
|
182
|
-
const route = c.find((route) => route.pattern === input.match.pattern);
|
|
183
|
-
return route === void 0 ? Effect.interrupt : Effect.succeed(route.loaderData);
|
|
184
|
-
}),
|
|
185
|
-
getLoaderData: (pattern) => {
|
|
186
|
-
return Effect.map(confirmed, (c) => {
|
|
187
|
-
return Option.map(Option.fromNullishOr(c.find((route) => route.pattern === pattern)), (route) => route.loaderData);
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
};
|
|
191
|
-
return Effect.map(Effect.forkScoped(input.activation === void 0 ? provideClientFn(Effect.suspend(() => input.clientLoader(context))) : Effect.provide(Effect.suspend(() => input.clientLoader(context)), input.activation.client)), (fiber) => {
|
|
192
|
-
return {
|
|
193
|
-
pattern: input.match.pattern,
|
|
194
|
-
fiber
|
|
195
|
-
};
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* Collects loader outcomes and reports the earliest failure in route order.
|
|
200
|
-
* The preparation owner observes failures and interrupts later work as soon
|
|
201
|
-
* as each loader starts, including before this collection begins.
|
|
202
|
-
*/
|
|
203
|
-
function awaitClientLoaders(publishEventFn, running) {
|
|
204
|
-
return Effect.gen(function* () {
|
|
205
|
-
const settled = yield* Effect.forEach(running, ({ index, pattern, fiber }) => {
|
|
206
|
-
return Effect.map(Fiber.await(fiber), (exit) => {
|
|
207
|
-
return {
|
|
208
|
-
index,
|
|
209
|
-
pattern,
|
|
210
|
-
exit
|
|
211
|
-
};
|
|
212
|
-
});
|
|
213
|
-
}, { concurrency: "unbounded" });
|
|
214
|
-
const results = /* @__PURE__ */ new Map();
|
|
215
|
-
let failure = null;
|
|
216
|
-
for (const { index, pattern, exit } of settled) if (Exit.isSuccess(exit)) results.set(index, AsyncResult.success(exit.value));
|
|
217
|
-
else if (!Cause.hasInterruptsOnly(exit.cause)) {
|
|
218
|
-
results.set(index, AsyncResult.failure(exit.cause));
|
|
219
|
-
if (failure === null || index < failure.index) failure = {
|
|
220
|
-
index,
|
|
221
|
-
pattern,
|
|
222
|
-
cause: exit.cause
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
if (failure !== null) publishEventFn({
|
|
226
|
-
_tag: "FailureReported",
|
|
227
|
-
source: "clientLoader",
|
|
228
|
-
cause: failure.cause
|
|
229
|
-
});
|
|
230
|
-
return {
|
|
231
|
-
results,
|
|
232
|
-
failure
|
|
233
|
-
};
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
//#endregion
|
|
237
|
-
//#region src/client/browser-http-client.ts
|
|
238
|
-
/** Executes a framework request with native Fetch controls and its caller's scope. */
|
|
239
|
-
function executeBrowserRequest(request) {
|
|
240
|
-
return Effect.gen(function* () {
|
|
241
|
-
const fetch = yield* FetchHttpClient.Fetch;
|
|
242
|
-
let followedUrl;
|
|
243
|
-
return {
|
|
244
|
-
response: yield* Effect.flatMap(HttpClient.HttpClient, (client) => HttpClient.withScope(client).execute(request)).pipe(Effect.provide(FetchHttpClient.layer), Effect.provideService(FetchHttpClient.Fetch, async (input, init) => {
|
|
245
|
-
const response = await fetch(input, init);
|
|
246
|
-
if (response.redirected) followedUrl = response.url;
|
|
247
|
-
return response;
|
|
248
|
-
})),
|
|
249
|
-
followedUrl
|
|
250
|
-
};
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
/** The address without the query keys that the client adds to its own route requests. */
|
|
254
|
-
function stripRouteQueryKeys(url) {
|
|
255
|
-
const stripped = new URL(url);
|
|
256
|
-
stripped.searchParams.delete(HTTP_PROTOCOL.queryKeys.viewPayload);
|
|
257
|
-
stripped.searchParams.delete(HTTP_PROTOCOL.queryKeys.deployment);
|
|
258
|
-
return stripped;
|
|
259
|
-
}
|
|
260
|
-
/** The observed redirect destination, excluding Vorma's route query keys. */
|
|
261
|
-
function findFollowedRedirect(followedUrl, requested) {
|
|
262
|
-
if (followedUrl === void 0 || followedUrl === "") return null;
|
|
263
|
-
const final = stripRouteQueryKeys(new URL(followedUrl));
|
|
264
|
-
return final.href === stripRouteQueryKeys(requested).href ? null : final.href;
|
|
265
|
-
}
|
|
266
|
-
//#endregion
|
|
267
|
-
//#region src/client/services.ts
|
|
268
|
-
/** The activation a route-local computation runs under; a mutation holds it until settlement. */
|
|
269
|
-
const CurrentActivation = Context.Service("vorma/client/CurrentActivation");
|
|
270
|
-
function declaresServices(view) {
|
|
271
|
-
return view.clientOnlyServices !== void 0 || view.universalServices !== void 0;
|
|
272
|
-
}
|
|
273
|
-
/** Builds the activation map in the application scope. */
|
|
274
|
-
function createActivationMap(getApplicationContextsFn) {
|
|
275
|
-
return RcMap.make({ lookup: (key) => {
|
|
276
|
-
return Effect.gen(function* () {
|
|
277
|
-
const scope = yield* Effect.scope;
|
|
278
|
-
const contexts = getApplicationContextsFn();
|
|
279
|
-
if (Option.isNone(contexts)) return yield* Effect.fail(CLIENT_UNAVAILABLE_ERROR);
|
|
280
|
-
const application = contexts.value;
|
|
281
|
-
const memoMap = yield* Layer.makeMemoMap;
|
|
282
|
-
const universalServices = key.view.universalServices === void 0 ? Layer.empty : key.view.universalServices;
|
|
283
|
-
const universal = yield* Layer.buildWithMemoMap(Layer.provide(universalServices, Layer.succeedContext(application.universal)), memoMap, scope);
|
|
284
|
-
const universalContext = Context.merge(application.universal, universal);
|
|
285
|
-
const clientOnlyServices = key.view.clientOnlyServices === void 0 ? Layer.empty : key.view.clientOnlyServices;
|
|
286
|
-
const client = yield* Layer.buildWithMemoMap(Layer.provide(clientOnlyServices, Layer.succeedContext(Context.merge(application.client, universalContext))), memoMap, scope);
|
|
287
|
-
const clientContext = Context.add(Context.merge(Context.merge(application.client, universalContext), client), CurrentActivation, key);
|
|
288
|
-
return {
|
|
289
|
-
key,
|
|
290
|
-
client: clientContext,
|
|
291
|
-
universal: universalContext,
|
|
292
|
-
clientRuntime: Atom.runtime(Layer.succeedContext(clientContext)),
|
|
293
|
-
universalRuntime: Atom.runtime(Layer.succeedContext(universalContext))
|
|
294
|
-
};
|
|
295
|
-
});
|
|
296
|
-
} });
|
|
297
|
-
}
|
|
298
|
-
/**
|
|
299
|
-
* Acquires every entry's activation in `scope`. A live preparation keeps its
|
|
300
|
-
* hold until this scope acquires it. A cached entry contains only the key;
|
|
301
|
-
* re-entry builds a new activation when no owner retains that key.
|
|
302
|
-
*/
|
|
303
|
-
function acquireActivations(activations, input) {
|
|
304
|
-
return Effect.forEach(input.entries, (entry) => {
|
|
305
|
-
const key = "activationKey" in entry ? entry.activationKey : entry.activation?.key;
|
|
306
|
-
if (key === void 0) return Effect.succeed("activationKey" in entry ? toAcquiredRouteEntry(entry, void 0) : entry);
|
|
307
|
-
return Effect.map(Effect.provideService(RcMap.get(activations, key), Scope.Scope, input.scope), (activation) => toAcquiredRouteEntry(entry, activation));
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
/** Omits acquired contexts and runtimes from a reusable entry. */
|
|
311
|
-
function toCachedRouteEntry(entry) {
|
|
312
|
-
const { activation, ...data } = entry;
|
|
313
|
-
return {
|
|
314
|
-
...data,
|
|
315
|
-
activationKey: activation?.key
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
function toAcquiredRouteEntry(entry, activation) {
|
|
319
|
-
if ("activationKey" in entry) {
|
|
320
|
-
const { activationKey: _activationKey, ...data } = entry;
|
|
321
|
-
return {
|
|
322
|
-
...data,
|
|
323
|
-
activation
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
return {
|
|
327
|
-
...entry,
|
|
328
|
-
activation
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
//#endregion
|
|
332
|
-
//#region src/client/endpoints.ts
|
|
333
|
-
const CLIENT_BUILD_ID_HEADER = HTTP_PROTOCOL.requestHeaders.clientBuildId;
|
|
334
|
-
const ACCEPTS_REDIRECT_HEADER = HTTP_PROTOCOL.requestHeaders.clientAcceptsRedirect;
|
|
335
|
-
const DEPLOYMENT_ID_HEADER = HTTP_PROTOCOL.requestHeaders.vercelDeploymentId;
|
|
336
|
-
const SERVER_BUILD_ID_HEADER = HTTP_PROTOCOL.responseHeaders.clientBuildId;
|
|
337
|
-
const BUILD_SKEW_HEADER = HTTP_PROTOCOL.responseHeaders.buildSkew;
|
|
338
|
-
const REDIRECT_HEADER = HTTP_PROTOCOL.responseHeaders.clientRedirect;
|
|
339
|
-
const REDIRECT_STATUS_HEADER = HTTP_PROTOCOL.responseHeaders.clientRedirectStatus;
|
|
340
|
-
const makeOperationId = Brand.nominal();
|
|
341
|
-
function classifyRedirectTarget(target) {
|
|
342
|
-
const parsed = Result.try(() => new URL(target));
|
|
343
|
-
if (Result.isFailure(parsed)) return { kind: "invalid" };
|
|
344
|
-
const url = parsed.success;
|
|
345
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") return { kind: "invalid" };
|
|
346
|
-
return url.origin === window.location.origin ? {
|
|
347
|
-
kind: "navigate",
|
|
348
|
-
url
|
|
349
|
-
} : {
|
|
350
|
-
kind: "document",
|
|
351
|
-
href: url.href
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
function createEndpointOperations(core, dependencies) {
|
|
355
|
-
/** The unfinished call per dedupe key; a later call with the key completes the earlier call's cancellation. */
|
|
356
|
-
const unfinishedByDedupeKey = /* @__PURE__ */ new Map();
|
|
357
|
-
/** Claims a dedupe key for this call, cancelling the earlier unfinished call that held it. */
|
|
358
|
-
function claimDedupeKey(key) {
|
|
359
|
-
const earlier = unfinishedByDedupeKey.get(key);
|
|
360
|
-
if (earlier !== void 0) Deferred.doneUnsafe(earlier, Effect.void);
|
|
361
|
-
const cancellation = Deferred.makeUnsafe();
|
|
362
|
-
unfinishedByDedupeKey.set(key, cancellation);
|
|
363
|
-
return cancellation;
|
|
364
|
-
}
|
|
365
|
-
function releaseDedupeKey(key, cancellation) {
|
|
366
|
-
if (unfinishedByDedupeKey.get(key) === cancellation) unfinishedByDedupeKey.delete(key);
|
|
367
|
-
}
|
|
368
|
-
/** Runs `work` unless the call's dedupe key is claimed by a later call first. */
|
|
369
|
-
function runUnlessCancelled(input, operationId, cancellation, work) {
|
|
370
|
-
if (cancellation === void 0 || input.dedupeKey === void 0) return work;
|
|
371
|
-
const dedupeKey = input.dedupeKey;
|
|
372
|
-
return Effect.raceFirst(work, Effect.andThen(Deferred.await(cancellation), Effect.fail(new CancelledByDedupeKeyError({
|
|
373
|
-
...makeBoundary(input, operationId),
|
|
374
|
-
dedupeKey
|
|
375
|
-
}))));
|
|
376
|
-
}
|
|
377
|
-
function trackOperation(operation) {
|
|
378
|
-
core.updateState((state) => {
|
|
379
|
-
return {
|
|
380
|
-
...state,
|
|
381
|
-
operations: new Map(state.operations).set(operation.operationId, operation)
|
|
382
|
-
};
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
function settleOperation(operation, outcome) {
|
|
386
|
-
const settled = {
|
|
387
|
-
...operation,
|
|
388
|
-
phase: "settled"
|
|
389
|
-
};
|
|
390
|
-
core.publishEvent({
|
|
391
|
-
_tag: "OperationSettled",
|
|
392
|
-
operation: settled,
|
|
393
|
-
outcome
|
|
394
|
-
});
|
|
395
|
-
trackOperation(settled);
|
|
396
|
-
core.updateState((state) => {
|
|
397
|
-
const operations = new Map(state.operations);
|
|
398
|
-
operations.delete(operation.operationId);
|
|
399
|
-
return {
|
|
400
|
-
...state,
|
|
401
|
-
operations
|
|
402
|
-
};
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
function makeBoundary(input, operationId) {
|
|
406
|
-
return {
|
|
407
|
-
method: input.method,
|
|
408
|
-
pattern: input.pattern,
|
|
409
|
-
operationId
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
function prepareRequest(input, operationId, kind) {
|
|
413
|
-
const boundary = makeBoundary(input, operationId);
|
|
414
|
-
return Effect.gen(function* () {
|
|
415
|
-
const isRead = input.method === "GET";
|
|
416
|
-
let search;
|
|
417
|
-
let formData;
|
|
418
|
-
let json;
|
|
419
|
-
if (input.input instanceof FormData) {
|
|
420
|
-
if (isRead) {
|
|
421
|
-
const params = new URLSearchParams();
|
|
422
|
-
for (const [name, value] of input.input) {
|
|
423
|
-
if (typeof value !== "string") return yield* new InputError({
|
|
424
|
-
...boundary,
|
|
425
|
-
message: `File field ${JSON.stringify(name)} cannot be encoded in a GET query`
|
|
426
|
-
});
|
|
427
|
-
params.append(name, encodeScalar(value));
|
|
428
|
-
}
|
|
429
|
-
search = params.toString();
|
|
430
|
-
} else formData = input.input;
|
|
431
|
-
} else if (input.input !== void 0 && isRead) {
|
|
432
|
-
const serialized = serializeSearchParams({
|
|
433
|
-
value: input.input,
|
|
434
|
-
typeEntries: CLIENT_TRANSPORT_TYPE_ENTRIES
|
|
435
|
-
});
|
|
436
|
-
if (Result.isFailure(serialized)) return yield* new InputError({
|
|
437
|
-
...boundary,
|
|
438
|
-
message: "Endpoint input cannot be serialized as search",
|
|
439
|
-
cause: serialized.failure
|
|
440
|
-
});
|
|
441
|
-
search = serialized.success;
|
|
442
|
-
} else if (input.input !== void 0) {
|
|
443
|
-
const transported = encodeTransportedValue({
|
|
444
|
-
value: input.input,
|
|
445
|
-
typeEntries: CLIENT_TRANSPORT_TYPE_ENTRIES
|
|
446
|
-
});
|
|
447
|
-
if (Result.isFailure(transported)) return yield* new InputError({
|
|
448
|
-
...boundary,
|
|
449
|
-
message: "Endpoint input cannot be transported",
|
|
450
|
-
cause: transported.failure
|
|
451
|
-
});
|
|
452
|
-
json = yield* Effect.try({
|
|
453
|
-
try: () => JSON.stringify(transported.success),
|
|
454
|
-
catch: (cause) => {
|
|
455
|
-
return new InputError({
|
|
456
|
-
...boundary,
|
|
457
|
-
message: "Endpoint input is not JSON serializable",
|
|
458
|
-
cause
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
const href = buildHref({
|
|
464
|
-
kind: "endpoint",
|
|
465
|
-
pattern: input.pattern,
|
|
466
|
-
transportTypeEntries: CLIENT_TRANSPORT_TYPE_ENTRIES,
|
|
467
|
-
...input.params === void 0 ? {} : { params: input.params },
|
|
468
|
-
...input.splatValues === void 0 ? {} : { splatValues: input.splatValues },
|
|
469
|
-
...search === void 0 ? {} : { search }
|
|
470
|
-
});
|
|
471
|
-
if (Result.isFailure(href)) return yield* new InputError({
|
|
472
|
-
...boundary,
|
|
473
|
-
message: href.failure.reason,
|
|
474
|
-
cause: href.failure
|
|
475
|
-
});
|
|
476
|
-
const resolved = yield* Effect.try({
|
|
477
|
-
try: () => new URL(href.success, window.location.href),
|
|
478
|
-
catch: (cause) => {
|
|
479
|
-
return new InputError({
|
|
480
|
-
...boundary,
|
|
481
|
-
message: "Endpoint target is not a valid URL",
|
|
482
|
-
cause
|
|
483
|
-
});
|
|
484
|
-
}
|
|
485
|
-
});
|
|
486
|
-
let headers = yield* Effect.try({
|
|
487
|
-
try: () => {
|
|
488
|
-
return Headers.fromInput(input.requestOptions?.headers);
|
|
489
|
-
},
|
|
490
|
-
catch: (cause) => {
|
|
491
|
-
return new InputError({
|
|
492
|
-
...boundary,
|
|
493
|
-
message: "Endpoint request headers are invalid",
|
|
494
|
-
cause
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
});
|
|
498
|
-
headers = Headers.set(headers, "accept", "application/json");
|
|
499
|
-
headers = Headers.set(headers, CLIENT_BUILD_ID_HEADER, core.getClientBuildId());
|
|
500
|
-
headers = Headers.set(headers, ACCEPTS_REDIRECT_HEADER, "1");
|
|
501
|
-
const deploymentId = core.getDeploymentId();
|
|
502
|
-
if (deploymentId !== void 0) headers = Headers.set(headers, DEPLOYMENT_ID_HEADER, deploymentId);
|
|
503
|
-
let request = HttpClientRequest.make(input.method)(resolved, { headers });
|
|
504
|
-
if (formData !== void 0) request = HttpClientRequest.setBody(request, HttpBody.formData(formData));
|
|
505
|
-
else if (json !== void 0) {
|
|
506
|
-
const contentType = headers["content-type"] === void 0 ? "application/json" : headers["content-type"];
|
|
507
|
-
request = HttpClientRequest.setBody(request, HttpBody.raw(json, { contentType }));
|
|
508
|
-
}
|
|
509
|
-
request = yield* core.prepareRequest({
|
|
510
|
-
request,
|
|
511
|
-
requestKind: kind,
|
|
512
|
-
operationId: Option.some(operationId)
|
|
513
|
-
}).pipe(Effect.mapError((cause) => {
|
|
514
|
-
return new TransportError({
|
|
515
|
-
...boundary,
|
|
516
|
-
message: "Endpoint request preparation failed",
|
|
517
|
-
cause
|
|
518
|
-
});
|
|
519
|
-
}));
|
|
520
|
-
return {
|
|
521
|
-
href: resolved.href,
|
|
522
|
-
request
|
|
523
|
-
};
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
function readErrorEnvelope(response, input, operationId) {
|
|
527
|
-
const boundary = makeBoundary(input, operationId);
|
|
528
|
-
return Effect.gen(function* () {
|
|
529
|
-
const json = yield* response.json.pipe(Effect.mapError((cause) => {
|
|
530
|
-
return new TransportError({
|
|
531
|
-
...boundary,
|
|
532
|
-
message: "Endpoint error response is not valid JSON",
|
|
533
|
-
cause,
|
|
534
|
-
status: response.status
|
|
535
|
-
});
|
|
536
|
-
}));
|
|
537
|
-
const envelope = readEndpointErrorEnvelope(json);
|
|
538
|
-
if (Result.isFailure(envelope) || envelope.success.error.status !== response.status) return yield* new TransportError({
|
|
539
|
-
...boundary,
|
|
540
|
-
message: "Endpoint error response does not satisfy the Vorma protocol",
|
|
541
|
-
cause: Result.isFailure(envelope) ? envelope.failure : json,
|
|
542
|
-
status: response.status
|
|
543
|
-
});
|
|
544
|
-
const domain = envelope.success.domain;
|
|
545
|
-
if (domain === void 0) return yield* new HandlerError({
|
|
546
|
-
...boundary,
|
|
547
|
-
status: response.status,
|
|
548
|
-
clientMessage: envelope.success.error.client_message
|
|
549
|
-
});
|
|
550
|
-
const revived = decodeTransportedValue({
|
|
551
|
-
transported: domain.data,
|
|
552
|
-
typeEntries: CLIENT_TRANSPORT_TYPE_ENTRIES
|
|
553
|
-
});
|
|
554
|
-
if (Result.isFailure(revived)) return yield* new TransportError({
|
|
555
|
-
...boundary,
|
|
556
|
-
message: "Endpoint error data does not satisfy the Vorma protocol",
|
|
557
|
-
cause: revived.failure,
|
|
558
|
-
status: response.status
|
|
559
|
-
});
|
|
560
|
-
return yield* Effect.fail(revived.success);
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
/** A success body is the handler's output transported by value; the marks restore it. */
|
|
564
|
-
function readSuccessBody(response, input, operationId) {
|
|
565
|
-
const boundary = makeBoundary(input, operationId);
|
|
566
|
-
if (!response.headers["content-type"]?.toLowerCase().includes("json")) return Effect.fail(new TransportError({
|
|
567
|
-
...boundary,
|
|
568
|
-
message: "Endpoint success response is not JSON",
|
|
569
|
-
cause: response.headers["content-type"],
|
|
570
|
-
status: response.status
|
|
571
|
-
}));
|
|
572
|
-
return Effect.gen(function* () {
|
|
573
|
-
const json = yield* response.json.pipe(Effect.mapError((cause) => {
|
|
574
|
-
return new TransportError({
|
|
575
|
-
...boundary,
|
|
576
|
-
message: "Endpoint success response is not valid JSON",
|
|
577
|
-
cause,
|
|
578
|
-
status: response.status
|
|
579
|
-
});
|
|
580
|
-
}));
|
|
581
|
-
const transported = readTransportedValue(json);
|
|
582
|
-
const revived = Result.flatMap(transported, (value) => {
|
|
583
|
-
return decodeTransportedValue({
|
|
584
|
-
transported: value,
|
|
585
|
-
typeEntries: CLIENT_TRANSPORT_TYPE_ENTRIES
|
|
586
|
-
});
|
|
587
|
-
});
|
|
588
|
-
if (Result.isFailure(revived)) return yield* new TransportError({
|
|
589
|
-
...boundary,
|
|
590
|
-
message: "Endpoint success response does not satisfy the Vorma protocol",
|
|
591
|
-
cause: revived.failure,
|
|
592
|
-
status: response.status
|
|
593
|
-
});
|
|
594
|
-
return revived.success;
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
function dispatchEndpointRequest(input, operationId, kind, prepared) {
|
|
598
|
-
const boundary = makeBoundary(input, operationId);
|
|
599
|
-
return Effect.gen(function* () {
|
|
600
|
-
const requestOptions = input.requestOptions;
|
|
601
|
-
const { response, followedUrl } = yield* core.provideClient(Effect.suspend(() => {
|
|
602
|
-
const execution = executeBrowserRequest(prepared.request);
|
|
603
|
-
return requestOptions === void 0 ? execution : execution.pipe(Effect.updateContext((context) => {
|
|
604
|
-
return Context.add(context, FetchHttpClient.RequestInit, {
|
|
605
|
-
...Context.getOrUndefined(context, FetchHttpClient.RequestInit),
|
|
606
|
-
...requestOptions
|
|
607
|
-
});
|
|
608
|
-
}));
|
|
609
|
-
})).pipe(Effect.mapError((cause) => {
|
|
610
|
-
return new TransportError({
|
|
611
|
-
...boundary,
|
|
612
|
-
message: "Endpoint request failed",
|
|
613
|
-
cause
|
|
614
|
-
});
|
|
615
|
-
}));
|
|
616
|
-
const followed = findFollowedRedirect(followedUrl, new URL(prepared.href));
|
|
617
|
-
if (followed !== null) return {
|
|
618
|
-
response,
|
|
619
|
-
followed
|
|
620
|
-
};
|
|
621
|
-
const serverBuildId = response.headers[SERVER_BUILD_ID_HEADER];
|
|
622
|
-
const mismatch = {
|
|
623
|
-
clientBuildId: core.getClientBuildId(),
|
|
624
|
-
serverBuildId,
|
|
625
|
-
requestedHref: prepared.href,
|
|
626
|
-
requestKind: kind,
|
|
627
|
-
status: response.status
|
|
628
|
-
};
|
|
629
|
-
if (response.headers[BUILD_SKEW_HEADER] === "1") {
|
|
630
|
-
core.reportBuildMismatch(mismatch);
|
|
631
|
-
dependencies.retireReusableFn();
|
|
632
|
-
return yield* new BuildMismatchError({
|
|
633
|
-
...boundary,
|
|
634
|
-
message: "Endpoint request was refused by another build"
|
|
635
|
-
});
|
|
636
|
-
}
|
|
637
|
-
if (serverBuildId === void 0) return yield* new TransportError({
|
|
638
|
-
...boundary,
|
|
639
|
-
message: "Endpoint response is missing its Vorma build identity",
|
|
640
|
-
cause: response.headers,
|
|
641
|
-
status: response.status
|
|
642
|
-
});
|
|
643
|
-
if (serverBuildId !== core.getClientBuildId()) core.reportBuildMismatch(mismatch);
|
|
644
|
-
return {
|
|
645
|
-
response,
|
|
646
|
-
followed: null
|
|
647
|
-
};
|
|
648
|
-
});
|
|
649
|
-
}
|
|
650
|
-
function classifyEndpointResponse(dispatched, input, operationId) {
|
|
651
|
-
const { response } = dispatched;
|
|
652
|
-
const boundary = makeBoundary(input, operationId);
|
|
653
|
-
const responseMetadata = {
|
|
654
|
-
status: response.status,
|
|
655
|
-
headers: response.headers
|
|
656
|
-
};
|
|
657
|
-
return Effect.gen(function* () {
|
|
658
|
-
if (dispatched.followed !== null) {
|
|
659
|
-
const disposition = classifyRedirectTarget(dispatched.followed);
|
|
660
|
-
if (disposition.kind === "invalid") return yield* new TransportError({
|
|
661
|
-
...boundary,
|
|
662
|
-
message: "Endpoint redirect target must be an HTTP or HTTPS URL",
|
|
663
|
-
cause: dispatched.followed,
|
|
664
|
-
status: response.status
|
|
665
|
-
});
|
|
666
|
-
return {
|
|
667
|
-
_tag: "Redirected",
|
|
668
|
-
redirectUrl: dispatched.followed,
|
|
669
|
-
redirectStatus: void 0,
|
|
670
|
-
responseMetadata,
|
|
671
|
-
disposition
|
|
672
|
-
};
|
|
673
|
-
}
|
|
674
|
-
const redirect = response.headers[REDIRECT_HEADER];
|
|
675
|
-
if (redirect !== void 0) {
|
|
676
|
-
const redirectStatus = readRedirectStatus(Number(response.headers[REDIRECT_STATUS_HEADER]));
|
|
677
|
-
if (Result.isFailure(redirectStatus)) return yield* new TransportError({
|
|
678
|
-
...boundary,
|
|
679
|
-
message: "Endpoint redirect is missing a valid intended HTTP status",
|
|
680
|
-
cause: redirectStatus.failure,
|
|
681
|
-
status: response.status
|
|
682
|
-
});
|
|
683
|
-
const target = Result.try(() => new URL(redirect, response.request.url).href);
|
|
684
|
-
const disposition = Result.isFailure(target) ? { kind: "invalid" } : classifyRedirectTarget(target.success);
|
|
685
|
-
if (disposition.kind === "invalid") return yield* new TransportError({
|
|
686
|
-
...boundary,
|
|
687
|
-
message: "Endpoint redirect target must be an HTTP or HTTPS URL",
|
|
688
|
-
cause: redirect,
|
|
689
|
-
status: response.status
|
|
690
|
-
});
|
|
691
|
-
return {
|
|
692
|
-
_tag: "Redirected",
|
|
693
|
-
redirectUrl: disposition.kind === "document" ? disposition.href : disposition.url.href,
|
|
694
|
-
redirectStatus: redirectStatus.success,
|
|
695
|
-
responseMetadata,
|
|
696
|
-
disposition
|
|
697
|
-
};
|
|
698
|
-
}
|
|
699
|
-
if (response.status < 200 || response.status >= 300) return yield* readErrorEnvelope(response, input, operationId);
|
|
700
|
-
return {
|
|
701
|
-
_tag: "Completed",
|
|
702
|
-
data: yield* readSuccessBody(response, input, operationId),
|
|
703
|
-
responseMetadata
|
|
704
|
-
};
|
|
705
|
-
});
|
|
706
|
-
}
|
|
707
|
-
/** Executes one query. Observer-owned; registers no freshness demand. */
|
|
708
|
-
function runEndpointQuery(input, reportDispatchFn) {
|
|
709
|
-
return Effect.gen(function* () {
|
|
710
|
-
const operationId = makeOperationId(crypto.randomUUID());
|
|
711
|
-
const operation = {
|
|
712
|
-
operationId,
|
|
713
|
-
kind: "Query",
|
|
714
|
-
request: {
|
|
715
|
-
method: input.method,
|
|
716
|
-
pattern: input.pattern,
|
|
717
|
-
href: Option.none()
|
|
718
|
-
},
|
|
719
|
-
phase: "preparing",
|
|
720
|
-
skipWorkIndicator: input.skipWorkIndicator === true,
|
|
721
|
-
skipAutoRevalidate: false
|
|
722
|
-
};
|
|
723
|
-
let current = operation;
|
|
724
|
-
trackOperation(operation);
|
|
725
|
-
const cancellation = input.dedupeKey === void 0 ? void 0 : claimDedupeKey(input.dedupeKey);
|
|
726
|
-
return yield* Effect.gen(function* () {
|
|
727
|
-
const prepared = yield* prepareRequest(input, operationId, "Query");
|
|
728
|
-
current = {
|
|
729
|
-
...operation,
|
|
730
|
-
phase: "dispatched",
|
|
731
|
-
request: {
|
|
732
|
-
...operation.request,
|
|
733
|
-
href: Option.some(prepared.href)
|
|
734
|
-
}
|
|
735
|
-
};
|
|
736
|
-
trackOperation(current);
|
|
737
|
-
reportDispatchFn?.({
|
|
738
|
-
sequence: core.advanceSequence(),
|
|
739
|
-
generation: core.getState().generation
|
|
740
|
-
});
|
|
741
|
-
const retirement = core.getGenerationRetirement();
|
|
742
|
-
const classified = yield* runUnlessCancelled(input, operationId, cancellation, Effect.raceFirst(Effect.flatMap(dispatchEndpointRequest(input, operationId, "Query", prepared), (dispatched) => {
|
|
743
|
-
return classifyEndpointResponse(dispatched, input, operationId);
|
|
744
|
-
}), Effect.andThen(Deferred.await(retirement), Effect.interrupt)));
|
|
745
|
-
if (classified._tag === "Redirected") {
|
|
746
|
-
const navigation = startRedirect(classified);
|
|
747
|
-
return yield* new QueryRedirected({
|
|
748
|
-
...makeBoundary(input, operationId),
|
|
749
|
-
redirectUrl: classified.redirectUrl,
|
|
750
|
-
redirectStatus: classified.redirectStatus,
|
|
751
|
-
navigation
|
|
752
|
-
});
|
|
753
|
-
}
|
|
754
|
-
return {
|
|
755
|
-
data: classified.data,
|
|
756
|
-
responseMetadata: classified.responseMetadata
|
|
757
|
-
};
|
|
758
|
-
}).pipe(Effect.scoped, Effect.onExit((outcome) => {
|
|
759
|
-
return Effect.sync(() => {
|
|
760
|
-
if (cancellation !== void 0 && input.dedupeKey !== void 0) releaseDedupeKey(input.dedupeKey, cancellation);
|
|
761
|
-
settleOperation(current, outcome);
|
|
762
|
-
});
|
|
763
|
-
}));
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
|
-
/** Starts the navigation a redirect requires and returns its observation. */
|
|
767
|
-
function startRedirect(classified) {
|
|
768
|
-
if (classified.disposition.kind === "document") {
|
|
769
|
-
dependencies.navigateDocumentFn(classified.redirectUrl);
|
|
770
|
-
return Effect.succeed({
|
|
771
|
-
_tag: "DocumentNavigation",
|
|
772
|
-
href: classified.redirectUrl
|
|
773
|
-
});
|
|
774
|
-
}
|
|
775
|
-
return dependencies.redirectFn(classified.disposition.url);
|
|
776
|
-
}
|
|
777
|
-
/** Executes one mutation with native caller interruption and a scoped activation hold. */
|
|
778
|
-
function runEndpointMutation(input) {
|
|
779
|
-
return Effect.gen(function* () {
|
|
780
|
-
const operationId = makeOperationId(crypto.randomUUID());
|
|
781
|
-
const skipAutoRevalidate = input.skipAutoRevalidate === true;
|
|
782
|
-
const skipWorkIndicator = input.skipWorkIndicator === true;
|
|
783
|
-
const operation = {
|
|
784
|
-
operationId,
|
|
785
|
-
kind: "Mutation",
|
|
786
|
-
request: {
|
|
787
|
-
method: input.method,
|
|
788
|
-
pattern: input.pattern,
|
|
789
|
-
href: Option.none()
|
|
790
|
-
},
|
|
791
|
-
phase: "preparing",
|
|
792
|
-
skipWorkIndicator,
|
|
793
|
-
skipAutoRevalidate
|
|
794
|
-
};
|
|
795
|
-
trackOperation(operation);
|
|
796
|
-
const cancellation = input.dedupeKey === void 0 ? void 0 : claimDedupeKey(input.dedupeKey);
|
|
797
|
-
const prepared = yield* prepareRequest(input, operationId, "Mutation").pipe(Effect.onError((cause) => {
|
|
798
|
-
return Effect.sync(() => {
|
|
799
|
-
if (cancellation !== void 0 && input.dedupeKey !== void 0) releaseDedupeKey(input.dedupeKey, cancellation);
|
|
800
|
-
settleOperation(operation, Exit.failCause(cause));
|
|
801
|
-
});
|
|
802
|
-
}));
|
|
803
|
-
const dispatched = {
|
|
804
|
-
...operation,
|
|
805
|
-
phase: "dispatched",
|
|
806
|
-
request: {
|
|
807
|
-
...operation.request,
|
|
808
|
-
href: Option.some(prepared.href)
|
|
809
|
-
}
|
|
810
|
-
};
|
|
811
|
-
const activation = yield* Effect.serviceOption(CurrentActivation);
|
|
812
|
-
let revalidation;
|
|
813
|
-
function scheduleRefresh() {
|
|
814
|
-
if (revalidation !== void 0) return revalidation;
|
|
815
|
-
revalidation = skipAutoRevalidate ? Effect.succeed({ _tag: "Skipped" }) : dependencies.requireRefreshFn({ skipWorkIndicator });
|
|
816
|
-
return revalidation;
|
|
817
|
-
}
|
|
818
|
-
function makeOwned(scope) {
|
|
819
|
-
return Effect.gen(function* () {
|
|
820
|
-
trackOperation(dispatched);
|
|
821
|
-
const classified = yield* runUnlessCancelled(input, operationId, cancellation, Effect.flatMap(dispatchEndpointRequest(input, operationId, "Mutation", prepared), (sent) => {
|
|
822
|
-
return classifyEndpointResponse(sent, input, operationId);
|
|
823
|
-
}));
|
|
824
|
-
if (classified._tag === "Redirected") {
|
|
825
|
-
if (classified.disposition.kind === "document") {
|
|
826
|
-
dependencies.navigateDocumentFn(classified.redirectUrl);
|
|
827
|
-
revalidation = Effect.succeed({
|
|
828
|
-
_tag: "DocumentNavigation",
|
|
829
|
-
href: classified.redirectUrl
|
|
830
|
-
});
|
|
831
|
-
return {
|
|
832
|
-
_tag: "Redirected",
|
|
833
|
-
operationId,
|
|
834
|
-
redirectUrl: classified.redirectUrl,
|
|
835
|
-
redirectStatus: classified.redirectStatus,
|
|
836
|
-
responseMetadata: classified.responseMetadata,
|
|
837
|
-
revalidation
|
|
838
|
-
};
|
|
839
|
-
}
|
|
840
|
-
const refresh = scheduleRefresh();
|
|
841
|
-
revalidation = dependencies.redirectFn(classified.disposition.url).pipe(Effect.matchEffect({
|
|
842
|
-
onFailure: (error) => {
|
|
843
|
-
return Effect.fail(new RevalidationError({
|
|
844
|
-
reason: "route",
|
|
845
|
-
cause: Cause.fail(error)
|
|
846
|
-
}));
|
|
847
|
-
},
|
|
848
|
-
onSuccess: (result) => {
|
|
849
|
-
switch (result._tag) {
|
|
850
|
-
case "DocumentNavigation": return Effect.succeed(result);
|
|
851
|
-
case "Superseded": return Effect.fail(new RevalidationError({
|
|
852
|
-
reason: "superseded",
|
|
853
|
-
cause: Cause.fail(result)
|
|
854
|
-
}));
|
|
855
|
-
default: return refresh;
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
}));
|
|
859
|
-
return {
|
|
860
|
-
_tag: "Redirected",
|
|
861
|
-
operationId,
|
|
862
|
-
redirectUrl: classified.redirectUrl,
|
|
863
|
-
redirectStatus: classified.redirectStatus,
|
|
864
|
-
responseMetadata: classified.responseMetadata,
|
|
865
|
-
revalidation
|
|
866
|
-
};
|
|
867
|
-
}
|
|
868
|
-
return {
|
|
869
|
-
_tag: "Completed",
|
|
870
|
-
operationId,
|
|
871
|
-
data: classified.data,
|
|
872
|
-
responseMetadata: classified.responseMetadata,
|
|
873
|
-
revalidation: scheduleRefresh()
|
|
874
|
-
};
|
|
875
|
-
}).pipe(Scope.provide(scope), Effect.onExit((outcome) => {
|
|
876
|
-
return Effect.andThen(Scope.close(scope, outcome), Effect.sync(() => {
|
|
877
|
-
const refused = Exit.isFailure(outcome) && Option.exists(Cause.findErrorOption(outcome.cause), (error) => error instanceof BuildMismatchError);
|
|
878
|
-
if (!Exit.isSuccess(outcome) && !refused) scheduleRefresh();
|
|
879
|
-
if (cancellation !== void 0 && input.dedupeKey !== void 0) releaseDedupeKey(input.dedupeKey, cancellation);
|
|
880
|
-
settleOperation(dispatched, outcome);
|
|
881
|
-
}));
|
|
882
|
-
}));
|
|
883
|
-
}
|
|
884
|
-
return yield* Effect.uninterruptibleMask((restore) => {
|
|
885
|
-
return Effect.gen(function* () {
|
|
886
|
-
const scope = yield* Scope.make();
|
|
887
|
-
if (Option.isSome(activation)) {
|
|
888
|
-
const held = yield* Effect.exit(Scope.provide(scope)(RcMap.get(core.activations, activation.value)));
|
|
889
|
-
if (Exit.isFailure(held)) {
|
|
890
|
-
yield* Scope.close(scope, held);
|
|
891
|
-
if (cancellation !== void 0 && input.dedupeKey !== void 0) releaseDedupeKey(input.dedupeKey, cancellation);
|
|
892
|
-
settleOperation(operation, held);
|
|
893
|
-
return yield* Effect.failCause(held.cause);
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
const fiber = yield* FiberSet.run(core.mutationFibers, restore(makeOwned(scope)));
|
|
897
|
-
return yield* restore(Fiber.join(fiber)).pipe(Effect.ensuring(Fiber.interrupt(fiber)));
|
|
898
|
-
});
|
|
899
|
-
});
|
|
900
|
-
});
|
|
901
|
-
}
|
|
902
|
-
return {
|
|
903
|
-
query: (input) => runEndpointQuery(input, void 0),
|
|
904
|
-
read: runEndpointQuery,
|
|
905
|
-
mutate: runEndpointMutation
|
|
906
|
-
};
|
|
907
|
-
}
|
|
908
|
-
//#endregion
|
|
909
|
-
//#region src/client/modules.ts
|
|
910
|
-
/** Writes a `modulepreload` link for each module path that has none yet. */
|
|
911
|
-
function preloadModules(paths) {
|
|
912
|
-
if (paths.length === 0) return;
|
|
913
|
-
const existing = new Set([...document.head.querySelectorAll("link[rel=\"modulepreload\"]")].map((link) => link.getAttribute("href")));
|
|
914
|
-
for (const path of paths) {
|
|
915
|
-
if (existing.has(path)) continue;
|
|
916
|
-
const link = document.createElement("link");
|
|
917
|
-
link.rel = "modulepreload";
|
|
918
|
-
link.href = path;
|
|
919
|
-
document.head.appendChild(link);
|
|
920
|
-
existing.add(path);
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
//#endregion
|
|
924
|
-
//#region src/client/payload.ts
|
|
925
|
-
function readRoute$1(input) {
|
|
926
|
-
const { record } = input;
|
|
927
|
-
function failField(field, cause) {
|
|
928
|
-
return Result.fail({
|
|
929
|
-
_tag: "PayloadReadError",
|
|
930
|
-
pattern: record.pattern,
|
|
931
|
-
field,
|
|
932
|
-
cause
|
|
933
|
-
});
|
|
934
|
-
}
|
|
935
|
-
let searchInput;
|
|
936
|
-
if (record.search_input !== void 0) {
|
|
937
|
-
const revived = decodeTransportedValue({
|
|
938
|
-
transported: record.search_input,
|
|
939
|
-
typeEntries: CLIENT_TRANSPORT_TYPE_ENTRIES
|
|
940
|
-
});
|
|
941
|
-
if (Result.isFailure(revived)) return failField("search_input", revived.failure);
|
|
942
|
-
searchInput = revived.success;
|
|
943
|
-
}
|
|
944
|
-
let loaderData;
|
|
945
|
-
if ("loader_data" in record) {
|
|
946
|
-
const transported = record.loader_data;
|
|
947
|
-
if (transported === null || transported === void 0) loaderData = null;
|
|
948
|
-
else {
|
|
949
|
-
const revived = decodeTransportedValue({
|
|
950
|
-
transported,
|
|
951
|
-
typeEntries: CLIENT_TRANSPORT_TYPE_ENTRIES
|
|
952
|
-
});
|
|
953
|
-
if (Result.isFailure(revived)) return failField("loader_data", revived.failure);
|
|
954
|
-
loaderData = revived.success;
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
return Result.succeed({
|
|
958
|
-
pattern: record.pattern,
|
|
959
|
-
params: record.params ?? {},
|
|
960
|
-
splatValues: record.splat_values ?? [],
|
|
961
|
-
compiledPattern: {
|
|
962
|
-
kind: "nested",
|
|
963
|
-
pattern: record.compiled_pattern.pattern ?? record.pattern,
|
|
964
|
-
segments: record.compiled_pattern.segments
|
|
965
|
-
},
|
|
966
|
-
searchContract: record.search_contract ?? null,
|
|
967
|
-
searchInput,
|
|
968
|
-
moduleReference: typeof record.import_url === "string" ? {
|
|
969
|
-
url: record.import_url,
|
|
970
|
-
exportNames: record.export_names
|
|
971
|
-
} : null,
|
|
972
|
-
loaderData
|
|
973
|
-
});
|
|
974
|
-
}
|
|
975
|
-
function readRoutes(wire) {
|
|
976
|
-
const serverError = wire.server_error;
|
|
977
|
-
const routes = [];
|
|
978
|
-
for (const record of wire.routes) {
|
|
979
|
-
const route = readRoute$1({ record });
|
|
980
|
-
if (Result.isFailure(route)) return Result.fail(route.failure);
|
|
981
|
-
routes.push(route.success);
|
|
982
|
-
}
|
|
983
|
-
return Result.succeed({
|
|
984
|
-
routes,
|
|
985
|
-
serverError: serverError === void 0 ? void 0 : {
|
|
986
|
-
index: serverError.index,
|
|
987
|
-
status: serverError.status,
|
|
988
|
-
clientMessage: serverError.client_message
|
|
989
|
-
}
|
|
990
|
-
});
|
|
991
|
-
}
|
|
992
|
-
function readNavigation(wire) {
|
|
993
|
-
return Result.map(readRoutes(wire), (routePayload) => {
|
|
994
|
-
return {
|
|
995
|
-
...routePayload,
|
|
996
|
-
head: {
|
|
997
|
-
title: wire.head.title,
|
|
998
|
-
meta: wire.head.meta ?? [],
|
|
999
|
-
rest: wire.head.rest ?? []
|
|
1000
|
-
},
|
|
1001
|
-
deps: wire.deps ?? [],
|
|
1002
|
-
cssBundles: wire.css_bundles ?? [],
|
|
1003
|
-
staticAssets: wire.static_assets ?? []
|
|
1004
|
-
};
|
|
1005
|
-
});
|
|
1006
|
-
}
|
|
1007
|
-
/** Reads a navigation payload this build's server wrote. */
|
|
1008
|
-
function readPayload(raw) {
|
|
1009
|
-
return readNavigation(raw);
|
|
1010
|
-
}
|
|
1011
|
-
/** Reads the initial document payload, including build identity. */
|
|
1012
|
-
function readBootstrapPayload(raw) {
|
|
1013
|
-
const wire = raw;
|
|
1014
|
-
return Result.map(readRoutes(wire), (routePayload) => {
|
|
1015
|
-
return {
|
|
1016
|
-
clientBuildId: wire.client_build_id,
|
|
1017
|
-
developmentMode: wire.is_dev === true,
|
|
1018
|
-
deploymentId: wire.deployment_id,
|
|
1019
|
-
...routePayload
|
|
1020
|
-
};
|
|
1021
|
-
});
|
|
1022
|
-
}
|
|
1023
|
-
//#endregion
|
|
1024
|
-
//#region src/client/static-assets.ts
|
|
1025
|
-
const preloads = /* @__PURE__ */ new WeakMap();
|
|
1026
|
-
const ATTRIBUTE = DOCUMENT_PROTOCOL.attributes.staticAsset;
|
|
1027
|
-
/** Adds shared, scoped resource hints without awaiting resource loading. */
|
|
1028
|
-
function preloadStaticAssets(urls) {
|
|
1029
|
-
return Effect.gen(function* () {
|
|
1030
|
-
for (const url of new Set(urls)) yield* Effect.acquireRelease(Effect.sync(() => {
|
|
1031
|
-
const existing = [...document.head.querySelectorAll("link")].find((link) => link.getAttribute(ATTRIBUTE) === url);
|
|
1032
|
-
let preload = existing === void 0 ? void 0 : preloads.get(existing);
|
|
1033
|
-
if (existing !== void 0 && preload === void 0) return;
|
|
1034
|
-
if (preload === void 0) {
|
|
1035
|
-
const destination = getStaticAssetDestination(url);
|
|
1036
|
-
const link = document.createElement("link");
|
|
1037
|
-
link.rel = "preload";
|
|
1038
|
-
link.as = destination;
|
|
1039
|
-
if (destination !== "image") link.crossOrigin = "anonymous";
|
|
1040
|
-
link.href = url;
|
|
1041
|
-
link.setAttribute(ATTRIBUTE, url);
|
|
1042
|
-
preload = {
|
|
1043
|
-
link,
|
|
1044
|
-
owners: 0
|
|
1045
|
-
};
|
|
1046
|
-
preloads.set(link, preload);
|
|
1047
|
-
document.head.append(link);
|
|
1048
|
-
}
|
|
1049
|
-
preload.owners += 1;
|
|
1050
|
-
return preload;
|
|
1051
|
-
}), (retained) => {
|
|
1052
|
-
return Effect.sync(() => {
|
|
1053
|
-
if (retained === void 0) return;
|
|
1054
|
-
retained.owners -= 1;
|
|
1055
|
-
if (retained.owners === 0) retained.link.remove();
|
|
1056
|
-
});
|
|
1057
|
-
});
|
|
1058
|
-
});
|
|
1059
|
-
}
|
|
1060
|
-
//#endregion
|
|
1061
|
-
//#region src/client/prepare.ts
|
|
1062
|
-
function failAt$1(stage, cause) {
|
|
1063
|
-
return new NavigationError({
|
|
1064
|
-
stage,
|
|
1065
|
-
cause: Cause.isCause(cause) ? cause : Cause.fail(cause)
|
|
1066
|
-
});
|
|
1067
|
-
}
|
|
1068
|
-
function readRoute(core, source) {
|
|
1069
|
-
return Effect.gen(function* () {
|
|
1070
|
-
const requestUrl = new URL(source.url);
|
|
1071
|
-
requestUrl.hash = "";
|
|
1072
|
-
const clientBuildId = core.getClientBuildId();
|
|
1073
|
-
requestUrl.searchParams.set(HTTP_PROTOCOL.queryKeys.viewPayload, clientBuildId);
|
|
1074
|
-
const deploymentId = core.getDeploymentId();
|
|
1075
|
-
if (source.requestKind === "Revalidation" && deploymentId !== void 0) requestUrl.searchParams.set(HTTP_PROTOCOL.queryKeys.deployment, deploymentId);
|
|
1076
|
-
const request = HttpClientRequest.get(requestUrl.href).pipe(HttpClientRequest.setHeaders({
|
|
1077
|
-
accept: "application/json",
|
|
1078
|
-
[HTTP_PROTOCOL.requestHeaders.clientAcceptsRedirect]: "1"
|
|
1079
|
-
}));
|
|
1080
|
-
const { response, followedUrl } = yield* Effect.flatMap(core.prepareRequest({
|
|
1081
|
-
request,
|
|
1082
|
-
requestKind: source.requestKind,
|
|
1083
|
-
operationId: Option.none()
|
|
1084
|
-
}), (preparedRequest) => {
|
|
1085
|
-
return core.provideClient(executeBrowserRequest(preparedRequest));
|
|
1086
|
-
}).pipe(Effect.mapError((cause) => failAt$1("request", cause)));
|
|
1087
|
-
const followed = findFollowedRedirect(followedUrl, requestUrl);
|
|
1088
|
-
if (followed !== null) return {
|
|
1089
|
-
_tag: "Redirect",
|
|
1090
|
-
href: followed
|
|
1091
|
-
};
|
|
1092
|
-
const serverBuildId = response.headers[HTTP_PROTOCOL.responseHeaders.clientBuildId];
|
|
1093
|
-
if (response.headers[HTTP_PROTOCOL.responseHeaders.buildSkew] !== void 0) return {
|
|
1094
|
-
_tag: "BuildSkew",
|
|
1095
|
-
mismatch: {
|
|
1096
|
-
clientBuildId,
|
|
1097
|
-
serverBuildId,
|
|
1098
|
-
requestedHref: source.url.href,
|
|
1099
|
-
requestKind: source.requestKind,
|
|
1100
|
-
status: response.status
|
|
1101
|
-
}
|
|
1102
|
-
};
|
|
1103
|
-
const redirect = response.headers[HTTP_PROTOCOL.responseHeaders.clientRedirect];
|
|
1104
|
-
if (redirect !== void 0) return {
|
|
1105
|
-
_tag: "Redirect",
|
|
1106
|
-
href: redirect
|
|
1107
|
-
};
|
|
1108
|
-
const contentType = response.headers["content-type"];
|
|
1109
|
-
if (contentType === void 0 || !contentType.startsWith("application/json")) return yield* failAt$1("request", {
|
|
1110
|
-
status: response.status,
|
|
1111
|
-
contentType
|
|
1112
|
-
});
|
|
1113
|
-
return {
|
|
1114
|
-
_tag: "Payload",
|
|
1115
|
-
raw: yield* Effect.mapError(response.json, (cause) => {
|
|
1116
|
-
return failAt$1("request", {
|
|
1117
|
-
status: response.status,
|
|
1118
|
-
cause
|
|
1119
|
-
});
|
|
1120
|
-
}),
|
|
1121
|
-
status: response.status,
|
|
1122
|
-
serverBuildId
|
|
1123
|
-
};
|
|
1124
|
-
});
|
|
1125
|
-
}
|
|
1126
|
-
/** Prepares one destination. Known client loaders start before the read settles when `prestart` is set. */
|
|
1127
|
-
function prepareRoute(core, input) {
|
|
1128
|
-
const url = input.source.url;
|
|
1129
|
-
let announced = false;
|
|
1130
|
-
function announceReadiness(exit) {
|
|
1131
|
-
if (input.reportReadinessFn !== void 0 && !announced) {
|
|
1132
|
-
announced = true;
|
|
1133
|
-
input.reportReadinessFn(exit);
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
const program = Effect.gen(function* () {
|
|
1137
|
-
const sequence = core.advanceSequence();
|
|
1138
|
-
const generation = core.getState().generation;
|
|
1139
|
-
const confirmation = Deferred.makeUnsafe();
|
|
1140
|
-
const positionsByPattern = /* @__PURE__ */ new Map();
|
|
1141
|
-
let failureBoundary = Infinity;
|
|
1142
|
-
const scope = yield* Effect.scope;
|
|
1143
|
-
const committed = core.getState().frame;
|
|
1144
|
-
function getRunningLoaders() {
|
|
1145
|
-
const running = [];
|
|
1146
|
-
for (const position of positionsByPattern.values()) if (position.loader !== void 0 && position.confirmation !== "rejected") running.push({
|
|
1147
|
-
index: position.index,
|
|
1148
|
-
pattern: position.match.pattern,
|
|
1149
|
-
fiber: position.loader
|
|
1150
|
-
});
|
|
1151
|
-
return running;
|
|
1152
|
-
}
|
|
1153
|
-
function canRunLoader(position) {
|
|
1154
|
-
return position.confirmation !== "rejected" && position.index <= failureBoundary;
|
|
1155
|
-
}
|
|
1156
|
-
/** One position acquires its services once and retains all of its preparation work. */
|
|
1157
|
-
function startPreparationPosition(inputPosition) {
|
|
1158
|
-
return Effect.gen(function* () {
|
|
1159
|
-
const { index, match, known, view } = inputPosition;
|
|
1160
|
-
const work = {
|
|
1161
|
-
index,
|
|
1162
|
-
match,
|
|
1163
|
-
confirmation: inputPosition.confirmation,
|
|
1164
|
-
loader: void 0
|
|
1165
|
-
};
|
|
1166
|
-
const preparation = yield* Effect.forkIn(Effect.gen(function* () {
|
|
1167
|
-
let activation;
|
|
1168
|
-
if (view !== void 0 && declaresServices(view)) {
|
|
1169
|
-
const retained = committed?.entries[index];
|
|
1170
|
-
const key = retained?.pattern === match.pattern && retained.activation !== void 0 ? retained.activation.key : {
|
|
1171
|
-
pattern: match.pattern,
|
|
1172
|
-
view
|
|
1173
|
-
};
|
|
1174
|
-
activation = yield* RcMap.get(core.activations, key).pipe(Effect.provideService(Scope.Scope, scope), Effect.mapError((cause) => failAt$1("assets", cause)));
|
|
1175
|
-
}
|
|
1176
|
-
if (!canRunLoader(work) || view?.clientLoader === void 0) return activation;
|
|
1177
|
-
const started = yield* startClientLoader(core.provideClient, {
|
|
1178
|
-
clientLoader: view.clientLoader,
|
|
1179
|
-
trigger: input.trigger,
|
|
1180
|
-
href: url.href,
|
|
1181
|
-
historyState: input.historyState,
|
|
1182
|
-
match,
|
|
1183
|
-
known,
|
|
1184
|
-
confirmation,
|
|
1185
|
-
activation
|
|
1186
|
-
}).pipe(Effect.provideService(Scope.Scope, scope));
|
|
1187
|
-
work.loader = started.fiber;
|
|
1188
|
-
if (!canRunLoader(work)) {
|
|
1189
|
-
yield* Fiber.interrupt(started.fiber);
|
|
1190
|
-
work.loader = void 0;
|
|
1191
|
-
return activation;
|
|
1192
|
-
}
|
|
1193
|
-
yield* Effect.forkIn(Effect.flatMap(Fiber.await(started.fiber), (exit) => {
|
|
1194
|
-
if (Exit.isSuccess(exit) || Cause.hasInterruptsOnly(exit.cause) || work.confirmation === "rejected") return Effect.void;
|
|
1195
|
-
failureBoundary = Math.min(failureBoundary, index);
|
|
1196
|
-
return Fiber.interruptAll(getRunningLoaders().filter((other) => {
|
|
1197
|
-
return other.index > failureBoundary;
|
|
1198
|
-
}).map((other) => other.fiber));
|
|
1199
|
-
}), scope);
|
|
1200
|
-
return activation;
|
|
1201
|
-
}), scope);
|
|
1202
|
-
const position = Object.assign(work, { preparation });
|
|
1203
|
-
positionsByPattern.set(match.pattern, position);
|
|
1204
|
-
if (!canRunLoader(position)) position.loader?.interruptUnsafe();
|
|
1205
|
-
return position;
|
|
1206
|
-
});
|
|
1207
|
-
}
|
|
1208
|
-
let payload;
|
|
1209
|
-
if (input.source.kind === "payload") payload = input.source.payload;
|
|
1210
|
-
else {
|
|
1211
|
-
const reading = yield* Effect.forkScoped(Effect.scoped(readRoute(core, input.source)));
|
|
1212
|
-
if (input.prestart) {
|
|
1213
|
-
const eligible = core.clientLoaders.findEligibleMatches(url);
|
|
1214
|
-
const known = eligible.map((entry) => entry.match);
|
|
1215
|
-
for (const [index, { match, view }] of eligible.entries()) if (view?.clientLoader !== void 0) yield* startPreparationPosition({
|
|
1216
|
-
index,
|
|
1217
|
-
match,
|
|
1218
|
-
known,
|
|
1219
|
-
view,
|
|
1220
|
-
confirmation: "pending"
|
|
1221
|
-
});
|
|
1222
|
-
}
|
|
1223
|
-
const response = yield* Fiber.join(reading);
|
|
1224
|
-
if (response._tag !== "Payload") return response;
|
|
1225
|
-
if (response.serverBuildId !== void 0 && response.serverBuildId !== core.getClientBuildId()) core.reportBuildMismatch({
|
|
1226
|
-
clientBuildId: core.getClientBuildId(),
|
|
1227
|
-
serverBuildId: response.serverBuildId,
|
|
1228
|
-
requestedHref: url.href,
|
|
1229
|
-
requestKind: input.source.requestKind,
|
|
1230
|
-
status: response.status
|
|
1231
|
-
});
|
|
1232
|
-
const read = readPayload(response.raw);
|
|
1233
|
-
if (Result.isFailure(read)) return yield* failAt$1("request", read.failure);
|
|
1234
|
-
payload = read.success;
|
|
1235
|
-
}
|
|
1236
|
-
const stylesheetReadiness = yield* Effect.forkIn(prepareStylesheets(payload.cssBundles).pipe(Effect.provideService(Scope.Scope, scope)), scope);
|
|
1237
|
-
yield* preloadStaticAssets(payload.staticAssets).pipe(Effect.provideService(Scope.Scope, scope));
|
|
1238
|
-
if (!core.runsInDevelopmentMode()) preloadModules(payload.deps);
|
|
1239
|
-
const matches = payload.routes.map(toKnownMatch);
|
|
1240
|
-
for (const [pattern, position] of positionsByPattern) if (matches[position.index]?.pattern === pattern) position.confirmation = "confirmed";
|
|
1241
|
-
else {
|
|
1242
|
-
position.confirmation = "rejected";
|
|
1243
|
-
position.preparation.interruptUnsafe();
|
|
1244
|
-
position.loader?.interruptUnsafe();
|
|
1245
|
-
positionsByPattern.delete(pattern);
|
|
1246
|
-
}
|
|
1247
|
-
Deferred.doneUnsafe(confirmation, Effect.succeed(payload.routes));
|
|
1248
|
-
failureBoundary = Infinity;
|
|
1249
|
-
for (const active of getRunningLoaders()) {
|
|
1250
|
-
const exit = active.fiber.pollUnsafe();
|
|
1251
|
-
if (exit !== void 0 && Exit.isFailure(exit) && !Cause.hasInterruptsOnly(exit.cause)) failureBoundary = Math.min(failureBoundary, active.index);
|
|
1252
|
-
}
|
|
1253
|
-
const positions = yield* Effect.forEach(payload.routes, (decoded, index) => {
|
|
1254
|
-
return Effect.gen(function* () {
|
|
1255
|
-
const route = yield* materializeRoute(decoded).pipe(Effect.mapError((cause) => failAt$1("assets", cause)));
|
|
1256
|
-
const position = positionsByPattern.get(route.pattern) ?? (yield* startPreparationPosition({
|
|
1257
|
-
index,
|
|
1258
|
-
match: toKnownMatch(route),
|
|
1259
|
-
known: matches,
|
|
1260
|
-
view: route.definition,
|
|
1261
|
-
confirmation: "confirmed"
|
|
1262
|
-
}));
|
|
1263
|
-
return {
|
|
1264
|
-
route,
|
|
1265
|
-
activation: yield* Fiber.join(position.preparation)
|
|
1266
|
-
};
|
|
1267
|
-
});
|
|
1268
|
-
}, { concurrency: "unbounded" });
|
|
1269
|
-
const routes = positions.map((position) => position.route);
|
|
1270
|
-
core.clientLoaders.recordConfirmedChain({
|
|
1271
|
-
url,
|
|
1272
|
-
routes
|
|
1273
|
-
});
|
|
1274
|
-
announceReadiness(Exit.void);
|
|
1275
|
-
const [clientLoaderOutcome] = yield* Effect.all([awaitClientLoaders(core.publishEvent, getRunningLoaders()), Fiber.join(stylesheetReadiness)], { concurrency: "unbounded" });
|
|
1276
|
-
const entries = positions.map(({ route, activation }, index) => {
|
|
1277
|
-
const settled = clientLoaderOutcome.results.get(index);
|
|
1278
|
-
return {
|
|
1279
|
-
pattern: route.pattern,
|
|
1280
|
-
params: route.params,
|
|
1281
|
-
splatValues: route.splatValues,
|
|
1282
|
-
input: route.searchInput,
|
|
1283
|
-
view: route.definition,
|
|
1284
|
-
loaderData: route.loaderData,
|
|
1285
|
-
clientLoaderData: settled === void 0 ? AsyncResult.initial() : settled,
|
|
1286
|
-
activation
|
|
1287
|
-
};
|
|
1288
|
-
});
|
|
1289
|
-
return {
|
|
1290
|
-
_tag: "Prepared",
|
|
1291
|
-
prepared: {
|
|
1292
|
-
url,
|
|
1293
|
-
entries,
|
|
1294
|
-
error: selectRouteError(payload.serverError, clientLoaderOutcome.failure),
|
|
1295
|
-
head: payload.head,
|
|
1296
|
-
cssBundles: payload.cssBundles,
|
|
1297
|
-
sequence,
|
|
1298
|
-
generation
|
|
1299
|
-
}
|
|
1300
|
-
};
|
|
1301
|
-
});
|
|
1302
|
-
return input.reportReadinessFn === void 0 ? program : Effect.onExit(program, (exit) => {
|
|
1303
|
-
return Effect.sync(() => {
|
|
1304
|
-
if (Exit.isFailure(exit)) announceReadiness(Exit.failCause(exit.cause));
|
|
1305
|
-
else if (exit.value._tag !== "Prepared") announceReadiness(Exit.fail(failAt$1(exit.value._tag === "Redirect" ? "target" : "request", exit.value)));
|
|
1306
|
-
});
|
|
1307
|
-
});
|
|
1308
|
-
}
|
|
1309
|
-
/** The route error a prepared route carries: the earliest client loader failure, else the failed server loader. */
|
|
1310
|
-
function selectRouteError(serverError, clientLoaderFailure) {
|
|
1311
|
-
if (clientLoaderFailure !== null) return {
|
|
1312
|
-
index: clientLoaderFailure.index,
|
|
1313
|
-
pattern: clientLoaderFailure.pattern,
|
|
1314
|
-
source: "clientLoader",
|
|
1315
|
-
error: clientLoaderFailure.cause
|
|
1316
|
-
};
|
|
1317
|
-
if (serverError === void 0) return null;
|
|
1318
|
-
return {
|
|
1319
|
-
index: serverError.index,
|
|
1320
|
-
pattern: void 0,
|
|
1321
|
-
source: "serverLoader",
|
|
1322
|
-
error: {
|
|
1323
|
-
status: serverError.status,
|
|
1324
|
-
clientMessage: serverError.clientMessage
|
|
1325
|
-
}
|
|
1326
|
-
};
|
|
1327
|
-
}
|
|
1328
|
-
//#endregion
|
|
1329
|
-
//#region src/client/prefetch.ts
|
|
1330
|
-
/** The part of an address a preparation serves: its path and query. */
|
|
1331
|
-
function readDestination(url) {
|
|
1332
|
-
return url.pathname + url.search;
|
|
1333
|
-
}
|
|
1334
|
-
function createPrefetch(core) {
|
|
1335
|
-
const entries = /* @__PURE__ */ new Map();
|
|
1336
|
-
function clearPending(entry) {
|
|
1337
|
-
if (entries.get(entry.destination) === entry) core.updateState((state) => {
|
|
1338
|
-
const prefetch = new Map(state.prefetch);
|
|
1339
|
-
prefetch.delete(entry.destination);
|
|
1340
|
-
return {
|
|
1341
|
-
...state,
|
|
1342
|
-
prefetch
|
|
1343
|
-
};
|
|
1344
|
-
});
|
|
1345
|
-
}
|
|
1346
|
-
function stopPreparation(entry) {
|
|
1347
|
-
entry.fiber?.interruptUnsafe();
|
|
1348
|
-
core.forkFiber(Scope.close(entry.scope, Exit.void));
|
|
1349
|
-
}
|
|
1350
|
-
function retireEntry(entry) {
|
|
1351
|
-
Deferred.doneUnsafe(entry.observation, Effect.fail(new PrefetchError({ reason: "superseded" })));
|
|
1352
|
-
clearPending(entry);
|
|
1353
|
-
if (entries.get(entry.destination) === entry) entries.delete(entry.destination);
|
|
1354
|
-
stopPreparation(entry);
|
|
1355
|
-
}
|
|
1356
|
-
function settlePreparation(entry, exit) {
|
|
1357
|
-
if (Exit.isSuccess(exit)) {
|
|
1358
|
-
Deferred.doneUnsafe(entry.result, Effect.succeed(exit.value));
|
|
1359
|
-
if (exit.value._tag === "Prepared") return;
|
|
1360
|
-
} else Deferred.doneUnsafe(entry.result, Effect.failCause(exit.cause));
|
|
1361
|
-
const cause = Exit.isFailure(exit) ? exit.cause : Cause.fail(new NavigationError({
|
|
1362
|
-
stage: "target",
|
|
1363
|
-
cause: Cause.fail(exit.value)
|
|
1364
|
-
}));
|
|
1365
|
-
Deferred.doneUnsafe(entry.observation, Effect.fail(new PrefetchError({
|
|
1366
|
-
reason: Cause.hasInterruptsOnly(cause) ? "superseded" : "preparation",
|
|
1367
|
-
cause
|
|
1368
|
-
})));
|
|
1369
|
-
clearPending(entry);
|
|
1370
|
-
if (entries.get(entry.destination) === entry) entries.delete(entry.destination);
|
|
1371
|
-
core.forkFiber(Scope.close(entry.scope, Exit.void));
|
|
1372
|
-
}
|
|
1373
|
-
function openPreparation(url, handle, skipWorkIndicator) {
|
|
1374
|
-
const entry = {
|
|
1375
|
-
destination: readDestination(url),
|
|
1376
|
-
handles: /* @__PURE__ */ new Set([handle]),
|
|
1377
|
-
startSequence: core.getState().sequence,
|
|
1378
|
-
observation: Deferred.makeUnsafe(),
|
|
1379
|
-
result: Deferred.makeUnsafe(),
|
|
1380
|
-
scope: Scope.makeUnsafe(),
|
|
1381
|
-
fiber: void 0,
|
|
1382
|
-
keptAfterObservers: false,
|
|
1383
|
-
consumed: false
|
|
1384
|
-
};
|
|
1385
|
-
entries.set(entry.destination, entry);
|
|
1386
|
-
core.updateState((state) => {
|
|
1387
|
-
return {
|
|
1388
|
-
...state,
|
|
1389
|
-
prefetch: new Map(state.prefetch).set(entry.destination, {
|
|
1390
|
-
href: url.href,
|
|
1391
|
-
skipWorkIndicator
|
|
1392
|
-
})
|
|
1393
|
-
};
|
|
1394
|
-
});
|
|
1395
|
-
entry.fiber = core.forkFiber(Effect.onExit(Effect.provideService(prepareRoute(core, {
|
|
1396
|
-
source: {
|
|
1397
|
-
kind: "request",
|
|
1398
|
-
url,
|
|
1399
|
-
requestKind: "Prefetch"
|
|
1400
|
-
},
|
|
1401
|
-
trigger: "prefetch",
|
|
1402
|
-
historyState: void 0,
|
|
1403
|
-
prestart: true,
|
|
1404
|
-
reportReadinessFn: (exit) => {
|
|
1405
|
-
if (Exit.isSuccess(exit)) {
|
|
1406
|
-
clearPending(entry);
|
|
1407
|
-
Deferred.doneUnsafe(entry.observation, Effect.void);
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
}), Scope.Scope, entry.scope), (exit) => Effect.sync(() => settlePreparation(entry, exit))));
|
|
1411
|
-
return entry;
|
|
1412
|
-
}
|
|
1413
|
-
function acquirePreparationHandle(url, skipWorkIndicator) {
|
|
1414
|
-
return Effect.suspend(() => {
|
|
1415
|
-
const handle = {};
|
|
1416
|
-
const destination = readDestination(url);
|
|
1417
|
-
const state = core.getState();
|
|
1418
|
-
const committed = state.frame === null ? null : readDestination(new URL(state.frame.href));
|
|
1419
|
-
const pending = Option.map(state.navigation.pending === null ? Option.none() : Option.some(state.navigation.pending), (item) => {
|
|
1420
|
-
return readDestination(new URL(item.href));
|
|
1421
|
-
});
|
|
1422
|
-
if (destination === committed || Option.contains(pending, destination)) return Effect.fail(new PrefetchError({ reason: "unavailable" }));
|
|
1423
|
-
const existing = entries.get(destination);
|
|
1424
|
-
if (existing !== void 0) {
|
|
1425
|
-
existing.handles.add(handle);
|
|
1426
|
-
return Effect.succeed({
|
|
1427
|
-
entry: existing,
|
|
1428
|
-
handle
|
|
1429
|
-
});
|
|
1430
|
-
}
|
|
1431
|
-
for (const entry of entries.values()) if (entry.handles.size === 0) retireEntry(entry);
|
|
1432
|
-
return Effect.succeed({
|
|
1433
|
-
entry: openPreparation(url, handle, skipWorkIndicator),
|
|
1434
|
-
handle
|
|
1435
|
-
});
|
|
1436
|
-
});
|
|
1437
|
-
}
|
|
1438
|
-
function releasePreparationHandle(entry, handle) {
|
|
1439
|
-
if (!entry.handles.delete(handle)) return;
|
|
1440
|
-
if (entries.get(entry.destination) !== entry) return;
|
|
1441
|
-
if (entry.handles.size === 0 && entry.keptAfterObservers && !entry.consumed) {
|
|
1442
|
-
for (const other of entries.values()) if (other !== entry && other.handles.size === 0) retireEntry(other);
|
|
1443
|
-
}
|
|
1444
|
-
if (entry.handles.size === 0 && !entry.keptAfterObservers && !entry.consumed) retireEntry(entry);
|
|
1445
|
-
}
|
|
1446
|
-
return {
|
|
1447
|
-
awaitReadiness: (url, options) => {
|
|
1448
|
-
return Effect.acquireUseRelease(acquirePreparationHandle(url, options.skipWorkIndicator), ({ entry }) => Deferred.await(entry.observation), ({ entry, handle }, exit) => {
|
|
1449
|
-
return Effect.sync(() => {
|
|
1450
|
-
if (Exit.isSuccess(exit)) entry.keptAfterObservers = true;
|
|
1451
|
-
releasePreparationHandle(entry, handle);
|
|
1452
|
-
});
|
|
1453
|
-
});
|
|
1454
|
-
},
|
|
1455
|
-
holdUntilInterrupted: (url, options) => {
|
|
1456
|
-
return Effect.acquireUseRelease(acquirePreparationHandle(url, options.skipWorkIndicator), () => Effect.never, ({ entry, handle }) => Effect.sync(() => releasePreparationHandle(entry, handle)));
|
|
1457
|
-
},
|
|
1458
|
-
takeForNavigation: (destination) => {
|
|
1459
|
-
const entry = entries.get(destination);
|
|
1460
|
-
if (entry === void 0) return null;
|
|
1461
|
-
clearPending(entry);
|
|
1462
|
-
entries.delete(destination);
|
|
1463
|
-
entry.consumed = true;
|
|
1464
|
-
entry.keptAfterObservers = false;
|
|
1465
|
-
Deferred.doneUnsafe(entry.observation, Effect.fail(new PrefetchError({ reason: "takenByNavigation" })));
|
|
1466
|
-
return {
|
|
1467
|
-
result: Deferred.await(entry.result),
|
|
1468
|
-
startSequence: entry.startSequence,
|
|
1469
|
-
release: () => stopPreparation(entry)
|
|
1470
|
-
};
|
|
1471
|
-
},
|
|
1472
|
-
retirePreparations: () => {
|
|
1473
|
-
for (const entry of entries.values()) retireEntry(entry);
|
|
1474
|
-
}
|
|
1475
|
-
};
|
|
1476
|
-
}
|
|
1477
|
-
//#endregion
|
|
1478
|
-
//#region src/client/scroll.ts
|
|
1479
|
-
function readScrollPosition() {
|
|
1480
|
-
return {
|
|
1481
|
-
x: window.scrollX,
|
|
1482
|
-
y: window.scrollY
|
|
1483
|
-
};
|
|
1484
|
-
}
|
|
1485
|
-
function normalizeFragment(hash) {
|
|
1486
|
-
const raw = hash.startsWith("#") ? hash.slice(1) : hash;
|
|
1487
|
-
if (raw.length === 0) return raw;
|
|
1488
|
-
return Result.getOrElse(Result.try(() => decodeURIComponent(raw)), () => raw);
|
|
1489
|
-
}
|
|
1490
|
-
/** Applies the target through the browser's scroll calls, the one platform boundary here; a failure comes back as a value. */
|
|
1491
|
-
function applyScroll(target) {
|
|
1492
|
-
return Result.try(() => {
|
|
1493
|
-
if ("hash" in target) {
|
|
1494
|
-
document.getElementById(normalizeFragment(target.hash))?.scrollIntoView();
|
|
1495
|
-
return;
|
|
1496
|
-
}
|
|
1497
|
-
window.scrollTo(target.x, target.y);
|
|
1498
|
-
});
|
|
1499
|
-
}
|
|
1500
|
-
//#endregion
|
|
1501
|
-
//#region src/client/navigation.ts
|
|
1502
|
-
/** The redirect chain length fetch itself accepts. */
|
|
1503
|
-
const REDIRECT_LIMIT = 20;
|
|
1504
|
-
/** The largest delay browsers accept without reducing it to a short timeout. */
|
|
1505
|
-
const MAX_TIMER_DELAY_MILLIS = 2147483647;
|
|
1506
|
-
function failAt(stage, cause) {
|
|
1507
|
-
return new NavigationError({
|
|
1508
|
-
stage,
|
|
1509
|
-
cause: Cause.isCause(cause) ? cause : Cause.fail(cause)
|
|
1510
|
-
});
|
|
1511
|
-
}
|
|
1512
|
-
/** A failed refresh read as a revalidation failure: the server's refusal is a build mismatch, anything else a route failure. */
|
|
1513
|
-
function classifyRefreshFailure(error) {
|
|
1514
|
-
const inner = Cause.findErrorOption(error.cause);
|
|
1515
|
-
const mismatch = Option.isSome(inner) && inner.value instanceof BuildMismatchError;
|
|
1516
|
-
return makeRevalidationError(mismatch ? "build-mismatch" : "route", error.cause);
|
|
1517
|
-
}
|
|
1518
|
-
/** The navigation options that carry a history entry's user state. */
|
|
1519
|
-
function makeHistoryStateOptions(historyState) {
|
|
1520
|
-
return historyState === void 0 ? {} : { historyState };
|
|
1521
|
-
}
|
|
1522
|
-
function matchesNavigationOptions(left, right) {
|
|
1523
|
-
const { historyState: leftState, ...leftOptions } = left;
|
|
1524
|
-
const { historyState: rightState, ...rightOptions } = right;
|
|
1525
|
-
return Object.is(leftState, rightState) && deepEquals({
|
|
1526
|
-
left: leftOptions,
|
|
1527
|
-
right: rightOptions
|
|
1528
|
-
});
|
|
1529
|
-
}
|
|
1530
|
-
function leaveDocument(href) {
|
|
1531
|
-
window.location.assign(href);
|
|
1532
|
-
return {
|
|
1533
|
-
_tag: "DocumentNavigation",
|
|
1534
|
-
href
|
|
1535
|
-
};
|
|
1536
|
-
}
|
|
1537
|
-
/** The deepest entry a scroll can wait for: the last entry, or the error position that replaces the rest. */
|
|
1538
|
-
function selectScrollEntryIndex(entryCount, errorIndex) {
|
|
1539
|
-
if (entryCount === 0 && errorIndex !== null) return 0;
|
|
1540
|
-
return errorIndex === null ? entryCount - 1 : Math.min(entryCount - 1, errorIndex);
|
|
1541
|
-
}
|
|
1542
|
-
/** Navigation follows an enabled fragment first, then a saved position, then the scroll-to-top setting. */
|
|
1543
|
-
function decideScrollIntent(input) {
|
|
1544
|
-
const entryIndex = selectScrollEntryIndex(input.entryCount, input.errorIndex);
|
|
1545
|
-
if (normalizeFragment(input.url.hash).length > 0 && input.options.scrollToFragment !== false) return {
|
|
1546
|
-
target: { hash: input.url.hash },
|
|
1547
|
-
entryIndex
|
|
1548
|
-
};
|
|
1549
|
-
if (input.restore !== void 0) return {
|
|
1550
|
-
target: input.restore,
|
|
1551
|
-
entryIndex
|
|
1552
|
-
};
|
|
1553
|
-
if (input.options.scrollToTop === false) return;
|
|
1554
|
-
return {
|
|
1555
|
-
target: {
|
|
1556
|
-
x: 0,
|
|
1557
|
-
y: 0
|
|
1558
|
-
},
|
|
1559
|
-
entryIndex
|
|
1560
|
-
};
|
|
1561
|
-
}
|
|
1562
|
-
/** The first frame adopts the browser's entry, or gives an entry without a key one. */
|
|
1563
|
-
function selectFirstFramePosition(current) {
|
|
1564
|
-
return current.key === void 0 ? {
|
|
1565
|
-
kind: "write",
|
|
1566
|
-
href: current.href,
|
|
1567
|
-
replace: true,
|
|
1568
|
-
userState: current.userState,
|
|
1569
|
-
key: createEntryKey()
|
|
1570
|
-
} : {
|
|
1571
|
-
kind: "adopt",
|
|
1572
|
-
position: current
|
|
1573
|
-
};
|
|
1574
|
-
}
|
|
1575
|
-
/** Restores a saved position or applies the initial fragment after the first frame renders. */
|
|
1576
|
-
function selectFirstFrameScrollIntent(restore, prepared) {
|
|
1577
|
-
const target = restore ?? (normalizeFragment(prepared.url.hash).length > 0 ? { hash: prepared.url.hash } : void 0);
|
|
1578
|
-
return target === void 0 ? void 0 : {
|
|
1579
|
-
target,
|
|
1580
|
-
entryIndex: selectScrollEntryIndex(prepared.entries.length, prepared.error === null ? null : prepared.error.index)
|
|
1581
|
-
};
|
|
1582
|
-
}
|
|
1583
|
-
function createNavigation(core, prefetch, options) {
|
|
1584
|
-
let active = null;
|
|
1585
|
-
const snapshots = /* @__PURE__ */ new Map();
|
|
1586
|
-
let snapshotExpirationTimer;
|
|
1587
|
-
/** Settles when boot commits its frame or fails; a navigation requested before then waits on it. */
|
|
1588
|
-
const firstFrame = Deferred.makeUnsafe();
|
|
1589
|
-
/** Set by disposal; a review scheduled before it must not touch the registry afterwards. */
|
|
1590
|
-
let retired = false;
|
|
1591
|
-
/** Each query's current computation; a release that names another computation is a replaced one's. */
|
|
1592
|
-
const queryComputations = /* @__PURE__ */ new Map();
|
|
1593
|
-
/** Expectations collected while a demand registers; the record holds the demand before they apply. */
|
|
1594
|
-
let collecting = null;
|
|
1595
|
-
function closeScope(scope) {
|
|
1596
|
-
core.forkFiber(Scope.close(scope, Exit.void));
|
|
1597
|
-
}
|
|
1598
|
-
function clearSnapshotExpirationTimer() {
|
|
1599
|
-
if (snapshotExpirationTimer !== void 0) {
|
|
1600
|
-
window.clearTimeout(snapshotExpirationTimer);
|
|
1601
|
-
snapshotExpirationTimer = void 0;
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1604
|
-
function deleteExpiredSnapshots(now) {
|
|
1605
|
-
for (const [destination, snapshot] of snapshots) if (snapshot.expiresAt <= now) snapshots.delete(destination);
|
|
1606
|
-
}
|
|
1607
|
-
/** Runs one timer for the earliest expiry; longer browser delays are resumed in bounded steps. */
|
|
1608
|
-
function scheduleSnapshotExpiration() {
|
|
1609
|
-
clearSnapshotExpirationTimer();
|
|
1610
|
-
let earliest = Infinity;
|
|
1611
|
-
for (const snapshot of snapshots.values()) earliest = Math.min(earliest, snapshot.expiresAt);
|
|
1612
|
-
if (!Number.isFinite(earliest)) return;
|
|
1613
|
-
const delay = Math.min(Math.max(0, earliest - Date.now()), MAX_TIMER_DELAY_MILLIS);
|
|
1614
|
-
snapshotExpirationTimer = window.setTimeout(() => {
|
|
1615
|
-
snapshotExpirationTimer = void 0;
|
|
1616
|
-
deleteExpiredSnapshots(Date.now());
|
|
1617
|
-
scheduleSnapshotExpiration();
|
|
1618
|
-
}, delay);
|
|
1619
|
-
}
|
|
1620
|
-
function retireSnapshots() {
|
|
1621
|
-
snapshots.clear();
|
|
1622
|
-
clearSnapshotExpirationTimer();
|
|
1623
|
-
}
|
|
1624
|
-
function retireReusable() {
|
|
1625
|
-
prefetch.retirePreparations();
|
|
1626
|
-
retireSnapshots();
|
|
1627
|
-
}
|
|
1628
|
-
/** The reusable preparation for `url`: a taken prefetch or a snapshot within its reuse window, whichever read is newer. */
|
|
1629
|
-
function takeReusable(url) {
|
|
1630
|
-
const frame = core.getState().frame;
|
|
1631
|
-
if (frame !== null && frame.error !== null) snapshots.delete(readDestination(new URL(frame.href)));
|
|
1632
|
-
deleteExpiredSnapshots(Date.now());
|
|
1633
|
-
const destination = readDestination(url);
|
|
1634
|
-
const snapshot = snapshots.get(destination);
|
|
1635
|
-
const consumed = prefetch.takeForNavigation(destination);
|
|
1636
|
-
if (consumed !== null) {
|
|
1637
|
-
if (snapshot !== void 0 && snapshot.prepared.sequence > consumed.startSequence) {
|
|
1638
|
-
consumed.release();
|
|
1639
|
-
return {
|
|
1640
|
-
kind: "snapshot",
|
|
1641
|
-
...snapshot
|
|
1642
|
-
};
|
|
1643
|
-
}
|
|
1644
|
-
return {
|
|
1645
|
-
kind: "prefetch",
|
|
1646
|
-
preparation: consumed
|
|
1647
|
-
};
|
|
1648
|
-
}
|
|
1649
|
-
return snapshot === void 0 ? null : {
|
|
1650
|
-
kind: "snapshot",
|
|
1651
|
-
...snapshot
|
|
1652
|
-
};
|
|
1653
|
-
}
|
|
1654
|
-
function applyFreshness(transition) {
|
|
1655
|
-
core.updateState((state) => {
|
|
1656
|
-
return {
|
|
1657
|
-
...state,
|
|
1658
|
-
freshness: transition.facts
|
|
1659
|
-
};
|
|
1660
|
-
});
|
|
1661
|
-
for (const complete of transition.complete) complete();
|
|
1662
|
-
if (transition.startAttempt) ensureAttempt();
|
|
1663
|
-
}
|
|
1664
|
-
/** The fiber scheduling refresh attempts and any application-configured retries. */
|
|
1665
|
-
let retryRunner = null;
|
|
1666
|
-
/** Manual and focus requests postpone route refresh until eight milliseconds after the last request. */
|
|
1667
|
-
let refreshDebounce = null;
|
|
1668
|
-
/**
|
|
1669
|
-
* Starts one refresh attempt for the outstanding demand when one may run.
|
|
1670
|
-
* Returns the candidate, or null when nothing started: no demand, a
|
|
1671
|
-
* navigation that will settle the demand itself, or a failed start.
|
|
1672
|
-
*/
|
|
1673
|
-
function startAttemptNow() {
|
|
1674
|
-
const state = core.getState();
|
|
1675
|
-
const facts = state.freshness;
|
|
1676
|
-
if (state.lifecycle._tag === "Disposed" || refreshDebounce !== null || !canStartAttempt(facts)) return null;
|
|
1677
|
-
const frame = state.frame;
|
|
1678
|
-
if (frame === null) {
|
|
1679
|
-
const position = readBrowserPosition();
|
|
1680
|
-
if (Result.isFailure(position)) {
|
|
1681
|
-
applyFreshness(failAttempt(facts, makeRevalidationError("route", position.failure)));
|
|
1682
|
-
return null;
|
|
1683
|
-
}
|
|
1684
|
-
core.updateState((state) => {
|
|
1685
|
-
return {
|
|
1686
|
-
...state,
|
|
1687
|
-
freshness: startAttempt(facts)
|
|
1688
|
-
};
|
|
1689
|
-
});
|
|
1690
|
-
return startCandidate({
|
|
1691
|
-
kind: "revalidation",
|
|
1692
|
-
source: "navigate",
|
|
1693
|
-
recoverByReload: false,
|
|
1694
|
-
href: position.success.href,
|
|
1695
|
-
options: makeHistoryStateOptions(position.success.userState),
|
|
1696
|
-
historyMode: "adopt",
|
|
1697
|
-
restoreScroll: position.success.key === void 0 ? void 0 : recallScroll(position.success.key)
|
|
1698
|
-
});
|
|
1699
|
-
}
|
|
1700
|
-
core.updateState((state) => {
|
|
1701
|
-
return {
|
|
1702
|
-
...state,
|
|
1703
|
-
freshness: startAttempt(facts)
|
|
1704
|
-
};
|
|
1705
|
-
});
|
|
1706
|
-
if (active?.kind === "navigation") return null;
|
|
1707
|
-
return startCandidate({
|
|
1708
|
-
kind: "revalidation",
|
|
1709
|
-
source: "navigate",
|
|
1710
|
-
recoverByReload: false,
|
|
1711
|
-
href: frame.href,
|
|
1712
|
-
options: makeHistoryStateOptions(frame.historyState),
|
|
1713
|
-
historyMode: "adopt",
|
|
1714
|
-
restoreScroll: void 0
|
|
1715
|
-
});
|
|
1716
|
-
}
|
|
1717
|
-
/** One attempt as the schedule sees it: a retryable failure fails, everything else ends the loop. */
|
|
1718
|
-
function attemptOnce() {
|
|
1719
|
-
return Effect.suspend(() => {
|
|
1720
|
-
const candidate = startAttemptNow();
|
|
1721
|
-
if (candidate === null) return Effect.void;
|
|
1722
|
-
return Effect.flatMap(Effect.exit(Deferred.await(candidate.intent.done)), (exit) => {
|
|
1723
|
-
if (Exit.isSuccess(exit) || core.getState().freshness.demand === null) return Effect.void;
|
|
1724
|
-
const error = Cause.findErrorOption(exit.cause);
|
|
1725
|
-
if (Option.isNone(error)) return Effect.void;
|
|
1726
|
-
const revalidationError = classifyRefreshFailure(error.value);
|
|
1727
|
-
return revalidationError.reason === "build-mismatch" ? Effect.void : Effect.fail(revalidationError);
|
|
1728
|
-
});
|
|
1729
|
-
});
|
|
1730
|
-
}
|
|
1731
|
-
/** Starts a refresh attempt for the outstanding demand when one may run, under the application's schedule when one is stated. */
|
|
1732
|
-
function ensureAttempt() {
|
|
1733
|
-
const state = core.getState();
|
|
1734
|
-
if (state.lifecycle._tag === "Disposed" || refreshDebounce !== null || !canStartAttempt(state.freshness)) return;
|
|
1735
|
-
const schedule = options.refreshRetrySchedule;
|
|
1736
|
-
if (schedule === void 0) {
|
|
1737
|
-
startAttemptNow();
|
|
1738
|
-
return;
|
|
1739
|
-
}
|
|
1740
|
-
retryRunner?.interruptUnsafe();
|
|
1741
|
-
retryRunner = core.forkFiber(Effect.retry(attemptOnce(), schedule).pipe(Effect.catchCause((cause) => {
|
|
1742
|
-
return Effect.sync(() => {
|
|
1743
|
-
const error = Cause.findErrorOption(cause);
|
|
1744
|
-
if (Option.isSome(error)) applyFreshness(failAttempt(core.getState().freshness, error.value));
|
|
1745
|
-
});
|
|
1746
|
-
})));
|
|
1747
|
-
}
|
|
1748
|
-
function setPending(candidate) {
|
|
1749
|
-
core.updateState((state) => {
|
|
1750
|
-
return {
|
|
1751
|
-
...state,
|
|
1752
|
-
navigation: {
|
|
1753
|
-
...state.navigation,
|
|
1754
|
-
pending: candidate === null ? null : {
|
|
1755
|
-
href: candidate.intent.href,
|
|
1756
|
-
source: candidate.source,
|
|
1757
|
-
skipWorkIndicator: candidate.intent.options.skipWorkIndicator === true
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
};
|
|
1761
|
-
});
|
|
1762
|
-
}
|
|
1763
|
-
/** Empties the slot; the occupant's region then finds it lost and ends without effect. */
|
|
1764
|
-
function vacateCandidateSlot() {
|
|
1765
|
-
const occupant = active;
|
|
1766
|
-
if (occupant === null) return;
|
|
1767
|
-
active = null;
|
|
1768
|
-
if (occupant.kind === "navigation") setPending(null);
|
|
1769
|
-
occupant.fiber?.interruptUnsafe();
|
|
1770
|
-
}
|
|
1771
|
-
function startCandidate(input) {
|
|
1772
|
-
const candidate = {
|
|
1773
|
-
kind: input.kind,
|
|
1774
|
-
source: input.source,
|
|
1775
|
-
recoverByReload: input.recoverByReload,
|
|
1776
|
-
restoreScroll: input.restoreScroll,
|
|
1777
|
-
reuse: input.kind === "navigation" ? takeReusable(new URL(input.href)) : null,
|
|
1778
|
-
intent: {
|
|
1779
|
-
href: input.href,
|
|
1780
|
-
options: input.options,
|
|
1781
|
-
historyMode: input.historyMode,
|
|
1782
|
-
done: Deferred.makeUnsafe()
|
|
1783
|
-
},
|
|
1784
|
-
fiber: void 0,
|
|
1785
|
-
sequence: void 0,
|
|
1786
|
-
outcome: void 0,
|
|
1787
|
-
frameScope: void 0
|
|
1788
|
-
};
|
|
1789
|
-
const previous = active;
|
|
1790
|
-
active = candidate;
|
|
1791
|
-
previous?.fiber?.interruptUnsafe();
|
|
1792
|
-
if (candidate.kind === "navigation") {
|
|
1793
|
-
setPending(candidate);
|
|
1794
|
-
core.updateState((state) => {
|
|
1795
|
-
return {
|
|
1796
|
-
...state,
|
|
1797
|
-
navigation: {
|
|
1798
|
-
...state.navigation,
|
|
1799
|
-
latest: AsyncResult.waiting(state.navigation.latest)
|
|
1800
|
-
}
|
|
1801
|
-
};
|
|
1802
|
-
});
|
|
1803
|
-
}
|
|
1804
|
-
candidate.fiber = core.forkFiber(Effect.onExit(runCandidate(candidate), (exit) => Effect.sync(() => finalizeCandidate(candidate, exit))));
|
|
1805
|
-
return candidate;
|
|
1806
|
-
}
|
|
1807
|
-
function runCandidate(candidate) {
|
|
1808
|
-
return Effect.gen(function* () {
|
|
1809
|
-
let url = new URL(candidate.intent.href);
|
|
1810
|
-
let redirects = 0;
|
|
1811
|
-
const reuse = candidate.reuse;
|
|
1812
|
-
if (reuse?.kind === "snapshot") {
|
|
1813
|
-
const frame = core.getState().frame;
|
|
1814
|
-
return yield* acceptRoute(candidate, {
|
|
1815
|
-
...reuse.prepared,
|
|
1816
|
-
entries: reuse.prepared.entries.map((entry, index) => {
|
|
1817
|
-
const retained = frame === null ? void 0 : frame.entries[index];
|
|
1818
|
-
return entry.activationKey !== void 0 && retained !== void 0 && retained.pattern === entry.pattern && retained.activation !== void 0 ? {
|
|
1819
|
-
...entry,
|
|
1820
|
-
activationKey: retained.activation.key
|
|
1821
|
-
} : entry;
|
|
1822
|
-
}),
|
|
1823
|
-
generation: core.getState().generation
|
|
1824
|
-
});
|
|
1825
|
-
}
|
|
1826
|
-
let promoted = reuse?.kind === "prefetch" ? reuse.preparation : null;
|
|
1827
|
-
while (true) {
|
|
1828
|
-
const preparationScope = Scope.makeUnsafe();
|
|
1829
|
-
const source = promoted === null ? Effect.provideService(prepareRoute(core, {
|
|
1830
|
-
source: {
|
|
1831
|
-
kind: "request",
|
|
1832
|
-
url,
|
|
1833
|
-
requestKind: candidate.kind === "navigation" ? "Navigation" : "Revalidation"
|
|
1834
|
-
},
|
|
1835
|
-
trigger: candidate.kind,
|
|
1836
|
-
historyState: candidate.intent.options.historyState,
|
|
1837
|
-
prestart: true,
|
|
1838
|
-
reportReadinessFn: void 0
|
|
1839
|
-
}), Scope.Scope, preparationScope) : promoted.result;
|
|
1840
|
-
const release = promoted === null ? Scope.close(preparationScope, Exit.void) : Effect.sync(promoted.release);
|
|
1841
|
-
promoted = null;
|
|
1842
|
-
const step = yield* Effect.ensuring(Effect.gen(function* () {
|
|
1843
|
-
const outcome = yield* source;
|
|
1844
|
-
if (outcome._tag === "Prepared") return {
|
|
1845
|
-
_tag: "Accepted",
|
|
1846
|
-
result: yield* acceptRoute(candidate, outcome.prepared)
|
|
1847
|
-
};
|
|
1848
|
-
return {
|
|
1849
|
-
_tag: "Unprepared",
|
|
1850
|
-
outcome
|
|
1851
|
-
};
|
|
1852
|
-
}), release);
|
|
1853
|
-
if (step._tag === "Accepted") return step.result;
|
|
1854
|
-
const outcome = step.outcome;
|
|
1855
|
-
if (outcome._tag === "BuildSkew") {
|
|
1856
|
-
core.reportBuildMismatch(outcome.mismatch);
|
|
1857
|
-
retireReusable();
|
|
1858
|
-
if (candidate.kind === "revalidation") return yield* new NavigationError({
|
|
1859
|
-
stage: "request",
|
|
1860
|
-
cause: Cause.fail(new BuildMismatchError({
|
|
1861
|
-
method: "GET",
|
|
1862
|
-
pattern: url.pathname,
|
|
1863
|
-
message: "The server refused the loaded build during revalidation"
|
|
1864
|
-
}))
|
|
1865
|
-
});
|
|
1866
|
-
return leaveDocument(url.href);
|
|
1867
|
-
}
|
|
1868
|
-
const target = Result.try(() => new URL(outcome.href, url.href));
|
|
1869
|
-
if (Result.isFailure(target)) return yield* failAt("target", target.failure);
|
|
1870
|
-
if (!/^https?:$/.test(target.success.protocol)) return yield* failAt("target", `Redirect target ${outcome.href} is not an HTTP URL`);
|
|
1871
|
-
if (target.success.origin !== window.location.origin) return leaveDocument(target.success.href);
|
|
1872
|
-
if (++redirects > REDIRECT_LIMIT) return yield* failAt("target", `More than ${REDIRECT_LIMIT} redirects`);
|
|
1873
|
-
const committed = core.getState().frame;
|
|
1874
|
-
if (candidate.kind === "navigation" && committed !== null && readDestination(target.success) === readDestination(new URL(committed.href))) return yield* completeAtCommitted(candidate, committed, target.success);
|
|
1875
|
-
url = target.success;
|
|
1876
|
-
candidate.source = "redirect";
|
|
1877
|
-
candidate.intent = {
|
|
1878
|
-
...candidate.intent,
|
|
1879
|
-
href: url.href,
|
|
1880
|
-
historyMode: candidate.intent.historyMode === "adopt" ? "replace" : candidate.intent.historyMode
|
|
1881
|
-
};
|
|
1882
|
-
if (active === candidate && candidate.kind === "navigation") setPending(candidate);
|
|
1883
|
-
}
|
|
1884
|
-
});
|
|
1885
|
-
}
|
|
1886
|
-
/** Completes a navigation whose redirect returned to the committed destination: Unchanged, or a local commit for a new fragment or state. */
|
|
1887
|
-
function completeAtCommitted(candidate, committed, url) {
|
|
1888
|
-
return Effect.suspend(() => {
|
|
1889
|
-
if (active !== candidate) return Effect.succeed({ _tag: "Superseded" });
|
|
1890
|
-
const intent = candidate.intent;
|
|
1891
|
-
const committedUrl = new URL(committed.href);
|
|
1892
|
-
if (!checkBrowserPositionChanged() && normalizeFragment(url.hash) === normalizeFragment(committedUrl.hash) && Object.is(intent.options.historyState, committed.historyState)) {
|
|
1893
|
-
candidate.outcome = {
|
|
1894
|
-
_tag: "Unchanged",
|
|
1895
|
-
href: committed.href
|
|
1896
|
-
};
|
|
1897
|
-
return Effect.succeed(candidate.outcome);
|
|
1898
|
-
}
|
|
1899
|
-
if (committed.key !== void 0) rememberScroll(committed.key, readScrollPosition());
|
|
1900
|
-
const result = commitLocal({
|
|
1901
|
-
reason: "navigation",
|
|
1902
|
-
frame: committed,
|
|
1903
|
-
url,
|
|
1904
|
-
position: {
|
|
1905
|
-
kind: "write",
|
|
1906
|
-
href: url.href,
|
|
1907
|
-
replace: intent.historyMode === "replace",
|
|
1908
|
-
userState: intent.options.historyState,
|
|
1909
|
-
key: createEntryKey()
|
|
1910
|
-
},
|
|
1911
|
-
options: intent.options,
|
|
1912
|
-
restore: void 0
|
|
1913
|
-
});
|
|
1914
|
-
if (Result.isSuccess(result)) candidate.outcome = result.success;
|
|
1915
|
-
return Effect.fromResult(result);
|
|
1916
|
-
});
|
|
1917
|
-
}
|
|
1918
|
-
function acceptRoute(candidate, prepared) {
|
|
1919
|
-
return Effect.scoped(Effect.gen(function* () {
|
|
1920
|
-
const frame = core.getState().frame;
|
|
1921
|
-
const preparationScope = yield* Effect.scope;
|
|
1922
|
-
const outgoing = frame === null ? [] : yield* Effect.mapError(acquireActivations(core.activations, {
|
|
1923
|
-
entries: frame.entries.filter((entry) => {
|
|
1924
|
-
return entry.view?.beforeRouteReplacement !== void 0;
|
|
1925
|
-
}),
|
|
1926
|
-
scope: preparationScope
|
|
1927
|
-
}), (cause) => failAt("transition", cause));
|
|
1928
|
-
candidate.sequence = prepared.sequence;
|
|
1929
|
-
const frameScope = Scope.makeUnsafe();
|
|
1930
|
-
candidate.frameScope = frameScope;
|
|
1931
|
-
const entries = yield* Effect.mapError(acquireActivations(core.activations, {
|
|
1932
|
-
entries: prepared.entries,
|
|
1933
|
-
scope: frameScope
|
|
1934
|
-
}), (cause) => failAt("assets", cause));
|
|
1935
|
-
const reason = candidate.kind === "revalidation" ? "revalidation" : candidate.recoverByReload ? "popstate" : "navigation";
|
|
1936
|
-
if (frame !== null) {
|
|
1937
|
-
const args = {
|
|
1938
|
-
trigger: reason,
|
|
1939
|
-
current: projectRouteState(frame),
|
|
1940
|
-
next: projectRouteState({
|
|
1941
|
-
href: candidate.intent.href,
|
|
1942
|
-
historyState: candidate.intent.options.historyState,
|
|
1943
|
-
entries: prepared.entries,
|
|
1944
|
-
error: prepared.error
|
|
1945
|
-
})
|
|
1946
|
-
};
|
|
1947
|
-
const hooks = [...outgoing.map((entry) => {
|
|
1948
|
-
return {
|
|
1949
|
-
entry,
|
|
1950
|
-
hook: entry.view?.beforeRouteReplacement
|
|
1951
|
-
};
|
|
1952
|
-
}), ...entries.map((entry) => {
|
|
1953
|
-
return {
|
|
1954
|
-
entry,
|
|
1955
|
-
hook: entry.view?.beforeRouteCommit
|
|
1956
|
-
};
|
|
1957
|
-
})];
|
|
1958
|
-
yield* Effect.catchCause(Effect.forEach(hooks, ({ entry, hook }) => {
|
|
1959
|
-
if (hook === void 0) return Effect.void;
|
|
1960
|
-
const effect = Effect.suspend(() => hook(args));
|
|
1961
|
-
return entry.activation === void 0 ? core.provideClient(effect) : Effect.provide(effect, entry.activation.client);
|
|
1962
|
-
}, {
|
|
1963
|
-
concurrency: "unbounded",
|
|
1964
|
-
discard: true
|
|
1965
|
-
}), (cause) => failAt("transition", cause));
|
|
1966
|
-
}
|
|
1967
|
-
return yield* commitRegion(candidate, {
|
|
1968
|
-
...prepared,
|
|
1969
|
-
entries
|
|
1970
|
-
}, reason, frameScope);
|
|
1971
|
-
}));
|
|
1972
|
-
}
|
|
1973
|
-
function commitRegion(candidate, prepared, reason, frameScope) {
|
|
1974
|
-
/** Synchronous, so nothing interleaves between its checks and the acceptance point. */
|
|
1975
|
-
function runRegion() {
|
|
1976
|
-
const state = core.getState();
|
|
1977
|
-
const lifecycleAdmits = state.lifecycle._tag === "Ready" || candidate.kind === "revalidation" && state.lifecycle._tag === "PreBoot";
|
|
1978
|
-
if (active !== candidate || !lifecycleAdmits || state.generation !== prepared.generation) return Result.succeed({ _tag: "Superseded" });
|
|
1979
|
-
const intent = candidate.intent;
|
|
1980
|
-
const firstFrame = state.frame === null;
|
|
1981
|
-
let position;
|
|
1982
|
-
if (intent.historyMode === "adopt") {
|
|
1983
|
-
const current = readBrowserPosition();
|
|
1984
|
-
if (Result.isFailure(current)) return Result.fail(failAt("transition", current.failure));
|
|
1985
|
-
if (current.success.href !== intent.href) return Result.fail(failAt("transition", "The browser location changed during preparation"));
|
|
1986
|
-
position = selectFirstFramePosition(current.success);
|
|
1987
|
-
} else {
|
|
1988
|
-
if (state.frame !== null && state.frame.key !== void 0) rememberScroll(state.frame.key, readScrollPosition());
|
|
1989
|
-
position = {
|
|
1990
|
-
kind: "write",
|
|
1991
|
-
href: intent.href,
|
|
1992
|
-
replace: intent.historyMode === "replace",
|
|
1993
|
-
userState: intent.options.historyState,
|
|
1994
|
-
key: createEntryKey()
|
|
1995
|
-
};
|
|
1996
|
-
}
|
|
1997
|
-
const committed = commitFrame(core, {
|
|
1998
|
-
reason: firstFrame ? "boot" : reason,
|
|
1999
|
-
entries: prepared.entries,
|
|
2000
|
-
error: prepared.error,
|
|
2001
|
-
head: prepared.head,
|
|
2002
|
-
cssBundles: prepared.cssBundles,
|
|
2003
|
-
documentUpdate: "apply",
|
|
2004
|
-
position,
|
|
2005
|
-
scrollIntent: firstFrame ? selectFirstFrameScrollIntent(candidate.restoreScroll, prepared) : candidate.kind === "revalidation" ? void 0 : decideScrollIntent({
|
|
2006
|
-
url: new URL(intent.href),
|
|
2007
|
-
options: intent.options,
|
|
2008
|
-
restore: candidate.restoreScroll,
|
|
2009
|
-
entryCount: prepared.entries.length,
|
|
2010
|
-
errorIndex: prepared.error === null ? null : prepared.error.index
|
|
2011
|
-
}),
|
|
2012
|
-
committedAt: candidate.reuse?.kind === "snapshot" ? candidate.reuse.committedAt : void 0,
|
|
2013
|
-
scope: frameScope
|
|
2014
|
-
});
|
|
2015
|
-
if (Result.isSuccess(committed)) {
|
|
2016
|
-
candidate.outcome = {
|
|
2017
|
-
_tag: "Committed",
|
|
2018
|
-
href: committed.success.href
|
|
2019
|
-
};
|
|
2020
|
-
return Result.succeed(candidate.outcome);
|
|
2021
|
-
}
|
|
2022
|
-
return Result.fail(committed.failure);
|
|
2023
|
-
}
|
|
2024
|
-
const transitionDocument = document;
|
|
2025
|
-
const startViewTransition = transitionDocument.startViewTransition;
|
|
2026
|
-
const outcome = candidate.kind === "navigation" && options.enableViewTransitions && startViewTransition !== void 0 ? Effect.callback((resume) => {
|
|
2027
|
-
const started = Result.try(() => {
|
|
2028
|
-
return startViewTransition.call(transitionDocument, () => resume(Effect.succeed(runRegion())));
|
|
2029
|
-
});
|
|
2030
|
-
if (Result.isFailure(started)) {
|
|
2031
|
-
resume(Effect.fail(failAt("transition", started.failure)));
|
|
2032
|
-
return;
|
|
2033
|
-
}
|
|
2034
|
-
started.success.ready.catch(() => void 0);
|
|
2035
|
-
started.success.updateCallbackDone.catch((cause) => {
|
|
2036
|
-
resume(Effect.fail(failAt("transition", cause)));
|
|
2037
|
-
});
|
|
2038
|
-
}) : Effect.sync(runRegion);
|
|
2039
|
-
return Effect.flatMap(outcome, Effect.fromResult);
|
|
2040
|
-
}
|
|
2041
|
-
function finalizeCandidate(candidate, rawExit) {
|
|
2042
|
-
const exit = candidate.outcome === void 0 ? rawExit : Exit.succeed(candidate.outcome);
|
|
2043
|
-
const held = active === candidate;
|
|
2044
|
-
if (held) active = null;
|
|
2045
|
-
const interrupted = Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause);
|
|
2046
|
-
if (candidate.frameScope !== void 0 && candidate.outcome === void 0) closeScope(candidate.frameScope);
|
|
2047
|
-
if (candidate.kind === "navigation" && held) {
|
|
2048
|
-
setPending(null);
|
|
2049
|
-
if (!interrupted) core.updateState((state) => {
|
|
2050
|
-
return {
|
|
2051
|
-
...state,
|
|
2052
|
-
navigation: {
|
|
2053
|
-
...state.navigation,
|
|
2054
|
-
latest: AsyncResult.fromExit(exit)
|
|
2055
|
-
}
|
|
2056
|
-
};
|
|
2057
|
-
});
|
|
2058
|
-
}
|
|
2059
|
-
if (held && Exit.isFailure(exit) && !interrupted) {
|
|
2060
|
-
const error = Cause.findErrorOption(exit.cause);
|
|
2061
|
-
if (!(Option.isSome(error) && error.value.stage === "publication") && (candidate.recoverByReload || checkBrowserPositionChanged())) reloadDocument(core, exit.cause);
|
|
2062
|
-
}
|
|
2063
|
-
const facts = core.getState().freshness;
|
|
2064
|
-
if (Exit.isSuccess(exit)) {
|
|
2065
|
-
if (exit.value._tag === "Committed" && candidate.sequence !== void 0) {
|
|
2066
|
-
const frame = core.getState().frame;
|
|
2067
|
-
const reuseWindow = frame?.entries.at(-1)?.view?.reuseLoaderDataWithin;
|
|
2068
|
-
const duration = reuseWindow === void 0 ? Option.none() : Duration.fromInput(reuseWindow);
|
|
2069
|
-
const durationMillis = Option.map(duration, Duration.toMillis);
|
|
2070
|
-
const retainedAt = Date.now();
|
|
2071
|
-
if (frame !== null && frame.error === null && candidate.reuse?.kind !== "snapshot" && Option.isSome(durationMillis) && Number.isFinite(durationMillis.value) && durationMillis.value > 0) {
|
|
2072
|
-
snapshots.set(readDestination(new URL(frame.href)), {
|
|
2073
|
-
prepared: {
|
|
2074
|
-
url: new URL(frame.href),
|
|
2075
|
-
entries: frame.entries.map(toCachedRouteEntry),
|
|
2076
|
-
error: frame.error,
|
|
2077
|
-
head: frame.head,
|
|
2078
|
-
cssBundles: frame.cssBundles,
|
|
2079
|
-
sequence: candidate.sequence,
|
|
2080
|
-
generation: core.getState().generation
|
|
2081
|
-
},
|
|
2082
|
-
committedAt: frame.committedAt,
|
|
2083
|
-
expiresAt: frame.committedAt + durationMillis.value
|
|
2084
|
-
});
|
|
2085
|
-
deleteExpiredSnapshots(retainedAt);
|
|
2086
|
-
scheduleSnapshotExpiration();
|
|
2087
|
-
}
|
|
2088
|
-
const error = frame === null ? null : frame.error;
|
|
2089
|
-
if (facts.demand !== null) applyFreshness(error === null ? satisfyDemand(facts, { sequence: candidate.sequence }) : failAttempt(facts, makeRevalidationError("route", error.error)));
|
|
2090
|
-
}
|
|
2091
|
-
} else if (interrupted) {
|
|
2092
|
-
if (active === null && facts.attempt.phase === "running") applyFreshness(abandonAttempt(facts));
|
|
2093
|
-
} else {
|
|
2094
|
-
const error = Cause.findErrorOption(exit.cause);
|
|
2095
|
-
if (Option.isNone(error)) core.publishEvent({
|
|
2096
|
-
_tag: "FailureReported",
|
|
2097
|
-
source: "navigation",
|
|
2098
|
-
cause: exit.cause
|
|
2099
|
-
});
|
|
2100
|
-
if (held && facts.attempt.phase === "running") {
|
|
2101
|
-
if (candidate.kind === "revalidation") {
|
|
2102
|
-
const revalidationError = Option.match(error, {
|
|
2103
|
-
onNone: () => {
|
|
2104
|
-
return makeRevalidationError("route", exit.cause);
|
|
2105
|
-
},
|
|
2106
|
-
onSome: classifyRefreshFailure
|
|
2107
|
-
});
|
|
2108
|
-
applyFreshness(options.refreshRetrySchedule !== void 0 && revalidationError.reason !== "build-mismatch" ? holdAttemptForRetry(facts, revalidationError) : failAttempt(facts, revalidationError));
|
|
2109
|
-
} else applyFreshness(abandonAttempt(facts));
|
|
2110
|
-
}
|
|
2111
|
-
}
|
|
2112
|
-
Deferred.doneUnsafe(candidate.intent.done, interrupted ? Effect.succeed({ _tag: "Superseded" }) : Exit.isSuccess(exit) ? Effect.succeed(exit.value) : Effect.failCause(exit.cause));
|
|
2113
|
-
}
|
|
2114
|
-
/** Commits the committed entries again at a new position, without a request. */
|
|
2115
|
-
function commitLocal(input) {
|
|
2116
|
-
return Result.map(commitFrame(core, {
|
|
2117
|
-
reason: input.reason,
|
|
2118
|
-
entries: input.frame.entries,
|
|
2119
|
-
error: input.frame.error,
|
|
2120
|
-
head: input.frame.head,
|
|
2121
|
-
cssBundles: input.frame.cssBundles,
|
|
2122
|
-
documentUpdate: "apply",
|
|
2123
|
-
position: input.position,
|
|
2124
|
-
scrollIntent: decideScrollIntent({
|
|
2125
|
-
url: input.url,
|
|
2126
|
-
options: input.options,
|
|
2127
|
-
restore: input.restore,
|
|
2128
|
-
entryCount: input.frame.entries.length,
|
|
2129
|
-
errorIndex: input.frame.error === null ? null : input.frame.error.index
|
|
2130
|
-
}),
|
|
2131
|
-
committedAt: input.frame.committedAt,
|
|
2132
|
-
scope: input.frame.scope
|
|
2133
|
-
}), (frame) => {
|
|
2134
|
-
if (active?.kind === "revalidation" && readDestination(new URL(active.intent.href)) === readDestination(new URL(frame.href))) active.intent = {
|
|
2135
|
-
...active.intent,
|
|
2136
|
-
href: frame.href,
|
|
2137
|
-
options: makeHistoryStateOptions(frame.historyState)
|
|
2138
|
-
};
|
|
2139
|
-
return {
|
|
2140
|
-
_tag: "Committed",
|
|
2141
|
-
href: frame.href
|
|
2142
|
-
};
|
|
2143
|
-
});
|
|
2144
|
-
}
|
|
2145
|
-
function navigateToHref(href, requested) {
|
|
2146
|
-
return Effect.suspend(() => beginNavigation(href, requested));
|
|
2147
|
-
}
|
|
2148
|
-
function recordNavigationTargetFailure(error) {
|
|
2149
|
-
core.updateState((state) => {
|
|
2150
|
-
return {
|
|
2151
|
-
...state,
|
|
2152
|
-
navigation: {
|
|
2153
|
-
...state.navigation,
|
|
2154
|
-
latest: AsyncResult.fail(error)
|
|
2155
|
-
}
|
|
2156
|
-
};
|
|
2157
|
-
});
|
|
2158
|
-
}
|
|
2159
|
-
function beginNavigation(href, requested) {
|
|
2160
|
-
{
|
|
2161
|
-
const state = core.getState();
|
|
2162
|
-
const frame = state.frame;
|
|
2163
|
-
if (state.lifecycle._tag === "PreBoot") {
|
|
2164
|
-
const queued = core.forkFiber(Effect.flatMap(Deferred.await(firstFrame), () => {
|
|
2165
|
-
return beginNavigation(href, requested);
|
|
2166
|
-
}));
|
|
2167
|
-
return Fiber.join(queued);
|
|
2168
|
-
}
|
|
2169
|
-
if (state.lifecycle._tag !== "Ready" || frame === null) return failAt("target", "The client is not ready to navigate");
|
|
2170
|
-
const parsed = Result.try(() => new URL(href, window.location.href));
|
|
2171
|
-
if (Result.isFailure(parsed)) return failAt("target", parsed.failure);
|
|
2172
|
-
const url = parsed.success;
|
|
2173
|
-
if (!/^https?:$/.test(url.protocol) || url.origin !== window.location.origin) return Effect.succeed(leaveDocument(url.href));
|
|
2174
|
-
const captured = Result.try(() => structuredClone(requested.historyState));
|
|
2175
|
-
if (Result.isFailure(captured)) return failAt("target", captured.failure);
|
|
2176
|
-
const options = {
|
|
2177
|
-
...requested,
|
|
2178
|
-
...makeHistoryStateOptions(captured.success)
|
|
2179
|
-
};
|
|
2180
|
-
const committed = new URL(frame.href);
|
|
2181
|
-
if (readDestination(url) === readDestination(committed)) {
|
|
2182
|
-
if (!checkBrowserPositionChanged() && normalizeFragment(url.hash) === normalizeFragment(committed.hash) && Object.is(options.historyState, frame.historyState)) return Effect.succeed({
|
|
2183
|
-
_tag: "Unchanged",
|
|
2184
|
-
href: frame.href
|
|
2185
|
-
});
|
|
2186
|
-
if (frame.key !== void 0) rememberScroll(frame.key, readScrollPosition());
|
|
2187
|
-
return Effect.fromResult(commitLocal({
|
|
2188
|
-
reason: "navigation",
|
|
2189
|
-
frame,
|
|
2190
|
-
url,
|
|
2191
|
-
position: {
|
|
2192
|
-
kind: "write",
|
|
2193
|
-
href: url.href,
|
|
2194
|
-
replace: options.replace === true,
|
|
2195
|
-
userState: options.historyState,
|
|
2196
|
-
key: createEntryKey()
|
|
2197
|
-
},
|
|
2198
|
-
options,
|
|
2199
|
-
restore: void 0
|
|
2200
|
-
}));
|
|
2201
|
-
}
|
|
2202
|
-
const historyMode = options.replace === true ? "replace" : "push";
|
|
2203
|
-
if (active?.kind === "navigation" && readDestination(new URL(active.intent.href)) === readDestination(url)) {
|
|
2204
|
-
const current = active.intent;
|
|
2205
|
-
if (current.href === url.href && matchesNavigationOptions(current.options, options)) return Deferred.await(current.done);
|
|
2206
|
-
active.intent = {
|
|
2207
|
-
href: url.href,
|
|
2208
|
-
options,
|
|
2209
|
-
historyMode,
|
|
2210
|
-
done: Deferred.makeUnsafe()
|
|
2211
|
-
};
|
|
2212
|
-
setPending(active);
|
|
2213
|
-
Deferred.doneUnsafe(current.done, Effect.succeed({ _tag: "Superseded" }));
|
|
2214
|
-
return Deferred.await(active.intent.done);
|
|
2215
|
-
}
|
|
2216
|
-
return Deferred.await(startCandidate({
|
|
2217
|
-
kind: "navigation",
|
|
2218
|
-
source: "navigate",
|
|
2219
|
-
recoverByReload: false,
|
|
2220
|
-
href: url.href,
|
|
2221
|
-
options,
|
|
2222
|
-
historyMode,
|
|
2223
|
-
restoreScroll: void 0
|
|
2224
|
-
}).intent.done);
|
|
2225
|
-
}
|
|
2226
|
-
}
|
|
2227
|
-
function requireRefresh(input) {
|
|
2228
|
-
const waiter = Deferred.makeUnsafe();
|
|
2229
|
-
retireSnapshots();
|
|
2230
|
-
const state = core.getState();
|
|
2231
|
-
refreshDebounce?.interruptUnsafe();
|
|
2232
|
-
refreshDebounce = input.debounce ? core.forkFiber(Effect.sleep("8 millis").pipe(Effect.andThen(Effect.sync(() => {
|
|
2233
|
-
refreshDebounce = null;
|
|
2234
|
-
ensureAttempt();
|
|
2235
|
-
})))) : null;
|
|
2236
|
-
const expected = /* @__PURE__ */ new Set();
|
|
2237
|
-
collecting = expected;
|
|
2238
|
-
applyFreshness(registerDemand(state.freshness, {
|
|
2239
|
-
sequence: state.sequence,
|
|
2240
|
-
waiter,
|
|
2241
|
-
skipWorkIndicator: input.skipWorkIndicator
|
|
2242
|
-
}));
|
|
2243
|
-
collecting = null;
|
|
2244
|
-
applyFreshness(expectQueries(core.getState().freshness, expected));
|
|
2245
|
-
return Deferred.await(waiter);
|
|
2246
|
-
}
|
|
2247
|
-
function invalidateSession() {
|
|
2248
|
-
return Effect.suspend(() => {
|
|
2249
|
-
core.retireGeneration();
|
|
2250
|
-
retireReusable();
|
|
2251
|
-
vacateCandidateSlot();
|
|
2252
|
-
return requireRefresh({
|
|
2253
|
-
skipWorkIndicator: false,
|
|
2254
|
-
debounce: false
|
|
2255
|
-
});
|
|
2256
|
-
});
|
|
2257
|
-
}
|
|
2258
|
-
function settleBoot(outcome) {
|
|
2259
|
-
Deferred.doneUnsafe(firstFrame, Result.isSuccess(outcome) ? Effect.void : Effect.fail(failAt("target", outcome.failure)));
|
|
2260
|
-
}
|
|
2261
|
-
/** Disposal: nothing outstanding survives the application. */
|
|
2262
|
-
function retireAll() {
|
|
2263
|
-
retired = true;
|
|
2264
|
-
Deferred.doneUnsafe(firstFrame, Effect.fail(failAt("target", "The client application is disposed")));
|
|
2265
|
-
retryRunner?.interruptUnsafe();
|
|
2266
|
-
refreshDebounce?.interruptUnsafe();
|
|
2267
|
-
refreshDebounce = null;
|
|
2268
|
-
retireReusable();
|
|
2269
|
-
vacateCandidateSlot();
|
|
2270
|
-
applyFreshness(retireDemand(core.getState().freshness, makeRevalidationError("disposed", "The client application is disposed")));
|
|
2271
|
-
}
|
|
2272
|
-
const queries = {
|
|
2273
|
-
expect: ({ id, token }) => {
|
|
2274
|
-
queryComputations.set(id, token);
|
|
2275
|
-
if (collecting !== null) {
|
|
2276
|
-
collecting.add(id);
|
|
2277
|
-
return;
|
|
2278
|
-
}
|
|
2279
|
-
if (!retired) applyFreshness(expectQueries(core.getState().freshness, /* @__PURE__ */ new Set([id])));
|
|
2280
|
-
},
|
|
2281
|
-
release: ({ id, token }) => {
|
|
2282
|
-
if (queryComputations.get(id) !== token) return;
|
|
2283
|
-
queryComputations.delete(id);
|
|
2284
|
-
if (!retired) applyFreshness(releaseQuery(core.getState().freshness, id));
|
|
2285
|
-
},
|
|
2286
|
-
acknowledge: (acknowledgment) => {
|
|
2287
|
-
const state = core.getState();
|
|
2288
|
-
if (acknowledgment.dispatched !== void 0 && acknowledgment.dispatched.generation !== state.generation) return;
|
|
2289
|
-
applyFreshness(acknowledgeQuery(state.freshness, {
|
|
2290
|
-
id: acknowledgment.id,
|
|
2291
|
-
sequence: acknowledgment.dispatched?.sequence,
|
|
2292
|
-
success: acknowledgment.success
|
|
2293
|
-
}));
|
|
2294
|
-
}
|
|
2295
|
-
};
|
|
2296
|
-
/** Whether the browser moved away from the committed frame during navigation. */
|
|
2297
|
-
function checkBrowserPositionChanged() {
|
|
2298
|
-
const frame = core.getState().frame;
|
|
2299
|
-
if (frame === null) return false;
|
|
2300
|
-
const position = readBrowserPosition();
|
|
2301
|
-
return Result.isFailure(position) || position.success.href !== frame.href || position.success.key !== frame.key;
|
|
2302
|
-
}
|
|
2303
|
-
function handlePopstate() {
|
|
2304
|
-
vacateCandidateSlot();
|
|
2305
|
-
const position = readBrowserPosition();
|
|
2306
|
-
const frame = core.getState().frame;
|
|
2307
|
-
if (Result.isFailure(position)) {
|
|
2308
|
-
reloadDocument(core, Cause.fail(position.failure));
|
|
2309
|
-
return;
|
|
2310
|
-
}
|
|
2311
|
-
if (frame === null) {
|
|
2312
|
-
reloadDocument(core, Cause.fail("No committed route is available for history traversal"));
|
|
2313
|
-
return;
|
|
2314
|
-
}
|
|
2315
|
-
const url = new URL(position.success.href);
|
|
2316
|
-
if (frame.key !== void 0 && frame.key !== position.success.key) rememberScroll(frame.key, readScrollPosition());
|
|
2317
|
-
const restore = position.success.key === void 0 ? void 0 : recallScroll(position.success.key);
|
|
2318
|
-
if (readDestination(url) === readDestination(new URL(frame.href))) {
|
|
2319
|
-
const committed = commitLocal({
|
|
2320
|
-
reason: "popstate",
|
|
2321
|
-
frame,
|
|
2322
|
-
url,
|
|
2323
|
-
position: {
|
|
2324
|
-
kind: "adopt",
|
|
2325
|
-
position: position.success
|
|
2326
|
-
},
|
|
2327
|
-
options: {},
|
|
2328
|
-
restore
|
|
2329
|
-
});
|
|
2330
|
-
if (Result.isFailure(committed) && committed.failure.stage !== "publication") reloadDocument(core, Cause.fail(committed.failure));
|
|
2331
|
-
return;
|
|
2332
|
-
}
|
|
2333
|
-
startCandidate({
|
|
2334
|
-
kind: "navigation",
|
|
2335
|
-
source: "popstate",
|
|
2336
|
-
recoverByReload: true,
|
|
2337
|
-
href: url.href,
|
|
2338
|
-
options: makeHistoryStateOptions(position.success.userState),
|
|
2339
|
-
historyMode: "adopt",
|
|
2340
|
-
restoreScroll: restore
|
|
2341
|
-
});
|
|
2342
|
-
}
|
|
2343
|
-
/** A reload or departure remembers the entry's scroll; boot restores it by the entry's key. */
|
|
2344
|
-
function handlePageHide() {
|
|
2345
|
-
const frame = core.getState().frame;
|
|
2346
|
-
if (frame !== null && frame.key !== void 0) rememberScroll(frame.key, readScrollPosition());
|
|
2347
|
-
}
|
|
2348
|
-
/** A document that becomes visible again is treated as focused; a hidden one is not. */
|
|
2349
|
-
function handleVisibilityChange() {
|
|
2350
|
-
if (document.visibilityState === "visible") handleFocus();
|
|
2351
|
-
}
|
|
2352
|
-
function handleFocus() {
|
|
2353
|
-
const focus = options.revalidateOnWindowFocus;
|
|
2354
|
-
const state = core.getState();
|
|
2355
|
-
const busy = active !== null || state.freshness.attempt.phase === "running" || [...state.operations.values()].some((operation) => operation.phase !== "settled");
|
|
2356
|
-
if (focus === void 0 || state.frame === null || busy || Date.now() - state.frame.committedAt < Duration.toMillis(focus.staleTime)) return;
|
|
2357
|
-
core.forkFiber(Effect.ignore(Effect.tapError(requireRefresh({
|
|
2358
|
-
skipWorkIndicator: focus.skipWorkIndicator,
|
|
2359
|
-
debounce: true
|
|
2360
|
-
}), (error) => {
|
|
2361
|
-
return Effect.sync(() => {
|
|
2362
|
-
core.publishEvent({
|
|
2363
|
-
_tag: "FailureReported",
|
|
2364
|
-
source: "revalidation",
|
|
2365
|
-
cause: Cause.fail(error)
|
|
2366
|
-
});
|
|
2367
|
-
});
|
|
2368
|
-
})));
|
|
2369
|
-
}
|
|
2370
|
-
return {
|
|
2371
|
-
navigate: navigateToHref,
|
|
2372
|
-
recordTargetFailure: recordNavigationTargetFailure,
|
|
2373
|
-
begin: beginNavigation,
|
|
2374
|
-
revalidate: (input) => Effect.suspend(() => requireRefresh({
|
|
2375
|
-
...input,
|
|
2376
|
-
debounce: true
|
|
2377
|
-
})),
|
|
2378
|
-
requireRefresh: (input) => requireRefresh({
|
|
2379
|
-
...input,
|
|
2380
|
-
debounce: false
|
|
2381
|
-
}),
|
|
2382
|
-
settleBoot,
|
|
2383
|
-
retireReusable,
|
|
2384
|
-
retireAll,
|
|
2385
|
-
invalidate: invalidateSession,
|
|
2386
|
-
queries,
|
|
2387
|
-
commitBoot: (prepared, scope, position) => {
|
|
2388
|
-
const restore = position.key === void 0 ? void 0 : recallScroll(position.key);
|
|
2389
|
-
const committed = commitFrame(core, {
|
|
2390
|
-
reason: "boot",
|
|
2391
|
-
entries: prepared.entries,
|
|
2392
|
-
error: prepared.error,
|
|
2393
|
-
head: prepared.head,
|
|
2394
|
-
cssBundles: prepared.cssBundles,
|
|
2395
|
-
documentUpdate: "adopt",
|
|
2396
|
-
position: selectFirstFramePosition(position),
|
|
2397
|
-
scrollIntent: selectFirstFrameScrollIntent(restore, prepared),
|
|
2398
|
-
committedAt: void 0,
|
|
2399
|
-
scope
|
|
2400
|
-
});
|
|
2401
|
-
if (Result.isSuccess(committed)) ensureAttempt();
|
|
2402
|
-
return committed;
|
|
2403
|
-
},
|
|
2404
|
-
attachBrowserListeners: () => {
|
|
2405
|
-
const previousScrollRestoration = window.history.scrollRestoration;
|
|
2406
|
-
window.history.scrollRestoration = "manual";
|
|
2407
|
-
window.addEventListener("popstate", handlePopstate);
|
|
2408
|
-
window.addEventListener("focus", handleFocus);
|
|
2409
|
-
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
2410
|
-
window.addEventListener("pagehide", handlePageHide);
|
|
2411
|
-
const detachPointerModality = attachPointerModalityTracking();
|
|
2412
|
-
return () => {
|
|
2413
|
-
if (window.history.scrollRestoration === "manual") window.history.scrollRestoration = previousScrollRestoration;
|
|
2414
|
-
detachPointerModality();
|
|
2415
|
-
window.removeEventListener("pagehide", handlePageHide);
|
|
2416
|
-
window.removeEventListener("popstate", handlePopstate);
|
|
2417
|
-
window.removeEventListener("focus", handleFocus);
|
|
2418
|
-
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
2419
|
-
};
|
|
2420
|
-
}
|
|
2421
|
-
};
|
|
2422
|
-
}
|
|
2423
|
-
//#endregion
|
|
2424
|
-
//#region src/client/request.ts
|
|
2425
|
-
/** Applies the application's request transformation before dispatch. */
|
|
2426
|
-
function prepareClientRequest(input) {
|
|
2427
|
-
if (input.prepareRequestFn === void 0) return Effect.succeed(input.context.request);
|
|
2428
|
-
return input.prepareRequestFn(input.context);
|
|
2429
|
-
}
|
|
2430
|
-
//#endregion
|
|
2431
|
-
//#region src/client/assembly.ts
|
|
2432
|
-
function createClientApplication(input) {
|
|
2433
|
-
const registry = AtomRegistry.make({ scheduleTask: input.scheduleTaskFn });
|
|
2434
|
-
const scope = Scope.makeUnsafe();
|
|
2435
|
-
const events = Effect.runSync(PubSub.unbounded());
|
|
2436
|
-
let contexts = Option.none();
|
|
2437
|
-
let retirement = Deferred.makeUnsafe();
|
|
2438
|
-
let clientBuildId = "";
|
|
2439
|
-
let deploymentId;
|
|
2440
|
-
let developmentMode = false;
|
|
2441
|
-
let disposal;
|
|
2442
|
-
function getState() {
|
|
2443
|
-
return registry.get(clientStateAtom);
|
|
2444
|
-
}
|
|
2445
|
-
function updateState(updateFn) {
|
|
2446
|
-
registry.update(clientStateAtom, updateFn);
|
|
2447
|
-
}
|
|
2448
|
-
function setLifecycle(lifecycle) {
|
|
2449
|
-
updateState((state) => {
|
|
2450
|
-
return {
|
|
2451
|
-
...state,
|
|
2452
|
-
lifecycle
|
|
2453
|
-
};
|
|
2454
|
-
});
|
|
2455
|
-
}
|
|
2456
|
-
function publishEvent(event) {
|
|
2457
|
-
if (disposal === void 0) PubSub.publishUnsafe(events, event);
|
|
2458
|
-
}
|
|
2459
|
-
function provideClient(effect) {
|
|
2460
|
-
return Effect.suspend(() => {
|
|
2461
|
-
return Option.isNone(contexts) ? Effect.fail(CLIENT_UNAVAILABLE_ERROR) : provideClientContext(contexts.value, effect);
|
|
2462
|
-
});
|
|
2463
|
-
}
|
|
2464
|
-
const core = {
|
|
2465
|
-
registry,
|
|
2466
|
-
scope,
|
|
2467
|
-
memoMap: Effect.runSync(Layer.makeMemoMap),
|
|
2468
|
-
mutationFibers: Effect.runSync(Effect.provideService(FiberSet.make(), Scope.Scope, scope)),
|
|
2469
|
-
events: Stream.fromPubSub(events),
|
|
2470
|
-
publishEvent,
|
|
2471
|
-
getState,
|
|
2472
|
-
updateState,
|
|
2473
|
-
forkFiber: (effect) => Effect.runSync(Effect.forkIn(effect, scope)),
|
|
2474
|
-
advanceSequence: () => {
|
|
2475
|
-
updateState((state) => {
|
|
2476
|
-
return {
|
|
2477
|
-
...state,
|
|
2478
|
-
sequence: state.sequence + 1
|
|
2479
|
-
};
|
|
2480
|
-
});
|
|
2481
|
-
return getState().sequence;
|
|
2482
|
-
},
|
|
2483
|
-
getGenerationRetirement: () => retirement,
|
|
2484
|
-
retireGeneration: () => {
|
|
2485
|
-
const retired = retirement;
|
|
2486
|
-
retirement = Deferred.makeUnsafe();
|
|
2487
|
-
updateState((state) => {
|
|
2488
|
-
return {
|
|
2489
|
-
...state,
|
|
2490
|
-
generation: state.generation + 1
|
|
2491
|
-
};
|
|
2492
|
-
});
|
|
2493
|
-
Deferred.doneUnsafe(retired, Effect.void);
|
|
2494
|
-
},
|
|
2495
|
-
getClientBuildId: () => clientBuildId,
|
|
2496
|
-
getDeploymentId: () => deploymentId,
|
|
2497
|
-
runsInDevelopmentMode: () => developmentMode,
|
|
2498
|
-
clientLoaders: createClientLoaderRegistry(),
|
|
2499
|
-
prepareRequest: (context) => {
|
|
2500
|
-
return provideClient(prepareClientRequest({
|
|
2501
|
-
context,
|
|
2502
|
-
prepareRequestFn: input.options.prepareRequestFn
|
|
2503
|
-
}));
|
|
2504
|
-
},
|
|
2505
|
-
provideClient,
|
|
2506
|
-
reportBuildMismatch: (mismatch) => {
|
|
2507
|
-
updateState((state) => {
|
|
2508
|
-
return {
|
|
2509
|
-
...state,
|
|
2510
|
-
buildMismatch: Option.some(mismatch)
|
|
2511
|
-
};
|
|
2512
|
-
});
|
|
2513
|
-
publishEvent({
|
|
2514
|
-
_tag: "BuildMismatchDetected",
|
|
2515
|
-
mismatch
|
|
2516
|
-
});
|
|
2517
|
-
},
|
|
2518
|
-
activations: Effect.runSync(Effect.provideService(createActivationMap(() => contexts), Scope.Scope, scope))
|
|
2519
|
-
};
|
|
2520
|
-
const prefetch = createPrefetch(core);
|
|
2521
|
-
const navigation = createNavigation(core, prefetch, {
|
|
2522
|
-
enableViewTransitions: input.options.enableViewTransitions === true,
|
|
2523
|
-
refreshRetrySchedule: input.options.refreshRetrySchedule,
|
|
2524
|
-
revalidateOnWindowFocus: input.options.revalidateOnWindowFocus === void 0 ? void 0 : {
|
|
2525
|
-
staleTime: input.options.revalidateOnWindowFocus.staleTime,
|
|
2526
|
-
skipWorkIndicator: input.options.revalidateOnWindowFocus.skipWorkIndicator === true
|
|
2527
|
-
}
|
|
2528
|
-
});
|
|
2529
|
-
const workIndicator = createWorkIndicator(core, input.options.workIndicator);
|
|
2530
|
-
const coordinator = {
|
|
2531
|
-
trackWork: workIndicator.track,
|
|
2532
|
-
registry,
|
|
2533
|
-
events: core.events,
|
|
2534
|
-
publishEvent,
|
|
2535
|
-
navigation,
|
|
2536
|
-
prefetch,
|
|
2537
|
-
queries: navigation.queries,
|
|
2538
|
-
fork: (effect) => core.forkFiber(effect),
|
|
2539
|
-
reportRenderFailure: (failure) => {
|
|
2540
|
-
publishEvent({
|
|
2541
|
-
_tag: "FailureReported",
|
|
2542
|
-
source: "render",
|
|
2543
|
-
cause: Cause.isCause(failure.error) ? failure.error : Cause.die(failure.error)
|
|
2544
|
-
});
|
|
2545
|
-
updateState((state) => {
|
|
2546
|
-
const frame = state.frame;
|
|
2547
|
-
if (frame === null || frame.revision !== failure.revision || failure.index === void 0 || failure.pattern === void 0 || frame.error !== null && frame.error.index <= failure.index) return state;
|
|
2548
|
-
return {
|
|
2549
|
-
...state,
|
|
2550
|
-
frame: {
|
|
2551
|
-
...frame,
|
|
2552
|
-
error: {
|
|
2553
|
-
index: failure.index,
|
|
2554
|
-
pattern: failure.pattern,
|
|
2555
|
-
source: "render",
|
|
2556
|
-
error: failure.error
|
|
2557
|
-
}
|
|
2558
|
-
}
|
|
2559
|
-
};
|
|
2560
|
-
});
|
|
2561
|
-
},
|
|
2562
|
-
endpoints: createEndpointOperations(core, {
|
|
2563
|
-
retireReusableFn: navigation.retireReusable,
|
|
2564
|
-
requireRefreshFn: navigation.requireRefresh,
|
|
2565
|
-
redirectFn: (url) => {
|
|
2566
|
-
return navigation.begin(url.href, { replace: true });
|
|
2567
|
-
},
|
|
2568
|
-
navigateDocumentFn: (href) => {
|
|
2569
|
-
window.location.assign(href);
|
|
2570
|
-
}
|
|
2571
|
-
})
|
|
2572
|
-
};
|
|
2573
|
-
const assembleServices = Effect.gen(function* () {
|
|
2574
|
-
const universalServices = input.options.universalServices === void 0 ? Layer.empty : input.options.universalServices;
|
|
2575
|
-
const universal = yield* Layer.buildWithMemoMap(universalServices, core.memoMap, scope);
|
|
2576
|
-
const universalLayer = Layer.succeedContext(universal);
|
|
2577
|
-
const clientOnly = input.options.clientOnlyServices === void 0 ? Layer.empty : Layer.provide(input.options.clientOnlyServices, universalLayer);
|
|
2578
|
-
const framework = Layer.mergeAll(Layer.succeed(ClientCoordinator, coordinator), Layer.succeed(AtomRegistry.AtomRegistry, registry), Reactivity.layer, FetchHttpClient.layer);
|
|
2579
|
-
return {
|
|
2580
|
-
universal,
|
|
2581
|
-
client: yield* Layer.buildWithMemoMap(Layer.merge(Layer.merge(framework, universalLayer), clientOnly), core.memoMap, scope)
|
|
2582
|
-
};
|
|
2583
|
-
});
|
|
2584
|
-
function startBrowserProgram() {
|
|
2585
|
-
const program = input.options.browserProgram;
|
|
2586
|
-
if (program === void 0) return;
|
|
2587
|
-
core.forkFiber(core.provideClient(program).pipe(Effect.tapCause((cause) => {
|
|
2588
|
-
return Cause.hasInterruptsOnly(cause) ? Effect.void : Effect.sync(() => {
|
|
2589
|
-
publishEvent({
|
|
2590
|
-
_tag: "FailureReported",
|
|
2591
|
-
source: "browserProgram",
|
|
2592
|
-
cause
|
|
2593
|
-
});
|
|
2594
|
-
});
|
|
2595
|
-
}), Effect.ignore));
|
|
2596
|
-
}
|
|
2597
|
-
/**
|
|
2598
|
-
* Imports the matching module in the background, after the first frame.
|
|
2599
|
-
* Once it is installed, client loaders start before the server answers
|
|
2600
|
-
* for every destination the registry can prove; until then, and if it
|
|
2601
|
-
* never arrives, nothing starts early and every navigation proceeds as
|
|
2602
|
-
* it would without the module.
|
|
2603
|
-
*/
|
|
2604
|
-
function loadRouteMatching() {
|
|
2605
|
-
core.forkFiber(Effect.tryPromise(() => import("../../route-matching-D_uSG2P9.js")).pipe(Effect.map((module) => {
|
|
2606
|
-
core.clientLoaders.installMatching(module);
|
|
2607
|
-
}), Effect.tapCause((cause) => {
|
|
2608
|
-
return Cause.hasInterruptsOnly(cause) ? Effect.void : Effect.sync(() => {
|
|
2609
|
-
publishEvent({
|
|
2610
|
-
_tag: "FailureReported",
|
|
2611
|
-
source: "clientLoaderEligibility",
|
|
2612
|
-
cause
|
|
2613
|
-
});
|
|
2614
|
-
});
|
|
2615
|
-
}), Effect.ignore));
|
|
2616
|
-
}
|
|
2617
|
-
/** Creates a typed boot failure; the outer boundary records it after cleanup. */
|
|
2618
|
-
function failBoot(message, cause) {
|
|
2619
|
-
const error = {
|
|
2620
|
-
_tag: "ClientBootError",
|
|
2621
|
-
message,
|
|
2622
|
-
...cause === void 0 ? {} : { cause }
|
|
2623
|
-
};
|
|
2624
|
-
return Exit.fail(error);
|
|
2625
|
-
}
|
|
2626
|
-
let bootStarted = false;
|
|
2627
|
-
let bootFiber;
|
|
2628
|
-
function bootApplication() {
|
|
2629
|
-
if (disposal !== void 0) return Promise.resolve(Exit.fail({
|
|
2630
|
-
_tag: "ClientBootError",
|
|
2631
|
-
message: "The client application is disposed"
|
|
2632
|
-
}));
|
|
2633
|
-
if (getState().lifecycle._tag !== "PreBoot" || bootStarted) return Promise.resolve(Exit.fail({
|
|
2634
|
-
_tag: "ClientBootError",
|
|
2635
|
-
message: "The client application boot already started"
|
|
2636
|
-
}));
|
|
2637
|
-
bootStarted = true;
|
|
2638
|
-
bootFiber = core.forkFiber(Effect.flatMap(Effect.scoped(prepareBoot()), (document) => {
|
|
2639
|
-
return Effect.suspend(() => finishBoot(document));
|
|
2640
|
-
}));
|
|
2641
|
-
return Effect.runPromise(Effect.map(Fiber.await(bootFiber), (exit) => {
|
|
2642
|
-
if (Exit.isSuccess(exit)) return exit;
|
|
2643
|
-
const failure = Exit.findErrorOption(exit);
|
|
2644
|
-
const error = Option.isSome(failure) ? failure.value : {
|
|
2645
|
-
_tag: "ClientBootError",
|
|
2646
|
-
message: "Client application boot did not complete",
|
|
2647
|
-
cause: exit.cause
|
|
2648
|
-
};
|
|
2649
|
-
if (disposal === void 0 && getState().lifecycle._tag === "PreBoot") {
|
|
2650
|
-
setLifecycle({
|
|
2651
|
-
_tag: "BootFailed",
|
|
2652
|
-
error
|
|
2653
|
-
});
|
|
2654
|
-
navigation.settleBoot(Result.fail(error));
|
|
2655
|
-
}
|
|
2656
|
-
return Exit.fail(error);
|
|
2657
|
-
}));
|
|
2658
|
-
}
|
|
2659
|
-
function prepareBoot() {
|
|
2660
|
-
return Effect.gen(function* () {
|
|
2661
|
-
const root = document.getElementById(DOCUMENT_PROTOCOL.elementIds.root);
|
|
2662
|
-
const dataElement = document.getElementById(DOCUMENT_PROTOCOL.elementIds.data);
|
|
2663
|
-
if (root === null || dataElement === null) return yield* failBoot("The document carries no Vorma root or bootstrap payload");
|
|
2664
|
-
const raw = Result.try(() => {
|
|
2665
|
-
return JSON.parse(dataElement.textContent);
|
|
2666
|
-
});
|
|
2667
|
-
if (Result.isFailure(raw)) return yield* failBoot("The bootstrap payload is not JSON", raw.failure);
|
|
2668
|
-
const url = new URL(window.location.href);
|
|
2669
|
-
const bootstrapRead = readBootstrapPayload(raw.success);
|
|
2670
|
-
if (Result.isFailure(bootstrapRead)) return yield* failBoot("The bootstrap payload's values do not restore", bootstrapRead.failure);
|
|
2671
|
-
const bootstrap = bootstrapRead.success;
|
|
2672
|
-
clientBuildId = bootstrap.clientBuildId;
|
|
2673
|
-
deploymentId = bootstrap.deploymentId;
|
|
2674
|
-
developmentMode = bootstrap.developmentMode;
|
|
2675
|
-
if (import.meta.env?.DEV && developmentMode) yield* (yield* Effect.tryPromise(() => import("../../development-B_rxuwY7.js")).pipe(Effect.catch((cause) => failBoot("Could not load development updates", cause)))).installDevelopmentUpdates({
|
|
2676
|
-
core,
|
|
2677
|
-
navigation
|
|
2678
|
-
});
|
|
2679
|
-
const documentHead = readDocumentHead();
|
|
2680
|
-
if (Result.isFailure(documentHead)) return yield* failBoot("The document's managed head could not be read", documentHead.failure);
|
|
2681
|
-
const payload = {
|
|
2682
|
-
routes: bootstrap.routes,
|
|
2683
|
-
serverError: bootstrap.serverError,
|
|
2684
|
-
head: documentHead.success,
|
|
2685
|
-
deps: [],
|
|
2686
|
-
cssBundles: readDocumentStylesheets(),
|
|
2687
|
-
staticAssets: []
|
|
2688
|
-
};
|
|
2689
|
-
const assembled = yield* Effect.result(assembleServices);
|
|
2690
|
-
if (getState().lifecycle._tag === "Disposed") return yield* failBoot("The client application was disposed during boot");
|
|
2691
|
-
if (Result.isFailure(assembled)) return yield* failBoot("Client application services could not be acquired", assembled.failure);
|
|
2692
|
-
contexts = Option.some(assembled.success);
|
|
2693
|
-
publishApplicationContexts({
|
|
2694
|
-
registry,
|
|
2695
|
-
contexts: assembled.success
|
|
2696
|
-
});
|
|
2697
|
-
registry.mount(clientRuntime);
|
|
2698
|
-
yield* workIndicator.start();
|
|
2699
|
-
startBrowserProgram();
|
|
2700
|
-
const read = readBrowserPosition();
|
|
2701
|
-
if (Result.isFailure(read)) publishEvent({
|
|
2702
|
-
_tag: "FailureReported",
|
|
2703
|
-
source: "navigation",
|
|
2704
|
-
cause: Cause.fail(read.failure)
|
|
2705
|
-
});
|
|
2706
|
-
const position = Result.isSuccess(read) ? read.success : {
|
|
2707
|
-
href: url.href,
|
|
2708
|
-
userState: void 0,
|
|
2709
|
-
key: void 0
|
|
2710
|
-
};
|
|
2711
|
-
const prepared = yield* Effect.result(prepareRoute(core, {
|
|
2712
|
-
source: {
|
|
2713
|
-
kind: "payload",
|
|
2714
|
-
url,
|
|
2715
|
-
payload
|
|
2716
|
-
},
|
|
2717
|
-
trigger: "boot",
|
|
2718
|
-
historyState: position.userState,
|
|
2719
|
-
prestart: false,
|
|
2720
|
-
reportReadinessFn: void 0
|
|
2721
|
-
}));
|
|
2722
|
-
if (getState().lifecycle._tag === "Disposed") return yield* failBoot("The client application was disposed during boot");
|
|
2723
|
-
if (Result.isFailure(prepared) || prepared.success._tag !== "Prepared") return yield* failBoot("The initial route could not be prepared", Result.isFailure(prepared) ? prepared.failure : prepared.success);
|
|
2724
|
-
if (getState().frame !== null) return {
|
|
2725
|
-
root,
|
|
2726
|
-
dataElement
|
|
2727
|
-
};
|
|
2728
|
-
const preparedRoute = prepared.success.prepared;
|
|
2729
|
-
return yield* Effect.acquireUseRelease(Scope.fork(scope), (frameScope) => {
|
|
2730
|
-
return Effect.gen(function* () {
|
|
2731
|
-
const entries = yield* Effect.result(acquireActivations(core.activations, {
|
|
2732
|
-
entries: preparedRoute.entries,
|
|
2733
|
-
scope: frameScope
|
|
2734
|
-
}));
|
|
2735
|
-
if (getState().lifecycle._tag === "Disposed") return yield* failBoot("The client application was disposed during boot");
|
|
2736
|
-
if (Result.isFailure(entries)) return yield* failBoot("The initial route's services could not be built", entries.failure);
|
|
2737
|
-
const committed = navigation.commitBoot({
|
|
2738
|
-
...preparedRoute,
|
|
2739
|
-
entries: entries.success
|
|
2740
|
-
}, frameScope, position);
|
|
2741
|
-
if (Result.isFailure(committed)) return yield* failBoot("The initial route could not be committed", committed.failure);
|
|
2742
|
-
return {
|
|
2743
|
-
root,
|
|
2744
|
-
dataElement
|
|
2745
|
-
};
|
|
2746
|
-
});
|
|
2747
|
-
}, (frameScope, exit) => {
|
|
2748
|
-
return getState().frame?.scope === frameScope ? Effect.void : Scope.close(frameScope, exit);
|
|
2749
|
-
});
|
|
2750
|
-
});
|
|
2751
|
-
}
|
|
2752
|
-
/** Renders the committed first frame, attaches the browser listeners, and marks the client ready. */
|
|
2753
|
-
function finishBoot({ root, dataElement }) {
|
|
2754
|
-
input.renderFn(root);
|
|
2755
|
-
const detach = navigation.attachBrowserListeners();
|
|
2756
|
-
Effect.runSync(Scope.addFinalizer(scope, Effect.sync(detach)));
|
|
2757
|
-
loadRouteMatching();
|
|
2758
|
-
dataElement.remove();
|
|
2759
|
-
setLifecycle({ _tag: "Ready" });
|
|
2760
|
-
navigation.settleBoot(Result.succeed(void 0));
|
|
2761
|
-
return Exit.void;
|
|
2762
|
-
}
|
|
2763
|
-
function disposeApplication() {
|
|
2764
|
-
if (disposal !== void 0) return disposal;
|
|
2765
|
-
setLifecycle({ _tag: "Disposed" });
|
|
2766
|
-
navigation.retireAll();
|
|
2767
|
-
const frame = getState().frame;
|
|
2768
|
-
disposal = Effect.runPromise((bootFiber === void 0 ? Effect.void : Fiber.interrupt(bootFiber)).pipe(Effect.andThen(Effect.sync(input.disposeRendererFn)), Effect.ensuring(frame === null ? Effect.void : Scope.close(frame.scope, Exit.void)), Effect.ensuring(Scope.close(scope, Exit.void)), Effect.ensuring(PubSub.shutdown(events)), Effect.ensuring(Effect.sync(() => registry.dispose()))));
|
|
2769
|
-
return disposal;
|
|
2770
|
-
}
|
|
2771
|
-
return {
|
|
2772
|
-
core,
|
|
2773
|
-
coordinator,
|
|
2774
|
-
navigation,
|
|
2775
|
-
prefetch,
|
|
2776
|
-
checkDisposed: () => disposal !== void 0,
|
|
2777
|
-
boot: bootApplication,
|
|
2778
|
-
dispose: disposeApplication
|
|
2779
|
-
};
|
|
2780
|
-
}
|
|
2781
|
-
//#endregion
|
|
2782
|
-
//#region src/client/react/view.tsx
|
|
2783
|
-
const Context$1 = createContext(null);
|
|
2784
|
-
const ApplicationProvider = Context$1.Provider;
|
|
2785
|
-
/** The mounted application. A Vorma component outside one cannot work, and React's protocol for that is a throw to the caller. */
|
|
2786
|
-
function useApplication() {
|
|
2787
|
-
const application = useContext(Context$1);
|
|
2788
|
-
if (application === null) throw new Error("Vorma React components render only inside a Vorma client application");
|
|
2789
|
-
return application;
|
|
2790
|
-
}
|
|
2791
|
-
function makeUiView(definition) {
|
|
2792
|
-
return definition;
|
|
2793
|
-
}
|
|
2794
|
-
const UiView = { make: makeUiView };
|
|
2795
|
-
const RENDER_FAILURE = Symbol("vorma/client/RenderFailure");
|
|
2796
|
-
function matchesTaggedFailure(error) {
|
|
2797
|
-
return typeof error === "object" && error !== null && RENDER_FAILURE in error;
|
|
2798
|
-
}
|
|
2799
|
-
/** Observes the navigation a query redirect started; renders its failure in the boundary and nothing otherwise. */
|
|
2800
|
-
function RedirectObservation(props) {
|
|
2801
|
-
const observation = useMemo(() => {
|
|
2802
|
-
return clientRuntime.atom(props.redirect.navigation);
|
|
2803
|
-
}, [props.redirect]);
|
|
2804
|
-
const result = useAtomValue(observation);
|
|
2805
|
-
const Boundary = props.boundary;
|
|
2806
|
-
if (result._tag === "Failure") return /* @__PURE__ */ jsx(Boundary, { error: projectRouteError({
|
|
2807
|
-
pattern: props.pattern,
|
|
2808
|
-
source: "render",
|
|
2809
|
-
error: result.cause
|
|
2810
|
-
}) });
|
|
2811
|
-
return null;
|
|
2812
|
-
}
|
|
2813
|
-
/** React's catch protocol, attributed to the matched entry that owns the boundary. */
|
|
2814
|
-
var RouteBoundary = class RouteBoundary extends Component {
|
|
2815
|
-
state = {
|
|
2816
|
-
revision: this.props.revision,
|
|
2817
|
-
caught: null
|
|
2818
|
-
};
|
|
2819
|
-
static getDerivedStateFromProps(props, state) {
|
|
2820
|
-
return props.revision === state.revision ? null : {
|
|
2821
|
-
revision: props.revision,
|
|
2822
|
-
caught: null
|
|
2823
|
-
};
|
|
2824
|
-
}
|
|
2825
|
-
static getDerivedStateFromError(error) {
|
|
2826
|
-
return { caught: {
|
|
2827
|
-
error,
|
|
2828
|
-
index: void 0,
|
|
2829
|
-
pattern: void 0
|
|
2830
|
-
} };
|
|
2831
|
-
}
|
|
2832
|
-
/** The failure with the position it belongs to: a rethrown one keeps its own, a new one is this boundary's. */
|
|
2833
|
-
attributeFailure(error) {
|
|
2834
|
-
return matchesTaggedFailure(error) ? error[RENDER_FAILURE] : {
|
|
2835
|
-
error,
|
|
2836
|
-
index: this.props.index,
|
|
2837
|
-
pattern: this.props.pattern
|
|
2838
|
-
};
|
|
2839
|
-
}
|
|
2840
|
-
componentDidCatch(error, _info) {
|
|
2841
|
-
const failure = this.attributeFailure(error);
|
|
2842
|
-
if (getQueryRedirect(failure.error) !== void 0) return;
|
|
2843
|
-
this.props.reportFn({
|
|
2844
|
-
index: failure.index,
|
|
2845
|
-
pattern: failure.pattern,
|
|
2846
|
-
revision: this.props.revision,
|
|
2847
|
-
error: failure.error
|
|
2848
|
-
});
|
|
2849
|
-
}
|
|
2850
|
-
render() {
|
|
2851
|
-
if (this.state.caught === null) return this.props.children;
|
|
2852
|
-
const failure = this.attributeFailure(this.state.caught.error);
|
|
2853
|
-
const Boundary = this.props.boundary;
|
|
2854
|
-
if (Boundary === void 0) throw { [RENDER_FAILURE]: failure };
|
|
2855
|
-
const redirect = getQueryRedirect(failure.error);
|
|
2856
|
-
return /* @__PURE__ */ jsx(RouteBoundary, {
|
|
2857
|
-
index: this.props.index,
|
|
2858
|
-
pattern: this.props.pattern,
|
|
2859
|
-
revision: this.props.revision,
|
|
2860
|
-
boundary: void 0,
|
|
2861
|
-
reportFn: this.props.reportFn,
|
|
2862
|
-
children: redirect === void 0 ? /* @__PURE__ */ jsx(Boundary, { error: projectRouteError({
|
|
2863
|
-
...failure,
|
|
2864
|
-
source: "render"
|
|
2865
|
-
}) }) : /* @__PURE__ */ jsx(RedirectObservation, {
|
|
2866
|
-
redirect,
|
|
2867
|
-
boundary: Boundary,
|
|
2868
|
-
pattern: failure.pattern
|
|
2869
|
-
})
|
|
2870
|
-
});
|
|
2871
|
-
}
|
|
2872
|
-
};
|
|
2873
|
-
/** The nearest authored boundary strictly above an error position, or the application boundary outside the route tree. */
|
|
2874
|
-
function selectBoundary(frame, application) {
|
|
2875
|
-
const error = frame.error;
|
|
2876
|
-
if (error === null) return null;
|
|
2877
|
-
for (let index = error.index - 1; index >= 0; index--) {
|
|
2878
|
-
const entry = frame.entries[index];
|
|
2879
|
-
const boundary = (entry?.view)?.descendantErrorBoundary;
|
|
2880
|
-
if (entry !== void 0 && boundary !== void 0) return {
|
|
2881
|
-
index,
|
|
2882
|
-
component: boundary
|
|
2883
|
-
};
|
|
2884
|
-
}
|
|
2885
|
-
return {
|
|
2886
|
-
index: -1,
|
|
2887
|
-
component: application.defaultErrorBoundary
|
|
2888
|
-
};
|
|
2889
|
-
}
|
|
2890
|
-
/** An owning view's Outlet contains its descendant boundary, not the owning component. */
|
|
2891
|
-
function RouteOutlet(props) {
|
|
2892
|
-
const application = useApplication();
|
|
2893
|
-
const frame = useAtomValue(committedFrameAtom);
|
|
2894
|
-
const boundary = frame === null ? null : selectBoundary(frame, application);
|
|
2895
|
-
const scrollIntent = frame?.scrollIntent;
|
|
2896
|
-
const revision = frame?.revision ?? 0;
|
|
2897
|
-
const scrollDepth = boundary === null ? scrollIntent?.entryIndex : boundary.index + 1;
|
|
2898
|
-
useLayoutEffect(() => {
|
|
2899
|
-
if (scrollIntent === void 0 || scrollDepth !== props.index) return;
|
|
2900
|
-
const applied = applyScroll(scrollIntent.target);
|
|
2901
|
-
if (Result.isFailure(applied)) application.coordinator.publishEvent({
|
|
2902
|
-
_tag: "FailureReported",
|
|
2903
|
-
source: "navigation",
|
|
2904
|
-
cause: Cause.die(applied.failure)
|
|
2905
|
-
});
|
|
2906
|
-
}, [
|
|
2907
|
-
application,
|
|
2908
|
-
props.index,
|
|
2909
|
-
revision,
|
|
2910
|
-
scrollDepth,
|
|
2911
|
-
scrollIntent
|
|
2912
|
-
]);
|
|
2913
|
-
if (frame === null) return null;
|
|
2914
|
-
const ownerIndex = props.index - 1;
|
|
2915
|
-
const owner = frame.entries[ownerIndex];
|
|
2916
|
-
const rendersFailure = boundary !== null && boundary.index === ownerIndex;
|
|
2917
|
-
const ownerView = owner?.view;
|
|
2918
|
-
const Boundary = boundary?.component;
|
|
2919
|
-
return /* @__PURE__ */ jsx(RouteBoundary, {
|
|
2920
|
-
index: ownerIndex < 0 ? void 0 : ownerIndex,
|
|
2921
|
-
pattern: owner?.pattern,
|
|
2922
|
-
revision: frame.revision,
|
|
2923
|
-
boundary: rendersFailure ? void 0 : ownerIndex < 0 ? application.defaultErrorBoundary : ownerView?.descendantErrorBoundary,
|
|
2924
|
-
reportFn: application.coordinator.reportRenderFailure,
|
|
2925
|
-
children: rendersFailure && Boundary !== void 0 && frame.error !== null ? /* @__PURE__ */ jsx(Boundary, { error: projectRouteError(frame.error) }) : /* @__PURE__ */ jsx(EntryAtIndex, { index: props.index })
|
|
2926
|
-
}, rendersFailure ? "failure" : "children");
|
|
2927
|
-
}
|
|
2928
|
-
/** Renders one successful position. Its own rendering failures propagate to an enclosing Outlet. */
|
|
2929
|
-
function EntryAtIndex(props) {
|
|
2930
|
-
const frame = useAtomValue(committedFrameAtom);
|
|
2931
|
-
const index = props.index;
|
|
2932
|
-
const entry = frame?.entries[index];
|
|
2933
|
-
const Outlet = useMemo(() => {
|
|
2934
|
-
return function Outlet() {
|
|
2935
|
-
return /* @__PURE__ */ jsx(RouteOutlet, { index: index + 1 });
|
|
2936
|
-
};
|
|
2937
|
-
}, [index]);
|
|
2938
|
-
if (frame === null || entry === void 0) return null;
|
|
2939
|
-
const view = entry.view;
|
|
2940
|
-
if (view === void 0) return /* @__PURE__ */ jsx(RouteOutlet, { index: index + 1 });
|
|
2941
|
-
return /* @__PURE__ */ jsx(MatchedView, {
|
|
2942
|
-
entry,
|
|
2943
|
-
frame,
|
|
2944
|
-
index,
|
|
2945
|
-
view,
|
|
2946
|
-
Outlet
|
|
2947
|
-
}, entry.pattern);
|
|
2948
|
-
}
|
|
2949
|
-
function MatchedView(props) {
|
|
2950
|
-
const application = useApplication();
|
|
2951
|
-
const [routeContext] = useState(() => {
|
|
2952
|
-
return createRouteContext(props.index, props.entry);
|
|
2953
|
-
});
|
|
2954
|
-
const activation = props.entry.activation;
|
|
2955
|
-
const viewProps = {
|
|
2956
|
-
...routeContext,
|
|
2957
|
-
Outlet: props.Outlet,
|
|
2958
|
-
clientRuntime: activation === void 0 ? clientRuntime : activation.clientRuntime,
|
|
2959
|
-
universalRuntime: activation === void 0 ? universalRuntime : activation.universalRuntime
|
|
2960
|
-
};
|
|
2961
|
-
const View = props.view.component;
|
|
2962
|
-
return /* @__PURE__ */ jsx(RouteBoundary, {
|
|
2963
|
-
index: props.index,
|
|
2964
|
-
pattern: props.entry.pattern,
|
|
2965
|
-
revision: props.frame.revision,
|
|
2966
|
-
boundary: void 0,
|
|
2967
|
-
reportFn: application.coordinator.reportRenderFailure,
|
|
2968
|
-
children: /* @__PURE__ */ jsx(View, { ...viewProps })
|
|
2969
|
-
});
|
|
2970
|
-
}
|
|
2971
|
-
/** The application boundary encloses every route. */
|
|
2972
|
-
function RootOutlet() {
|
|
2973
|
-
return /* @__PURE__ */ jsx(RouteOutlet, { index: 0 });
|
|
2974
|
-
}
|
|
2975
|
-
//#endregion
|
|
2976
|
-
//#region src/client/react/application.tsx
|
|
2977
|
-
function makeClientApplication(options) {
|
|
2978
|
-
let root;
|
|
2979
|
-
function renderIntoRoot(rootElement, node) {
|
|
2980
|
-
if (root === void 0) root = createRoot(rootElement);
|
|
2981
|
-
root.render(node);
|
|
2982
|
-
}
|
|
2983
|
-
function Providers(props) {
|
|
2984
|
-
return /* @__PURE__ */ jsx(RegistryContext.Provider, {
|
|
2985
|
-
value: application.core.registry,
|
|
2986
|
-
children: /* @__PURE__ */ jsx(ApplicationProvider, {
|
|
2987
|
-
value: {
|
|
2988
|
-
coordinator: application.coordinator,
|
|
2989
|
-
defaultErrorBoundary: options.defaultErrorBoundary,
|
|
2990
|
-
defaultLinkProps: options.defaultLinkProps
|
|
2991
|
-
},
|
|
2992
|
-
children: props.children
|
|
2993
|
-
})
|
|
2994
|
-
});
|
|
2995
|
-
}
|
|
2996
|
-
const application = createClientApplication({
|
|
2997
|
-
options,
|
|
2998
|
-
scheduleTaskFn: scheduleTask,
|
|
2999
|
-
renderFn: (rootElement) => {
|
|
3000
|
-
renderIntoRoot(rootElement, /* @__PURE__ */ jsx(Providers, { children: /* @__PURE__ */ jsx(RootOutlet, {}) }));
|
|
3001
|
-
},
|
|
3002
|
-
disposeRendererFn: () => {
|
|
3003
|
-
const mounted = root;
|
|
3004
|
-
root = void 0;
|
|
3005
|
-
mounted?.unmount();
|
|
3006
|
-
}
|
|
3007
|
-
});
|
|
3008
|
-
async function boot() {
|
|
3009
|
-
const exit = await application.boot();
|
|
3010
|
-
if (Exit.isSuccess(exit)) return exit;
|
|
3011
|
-
application.core.publishEvent({
|
|
3012
|
-
_tag: "FailureReported",
|
|
3013
|
-
source: "navigation",
|
|
3014
|
-
cause: exit.cause
|
|
3015
|
-
});
|
|
3016
|
-
const rendered = Result.try(() => {
|
|
3017
|
-
const rootElement = document.getElementById(DOCUMENT_PROTOCOL.elementIds.root);
|
|
3018
|
-
const Boundary = options.defaultErrorBoundary;
|
|
3019
|
-
if (rootElement !== null && !application.checkDisposed()) renderIntoRoot(rootElement, /* @__PURE__ */ jsx(Providers, { children: /* @__PURE__ */ jsx(Boundary, { error: {
|
|
3020
|
-
source: "boot",
|
|
3021
|
-
cause: exit.cause,
|
|
3022
|
-
status: 500,
|
|
3023
|
-
clientMessage: Cause.hasInterruptsOnly(exit.cause) ? "The application stopped before it started." : "The application could not start."
|
|
3024
|
-
} }) }));
|
|
3025
|
-
});
|
|
3026
|
-
if (Result.isFailure(rendered)) application.core.publishEvent({
|
|
3027
|
-
_tag: "FailureReported",
|
|
3028
|
-
source: "render",
|
|
3029
|
-
cause: Cause.die(rendered.failure)
|
|
3030
|
-
});
|
|
3031
|
-
return exit;
|
|
3032
|
-
}
|
|
3033
|
-
return {
|
|
3034
|
-
_tag: "ClientApplication",
|
|
3035
|
-
boot,
|
|
3036
|
-
dispose: () => application.dispose()
|
|
3037
|
-
};
|
|
3038
|
-
}
|
|
3039
|
-
const ClientApplication = { make: makeClientApplication };
|
|
3040
|
-
//#endregion
|
|
3041
|
-
//#region src/client/react/hooks.ts
|
|
3042
|
-
/** Reads a projection; without a selector the value itself is the selection. */
|
|
3043
|
-
function useProjection(atom, selectFn) {
|
|
3044
|
-
return useAtomValue(atom, selectFn === void 0 ? identity : selectFn);
|
|
3045
|
-
}
|
|
3046
|
-
/** Keeps the browser URL synchronized with one typed React target. */
|
|
3047
|
-
function useRouteSynchronization(input) {
|
|
3048
|
-
const application = useApplication();
|
|
3049
|
-
const committedHref = useAtomValue(routeStateAtom, (route) => Option.isSome(route) ? route.value.href : void 0);
|
|
3050
|
-
const resolvedTarget = toHref(input);
|
|
3051
|
-
const targetReference = useRef(resolvedTarget);
|
|
3052
|
-
targetReference.current = resolvedTarget;
|
|
3053
|
-
const targetIdentity = Result.isSuccess(resolvedTarget) ? `success\0${resolvedTarget.success}` : `failure\0${input.pattern}\0${resolvedTarget.failure.message}`;
|
|
3054
|
-
const enabled = input.enabled;
|
|
3055
|
-
const debounceDelay = input.debounceDelay;
|
|
3056
|
-
const replace = input.replace;
|
|
3057
|
-
const scrollToTop = input.scrollToTop;
|
|
3058
|
-
const scrollToFragment = input.scrollToFragment;
|
|
3059
|
-
const skipWorkIndicator = input.skipWorkIndicator;
|
|
3060
|
-
useEffect(() => {
|
|
3061
|
-
if (enabled === false) return;
|
|
3062
|
-
const fiber = application.coordinator.fork(Effect.ignore(Effect.flatMap(readSynchronizationTarget({
|
|
3063
|
-
coordinator: application.coordinator,
|
|
3064
|
-
href: targetReference.current,
|
|
3065
|
-
options: {
|
|
3066
|
-
...replace === void 0 ? {} : { replace },
|
|
3067
|
-
...scrollToTop === void 0 ? {} : { scrollToTop },
|
|
3068
|
-
...scrollToFragment === void 0 ? {} : { scrollToFragment },
|
|
3069
|
-
...skipWorkIndicator === void 0 ? {} : { skipWorkIndicator }
|
|
3070
|
-
}
|
|
3071
|
-
}), (target) => {
|
|
3072
|
-
return runSynchronizationTarget({
|
|
3073
|
-
coordinator: application.coordinator,
|
|
3074
|
-
target,
|
|
3075
|
-
debounceDelay
|
|
3076
|
-
});
|
|
3077
|
-
})));
|
|
3078
|
-
return () => fiber.interruptUnsafe();
|
|
3079
|
-
}, [
|
|
3080
|
-
application.coordinator,
|
|
3081
|
-
committedHref,
|
|
3082
|
-
debounceDelay,
|
|
3083
|
-
enabled,
|
|
3084
|
-
replace,
|
|
3085
|
-
scrollToTop,
|
|
3086
|
-
scrollToFragment,
|
|
3087
|
-
skipWorkIndicator,
|
|
3088
|
-
targetIdentity
|
|
3089
|
-
]);
|
|
3090
|
-
}
|
|
3091
|
-
function useLoaderData(input, selectFn) {
|
|
3092
|
-
return useProjection(getLoaderDataAtom(input), selectFn);
|
|
3093
|
-
}
|
|
3094
|
-
function useClientLoaderData(input, selectFn) {
|
|
3095
|
-
return useProjection(getClientLoaderDataAtom(input), selectFn);
|
|
3096
|
-
}
|
|
3097
|
-
function useRouteMatch(input, selectFn) {
|
|
3098
|
-
return useProjection(getRouteMatchAtom(input), selectFn);
|
|
3099
|
-
}
|
|
3100
|
-
function useRouteState(selectFn) {
|
|
3101
|
-
return useProjection(routeStateAtom, selectFn);
|
|
3102
|
-
}
|
|
3103
|
-
function useNavigationState(selectFn) {
|
|
3104
|
-
return useProjection(navigationStateAtom, selectFn);
|
|
3105
|
-
}
|
|
3106
|
-
function useRevalidationState(selectFn) {
|
|
3107
|
-
return useProjection(revalidationStateAtom, selectFn);
|
|
3108
|
-
}
|
|
3109
|
-
function useWorkState(selectFn) {
|
|
3110
|
-
return useProjection(workStateAtom, selectFn);
|
|
3111
|
-
}
|
|
3112
|
-
function useBuildMismatch(selectFn) {
|
|
3113
|
-
return useProjection(buildMismatchAtom, selectFn);
|
|
3114
|
-
}
|
|
3115
|
-
//#endregion
|
|
3116
|
-
//#region src/client/react/link.tsx
|
|
3117
|
-
const NO_LINK_STATE = Atom.make({
|
|
3118
|
-
active: {
|
|
3119
|
-
exact: false,
|
|
3120
|
-
ancestor: false
|
|
3121
|
-
},
|
|
3122
|
-
pending: {
|
|
3123
|
-
exact: false,
|
|
3124
|
-
ancestor: false
|
|
3125
|
-
}
|
|
3126
|
-
});
|
|
3127
|
-
const ATTRIBUTES = DOCUMENT_PROTOCOL.attributes;
|
|
3128
|
-
/** The active and pending facts of a target under its match rules; an invalid target follows the applicable route boundary. */
|
|
3129
|
-
function useLinkState(input) {
|
|
3130
|
-
const href = toHref(input);
|
|
3131
|
-
if (Result.isFailure(href)) throw href.failure;
|
|
3132
|
-
return useAtomValue(getLinkStateAtomFor({
|
|
3133
|
-
href: href.success,
|
|
3134
|
-
pattern: input.pattern,
|
|
3135
|
-
rules: input.matchRules
|
|
3136
|
-
}));
|
|
3137
|
-
}
|
|
3138
|
-
function LinkComponent(rawProps) {
|
|
3139
|
-
const application = useApplication();
|
|
3140
|
-
const props = {
|
|
3141
|
-
...application.defaultLinkProps,
|
|
3142
|
-
...rawProps
|
|
3143
|
-
};
|
|
3144
|
-
const { href: _href, pattern, params: _params, splatValues: _splatValues, input: _input, hash: _hash, prefetch, prefetchDelayMs, visitOnPointerDown, matchRules, replace, scrollToTop, scrollToFragment, historyState, skipWorkIndicator, children, ...anchorProps } = props;
|
|
3145
|
-
const resolved = toHref(props);
|
|
3146
|
-
if (Result.isFailure(resolved)) throw resolved.failure;
|
|
3147
|
-
const href = resolved.success;
|
|
3148
|
-
const external = Result.map(Result.try(() => new URL(href, window.location.href)), (url) => {
|
|
3149
|
-
return !/^https?:$/.test(url.protocol) || url.origin !== window.location.origin;
|
|
3150
|
-
});
|
|
3151
|
-
const isExternal = Result.getOrElse(external, () => true);
|
|
3152
|
-
const state = useAtomValue(isExternal || matchRules?.skip === true ? NO_LINK_STATE : getLinkStateAtomFor({
|
|
3153
|
-
href,
|
|
3154
|
-
pattern,
|
|
3155
|
-
rules: matchRules
|
|
3156
|
-
}));
|
|
3157
|
-
const wantsPrefetch = prefetch === "intent" && !isExternal;
|
|
3158
|
-
const intent = useMemo(() => {
|
|
3159
|
-
return createLinkIntent({
|
|
3160
|
-
coordinator: application.coordinator,
|
|
3161
|
-
getHrefFn: () => href,
|
|
3162
|
-
skipWorkIndicator: skipWorkIndicator === true
|
|
3163
|
-
});
|
|
3164
|
-
}, [
|
|
3165
|
-
application.coordinator,
|
|
3166
|
-
href,
|
|
3167
|
-
skipWorkIndicator
|
|
3168
|
-
]);
|
|
3169
|
-
useEffect(() => () => intent.reset(), [intent]);
|
|
3170
|
-
/** Whether the browser would handle this activation itself. */
|
|
3171
|
-
function leavesToBrowser(event) {
|
|
3172
|
-
return isExternal || event.defaultPrevented || anchorProps.download !== void 0 || !allowsInterception(event, anchorProps.target);
|
|
3173
|
-
}
|
|
3174
|
-
function navigateToLinkHref() {
|
|
3175
|
-
application.coordinator.fork(Effect.ignore(application.coordinator.navigation.navigate(href, {
|
|
3176
|
-
...replace === void 0 ? {} : { replace },
|
|
3177
|
-
...scrollToTop === void 0 ? {} : { scrollToTop },
|
|
3178
|
-
...scrollToFragment === void 0 ? {} : { scrollToFragment },
|
|
3179
|
-
...historyState === void 0 ? {} : { historyState },
|
|
3180
|
-
...skipWorkIndicator === void 0 ? {} : { skipWorkIndicator }
|
|
3181
|
-
})));
|
|
3182
|
-
}
|
|
3183
|
-
function handleClick(event) {
|
|
3184
|
-
anchorProps.onClick?.(event);
|
|
3185
|
-
if (leavesToBrowser(event)) return;
|
|
3186
|
-
event.preventDefault();
|
|
3187
|
-
navigateToLinkHref();
|
|
3188
|
-
}
|
|
3189
|
-
function handlePointerDown(event) {
|
|
3190
|
-
anchorProps.onPointerDown?.(event);
|
|
3191
|
-
if (visitOnPointerDown !== true || event.pointerType !== "mouse" && event.pointerType !== "pen" || leavesToBrowser(event)) return;
|
|
3192
|
-
event.preventDefault();
|
|
3193
|
-
event.currentTarget.addEventListener("click", (click) => click.preventDefault(), { once: true });
|
|
3194
|
-
navigateToLinkHref();
|
|
3195
|
-
}
|
|
3196
|
-
const attributes = {};
|
|
3197
|
-
if (!isExternal && matchRules?.skip !== true) {
|
|
3198
|
-
if (state.active.exact) {
|
|
3199
|
-
attributes[ATTRIBUTES.linkActiveExact] = "";
|
|
3200
|
-
attributes["aria-current"] = anchorProps["aria-current"] === void 0 ? "page" : anchorProps["aria-current"];
|
|
3201
|
-
}
|
|
3202
|
-
if (state.active.ancestor) attributes[ATTRIBUTES.linkActiveAncestor] = "";
|
|
3203
|
-
if (state.pending.exact) attributes[ATTRIBUTES.linkPendingExact] = "";
|
|
3204
|
-
if (state.pending.ancestor) attributes[ATTRIBUTES.linkPendingAncestor] = "";
|
|
3205
|
-
}
|
|
3206
|
-
return /* @__PURE__ */ jsx("a", {
|
|
3207
|
-
...anchorProps,
|
|
3208
|
-
...attributes,
|
|
3209
|
-
href,
|
|
3210
|
-
onClick: handleClick,
|
|
3211
|
-
onPointerDown: handlePointerDown,
|
|
3212
|
-
onPointerEnter: (event) => {
|
|
3213
|
-
if (wantsPrefetch) intent.begin("pointer", prefetchDelayMs);
|
|
3214
|
-
anchorProps.onPointerEnter?.(event);
|
|
3215
|
-
},
|
|
3216
|
-
onPointerLeave: (event) => {
|
|
3217
|
-
if (wantsPrefetch) intent.end("pointer");
|
|
3218
|
-
anchorProps.onPointerLeave?.(event);
|
|
3219
|
-
},
|
|
3220
|
-
onFocus: (event) => {
|
|
3221
|
-
if (wantsPrefetch) intent.begin("focus", prefetchDelayMs);
|
|
3222
|
-
anchorProps.onFocus?.(event);
|
|
3223
|
-
},
|
|
3224
|
-
onBlur: (event) => {
|
|
3225
|
-
if (wantsPrefetch) intent.end("focus");
|
|
3226
|
-
anchorProps.onBlur?.(event);
|
|
3227
|
-
},
|
|
3228
|
-
onTouchCancel: (event) => {
|
|
3229
|
-
if (wantsPrefetch) intent.reset();
|
|
3230
|
-
anchorProps.onTouchCancel?.(event);
|
|
3231
|
-
},
|
|
3232
|
-
children
|
|
3233
|
-
});
|
|
3234
|
-
}
|
|
3235
|
-
const Link = memo(LinkComponent);
|
|
3236
|
-
//#endregion
|
|
3237
|
-
export { ClientApplication, Link, UiView, useBuildMismatch, useClientLoaderData, useLinkState, useLoaderData, useNavigationState, useRevalidationState, useRouteMatch, useRouteState, useRouteSynchronization, useWorkState };
|