@typed/router 1.0.0-beta.1 → 1.0.0-beta.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.
Files changed (29) hide show
  1. package/dist/Matcher.d.ts +18 -13
  2. package/dist/Matcher.d.ts.map +1 -1
  3. package/dist/Matcher.js +102 -97
  4. package/dist/MatcherV2.d.ts +3 -0
  5. package/dist/MatcherV2.d.ts.map +1 -0
  6. package/dist/MatcherV2.js +1 -0
  7. package/dist/Route.d.ts +5 -0
  8. package/dist/Route.d.ts.map +1 -1
  9. package/dist/test-utils/matcherBrowserHarness.d.ts +10 -0
  10. package/dist/test-utils/matcherBrowserHarness.d.ts.map +1 -0
  11. package/dist/test-utils/matcherBrowserHarness.js +13 -0
  12. package/package.json +21 -18
  13. package/src/Matcher.browser.test.ts +771 -0
  14. package/src/Matcher.test.ts +344 -67
  15. package/src/Matcher.ts +165 -132
  16. package/src/Route.ts +6 -0
  17. package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--back---restores-previous-match-1.png +0 -0
  18. package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--catchTag-RouteNotFound-can-navigate-and-re-match--browser-history--1.png +0 -0
  19. package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--decodes-query-params-from-pathname-search-1.png +0 -0
  20. 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
  21. package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--history-back-restores-previous-match--popstate-sync--1.png +0 -0
  22. package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--path-and-query-params-both-decode--distinct-names--1.png +0 -0
  23. package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--prefix-scopes-routes-under-a-path-segment-1.png +0 -0
  24. package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--provideService-supplies-a-service-to-handlers-1.png +0 -0
  25. package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--query-param-wins-over-path-param-when-names-collide-1.png +0 -0
  26. package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--redirectTo-navigates-away-from-unmatched-path--side-effect--1.png +0 -0
  27. package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--redirectTo-navigates-then-matches-target-route-1.png +0 -0
  28. package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--reuses-shared-layers-and-layouts-across-route-changes-1.png +0 -0
  29. 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, isSuccess } from "effect/Exit";
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";
@@ -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,21 +58,21 @@ export type AnyLayer =
57
58
  | Layer.Layer<never, any, never>
58
59
  | Layer.Layer<never, never, any>;
59
60
 
60
- type AnyServiceMap = ServiceMap.ServiceMap<any> | ServiceMap.ServiceMap<never>;
61
- type AnyDependency = AnyLayer | AnyServiceMap;
61
+ export type AnyServiceMap = ServiceMap.ServiceMap<any> | ServiceMap.ServiceMap<never>;
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>;
64
65
  type AnyGuard = GuardType<any, any, any, any>;
65
66
  type AnyMatchHandler = (params: RefSubject.RefSubject<any>) => Fx.Fx<any, any, any>;
66
67
 
67
- type DependencyProvided<D> =
68
+ export type DependencyProvided<D> =
68
69
  D extends Layer.Layer<infer Provided, any, any>
69
70
  ? Provided
70
71
  : D extends ServiceMap.ServiceMap<infer Provided>
71
72
  ? Provided
72
73
  : never;
73
- type DependencyError<D> = D extends Layer.Layer<any, infer E, any> ? E : never;
74
- type DependencyRequirements<D> = D extends Layer.Layer<any, any, infer R> ? R : never;
74
+ export type DependencyError<D> = D extends Layer.Layer<any, infer E, any> ? E : never;
75
+ export type DependencyRequirements<D> = D extends Layer.Layer<any, any, infer R> ? R : never;
75
76
 
76
77
  type LayerSuccess<L> = L extends Layer.Layer<infer Provided, any, any> ? Provided : never;
77
78
  type LayerError<L> = L extends Layer.Layer<any, infer E, any> ? E : never;
@@ -85,19 +86,19 @@ export interface AsGuard<I, O, E = never, R = never> {
85
86
  }
86
87
  export type GuardInput<I, O, E = never, R = never> = GuardType<I, O, E, R> | AsGuard<I, O, E, R>;
87
88
 
88
- type GuardOutput<G> =
89
+ export type GuardOutput<G> =
89
90
  G extends GuardType<any, infer O, any, any>
90
91
  ? O
91
92
  : G extends AsGuard<any, infer O, any, any>
92
93
  ? O
93
94
  : never;
94
- type GuardError<G> =
95
+ export type GuardError<G> =
95
96
  G extends GuardType<any, any, infer E, any>
96
97
  ? E
97
98
  : G extends AsGuard<any, any, infer E, any>
98
99
  ? E
99
100
  : never;
100
- type GuardServices<G> =
101
+ export type GuardServices<G> =
101
102
  G extends GuardType<any, any, any, infer R>
102
103
  ? R
103
104
  : G extends AsGuard<any, any, any, infer R>
@@ -114,7 +115,7 @@ type MatchOptions<Rt extends Route.Any, B, E2, R2, D, LB, LE2, LR2, C> = {
114
115
  readonly catch?: C;
115
116
  };
116
117
 
117
- type MatchHandlerReturnValue<A, E, R> =
118
+ export type MatchHandlerReturnValue<A, E, R> =
118
119
  | Fx.Fx<A, E, R>
119
120
  | Stream.Stream<A, E, R>
120
121
  | Effect.Effect<A, E, R>
@@ -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> extends Pipeable {
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)
@@ -431,6 +435,15 @@ function parseMatchArgs(args: [unknown, ...Array<unknown>]): ParsedMatch {
431
435
  }
432
436
 
433
437
  class MatcherImpl<A, E, R> implements Matcher<A, E, R> {
438
+ readonly [FxTypeId]: Fx.Fx.Variance<
439
+ A,
440
+ E | RouteNotFound | RouteDecodeError | RouteGuardError,
441
+ R | Scope.Scope | Router
442
+ > = {
443
+ _A: identity,
444
+ _E: identity,
445
+ _R: identity,
446
+ };
434
447
  readonly cases: ReadonlyArray<MatchAst>;
435
448
  constructor(cases: ReadonlyArray<MatchAst>) {
436
449
  this.cases = cases;
@@ -660,118 +673,13 @@ class MatcherImpl<A, E, R> implements Matcher<A, E, R> {
660
673
  pipe() {
661
674
  return pipeArguments(this, arguments);
662
675
  }
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
676
 
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
-
728
- export class RouteGuardError extends Schema.ErrorClass<RouteGuardError>(
729
- "@typed/router/RouteGuardError",
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* () {
677
+ run<RSink>(sink: Sink.Sink<A, E | RouteNotFound | RouteDecodeError | RouteGuardError, RSink>) {
678
+ return Effect.gen({ self: this }, function* () {
771
679
  const fiberId = yield* Effect.fiberId;
772
680
  const rootScope = yield* Effect.scope;
773
681
  const current = yield* CurrentRoute;
774
- const prefixed = matcher.prefix(current.route);
682
+ const prefixed = this.prefix(current.route);
775
683
  const entries = compile(prefixed.cases);
776
684
  const router = findMyWay.make<ReadonlyArray<CompiledEntry>>({
777
685
  ignoreTrailingSlash: true,
@@ -798,11 +706,11 @@ export function run<M extends Matcher.Any>(
798
706
  let currentState: {
799
707
  entry: CompiledEntry;
800
708
  params: RefSubject.RefSubject<any>;
801
- fx: Fx.Fx<Matcher.Success<M>, Matcher.Error<M>, Matcher.Services<M> | Scope.Scope | Router>;
709
+ fx: Fx.Fx<A, E, R | Scope.Scope | Router>;
802
710
  scope: Scope.Closeable;
803
711
  } | null = null;
804
712
 
805
- return CurrentPath.pipe(
713
+ const stream = CurrentPath.pipe(
806
714
  mapEffect(
807
715
  Effect.fn(function* (path) {
808
716
  const result = router.find("GET", path);
@@ -905,10 +813,109 @@ export function run<M extends Matcher.Any>(
905
813
  skipRepeats,
906
814
  switchMap(identity),
907
815
  );
908
- }),
909
- );
816
+
817
+ return yield* stream.run(sink);
818
+ });
819
+ }
910
820
  }
911
821
 
822
+ function normalizeHandler<Params, B, E2 = never, R2 = never>(
823
+ handler: MatchHandler<Params, B, E2, R2>,
824
+ ): (params: RefSubject.RefSubject<Params>) => Fx.Fx<B, E2, R2> {
825
+ if (isMatchHandlerFn(handler)) return (params) => toFx(handler(params));
826
+ return () => toFx(handler);
827
+ }
828
+
829
+ function toFx<A, E, R>(
830
+ value: Fx.Fx<A, E, R> | Stream.Stream<A, E, R> | Effect.Effect<A, E, R> | A,
831
+ ): Fx.Fx<A, E, R> {
832
+ if (isFx(value)) return value;
833
+ if (Stream.isStream(value)) return fromStream(value);
834
+ if (Effect.isEffect(value)) return fromEffect(value);
835
+ return succeed(value);
836
+ }
837
+
838
+ export const empty: Matcher<never> = new MatcherImpl([]);
839
+ export const match = empty.match.bind(empty);
840
+
841
+ /**
842
+ * Merge multiple matchers into one. Each matcher's layouts and provide apply only to its own routes.
843
+ * Use this so directory layouts (e.g. api/_layout) and directory dependencies apply only to routes under that directory.
844
+ */
845
+ export function merge<const Matchers extends ReadonlyArray<Matcher.Any>>(
846
+ ...matchers: Matchers
847
+ ): Matcher<
848
+ Matcher.MergeSuccess<Matchers>,
849
+ Matcher.MergeError<Matchers>,
850
+ Matcher.MergeServices<Matchers>
851
+ > {
852
+ if (matchers.length === 0) {
853
+ return empty as unknown as Matcher<
854
+ Matcher.MergeSuccess<Matchers>,
855
+ Matcher.MergeError<Matchers>,
856
+ Matcher.MergeServices<Matchers>
857
+ >;
858
+ }
859
+ if (matchers.length === 1) {
860
+ return matchers[0] as unknown as Matcher<
861
+ Matcher.MergeSuccess<Matchers>,
862
+ Matcher.MergeError<Matchers>,
863
+ Matcher.MergeServices<Matchers>
864
+ >;
865
+ }
866
+ const first = matchers[0] as MatcherImpl<
867
+ Matcher.MergeSuccess<Matchers>,
868
+ Matcher.MergeError<Matchers>,
869
+ Matcher.MergeServices<Matchers>
870
+ >;
871
+ const rest = matchers.slice(1) as ReadonlyArray<
872
+ Matcher<
873
+ Matcher.MergeSuccess<Matchers>,
874
+ Matcher.MergeError<Matchers>,
875
+ Matcher.MergeServices<Matchers>
876
+ >
877
+ >;
878
+ return first.merge(...rest) as unknown as Matcher<
879
+ Matcher.MergeSuccess<Matchers>,
880
+ Matcher.MergeError<Matchers>,
881
+ Matcher.MergeServices<Matchers>
882
+ >;
883
+ }
884
+
885
+ export class RouteGuardError extends Schema.ErrorClass<RouteGuardError>(
886
+ "@typed/router/RouteGuardError",
887
+ )({
888
+ _tag: Schema.tag("RouteGuardError"),
889
+ path: Schema.String,
890
+ causes: Schema.Array(Schema.Unknown),
891
+ }) {}
892
+
893
+ export class RouteNotFound extends Schema.ErrorClass<RouteNotFound>("@typed/router/RouteNotFound")({
894
+ _tag: Schema.tag("RouteNotFound"),
895
+ path: Schema.String,
896
+ }) {}
897
+
898
+ export class RouteDecodeError extends Schema.ErrorClass<RouteDecodeError>(
899
+ "@typed/router/RouteDecodeError",
900
+ )({
901
+ _tag: Schema.tag("RouteDecodeError"),
902
+ path: Schema.String,
903
+ cause: Schema.String,
904
+ }) {}
905
+
906
+ /**
907
+ * @internal
908
+ */
909
+ export type CompiledEntry = {
910
+ readonly route: Route.Any;
911
+ readonly guard: AnyGuard;
912
+ readonly handler: AnyMatchHandler;
913
+ readonly layers: ReadonlyArray<AnyLayer>;
914
+ readonly layouts: ReadonlyArray<AnyLayout>;
915
+ readonly catches: ReadonlyArray<AnyCatch>;
916
+ readonly decode: (input: unknown) => Effect.Effect<any, Schema.SchemaError, any>;
917
+ };
918
+
912
919
  type InputSucces<T> = [Matcher.Success<T> | Fx.Fx.Success<T>] extends [infer A] ? A : never;
913
920
  type InputError<T> = [Matcher.Error<T> | Fx.Fx.Error<T>] extends [infer E] ? E : never;
914
921
  type InputServices<T> = [Matcher.Services<T> | Fx.Fx.Services<T>] extends [infer R] ? R : never;
@@ -943,11 +950,10 @@ export const catchCause: {
943
950
  const eff = Effect.gen(function* () {
944
951
  const fiberId = yield* Effect.fiberId;
945
952
  const rootScope = yield* Effect.scope;
946
- const fx = isFx(input) ? input : run(input);
947
953
  const manager = makeCatchManager(rootScope, fiberId);
948
954
  const result = yield* manager.apply(
949
955
  [f],
950
- fx,
956
+ input,
951
957
  ServiceMap.empty() as ServiceMap.ServiceMap<any>,
952
958
  );
953
959
  return result as Fx.Fx<A | B, E2, R | R2 | Router | Scope.Scope>;
@@ -960,6 +966,11 @@ export const catch_: {
960
966
  <I extends Fx.Fx.Any | Matcher.Any, B, E2, R2>(
961
967
  f: (e: InputError<I>) => Fx.Fx<B, E2, R2>,
962
968
  ): (input: I) => Fx.Fx<InputSucces<I> | B, E2, InputServices<I> | R2 | Router | Scope.Scope>;
969
+
970
+ <I extends Fx.Fx.Any | Matcher.Any, B, E2, R2>(
971
+ input: I,
972
+ f: (e: InputError<I>) => Fx.Fx<B, E2, R2>,
973
+ ): Fx.Fx<InputSucces<I> | B, E2, InputServices<I> | R2 | Router | Scope.Scope>;
963
974
  } = dual(
964
975
  2,
965
976
  <I extends Fx.Fx.Any | Matcher.Any, B, E2, R2>(
@@ -1092,16 +1103,38 @@ function normalizeDependencies(
1092
1103
  return dependencies.map(toSingleLayer);
1093
1104
  }
1094
1105
 
1106
+ type NormalizeLayer<T extends AnyDependency> =
1107
+ T extends Layer.Layer<infer A, infer E, infer R>
1108
+ ? Layer.Layer<A, E, R>
1109
+ : T extends ServiceMap.ServiceMap<infer R>
1110
+ ? Layer.Layer<R>
1111
+ : never;
1112
+
1113
+ type NormalizeLayers<T extends ReadonlyArray<AnyDependency>> = {
1114
+ [K in keyof T]: NormalizeLayer<T[K]>;
1115
+ };
1116
+
1117
+ type ToLayer<T> =
1118
+ T extends ReadonlyArray<AnyLayer>
1119
+ ? Layer.Layer<Layer.Success<T[number]>, Layer.Error<T[number]>, Layer.Services<T[number]>>
1120
+ : never;
1121
+
1122
+ type NormalizeDeps<T extends AnyDependency | ReadonlyArray<AnyDependency>> = T extends AnyDependency
1123
+ ? NormalizeLayer<T>
1124
+ : T extends ReadonlyArray<AnyDependency>
1125
+ ? ToLayer<NormalizeLayers<T>>
1126
+ : never;
1127
+
1095
1128
  /**
1096
1129
  * Normalize dependency input (ServiceMap | Layer | Array of either) into a single Layer.
1097
1130
  * Use with `.provide(normalizeDependencyInput(deps))`.
1098
1131
  */
1099
- export function normalizeDependencyInput(
1100
- input: AnyDependency | ReadonlyArray<AnyDependency>,
1101
- ): AnyLayer {
1132
+ export function normalizeDependencyInput<Deps extends AnyDependency | ReadonlyArray<AnyDependency>>(
1133
+ input: Deps,
1134
+ ): NormalizeDeps<Deps> {
1102
1135
  const arr = Array.isArray(input) ? input : [input];
1103
- const layers = normalizeDependencies(arr);
1104
- return mergeLayers(layers);
1136
+ const layers = normalizeDependencies(arr as AnyDependency[]);
1137
+ return mergeLayers(layers) as NormalizeDeps<Deps>;
1105
1138
  }
1106
1139
 
1107
1140
  function getGuard<I, O, E, R>(guard: GuardInput<I, O, E, R>): GuardType<I, O, E, R> {
@@ -1411,7 +1444,7 @@ export function makeCatchManager(rootScope: Scope.Scope, fiberId: number) {
1411
1444
  exit,
1412
1445
  mapEffect(
1413
1446
  Effect.fn(function* (e) {
1414
- if (isSuccess(e)) return succeed(e.value);
1447
+ if (Exit.isSuccess(e)) return succeed(e.value);
1415
1448
  yield* RefSubject.set(causes, e.cause);
1416
1449
  return fallback;
1417
1450
  }),
package/src/Route.ts CHANGED
@@ -35,6 +35,12 @@ export declare namespace Route {
35
35
  export type QueryType<T extends Any> = T["querySchema"]["Type"];
36
36
  }
37
37
 
38
+ export type Any = Route.Any;
39
+ export type Params<T> = Route.Params<T>;
40
+ export type Type<T> = Route.Type<T>;
41
+ export type PathType<T extends Any> = Route.PathType<T>;
42
+ export type QueryType<T extends Any> = Route.QueryType<T>;
43
+
38
44
  export function make<
39
45
  const P extends string,
40
46
  S extends Schema.Codec<any, Path.Params<P>, any, any> = Schema.Codec<Path.Params<P>>,
@@ -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);