@typed/router 0.32.0 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +129 -2
- package/dist/AST.d.ts +96 -0
- package/dist/AST.d.ts.map +1 -0
- package/dist/AST.js +32 -0
- package/dist/CurrentRoute.d.ts +18 -0
- package/dist/CurrentRoute.d.ts.map +1 -0
- package/dist/CurrentRoute.js +18 -0
- package/dist/Matcher.d.ts +209 -0
- package/dist/Matcher.d.ts.map +1 -0
- package/dist/Matcher.js +633 -0
- package/dist/Parser.d.ts +92 -0
- package/dist/Parser.d.ts.map +1 -0
- package/dist/Parser.js +1 -0
- package/dist/Path.d.ts +216 -0
- package/dist/Path.d.ts.map +1 -0
- package/dist/Path.js +248 -0
- package/dist/Route.d.ts +57 -0
- package/dist/Route.d.ts.map +1 -0
- package/dist/Route.js +151 -0
- package/dist/Router.d.ts +9 -0
- package/dist/Router.d.ts.map +1 -0
- package/dist/Router.js +8 -0
- package/dist/Uri.d.ts +115 -0
- package/dist/Uri.d.ts.map +1 -0
- package/dist/Uri.js +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/package.json +32 -73
- package/src/AST.ts +166 -0
- package/src/CurrentRoute.ts +30 -331
- package/src/Matcher.test.ts +496 -0
- package/src/Matcher.ts +1375 -325
- package/src/Parser.ts +276 -0
- package/src/Path.test.ts +318 -0
- package/src/Path.ts +691 -0
- package/src/Route.test.ts +268 -0
- package/src/Route.ts +316 -0
- package/src/Router.ts +33 -0
- package/src/Uri.ts +214 -0
- package/src/index.ts +4 -28
- package/CurrentRoute/package.json +0 -6
- package/LICENSE +0 -21
- package/MatchInput/package.json +0 -6
- package/Matcher/package.json +0 -6
- package/RouteGuard/package.json +0 -6
- package/RouteMatch/package.json +0 -6
- package/dist/cjs/CurrentRoute.js +0 -170
- package/dist/cjs/CurrentRoute.js.map +0 -1
- package/dist/cjs/MatchInput.js +0 -96
- package/dist/cjs/MatchInput.js.map +0 -1
- package/dist/cjs/Matcher.js +0 -138
- package/dist/cjs/Matcher.js.map +0 -1
- package/dist/cjs/RouteGuard.js +0 -78
- package/dist/cjs/RouteGuard.js.map +0 -1
- package/dist/cjs/RouteMatch.js +0 -49
- package/dist/cjs/RouteMatch.js.map +0 -1
- package/dist/cjs/index.js +0 -53
- package/dist/cjs/index.js.map +0 -1
- package/dist/dts/CurrentRoute.d.ts +0 -94
- package/dist/dts/CurrentRoute.d.ts.map +0 -1
- package/dist/dts/MatchInput.d.ts +0 -143
- package/dist/dts/MatchInput.d.ts.map +0 -1
- package/dist/dts/Matcher.d.ts +0 -121
- package/dist/dts/Matcher.d.ts.map +0 -1
- package/dist/dts/RouteGuard.d.ts +0 -94
- package/dist/dts/RouteGuard.d.ts.map +0 -1
- package/dist/dts/RouteMatch.d.ts +0 -50
- package/dist/dts/RouteMatch.d.ts.map +0 -1
- package/dist/dts/index.d.ts +0 -24
- package/dist/dts/index.d.ts.map +0 -1
- package/dist/esm/CurrentRoute.js +0 -152
- package/dist/esm/CurrentRoute.js.map +0 -1
- package/dist/esm/MatchInput.js +0 -79
- package/dist/esm/MatchInput.js.map +0 -1
- package/dist/esm/Matcher.js +0 -130
- package/dist/esm/Matcher.js.map +0 -1
- package/dist/esm/RouteGuard.js +0 -57
- package/dist/esm/RouteGuard.js.map +0 -1
- package/dist/esm/RouteMatch.js +0 -29
- package/dist/esm/RouteMatch.js.map +0 -1
- package/dist/esm/index.js +0 -24
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/package.json +0 -4
- package/src/MatchInput.ts +0 -303
- package/src/RouteGuard.ts +0 -217
- package/src/RouteMatch.ts +0 -104
package/dist/Matcher.js
ADDED
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
import * as findMyWay from "find-my-way-ts";
|
|
2
|
+
import * as Cause from "effect/Cause";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Exit from "effect/Exit";
|
|
5
|
+
import { interrupt, isSuccess } from "effect/Exit";
|
|
6
|
+
import { dual, identity } from "effect/Function";
|
|
7
|
+
import * as Result from "effect/Result";
|
|
8
|
+
import * as Layer from "effect/Layer";
|
|
9
|
+
import * as Option from "effect/Option";
|
|
10
|
+
import { pipeArguments } from "effect/Pipeable";
|
|
11
|
+
import * as Schema from "effect/Schema";
|
|
12
|
+
import { makeFormatterDefault } from "effect/SchemaIssue";
|
|
13
|
+
import * as Scope from "effect/Scope";
|
|
14
|
+
import * as ServiceMap from "effect/ServiceMap";
|
|
15
|
+
import * as Stream from "effect/Stream";
|
|
16
|
+
import { exit } from "@typed/fx/Fx";
|
|
17
|
+
import { mapEffect } from "@typed/fx/Fx/combinators/mapEffect";
|
|
18
|
+
import { provideServices } from "@typed/fx/Fx/combinators/provide";
|
|
19
|
+
import { skipRepeats } from "@typed/fx/Fx/combinators/skipRepeats";
|
|
20
|
+
import { switchMap } from "@typed/fx/Fx/combinators/switchMap";
|
|
21
|
+
import { unwrap } from "@typed/fx/Fx/combinators/unwrap";
|
|
22
|
+
import { fromEffect, never } from "@typed/fx/Fx/constructors/fromEffect";
|
|
23
|
+
import { succeed } from "@typed/fx/Fx/constructors/succeed";
|
|
24
|
+
import { fromStream } from "@typed/fx/Fx/stream";
|
|
25
|
+
import { isFx } from "@typed/fx/Fx/TypeId";
|
|
26
|
+
import { RefSubject } from "@typed/fx/RefSubject";
|
|
27
|
+
import { CurrentPath, Navigation } from "@typed/navigation/Navigation";
|
|
28
|
+
import * as AST from "./AST.js";
|
|
29
|
+
import { CurrentRoute } from "./CurrentRoute.js";
|
|
30
|
+
import { Join, make as makeRoute } from "./Route.js";
|
|
31
|
+
function isMatchHandlerFn(handler) {
|
|
32
|
+
return typeof handler === "function";
|
|
33
|
+
}
|
|
34
|
+
function isHandlerOptions(value) {
|
|
35
|
+
return typeof value === "object" && value !== null && "handler" in value;
|
|
36
|
+
}
|
|
37
|
+
function parseMatchArgs(args) {
|
|
38
|
+
const [first, second, third] = args;
|
|
39
|
+
// Single arg: full options object (Overload 9)
|
|
40
|
+
if (second === undefined) {
|
|
41
|
+
const opts = first;
|
|
42
|
+
return {
|
|
43
|
+
route: opts.route,
|
|
44
|
+
handler: opts.handler,
|
|
45
|
+
guard: undefined,
|
|
46
|
+
layout: opts.layout,
|
|
47
|
+
catchFn: opts.catch,
|
|
48
|
+
dependencies: opts.dependencies,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// Two args
|
|
52
|
+
if (third === undefined) {
|
|
53
|
+
if (isHandlerOptions(second)) {
|
|
54
|
+
// Overload 3: match(route, options)
|
|
55
|
+
const opts = second;
|
|
56
|
+
return {
|
|
57
|
+
route: first,
|
|
58
|
+
handler: opts.handler,
|
|
59
|
+
guard: undefined,
|
|
60
|
+
layout: opts.layout,
|
|
61
|
+
catchFn: opts.catch,
|
|
62
|
+
dependencies: opts.dependencies,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// Overloads 1, 2, 4: match(route, handler)
|
|
66
|
+
return {
|
|
67
|
+
route: first,
|
|
68
|
+
handler: second,
|
|
69
|
+
guard: undefined,
|
|
70
|
+
layout: undefined,
|
|
71
|
+
catchFn: undefined,
|
|
72
|
+
dependencies: undefined,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// Three args
|
|
76
|
+
if (isHandlerOptions(third)) {
|
|
77
|
+
// Overload 7: match(route, guard, options)
|
|
78
|
+
const opts = third;
|
|
79
|
+
return {
|
|
80
|
+
route: first,
|
|
81
|
+
handler: opts.handler,
|
|
82
|
+
guard: second,
|
|
83
|
+
layout: opts.layout,
|
|
84
|
+
catchFn: opts.catch,
|
|
85
|
+
dependencies: opts.dependencies,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// Overloads 5, 6, 8: match(route, guard, handler)
|
|
89
|
+
return {
|
|
90
|
+
route: first,
|
|
91
|
+
handler: third,
|
|
92
|
+
guard: second,
|
|
93
|
+
layout: undefined,
|
|
94
|
+
catchFn: undefined,
|
|
95
|
+
dependencies: undefined,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
class MatcherImpl {
|
|
99
|
+
cases;
|
|
100
|
+
constructor(cases) {
|
|
101
|
+
this.cases = cases;
|
|
102
|
+
this.match = this.match.bind(this);
|
|
103
|
+
this.catch = this.catch.bind(this);
|
|
104
|
+
this.catchTag = this.catchTag.bind(this);
|
|
105
|
+
this.layout = this.layout.bind(this);
|
|
106
|
+
this.provide = this.provide.bind(this);
|
|
107
|
+
this.provideService = this.provideService.bind(this);
|
|
108
|
+
}
|
|
109
|
+
match(...args) {
|
|
110
|
+
const parsed = parseMatchArgs(args);
|
|
111
|
+
const normalizedGuard = parsed.guard !== undefined
|
|
112
|
+
? getGuard(parsed.guard)
|
|
113
|
+
: defaultGuard();
|
|
114
|
+
const routeAst = AST.route(parsed.route.ast, parsed.handler, normalizedGuard);
|
|
115
|
+
let matches = [routeAst];
|
|
116
|
+
if (parsed.layout !== undefined) {
|
|
117
|
+
matches = [AST.layout(matches, parsed.layout)];
|
|
118
|
+
}
|
|
119
|
+
if (parsed.catchFn !== undefined) {
|
|
120
|
+
matches = [AST.catchCause(matches, parsed.catchFn)];
|
|
121
|
+
}
|
|
122
|
+
if (parsed.dependencies !== undefined && parsed.dependencies.length > 0) {
|
|
123
|
+
matches = [AST.layer(matches, normalizeDependencies(parsed.dependencies))];
|
|
124
|
+
}
|
|
125
|
+
return new MatcherImpl([...this.cases, ...matches]);
|
|
126
|
+
}
|
|
127
|
+
prefix(route) {
|
|
128
|
+
return new MatcherImpl([AST.prefixed(this.cases, route.ast)]);
|
|
129
|
+
}
|
|
130
|
+
provide(...layers) {
|
|
131
|
+
return new MatcherImpl([AST.layer(this.cases, layers)]);
|
|
132
|
+
}
|
|
133
|
+
provideService(tag, service) {
|
|
134
|
+
return this.provideServices(ServiceMap.make(tag, service));
|
|
135
|
+
}
|
|
136
|
+
provideServices(services) {
|
|
137
|
+
return this.provide(Layer.succeedServices(services));
|
|
138
|
+
}
|
|
139
|
+
catchCause(f) {
|
|
140
|
+
return new MatcherImpl([AST.catchCause(this.cases, f)]);
|
|
141
|
+
}
|
|
142
|
+
catch(f) {
|
|
143
|
+
return this.catchCause((causeRef) => unwrap(Effect.gen(function* () {
|
|
144
|
+
const cause = yield* causeRef;
|
|
145
|
+
const result = Cause.findFail(cause);
|
|
146
|
+
if (Result.isFailure(result)) {
|
|
147
|
+
return fromEffect(Effect.failCause(result.failure));
|
|
148
|
+
}
|
|
149
|
+
return f(result.success.error);
|
|
150
|
+
})));
|
|
151
|
+
}
|
|
152
|
+
catchTag(tag, f) {
|
|
153
|
+
const rethrow = (cause) => fromEffect(Effect.failCause(cause));
|
|
154
|
+
return new MatcherImpl([
|
|
155
|
+
AST.catchCause(this.cases, (causeRef) => unwrap(Effect.gen(function* () {
|
|
156
|
+
const cause = yield* causeRef;
|
|
157
|
+
const result = Cause.findFail(cause);
|
|
158
|
+
if (Result.isFailure(result)) {
|
|
159
|
+
return rethrow(cause);
|
|
160
|
+
}
|
|
161
|
+
if (matchesTag(tag, result.success.error)) {
|
|
162
|
+
return f(result.success.error);
|
|
163
|
+
}
|
|
164
|
+
return rethrow(cause);
|
|
165
|
+
}))),
|
|
166
|
+
]);
|
|
167
|
+
}
|
|
168
|
+
layout(layout) {
|
|
169
|
+
return new MatcherImpl([
|
|
170
|
+
AST.layout(this.cases, layout),
|
|
171
|
+
]);
|
|
172
|
+
}
|
|
173
|
+
merge(...others) {
|
|
174
|
+
const allCases = [...this.cases, ...others.flatMap((m) => m.cases)];
|
|
175
|
+
return new MatcherImpl(allCases);
|
|
176
|
+
}
|
|
177
|
+
pipe() {
|
|
178
|
+
return pipeArguments(this, arguments);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function normalizeHandler(handler) {
|
|
182
|
+
if (isMatchHandlerFn(handler))
|
|
183
|
+
return (params) => toFx(handler(params));
|
|
184
|
+
return () => toFx(handler);
|
|
185
|
+
}
|
|
186
|
+
function toFx(value) {
|
|
187
|
+
if (isFx(value))
|
|
188
|
+
return value;
|
|
189
|
+
if (Stream.isStream(value))
|
|
190
|
+
return fromStream(value);
|
|
191
|
+
if (Effect.isEffect(value))
|
|
192
|
+
return fromEffect(value);
|
|
193
|
+
return succeed(value);
|
|
194
|
+
}
|
|
195
|
+
export const empty = new MatcherImpl([]);
|
|
196
|
+
export const match = empty.match.bind(empty);
|
|
197
|
+
/**
|
|
198
|
+
* Merge multiple matchers into one. Each matcher's layouts and provide apply only to its own routes.
|
|
199
|
+
* Use this so directory layouts (e.g. api/_layout) and directory dependencies apply only to routes under that directory.
|
|
200
|
+
*/
|
|
201
|
+
export function merge(...matchers) {
|
|
202
|
+
if (matchers.length === 0) {
|
|
203
|
+
return empty;
|
|
204
|
+
}
|
|
205
|
+
if (matchers.length === 1) {
|
|
206
|
+
return matchers[0];
|
|
207
|
+
}
|
|
208
|
+
const first = matchers[0];
|
|
209
|
+
const rest = matchers.slice(1);
|
|
210
|
+
return first.merge(...rest);
|
|
211
|
+
}
|
|
212
|
+
export class RouteGuardError extends Schema.ErrorClass("@typed/router/RouteGuardError")({
|
|
213
|
+
_tag: Schema.tag("RouteGuardError"),
|
|
214
|
+
path: Schema.String,
|
|
215
|
+
causes: Schema.Array(Schema.Unknown),
|
|
216
|
+
}) {
|
|
217
|
+
}
|
|
218
|
+
export class RouteNotFound extends Schema.ErrorClass("@typed/router/RouteNotFound")({
|
|
219
|
+
_tag: Schema.tag("RouteNotFound"),
|
|
220
|
+
path: Schema.String,
|
|
221
|
+
}) {
|
|
222
|
+
}
|
|
223
|
+
export class RouteDecodeError extends Schema.ErrorClass("@typed/router/RouteDecodeError")({
|
|
224
|
+
_tag: Schema.tag("RouteDecodeError"),
|
|
225
|
+
path: Schema.String,
|
|
226
|
+
cause: Schema.String,
|
|
227
|
+
}) {
|
|
228
|
+
}
|
|
229
|
+
export function run(matcher) {
|
|
230
|
+
return unwrap(Effect.gen(function* () {
|
|
231
|
+
const fiberId = yield* Effect.fiberId;
|
|
232
|
+
const rootScope = yield* Effect.scope;
|
|
233
|
+
const current = yield* CurrentRoute;
|
|
234
|
+
const prefixed = matcher.prefix(current.route);
|
|
235
|
+
const entries = compile(prefixed.cases);
|
|
236
|
+
const router = findMyWay.make({
|
|
237
|
+
ignoreTrailingSlash: true,
|
|
238
|
+
caseSensitive: false,
|
|
239
|
+
});
|
|
240
|
+
const handlersByPath = new Map();
|
|
241
|
+
const memoMap = yield* Layer.makeMemoMap;
|
|
242
|
+
const layerManager = makeLayerManager(memoMap, rootScope, fiberId);
|
|
243
|
+
const layoutManager = makeLayoutManager(rootScope, fiberId);
|
|
244
|
+
const catchManager = makeCatchManager(rootScope, fiberId);
|
|
245
|
+
for (const entry of entries) {
|
|
246
|
+
const path = entry.route.path;
|
|
247
|
+
const existing = handlersByPath.get(path);
|
|
248
|
+
if (existing !== undefined) {
|
|
249
|
+
existing.push(entry);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
const list = [entry];
|
|
253
|
+
handlersByPath.set(path, list);
|
|
254
|
+
router.all(path, list);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
let currentState = null;
|
|
258
|
+
return CurrentPath.pipe(mapEffect(Effect.fn(function* (path) {
|
|
259
|
+
const result = router.find("GET", path);
|
|
260
|
+
if (result === undefined)
|
|
261
|
+
return yield* new RouteNotFound({ path });
|
|
262
|
+
const input = { ...result.params, ...result.searchParams };
|
|
263
|
+
const entries = result.handler;
|
|
264
|
+
const guardCauses = [];
|
|
265
|
+
let matchedEntry = undefined;
|
|
266
|
+
let matchedParams = undefined;
|
|
267
|
+
let matchedPrepared = undefined;
|
|
268
|
+
for (const entry of entries) {
|
|
269
|
+
const params = yield* Effect.mapErrorEager(entry.decode(input), (cause) => new RouteDecodeError({ path, cause: makeFormatterDefault()(cause.issue) }));
|
|
270
|
+
const prepared = yield* layerManager.prepare(entry.layers);
|
|
271
|
+
const guardExit = yield* entry
|
|
272
|
+
.guard(params)
|
|
273
|
+
.pipe(Effect.provideServices(prepared.services), Effect.exit);
|
|
274
|
+
if (Exit.isFailure(guardExit)) {
|
|
275
|
+
guardCauses.push(guardExit.cause);
|
|
276
|
+
yield* prepared.rollback;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (Option.isNone(guardExit.value)) {
|
|
280
|
+
yield* prepared.rollback;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
matchedEntry = entry;
|
|
284
|
+
matchedParams = guardExit.value.value;
|
|
285
|
+
matchedPrepared = prepared;
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
if (matchedEntry === undefined || matchedPrepared === undefined) {
|
|
289
|
+
return yield* new RouteGuardError({ path, causes: guardCauses });
|
|
290
|
+
}
|
|
291
|
+
yield* matchedPrepared.commit;
|
|
292
|
+
if (currentState !== null && currentState.entry === matchedEntry) {
|
|
293
|
+
yield* RefSubject.set(currentState.params, matchedParams);
|
|
294
|
+
yield* layoutManager.updateParams(matchedEntry.layouts, matchedParams);
|
|
295
|
+
return currentState.fx;
|
|
296
|
+
}
|
|
297
|
+
if (currentState !== null) {
|
|
298
|
+
yield* Scope.close(currentState.scope, interrupt(fiberId));
|
|
299
|
+
currentState = null;
|
|
300
|
+
}
|
|
301
|
+
const scope = yield* Scope.fork(rootScope);
|
|
302
|
+
const paramsRef = yield* RefSubject.make(matchedParams).pipe(Scope.provide(scope));
|
|
303
|
+
const preparedServices = matchedPrepared.services;
|
|
304
|
+
const handlerServices = ServiceMap.merge(preparedServices, ServiceMap.make(Scope.Scope, scope));
|
|
305
|
+
const handlerFx = matchedEntry
|
|
306
|
+
.handler(paramsRef)
|
|
307
|
+
.pipe(provideServices(handlerServices));
|
|
308
|
+
const withLayouts = yield* layoutManager.apply(matchedEntry.layouts, matchedParams, handlerFx, preparedServices);
|
|
309
|
+
const withCatches = yield* catchManager.apply(matchedEntry.catches, withLayouts, preparedServices);
|
|
310
|
+
const fx = withCatches;
|
|
311
|
+
currentState = {
|
|
312
|
+
entry: matchedEntry,
|
|
313
|
+
params: paramsRef,
|
|
314
|
+
scope,
|
|
315
|
+
fx,
|
|
316
|
+
};
|
|
317
|
+
return currentState.fx;
|
|
318
|
+
})), skipRepeats, switchMap(identity));
|
|
319
|
+
}));
|
|
320
|
+
}
|
|
321
|
+
export const catchCause = dual(2, (input, f) => {
|
|
322
|
+
const eff = Effect.gen(function* () {
|
|
323
|
+
const fiberId = yield* Effect.fiberId;
|
|
324
|
+
const rootScope = yield* Effect.scope;
|
|
325
|
+
const fx = isFx(input) ? input : run(input);
|
|
326
|
+
const manager = makeCatchManager(rootScope, fiberId);
|
|
327
|
+
const result = yield* manager.apply([f], fx, ServiceMap.empty());
|
|
328
|
+
return result;
|
|
329
|
+
});
|
|
330
|
+
return unwrap(eff);
|
|
331
|
+
});
|
|
332
|
+
export const catch_ = dual(2, (input, f) => catchCause(input, (causeRef) => unwrap(Effect.gen(function* () {
|
|
333
|
+
const cause = yield* causeRef;
|
|
334
|
+
const result = Cause.findFail(cause);
|
|
335
|
+
if (Result.isFailure(result)) {
|
|
336
|
+
return fromEffect(Effect.failCause(result.failure));
|
|
337
|
+
}
|
|
338
|
+
return f(result.success.error);
|
|
339
|
+
}))));
|
|
340
|
+
export { catch_ as catch };
|
|
341
|
+
export const catchTag = dual(3, (input, k, f) => catchCause(input, (causeRef) => unwrap(Effect.gen(function* () {
|
|
342
|
+
const cause = yield* causeRef;
|
|
343
|
+
const result = Cause.findFail(cause);
|
|
344
|
+
if (Result.isFailure(result)) {
|
|
345
|
+
return fromEffect(Effect.failCause(result.failure));
|
|
346
|
+
}
|
|
347
|
+
if (matchesTag(k, result.success.error)) {
|
|
348
|
+
return f(result.success.error);
|
|
349
|
+
}
|
|
350
|
+
return fromEffect(Effect.fail(result.success.error));
|
|
351
|
+
}))));
|
|
352
|
+
export const redirectTo = (path) => (input) => catchCause(input, (_) => Navigation.navigate(path).pipe(Effect.matchCause({
|
|
353
|
+
onFailure: () => never,
|
|
354
|
+
onSuccess: () => never,
|
|
355
|
+
}), unwrap));
|
|
356
|
+
const hasTag = (u) => typeof u === "object" &&
|
|
357
|
+
u !== null &&
|
|
358
|
+
"_tag" in u &&
|
|
359
|
+
typeof u["_tag"] === "string";
|
|
360
|
+
const matchesTag = (tag, error) => {
|
|
361
|
+
if (!hasTag(error))
|
|
362
|
+
return false;
|
|
363
|
+
if (typeof tag === "string")
|
|
364
|
+
return error._tag === tag;
|
|
365
|
+
return tag.some((t) => t === error._tag);
|
|
366
|
+
};
|
|
367
|
+
function isServiceMap(dep) {
|
|
368
|
+
return !Layer.isLayer(dep);
|
|
369
|
+
}
|
|
370
|
+
function toSingleLayer(dep) {
|
|
371
|
+
if (isServiceMap(dep))
|
|
372
|
+
return Layer.succeedServices(dep);
|
|
373
|
+
return dep;
|
|
374
|
+
}
|
|
375
|
+
function normalizeDependencies(dependencies) {
|
|
376
|
+
return dependencies.map(toSingleLayer);
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Normalize dependency input (ServiceMap | Layer | Array of either) into a single Layer.
|
|
380
|
+
* Use with `.provide(normalizeDependencyInput(deps))`.
|
|
381
|
+
*/
|
|
382
|
+
export function normalizeDependencyInput(input) {
|
|
383
|
+
const arr = Array.isArray(input) ? input : [input];
|
|
384
|
+
const layers = normalizeDependencies(arr);
|
|
385
|
+
return mergeLayers(layers);
|
|
386
|
+
}
|
|
387
|
+
function getGuard(guard) {
|
|
388
|
+
return "asGuard" in guard ? guard.asGuard() : guard;
|
|
389
|
+
}
|
|
390
|
+
function defaultGuard() {
|
|
391
|
+
return Effect.succeedSome;
|
|
392
|
+
}
|
|
393
|
+
function mergeLayers(layers) {
|
|
394
|
+
if (layers.length === 0)
|
|
395
|
+
return Layer.empty;
|
|
396
|
+
if (layers.length === 1)
|
|
397
|
+
return layers[0];
|
|
398
|
+
let current = layers[0];
|
|
399
|
+
for (let i = 1; i < layers.length; i++) {
|
|
400
|
+
current = Layer.merge(current, layers[i]);
|
|
401
|
+
}
|
|
402
|
+
return current;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* @internal
|
|
406
|
+
*/
|
|
407
|
+
export function compile(cases) {
|
|
408
|
+
const entries = [];
|
|
409
|
+
const visit = (matches, context) => {
|
|
410
|
+
for (const match of matches) {
|
|
411
|
+
switch (match.type) {
|
|
412
|
+
case "route": {
|
|
413
|
+
const baseRoute = makeRoute(match.route);
|
|
414
|
+
const prefixedRoute = applyPrefixes(baseRoute, context.prefixes);
|
|
415
|
+
entries.push({
|
|
416
|
+
route: prefixedRoute,
|
|
417
|
+
guard: getGuard(match.guard),
|
|
418
|
+
handler: normalizeHandler(match.handler),
|
|
419
|
+
layers: context.layers,
|
|
420
|
+
layouts: context.layouts,
|
|
421
|
+
catches: context.catches,
|
|
422
|
+
decode: Schema.decodeUnknownEffect(prefixedRoute.paramsSchema),
|
|
423
|
+
});
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
case "layer": {
|
|
427
|
+
const merged = mergeLayers(match.deps);
|
|
428
|
+
visit(match.matches, {
|
|
429
|
+
...context,
|
|
430
|
+
layers: [...context.layers, merged],
|
|
431
|
+
});
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
case "layout": {
|
|
435
|
+
visit(match.matches, {
|
|
436
|
+
...context,
|
|
437
|
+
layouts: [...context.layouts, match.layout],
|
|
438
|
+
});
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
case "prefixed": {
|
|
442
|
+
visit(match.matches, {
|
|
443
|
+
...context,
|
|
444
|
+
prefixes: [...context.prefixes, match.prefix],
|
|
445
|
+
});
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
case "catch": {
|
|
449
|
+
visit(match.matches, {
|
|
450
|
+
...context,
|
|
451
|
+
catches: [...context.catches, match.f],
|
|
452
|
+
});
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
visit(cases, { layers: [], layouts: [], catches: [], prefixes: [] });
|
|
459
|
+
return entries;
|
|
460
|
+
}
|
|
461
|
+
function applyPrefixes(route, prefixes) {
|
|
462
|
+
if (prefixes.length === 0)
|
|
463
|
+
return route;
|
|
464
|
+
const prefixRoutes = prefixes.map((prefix) => makeRoute(prefix));
|
|
465
|
+
return Join(...prefixRoutes, route);
|
|
466
|
+
}
|
|
467
|
+
// Parallel scope cleanup helper
|
|
468
|
+
const closeScopes = (scopes, fiberId) => Effect.forEach(scopes, (scope) => Scope.close(scope, interrupt(fiberId)), {
|
|
469
|
+
concurrency: "unbounded",
|
|
470
|
+
discard: true,
|
|
471
|
+
});
|
|
472
|
+
/**
|
|
473
|
+
* @internal
|
|
474
|
+
*/
|
|
475
|
+
export function makeLayerManager(memoMap, rootScope, fiberId) {
|
|
476
|
+
const states = new Map();
|
|
477
|
+
let order = [];
|
|
478
|
+
let cachedDesiredSet = undefined;
|
|
479
|
+
let cachedOrder = undefined;
|
|
480
|
+
const prepare = (desired) => Effect.gen(function* () {
|
|
481
|
+
const desiredSet = cachedOrder === desired
|
|
482
|
+
? cachedDesiredSet
|
|
483
|
+
: ((cachedDesiredSet = new Set(desired)), (cachedOrder = desired), cachedDesiredSet);
|
|
484
|
+
const removed = order.filter((layer) => !desiredSet.has(layer));
|
|
485
|
+
const added = [];
|
|
486
|
+
let services = ServiceMap.empty();
|
|
487
|
+
for (const layer of desired) {
|
|
488
|
+
const existing = states.get(layer);
|
|
489
|
+
if (existing) {
|
|
490
|
+
services = ServiceMap.merge(services, existing.services);
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
const scope = yield* Scope.fork(rootScope);
|
|
494
|
+
const buildExit = yield* Layer.buildWithMemoMap(layer, memoMap, scope).pipe(Effect.provideServices(services), Effect.exit);
|
|
495
|
+
if (Exit.isFailure(buildExit)) {
|
|
496
|
+
for (let i = added.length - 1; i >= 0; i--) {
|
|
497
|
+
const addedLayer = added[i];
|
|
498
|
+
const addedState = states.get(addedLayer);
|
|
499
|
+
if (addedState) {
|
|
500
|
+
states.delete(addedLayer);
|
|
501
|
+
yield* Scope.close(addedState.scope, interrupt(fiberId));
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
yield* Scope.close(scope, buildExit);
|
|
505
|
+
return yield* Effect.failCause(buildExit.cause);
|
|
506
|
+
}
|
|
507
|
+
const servicesForLayer = buildExit.value;
|
|
508
|
+
services = ServiceMap.merge(services, servicesForLayer);
|
|
509
|
+
states.set(layer, { scope, services: servicesForLayer });
|
|
510
|
+
added.push(layer);
|
|
511
|
+
}
|
|
512
|
+
const commit = Effect.gen(function* () {
|
|
513
|
+
for (let i = removed.length - 1; i >= 0; i--) {
|
|
514
|
+
const layer = removed[i];
|
|
515
|
+
const state = states.get(layer);
|
|
516
|
+
if (state) {
|
|
517
|
+
states.delete(layer);
|
|
518
|
+
yield* Scope.close(state.scope, interrupt(fiberId));
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
order = desired;
|
|
522
|
+
});
|
|
523
|
+
const rollback = Effect.gen(function* () {
|
|
524
|
+
for (let i = added.length - 1; i >= 0; i--) {
|
|
525
|
+
const layer = added[i];
|
|
526
|
+
const state = states.get(layer);
|
|
527
|
+
if (state) {
|
|
528
|
+
states.delete(layer);
|
|
529
|
+
yield* Scope.close(state.scope, interrupt(fiberId));
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
return { services, commit, rollback };
|
|
534
|
+
});
|
|
535
|
+
return { prepare };
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* @internal
|
|
539
|
+
*/
|
|
540
|
+
export function makeLayoutManager(rootScope, fiberId) {
|
|
541
|
+
const states = new Map();
|
|
542
|
+
let active = [];
|
|
543
|
+
const removeUnused = (layouts) => Effect.gen(function* () {
|
|
544
|
+
const next = new Set(layouts);
|
|
545
|
+
const removed = active.filter((layout) => !next.has(layout));
|
|
546
|
+
const scopes = removed.map((layout) => {
|
|
547
|
+
const state = states.get(layout);
|
|
548
|
+
states.delete(layout);
|
|
549
|
+
return state.scope;
|
|
550
|
+
});
|
|
551
|
+
yield* closeScopes(scopes, fiberId);
|
|
552
|
+
active = layouts;
|
|
553
|
+
});
|
|
554
|
+
const apply = (layouts, paramsValue, inner, services) => Effect.gen(function* () {
|
|
555
|
+
let current = inner;
|
|
556
|
+
for (let i = layouts.length - 1; i >= 0; i--) {
|
|
557
|
+
const layout = layouts[i];
|
|
558
|
+
const state = states.get(layout);
|
|
559
|
+
if (state === undefined) {
|
|
560
|
+
const scope = yield* Scope.fork(rootScope);
|
|
561
|
+
const params = yield* RefSubject.make(paramsValue).pipe(Scope.provide(scope));
|
|
562
|
+
const content = yield* RefSubject.make(Effect.succeed(current), {
|
|
563
|
+
eq: (left, right) => left === right,
|
|
564
|
+
}).pipe(Scope.provide(scope));
|
|
565
|
+
const fx = layout({ params, content: content.pipe(switchMap(identity)) }).pipe(provideServices(ServiceMap.merge(services, ServiceMap.make(Scope.Scope, scope))));
|
|
566
|
+
states.set(layout, { params, content, fx, scope });
|
|
567
|
+
current = fx;
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
yield* RefSubject.set(state.params, paramsValue);
|
|
571
|
+
// @effect-diagnostics-next-line floatingEffect:off
|
|
572
|
+
yield* RefSubject.set(state.content, current);
|
|
573
|
+
current = state.fx;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
yield* removeUnused(layouts);
|
|
577
|
+
return current;
|
|
578
|
+
});
|
|
579
|
+
const updateParams = (layouts, paramsValue) => Effect.forEach(layouts, (layout) => {
|
|
580
|
+
const state = states.get(layout);
|
|
581
|
+
return state !== undefined ? RefSubject.set(state.params, paramsValue) : Effect.void;
|
|
582
|
+
}, { discard: true });
|
|
583
|
+
return { apply, updateParams };
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* @internal
|
|
587
|
+
*/
|
|
588
|
+
export function makeCatchManager(rootScope, fiberId) {
|
|
589
|
+
const states = new Map();
|
|
590
|
+
let active = [];
|
|
591
|
+
const removeUnused = (catches) => Effect.gen(function* () {
|
|
592
|
+
const next = new Set(catches);
|
|
593
|
+
const removed = active.filter((c) => !next.has(c));
|
|
594
|
+
const scopes = removed.map((c) => {
|
|
595
|
+
const state = states.get(c);
|
|
596
|
+
states.delete(c);
|
|
597
|
+
return state.scope;
|
|
598
|
+
});
|
|
599
|
+
yield* closeScopes(scopes, fiberId);
|
|
600
|
+
active = catches;
|
|
601
|
+
});
|
|
602
|
+
const apply = (catches, inner, services) => Effect.gen(function* () {
|
|
603
|
+
let current = inner;
|
|
604
|
+
for (let i = catches.length - 1; i >= 0; i--) {
|
|
605
|
+
const catcher = catches[i];
|
|
606
|
+
const state = states.get(catcher);
|
|
607
|
+
if (state === undefined) {
|
|
608
|
+
const scope = yield* Scope.fork(rootScope);
|
|
609
|
+
const causes = yield* RefSubject.make(Cause.fail(undefined)).pipe(Scope.provide(scope));
|
|
610
|
+
const content = yield* RefSubject.make(Effect.succeed(current), {
|
|
611
|
+
eq: (left, right) => left === right,
|
|
612
|
+
}).pipe(Scope.provide(scope));
|
|
613
|
+
const fallback = catcher(causes).pipe(provideServices(ServiceMap.merge(services, ServiceMap.make(Scope.Scope, scope))));
|
|
614
|
+
const fx = content.pipe(switchMap(identity), exit, mapEffect(Effect.fn(function* (e) {
|
|
615
|
+
if (isSuccess(e))
|
|
616
|
+
return succeed(e.value);
|
|
617
|
+
yield* RefSubject.set(causes, e.cause);
|
|
618
|
+
return fallback;
|
|
619
|
+
})), skipRepeats, switchMap(identity));
|
|
620
|
+
states.set(catcher, { causes, content, fx, scope });
|
|
621
|
+
current = fx;
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
// @effect-diagnostics-next-line floatingEffect:off
|
|
625
|
+
yield* RefSubject.set(state.content, current);
|
|
626
|
+
current = state.fx;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
yield* removeUnused(catches);
|
|
630
|
+
return current;
|
|
631
|
+
});
|
|
632
|
+
return { apply };
|
|
633
|
+
}
|