@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.
Files changed (31) hide show
  1. package/README.md +1 -1
  2. package/dist/CurrentRoute.d.ts +2 -2
  3. package/dist/CurrentRoute.d.ts.map +1 -1
  4. package/dist/CurrentRoute.js +4 -4
  5. package/dist/Matcher.d.ts +18 -19
  6. package/dist/Matcher.d.ts.map +1 -1
  7. package/dist/Matcher.js +112 -109
  8. package/dist/MatcherV2.d.ts +3 -0
  9. package/dist/MatcherV2.d.ts.map +1 -0
  10. package/dist/MatcherV2.js +1 -0
  11. package/dist/test-utils/matcherBrowserHarness.d.ts +10 -0
  12. package/dist/test-utils/matcherBrowserHarness.d.ts.map +1 -0
  13. package/dist/test-utils/matcherBrowserHarness.js +13 -0
  14. package/package.json +21 -18
  15. package/src/CurrentRoute.ts +5 -5
  16. package/src/Matcher.browser.test.ts +767 -0
  17. package/src/Matcher.test.ts +348 -73
  18. package/src/Matcher.ts +170 -166
  19. package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--back---restores-previous-match-1.png +0 -0
  20. package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--catchTag-RouteNotFound-can-navigate-and-re-match--browser-history--1.png +0 -0
  21. package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--decodes-query-params-from-pathname-search-1.png +0 -0
  22. 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
  23. package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--history-back-restores-previous-match--popstate-sync--1.png +0 -0
  24. package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--path-and-query-params-both-decode--distinct-names--1.png +0 -0
  25. package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--prefix-scopes-routes-under-a-path-segment-1.png +0 -0
  26. package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--provideService-supplies-a-service-to-handlers-1.png +0 -0
  27. package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--query-param-wins-over-path-param-when-names-collide-1.png +0 -0
  28. package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--redirectTo-navigates-away-from-unmatched-path--side-effect--1.png +0 -0
  29. package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--redirectTo-navigates-then-matches-target-route-1.png +0 -0
  30. package/src/__screenshots__/Matcher.browser.test.ts/typed-router-Matcher--browser--reuses-shared-layers-and-layouts-across-route-changes-1.png +0 -0
  31. 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";
@@ -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 ServiceMap from "effect/ServiceMap";
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 { provideServices } from "@typed/fx/Fx/combinators/provide";
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 = ServiceMap.ServiceMap<any> | ServiceMap.ServiceMap<never>;
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 ServiceMap.ServiceMap<infer Provided>
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> 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)
@@ -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: ServiceMap.Service<Id, S>,
281
+ tag: Context.Service<Id, S>,
278
282
  service: S,
279
283
  ) => Matcher<A, E, Exclude<R, Id>>;
280
284
 
281
- readonly provideServices: <R2>(
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: ServiceMap.Service<Id, S>, service: S): Matcher<A, E, Exclude<R, Id>> {
573
- return this.provideServices(ServiceMap.make(tag, service));
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
- provideServices<R2>(services: ServiceMap.ServiceMap<R2>): Matcher<A, E, Exclude<R, R2>> {
577
- return this.provide(Layer.succeedServices(services));
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
- 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* () {
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 = matcher.prefix(current.route);
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<Matcher.Success<M>, Matcher.Error<M>, Matcher.Services<M> | Scope.Scope | Router>;
707
+ fx: Fx.Fx<A, E, R | Scope.Scope | Router>;
802
708
  scope: Scope.Closeable;
803
709
  } | null = null;
804
710
 
805
- return CurrentPath.pipe(
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.provideServices(prepared.services), Effect.exit);
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 ServiceMap.ServiceMap<any>;
874
- const handlerServices = ServiceMap.merge(
779
+ const preparedServices = matchedPrepared.services as Context.Context<any>;
780
+ const handlerServices = Context.merge(
875
781
  preparedServices,
876
- ServiceMap.make(Scope.Scope, scope),
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.succeedServices(dep);
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> ? Layer.Layer<A, E, R> : T extends ServiceMap.ServiceMap<infer R> ? Layer.Layer<R> : never
1097
-
1098
- type NormalizeLayers<T extends ReadonlyArray<AnyDependency>> ={
1099
- [K in keyof T]: NormalizeLayer<T[K]>
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> ? Layer.Layer<
1104
- Layer.Success<T[number]>,
1105
- Layer.Error<T[number]>,
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
- ? ToLayer<NormalizeLayers<T>>
1114
- : never
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 = ServiceMap.empty();
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 = ServiceMap.merge(services, existing.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.provideServices(services),
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 = ServiceMap.merge(services, servicesForLayer);
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: ServiceMap.ServiceMap<any>,
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
- provideServices(ServiceMap.merge(services, ServiceMap.make(Scope.Scope, scope))),
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: ServiceMap.ServiceMap<any>,
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
- provideServices(ServiceMap.merge(services, ServiceMap.make(Scope.Scope, scope))),
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
  }),
@@ -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);