@typed/router 1.0.0-beta.2 → 1.0.0-beta.4
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 +1 -1
- package/dist/CurrentRoute.d.ts +2 -2
- package/dist/CurrentRoute.d.ts.map +1 -1
- package/dist/CurrentRoute.js +4 -4
- package/dist/Matcher.d.ts +18 -19
- package/dist/Matcher.d.ts.map +1 -1
- package/dist/Matcher.js +112 -109
- package/dist/MatcherV2.d.ts +3 -0
- package/dist/MatcherV2.d.ts.map +1 -0
- package/dist/MatcherV2.js +1 -0
- package/dist/test-utils/matcherBrowserHarness.d.ts +10 -0
- package/dist/test-utils/matcherBrowserHarness.d.ts.map +1 -0
- package/dist/test-utils/matcherBrowserHarness.js +13 -0
- package/package.json +21 -18
- package/src/CurrentRoute.ts +5 -5
- package/src/Matcher.browser.test.ts +767 -0
- package/src/Matcher.test.ts +348 -73
- package/src/Matcher.ts +170 -166
- package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--back---restores-previous-match-1.png +0 -0
- package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--catchTag-RouteNotFound-can-navigate-and-re-match--browser-history--1.png +0 -0
- package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--decodes-query-params-from-pathname-search-1.png +0 -0
- package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--function-Matcher-catch--and-composition--browser--instance-catchTag-does-not-catch-RouteGuardError-from-guards-1.png +0 -0
- package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--history-back-restores-previous-match--popstate-sync--1.png +0 -0
- package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--path-and-query-params-both-decode--distinct-names--1.png +0 -0
- package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--prefix-scopes-routes-under-a-path-segment-1.png +0 -0
- package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--provideService-supplies-a-service-to-handlers-1.png +0 -0
- package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--query-param-wins-over-path-param-when-names-collide-1.png +0 -0
- package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--redirectTo-navigates-away-from-unmatched-path--side-effect--1.png +0 -0
- package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--redirectTo-navigates-then-matches-target-route-1.png +0 -0
- package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--reuses-shared-layers-and-layouts-across-route-changes-1.png +0 -0
- package/src/test-utils/matcherBrowserHarness.ts +22 -0
package/src/Matcher.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type * as Arr from "effect/Array";
|
|
|
3
3
|
import * as Cause from "effect/Cause";
|
|
4
4
|
import * as Effect from "effect/Effect";
|
|
5
5
|
import * as Exit from "effect/Exit";
|
|
6
|
-
import { interrupt
|
|
6
|
+
import { interrupt } from "effect/Exit";
|
|
7
7
|
import { dual, identity } from "effect/Function";
|
|
8
8
|
import * as Result from "effect/Result";
|
|
9
9
|
import * as Layer from "effect/Layer";
|
|
@@ -12,12 +12,12 @@ import { type Pipeable, pipeArguments } from "effect/Pipeable";
|
|
|
12
12
|
import * as Schema from "effect/Schema";
|
|
13
13
|
import { makeFormatterDefault } from "effect/SchemaIssue";
|
|
14
14
|
import * as Scope from "effect/Scope";
|
|
15
|
-
import * as
|
|
15
|
+
import * as Context from "effect/Context";
|
|
16
16
|
import * as Stream from "effect/Stream";
|
|
17
17
|
import type { ExcludeTag, ExtractTag, NoInfer, Tags } from "effect/Types";
|
|
18
18
|
import { exit } from "@typed/fx/Fx";
|
|
19
19
|
import { mapEffect } from "@typed/fx/Fx/combinators/mapEffect";
|
|
20
|
-
import {
|
|
20
|
+
import { provideContext } from "@typed/fx/Fx/combinators/provide";
|
|
21
21
|
import { skipRepeats } from "@typed/fx/Fx/combinators/skipRepeats";
|
|
22
22
|
import { switchMap } from "@typed/fx/Fx/combinators/switchMap";
|
|
23
23
|
import { unwrap } from "@typed/fx/Fx/combinators/unwrap";
|
|
@@ -25,7 +25,7 @@ import { fromEffect, never } from "@typed/fx/Fx/constructors/fromEffect";
|
|
|
25
25
|
import { succeed } from "@typed/fx/Fx/constructors/succeed";
|
|
26
26
|
import type * as Fx from "@typed/fx/Fx/Fx";
|
|
27
27
|
import { fromStream } from "@typed/fx/Fx/stream";
|
|
28
|
-
import { isFx } from "@typed/fx/Fx/TypeId";
|
|
28
|
+
import { FxTypeId, isFx } from "@typed/fx/Fx/TypeId";
|
|
29
29
|
import { RefSubject } from "@typed/fx/RefSubject";
|
|
30
30
|
import { CurrentPath, Navigation } from "@typed/navigation/Navigation";
|
|
31
31
|
import type { MatchAst, RouteAst } from "./AST.js";
|
|
@@ -33,6 +33,7 @@ import * as AST from "./AST.js";
|
|
|
33
33
|
import { CurrentRoute } from "./CurrentRoute.js";
|
|
34
34
|
import { Join, make as makeRoute, type Route } from "./Route.js";
|
|
35
35
|
import type { Router } from "./Router.js";
|
|
36
|
+
import { Sink } from "@typed/fx";
|
|
36
37
|
|
|
37
38
|
export type Layout<Params, A, E, R, B, E2, R2> = (
|
|
38
39
|
params: LayoutParams<Params, A, E, R>,
|
|
@@ -57,7 +58,7 @@ export type AnyLayer =
|
|
|
57
58
|
| Layer.Layer<never, any, never>
|
|
58
59
|
| Layer.Layer<never, never, any>;
|
|
59
60
|
|
|
60
|
-
export type AnyServiceMap =
|
|
61
|
+
export type AnyServiceMap = Context.Context<any> | Context.Context<never>;
|
|
61
62
|
export type AnyDependency = AnyLayer | AnyServiceMap;
|
|
62
63
|
type AnyLayout = Layout<any, any, any, any, any, any, any>;
|
|
63
64
|
type AnyCatch = CatchHandler<any, any, any, any>;
|
|
@@ -67,7 +68,7 @@ type AnyMatchHandler = (params: RefSubject.RefSubject<any>) => Fx.Fx<any, any, a
|
|
|
67
68
|
export type DependencyProvided<D> =
|
|
68
69
|
D extends Layer.Layer<infer Provided, any, any>
|
|
69
70
|
? Provided
|
|
70
|
-
: D extends
|
|
71
|
+
: D extends Context.Context<infer Provided>
|
|
71
72
|
? Provided
|
|
72
73
|
: never;
|
|
73
74
|
export type DependencyError<D> = D extends Layer.Layer<any, infer E, any> ? E : never;
|
|
@@ -149,7 +150,10 @@ type ComputeMatchResult<E2, R2, D, LB, LE2, LR2, C, GE, GR> = ApplyCatch<
|
|
|
149
150
|
C
|
|
150
151
|
>;
|
|
151
152
|
|
|
152
|
-
export interface Matcher<A, E = never, R = never>
|
|
153
|
+
export interface Matcher<A, E = never, R = never>
|
|
154
|
+
extends
|
|
155
|
+
Fx.Fx<A, E | RouteNotFound | RouteDecodeError | RouteGuardError, R | Router | Scope.Scope>,
|
|
156
|
+
Pipeable {
|
|
153
157
|
readonly cases: ReadonlyArray<MatchAst>;
|
|
154
158
|
|
|
155
159
|
// Overload 1: match(route, handler) - function handler (must be first for inference)
|
|
@@ -274,13 +278,11 @@ export interface Matcher<A, E = never, R = never> extends Pipeable {
|
|
|
274
278
|
>;
|
|
275
279
|
|
|
276
280
|
readonly provideService: <Id, S>(
|
|
277
|
-
tag:
|
|
281
|
+
tag: Context.Service<Id, S>,
|
|
278
282
|
service: S,
|
|
279
283
|
) => Matcher<A, E, Exclude<R, Id>>;
|
|
280
284
|
|
|
281
|
-
readonly
|
|
282
|
-
services: ServiceMap.ServiceMap<R2>,
|
|
283
|
-
) => Matcher<A, E, Exclude<R, R2>>;
|
|
285
|
+
readonly provideContext: <R2>(services: Context.Context<R2>) => Matcher<A, E, Exclude<R, R2>>;
|
|
284
286
|
|
|
285
287
|
readonly catchCause: <B, E2, R2>(f: CatchHandler<E, B, E2, R2>) => Matcher<A | B, E2, R | R2>;
|
|
286
288
|
|
|
@@ -431,6 +433,15 @@ function parseMatchArgs(args: [unknown, ...Array<unknown>]): ParsedMatch {
|
|
|
431
433
|
}
|
|
432
434
|
|
|
433
435
|
class MatcherImpl<A, E, R> implements Matcher<A, E, R> {
|
|
436
|
+
readonly [FxTypeId]: Fx.Fx.Variance<
|
|
437
|
+
A,
|
|
438
|
+
E | RouteNotFound | RouteDecodeError | RouteGuardError,
|
|
439
|
+
R | Scope.Scope | Router
|
|
440
|
+
> = {
|
|
441
|
+
_A: identity,
|
|
442
|
+
_E: identity,
|
|
443
|
+
_R: identity,
|
|
444
|
+
};
|
|
434
445
|
readonly cases: ReadonlyArray<MatchAst>;
|
|
435
446
|
constructor(cases: ReadonlyArray<MatchAst>) {
|
|
436
447
|
this.cases = cases;
|
|
@@ -569,12 +580,12 @@ class MatcherImpl<A, E, R> implements Matcher<A, E, R> {
|
|
|
569
580
|
>;
|
|
570
581
|
}
|
|
571
582
|
|
|
572
|
-
provideService<Id, S>(tag:
|
|
573
|
-
return this.
|
|
583
|
+
provideService<Id, S>(tag: Context.Service<Id, S>, service: S): Matcher<A, E, Exclude<R, Id>> {
|
|
584
|
+
return this.provideContext(Context.make(tag, service));
|
|
574
585
|
}
|
|
575
586
|
|
|
576
|
-
|
|
577
|
-
return this.provide(Layer.
|
|
587
|
+
provideContext<R2>(services: Context.Context<R2>): Matcher<A, E, Exclude<R, R2>> {
|
|
588
|
+
return this.provide(Layer.succeedContext(services));
|
|
578
589
|
}
|
|
579
590
|
|
|
580
591
|
catchCause<B, E2, R2>(f: CatchHandler<E, B, E2, R2>): Matcher<A | B, E2, R | R2> {
|
|
@@ -660,118 +671,13 @@ class MatcherImpl<A, E, R> implements Matcher<A, E, R> {
|
|
|
660
671
|
pipe() {
|
|
661
672
|
return pipeArguments(this, arguments);
|
|
662
673
|
}
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
function normalizeHandler<Params, B, E2 = never, R2 = never>(
|
|
666
|
-
handler: MatchHandler<Params, B, E2, R2>,
|
|
667
|
-
): (params: RefSubject.RefSubject<Params>) => Fx.Fx<B, E2, R2> {
|
|
668
|
-
if (isMatchHandlerFn(handler)) return (params) => toFx(handler(params));
|
|
669
|
-
return () => toFx(handler);
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
function toFx<A, E, R>(
|
|
673
|
-
value: Fx.Fx<A, E, R> | Stream.Stream<A, E, R> | Effect.Effect<A, E, R> | A,
|
|
674
|
-
): Fx.Fx<A, E, R> {
|
|
675
|
-
if (isFx(value)) return value;
|
|
676
|
-
if (Stream.isStream(value)) return fromStream(value);
|
|
677
|
-
if (Effect.isEffect(value)) return fromEffect(value);
|
|
678
|
-
return succeed(value);
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
export const empty: Matcher<never> = new MatcherImpl([]);
|
|
682
|
-
export const match = empty.match.bind(empty);
|
|
683
|
-
|
|
684
|
-
/**
|
|
685
|
-
* Merge multiple matchers into one. Each matcher's layouts and provide apply only to its own routes.
|
|
686
|
-
* Use this so directory layouts (e.g. api/_layout) and directory dependencies apply only to routes under that directory.
|
|
687
|
-
*/
|
|
688
|
-
export function merge<const Matchers extends ReadonlyArray<Matcher.Any>>(
|
|
689
|
-
...matchers: Matchers
|
|
690
|
-
): Matcher<
|
|
691
|
-
Matcher.MergeSuccess<Matchers>,
|
|
692
|
-
Matcher.MergeError<Matchers>,
|
|
693
|
-
Matcher.MergeServices<Matchers>
|
|
694
|
-
> {
|
|
695
|
-
if (matchers.length === 0) {
|
|
696
|
-
return empty as unknown as Matcher<
|
|
697
|
-
Matcher.MergeSuccess<Matchers>,
|
|
698
|
-
Matcher.MergeError<Matchers>,
|
|
699
|
-
Matcher.MergeServices<Matchers>
|
|
700
|
-
>;
|
|
701
|
-
}
|
|
702
|
-
if (matchers.length === 1) {
|
|
703
|
-
return matchers[0] as unknown as Matcher<
|
|
704
|
-
Matcher.MergeSuccess<Matchers>,
|
|
705
|
-
Matcher.MergeError<Matchers>,
|
|
706
|
-
Matcher.MergeServices<Matchers>
|
|
707
|
-
>;
|
|
708
|
-
}
|
|
709
|
-
const first = matchers[0] as MatcherImpl<
|
|
710
|
-
Matcher.MergeSuccess<Matchers>,
|
|
711
|
-
Matcher.MergeError<Matchers>,
|
|
712
|
-
Matcher.MergeServices<Matchers>
|
|
713
|
-
>;
|
|
714
|
-
const rest = matchers.slice(1) as ReadonlyArray<
|
|
715
|
-
Matcher<
|
|
716
|
-
Matcher.MergeSuccess<Matchers>,
|
|
717
|
-
Matcher.MergeError<Matchers>,
|
|
718
|
-
Matcher.MergeServices<Matchers>
|
|
719
|
-
>
|
|
720
|
-
>;
|
|
721
|
-
return first.merge(...rest) as unknown as Matcher<
|
|
722
|
-
Matcher.MergeSuccess<Matchers>,
|
|
723
|
-
Matcher.MergeError<Matchers>,
|
|
724
|
-
Matcher.MergeServices<Matchers>
|
|
725
|
-
>;
|
|
726
|
-
}
|
|
727
674
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
)({
|
|
731
|
-
_tag: Schema.tag("RouteGuardError"),
|
|
732
|
-
path: Schema.String,
|
|
733
|
-
causes: Schema.Array(Schema.Unknown),
|
|
734
|
-
}) {}
|
|
735
|
-
|
|
736
|
-
export class RouteNotFound extends Schema.ErrorClass<RouteNotFound>("@typed/router/RouteNotFound")({
|
|
737
|
-
_tag: Schema.tag("RouteNotFound"),
|
|
738
|
-
path: Schema.String,
|
|
739
|
-
}) {}
|
|
740
|
-
|
|
741
|
-
export class RouteDecodeError extends Schema.ErrorClass<RouteDecodeError>(
|
|
742
|
-
"@typed/router/RouteDecodeError",
|
|
743
|
-
)({
|
|
744
|
-
_tag: Schema.tag("RouteDecodeError"),
|
|
745
|
-
path: Schema.String,
|
|
746
|
-
cause: Schema.String,
|
|
747
|
-
}) {}
|
|
748
|
-
|
|
749
|
-
/**
|
|
750
|
-
* @internal
|
|
751
|
-
*/
|
|
752
|
-
export type CompiledEntry = {
|
|
753
|
-
readonly route: Route.Any;
|
|
754
|
-
readonly guard: AnyGuard;
|
|
755
|
-
readonly handler: AnyMatchHandler;
|
|
756
|
-
readonly layers: ReadonlyArray<AnyLayer>;
|
|
757
|
-
readonly layouts: ReadonlyArray<AnyLayout>;
|
|
758
|
-
readonly catches: ReadonlyArray<AnyCatch>;
|
|
759
|
-
readonly decode: (input: unknown) => Effect.Effect<any, Schema.SchemaError, any>;
|
|
760
|
-
};
|
|
761
|
-
|
|
762
|
-
export function run<M extends Matcher.Any>(
|
|
763
|
-
matcher: M,
|
|
764
|
-
): Fx.Fx<
|
|
765
|
-
Matcher.Success<M>,
|
|
766
|
-
Matcher.Error<M> | RouteNotFound | RouteDecodeError | RouteGuardError,
|
|
767
|
-
Matcher.Services<M> | Router | CurrentRoute | Scope.Scope
|
|
768
|
-
> {
|
|
769
|
-
return unwrap(
|
|
770
|
-
Effect.gen(function* () {
|
|
675
|
+
run<RSink>(sink: Sink.Sink<A, E | RouteNotFound | RouteDecodeError | RouteGuardError, RSink>) {
|
|
676
|
+
return Effect.gen({ self: this }, function* () {
|
|
771
677
|
const fiberId = yield* Effect.fiberId;
|
|
772
678
|
const rootScope = yield* Effect.scope;
|
|
773
679
|
const current = yield* CurrentRoute;
|
|
774
|
-
const prefixed =
|
|
680
|
+
const prefixed = this.prefix(current.route);
|
|
775
681
|
const entries = compile(prefixed.cases);
|
|
776
682
|
const router = findMyWay.make<ReadonlyArray<CompiledEntry>>({
|
|
777
683
|
ignoreTrailingSlash: true,
|
|
@@ -798,11 +704,11 @@ export function run<M extends Matcher.Any>(
|
|
|
798
704
|
let currentState: {
|
|
799
705
|
entry: CompiledEntry;
|
|
800
706
|
params: RefSubject.RefSubject<any>;
|
|
801
|
-
fx: Fx.Fx<
|
|
707
|
+
fx: Fx.Fx<A, E, R | Scope.Scope | Router>;
|
|
802
708
|
scope: Scope.Closeable;
|
|
803
709
|
} | null = null;
|
|
804
710
|
|
|
805
|
-
|
|
711
|
+
const stream = CurrentPath.pipe(
|
|
806
712
|
mapEffect(
|
|
807
713
|
Effect.fn(function* (path) {
|
|
808
714
|
const result = router.find("GET", path);
|
|
@@ -831,7 +737,7 @@ export function run<M extends Matcher.Any>(
|
|
|
831
737
|
const prepared = yield* layerManager.prepare(entry.layers);
|
|
832
738
|
const guardExit = yield* entry
|
|
833
739
|
.guard(params)
|
|
834
|
-
.pipe(Effect.
|
|
740
|
+
.pipe(Effect.provideContext(prepared.services), Effect.exit);
|
|
835
741
|
|
|
836
742
|
if (Exit.isFailure(guardExit)) {
|
|
837
743
|
guardCauses.push(guardExit.cause);
|
|
@@ -870,15 +776,13 @@ export function run<M extends Matcher.Any>(
|
|
|
870
776
|
const scope = yield* Scope.fork(rootScope);
|
|
871
777
|
const paramsRef = yield* RefSubject.make(matchedParams).pipe(Scope.provide(scope));
|
|
872
778
|
|
|
873
|
-
const preparedServices = matchedPrepared.services as
|
|
874
|
-
const handlerServices =
|
|
779
|
+
const preparedServices = matchedPrepared.services as Context.Context<any>;
|
|
780
|
+
const handlerServices = Context.merge(
|
|
875
781
|
preparedServices,
|
|
876
|
-
|
|
782
|
+
Context.make(Scope.Scope, scope),
|
|
877
783
|
);
|
|
878
784
|
|
|
879
|
-
const handlerFx = matchedEntry
|
|
880
|
-
.handler(paramsRef)
|
|
881
|
-
.pipe(provideServices(handlerServices));
|
|
785
|
+
const handlerFx = matchedEntry.handler(paramsRef).pipe(provideContext(handlerServices));
|
|
882
786
|
const withLayouts = yield* layoutManager.apply(
|
|
883
787
|
matchedEntry.layouts,
|
|
884
788
|
matchedParams,
|
|
@@ -905,10 +809,109 @@ export function run<M extends Matcher.Any>(
|
|
|
905
809
|
skipRepeats,
|
|
906
810
|
switchMap(identity),
|
|
907
811
|
);
|
|
908
|
-
|
|
909
|
-
|
|
812
|
+
|
|
813
|
+
return yield* stream.run(sink);
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function normalizeHandler<Params, B, E2 = never, R2 = never>(
|
|
819
|
+
handler: MatchHandler<Params, B, E2, R2>,
|
|
820
|
+
): (params: RefSubject.RefSubject<Params>) => Fx.Fx<B, E2, R2> {
|
|
821
|
+
if (isMatchHandlerFn(handler)) return (params) => toFx(handler(params));
|
|
822
|
+
return () => toFx(handler);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function toFx<A, E, R>(
|
|
826
|
+
value: Fx.Fx<A, E, R> | Stream.Stream<A, E, R> | Effect.Effect<A, E, R> | A,
|
|
827
|
+
): Fx.Fx<A, E, R> {
|
|
828
|
+
if (isFx(value)) return value;
|
|
829
|
+
if (Stream.isStream(value)) return fromStream(value);
|
|
830
|
+
if (Effect.isEffect(value)) return fromEffect(value);
|
|
831
|
+
return succeed(value);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
export const empty: Matcher<never> = new MatcherImpl([]);
|
|
835
|
+
export const match = empty.match.bind(empty);
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Merge multiple matchers into one. Each matcher's layouts and provide apply only to its own routes.
|
|
839
|
+
* Use this so directory layouts (e.g. api/_layout) and directory dependencies apply only to routes under that directory.
|
|
840
|
+
*/
|
|
841
|
+
export function merge<const Matchers extends ReadonlyArray<Matcher.Any>>(
|
|
842
|
+
...matchers: Matchers
|
|
843
|
+
): Matcher<
|
|
844
|
+
Matcher.MergeSuccess<Matchers>,
|
|
845
|
+
Matcher.MergeError<Matchers>,
|
|
846
|
+
Matcher.MergeServices<Matchers>
|
|
847
|
+
> {
|
|
848
|
+
if (matchers.length === 0) {
|
|
849
|
+
return empty as unknown as Matcher<
|
|
850
|
+
Matcher.MergeSuccess<Matchers>,
|
|
851
|
+
Matcher.MergeError<Matchers>,
|
|
852
|
+
Matcher.MergeServices<Matchers>
|
|
853
|
+
>;
|
|
854
|
+
}
|
|
855
|
+
if (matchers.length === 1) {
|
|
856
|
+
return matchers[0] as unknown as Matcher<
|
|
857
|
+
Matcher.MergeSuccess<Matchers>,
|
|
858
|
+
Matcher.MergeError<Matchers>,
|
|
859
|
+
Matcher.MergeServices<Matchers>
|
|
860
|
+
>;
|
|
861
|
+
}
|
|
862
|
+
const first = matchers[0] as MatcherImpl<
|
|
863
|
+
Matcher.MergeSuccess<Matchers>,
|
|
864
|
+
Matcher.MergeError<Matchers>,
|
|
865
|
+
Matcher.MergeServices<Matchers>
|
|
866
|
+
>;
|
|
867
|
+
const rest = matchers.slice(1) as ReadonlyArray<
|
|
868
|
+
Matcher<
|
|
869
|
+
Matcher.MergeSuccess<Matchers>,
|
|
870
|
+
Matcher.MergeError<Matchers>,
|
|
871
|
+
Matcher.MergeServices<Matchers>
|
|
872
|
+
>
|
|
873
|
+
>;
|
|
874
|
+
return first.merge(...rest) as unknown as Matcher<
|
|
875
|
+
Matcher.MergeSuccess<Matchers>,
|
|
876
|
+
Matcher.MergeError<Matchers>,
|
|
877
|
+
Matcher.MergeServices<Matchers>
|
|
878
|
+
>;
|
|
910
879
|
}
|
|
911
880
|
|
|
881
|
+
export class RouteGuardError extends Schema.ErrorClass<RouteGuardError>(
|
|
882
|
+
"@typed/router/RouteGuardError",
|
|
883
|
+
)({
|
|
884
|
+
_tag: Schema.tag("RouteGuardError"),
|
|
885
|
+
path: Schema.String,
|
|
886
|
+
causes: Schema.Array(Schema.Unknown),
|
|
887
|
+
}) {}
|
|
888
|
+
|
|
889
|
+
export class RouteNotFound extends Schema.ErrorClass<RouteNotFound>("@typed/router/RouteNotFound")({
|
|
890
|
+
_tag: Schema.tag("RouteNotFound"),
|
|
891
|
+
path: Schema.String,
|
|
892
|
+
}) {}
|
|
893
|
+
|
|
894
|
+
export class RouteDecodeError extends Schema.ErrorClass<RouteDecodeError>(
|
|
895
|
+
"@typed/router/RouteDecodeError",
|
|
896
|
+
)({
|
|
897
|
+
_tag: Schema.tag("RouteDecodeError"),
|
|
898
|
+
path: Schema.String,
|
|
899
|
+
cause: Schema.String,
|
|
900
|
+
}) {}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* @internal
|
|
904
|
+
*/
|
|
905
|
+
export type CompiledEntry = {
|
|
906
|
+
readonly route: Route.Any;
|
|
907
|
+
readonly guard: AnyGuard;
|
|
908
|
+
readonly handler: AnyMatchHandler;
|
|
909
|
+
readonly layers: ReadonlyArray<AnyLayer>;
|
|
910
|
+
readonly layouts: ReadonlyArray<AnyLayout>;
|
|
911
|
+
readonly catches: ReadonlyArray<AnyCatch>;
|
|
912
|
+
readonly decode: (input: unknown) => Effect.Effect<any, Schema.SchemaError, any>;
|
|
913
|
+
};
|
|
914
|
+
|
|
912
915
|
type InputSucces<T> = [Matcher.Success<T> | Fx.Fx.Success<T>] extends [infer A] ? A : never;
|
|
913
916
|
type InputError<T> = [Matcher.Error<T> | Fx.Fx.Error<T>] extends [infer E] ? E : never;
|
|
914
917
|
type InputServices<T> = [Matcher.Services<T> | Fx.Fx.Services<T>] extends [infer R] ? R : never;
|
|
@@ -943,13 +946,8 @@ export const catchCause: {
|
|
|
943
946
|
const eff = Effect.gen(function* () {
|
|
944
947
|
const fiberId = yield* Effect.fiberId;
|
|
945
948
|
const rootScope = yield* Effect.scope;
|
|
946
|
-
const fx = isFx(input) ? input : run(input);
|
|
947
949
|
const manager = makeCatchManager(rootScope, fiberId);
|
|
948
|
-
const result = yield* manager.apply(
|
|
949
|
-
[f],
|
|
950
|
-
fx,
|
|
951
|
-
ServiceMap.empty() as ServiceMap.ServiceMap<any>,
|
|
952
|
-
);
|
|
950
|
+
const result = yield* manager.apply([f], input, Context.empty() as Context.Context<any>);
|
|
953
951
|
return result as Fx.Fx<A | B, E2, R | R2 | Router | Scope.Scope>;
|
|
954
952
|
});
|
|
955
953
|
return unwrap(eff);
|
|
@@ -960,6 +958,11 @@ export const catch_: {
|
|
|
960
958
|
<I extends Fx.Fx.Any | Matcher.Any, B, E2, R2>(
|
|
961
959
|
f: (e: InputError<I>) => Fx.Fx<B, E2, R2>,
|
|
962
960
|
): (input: I) => Fx.Fx<InputSucces<I> | B, E2, InputServices<I> | R2 | Router | Scope.Scope>;
|
|
961
|
+
|
|
962
|
+
<I extends Fx.Fx.Any | Matcher.Any, B, E2, R2>(
|
|
963
|
+
input: I,
|
|
964
|
+
f: (e: InputError<I>) => Fx.Fx<B, E2, R2>,
|
|
965
|
+
): Fx.Fx<InputSucces<I> | B, E2, InputServices<I> | R2 | Router | Scope.Scope>;
|
|
963
966
|
} = dual(
|
|
964
967
|
2,
|
|
965
968
|
<I extends Fx.Fx.Any | Matcher.Any, B, E2, R2>(
|
|
@@ -1082,7 +1085,7 @@ function isServiceMap(dep: AnyDependency): dep is AnyServiceMap {
|
|
|
1082
1085
|
}
|
|
1083
1086
|
|
|
1084
1087
|
function toSingleLayer(dep: AnyDependency): AnyLayer {
|
|
1085
|
-
if (isServiceMap(dep)) return Layer.
|
|
1088
|
+
if (isServiceMap(dep)) return Layer.succeedContext(dep);
|
|
1086
1089
|
return dep;
|
|
1087
1090
|
}
|
|
1088
1091
|
|
|
@@ -1092,26 +1095,27 @@ function normalizeDependencies(
|
|
|
1092
1095
|
return dependencies.map(toSingleLayer);
|
|
1093
1096
|
}
|
|
1094
1097
|
|
|
1095
|
-
type NormalizeLayer<T extends AnyDependency> =
|
|
1096
|
-
T extends Layer.Layer<infer A, infer E, infer R>
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1098
|
+
type NormalizeLayer<T extends AnyDependency> =
|
|
1099
|
+
T extends Layer.Layer<infer A, infer E, infer R>
|
|
1100
|
+
? Layer.Layer<A, E, R>
|
|
1101
|
+
: T extends Context.Context<infer R>
|
|
1102
|
+
? Layer.Layer<R>
|
|
1103
|
+
: never;
|
|
1104
|
+
|
|
1105
|
+
type NormalizeLayers<T extends ReadonlyArray<AnyDependency>> = {
|
|
1106
|
+
[K in keyof T]: NormalizeLayer<T[K]>;
|
|
1107
|
+
};
|
|
1101
1108
|
|
|
1102
|
-
type ToLayer<T> =
|
|
1103
|
-
T extends ReadonlyArray<AnyLayer>
|
|
1104
|
-
Layer.Success<T[number]>,
|
|
1105
|
-
|
|
1106
|
-
Layer.Services<T[number]>
|
|
1107
|
-
> : never
|
|
1109
|
+
type ToLayer<T> =
|
|
1110
|
+
T extends ReadonlyArray<AnyLayer>
|
|
1111
|
+
? Layer.Layer<Layer.Success<T[number]>, Layer.Error<T[number]>, Layer.Services<T[number]>>
|
|
1112
|
+
: never;
|
|
1108
1113
|
|
|
1109
|
-
type NormalizeDeps<T extends AnyDependency | ReadonlyArray<AnyDependency>> =
|
|
1110
|
-
T extends AnyDependency
|
|
1114
|
+
type NormalizeDeps<T extends AnyDependency | ReadonlyArray<AnyDependency>> = T extends AnyDependency
|
|
1111
1115
|
? NormalizeLayer<T>
|
|
1112
1116
|
: T extends ReadonlyArray<AnyDependency>
|
|
1113
|
-
|
|
1114
|
-
|
|
1117
|
+
? ToLayer<NormalizeLayers<T>>
|
|
1118
|
+
: never;
|
|
1115
1119
|
|
|
1116
1120
|
/**
|
|
1117
1121
|
* Normalize dependency input (ServiceMap | Layer | Array of either) into a single Layer.
|
|
@@ -1241,18 +1245,18 @@ export function makeLayerManager(memoMap: Layer.MemoMap, rootScope: Scope.Scope,
|
|
|
1241
1245
|
: ((cachedDesiredSet = new Set(desired)), (cachedOrder = desired), cachedDesiredSet);
|
|
1242
1246
|
const removed = order.filter((layer) => !desiredSet.has(layer));
|
|
1243
1247
|
const added: Array<AnyLayer> = [];
|
|
1244
|
-
let services =
|
|
1248
|
+
let services = Context.empty();
|
|
1245
1249
|
|
|
1246
1250
|
for (const layer of desired) {
|
|
1247
1251
|
const existing = states.get(layer);
|
|
1248
1252
|
if (existing) {
|
|
1249
|
-
services =
|
|
1253
|
+
services = Context.merge(services, existing.services);
|
|
1250
1254
|
continue;
|
|
1251
1255
|
}
|
|
1252
1256
|
|
|
1253
1257
|
const scope = yield* Scope.fork(rootScope);
|
|
1254
1258
|
const buildExit = yield* Layer.buildWithMemoMap(layer, memoMap, scope).pipe(
|
|
1255
|
-
Effect.
|
|
1259
|
+
Effect.provideContext(services),
|
|
1256
1260
|
Effect.exit,
|
|
1257
1261
|
);
|
|
1258
1262
|
|
|
@@ -1270,7 +1274,7 @@ export function makeLayerManager(memoMap: Layer.MemoMap, rootScope: Scope.Scope,
|
|
|
1270
1274
|
}
|
|
1271
1275
|
|
|
1272
1276
|
const servicesForLayer = buildExit.value;
|
|
1273
|
-
services =
|
|
1277
|
+
services = Context.merge(services, servicesForLayer);
|
|
1274
1278
|
states.set(layer, { scope, services: servicesForLayer });
|
|
1275
1279
|
added.push(layer);
|
|
1276
1280
|
}
|
|
@@ -1336,7 +1340,7 @@ export function makeLayoutManager(rootScope: Scope.Scope, fiberId: number) {
|
|
|
1336
1340
|
layouts: ReadonlyArray<AnyLayout>,
|
|
1337
1341
|
paramsValue: any,
|
|
1338
1342
|
inner: Fx.Fx<any, any, any>,
|
|
1339
|
-
services:
|
|
1343
|
+
services: Context.Context<any>,
|
|
1340
1344
|
) =>
|
|
1341
1345
|
Effect.gen(function* () {
|
|
1342
1346
|
let current = inner;
|
|
@@ -1350,7 +1354,7 @@ export function makeLayoutManager(rootScope: Scope.Scope, fiberId: number) {
|
|
|
1350
1354
|
eq: (left, right) => left === right,
|
|
1351
1355
|
}).pipe(Scope.provide(scope));
|
|
1352
1356
|
const fx = layout({ params, content: content.pipe(switchMap(identity)) }).pipe(
|
|
1353
|
-
|
|
1357
|
+
provideContext(Context.merge(services, Context.make(Scope.Scope, scope))),
|
|
1354
1358
|
);
|
|
1355
1359
|
states.set(layout, { params, content, fx, scope });
|
|
1356
1360
|
current = fx;
|
|
@@ -1409,7 +1413,7 @@ export function makeCatchManager(rootScope: Scope.Scope, fiberId: number) {
|
|
|
1409
1413
|
const apply = (
|
|
1410
1414
|
catches: ReadonlyArray<AnyCatch>,
|
|
1411
1415
|
inner: Fx.Fx<any, any, any>,
|
|
1412
|
-
services:
|
|
1416
|
+
services: Context.Context<any>,
|
|
1413
1417
|
) =>
|
|
1414
1418
|
Effect.gen(function* () {
|
|
1415
1419
|
let current = inner;
|
|
@@ -1425,14 +1429,14 @@ export function makeCatchManager(rootScope: Scope.Scope, fiberId: number) {
|
|
|
1425
1429
|
eq: (left, right) => left === right,
|
|
1426
1430
|
}).pipe(Scope.provide(scope));
|
|
1427
1431
|
const fallback = catcher(causes).pipe(
|
|
1428
|
-
|
|
1432
|
+
provideContext(Context.merge(services, Context.make(Scope.Scope, scope))),
|
|
1429
1433
|
);
|
|
1430
1434
|
const fx = content.pipe(
|
|
1431
1435
|
switchMap(identity),
|
|
1432
1436
|
exit,
|
|
1433
1437
|
mapEffect(
|
|
1434
1438
|
Effect.fn(function* (e) {
|
|
1435
|
-
if (isSuccess(e)) return succeed(e.value);
|
|
1439
|
+
if (Exit.isSuccess(e)) return succeed(e.value);
|
|
1436
1440
|
yield* RefSubject.set(causes, e.cause);
|
|
1437
1441
|
return fallback;
|
|
1438
1442
|
}),
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import { BrowserRouter, Router } from "../Router.js";
|
|
3
|
+
import { Scope } from "effect";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Build an absolute URL for the Vitest browser page origin (same pattern as in-memory tests using http://localhost/...).
|
|
7
|
+
*/
|
|
8
|
+
export const absoluteUrl = (
|
|
9
|
+
path: string,
|
|
10
|
+
win: Window & typeof globalThis = globalThis.window,
|
|
11
|
+
): string => {
|
|
12
|
+
const normalized = path.startsWith("/") ? path : `/${path}`;
|
|
13
|
+
return new URL(normalized, win.location.origin).href;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Run an effect with {@link BrowserRouter} scoped over the real (or test) `window`.
|
|
18
|
+
*/
|
|
19
|
+
export const runWithBrowserRouter = <A, E>(
|
|
20
|
+
effect: Effect.Effect<A, E, Router | Scope.Scope>,
|
|
21
|
+
win: Window & typeof globalThis = globalThis.window,
|
|
22
|
+
): Promise<A> => effect.pipe(Effect.provide(BrowserRouter(win)), Effect.scoped, Effect.runPromise);
|