@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.
Files changed (87) hide show
  1. package/README.md +129 -2
  2. package/dist/AST.d.ts +96 -0
  3. package/dist/AST.d.ts.map +1 -0
  4. package/dist/AST.js +32 -0
  5. package/dist/CurrentRoute.d.ts +18 -0
  6. package/dist/CurrentRoute.d.ts.map +1 -0
  7. package/dist/CurrentRoute.js +18 -0
  8. package/dist/Matcher.d.ts +209 -0
  9. package/dist/Matcher.d.ts.map +1 -0
  10. package/dist/Matcher.js +633 -0
  11. package/dist/Parser.d.ts +92 -0
  12. package/dist/Parser.d.ts.map +1 -0
  13. package/dist/Parser.js +1 -0
  14. package/dist/Path.d.ts +216 -0
  15. package/dist/Path.d.ts.map +1 -0
  16. package/dist/Path.js +248 -0
  17. package/dist/Route.d.ts +57 -0
  18. package/dist/Route.d.ts.map +1 -0
  19. package/dist/Route.js +151 -0
  20. package/dist/Router.d.ts +9 -0
  21. package/dist/Router.d.ts.map +1 -0
  22. package/dist/Router.js +8 -0
  23. package/dist/Uri.d.ts +115 -0
  24. package/dist/Uri.d.ts.map +1 -0
  25. package/dist/Uri.js +1 -0
  26. package/dist/index.d.ts +5 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +4 -0
  29. package/package.json +32 -73
  30. package/src/AST.ts +166 -0
  31. package/src/CurrentRoute.ts +30 -331
  32. package/src/Matcher.test.ts +496 -0
  33. package/src/Matcher.ts +1375 -325
  34. package/src/Parser.ts +276 -0
  35. package/src/Path.test.ts +318 -0
  36. package/src/Path.ts +691 -0
  37. package/src/Route.test.ts +268 -0
  38. package/src/Route.ts +316 -0
  39. package/src/Router.ts +33 -0
  40. package/src/Uri.ts +214 -0
  41. package/src/index.ts +4 -28
  42. package/CurrentRoute/package.json +0 -6
  43. package/LICENSE +0 -21
  44. package/MatchInput/package.json +0 -6
  45. package/Matcher/package.json +0 -6
  46. package/RouteGuard/package.json +0 -6
  47. package/RouteMatch/package.json +0 -6
  48. package/dist/cjs/CurrentRoute.js +0 -170
  49. package/dist/cjs/CurrentRoute.js.map +0 -1
  50. package/dist/cjs/MatchInput.js +0 -96
  51. package/dist/cjs/MatchInput.js.map +0 -1
  52. package/dist/cjs/Matcher.js +0 -138
  53. package/dist/cjs/Matcher.js.map +0 -1
  54. package/dist/cjs/RouteGuard.js +0 -78
  55. package/dist/cjs/RouteGuard.js.map +0 -1
  56. package/dist/cjs/RouteMatch.js +0 -49
  57. package/dist/cjs/RouteMatch.js.map +0 -1
  58. package/dist/cjs/index.js +0 -53
  59. package/dist/cjs/index.js.map +0 -1
  60. package/dist/dts/CurrentRoute.d.ts +0 -94
  61. package/dist/dts/CurrentRoute.d.ts.map +0 -1
  62. package/dist/dts/MatchInput.d.ts +0 -143
  63. package/dist/dts/MatchInput.d.ts.map +0 -1
  64. package/dist/dts/Matcher.d.ts +0 -121
  65. package/dist/dts/Matcher.d.ts.map +0 -1
  66. package/dist/dts/RouteGuard.d.ts +0 -94
  67. package/dist/dts/RouteGuard.d.ts.map +0 -1
  68. package/dist/dts/RouteMatch.d.ts +0 -50
  69. package/dist/dts/RouteMatch.d.ts.map +0 -1
  70. package/dist/dts/index.d.ts +0 -24
  71. package/dist/dts/index.d.ts.map +0 -1
  72. package/dist/esm/CurrentRoute.js +0 -152
  73. package/dist/esm/CurrentRoute.js.map +0 -1
  74. package/dist/esm/MatchInput.js +0 -79
  75. package/dist/esm/MatchInput.js.map +0 -1
  76. package/dist/esm/Matcher.js +0 -130
  77. package/dist/esm/Matcher.js.map +0 -1
  78. package/dist/esm/RouteGuard.js +0 -57
  79. package/dist/esm/RouteGuard.js.map +0 -1
  80. package/dist/esm/RouteMatch.js +0 -29
  81. package/dist/esm/RouteMatch.js.map +0 -1
  82. package/dist/esm/index.js +0 -24
  83. package/dist/esm/index.js.map +0 -1
  84. package/dist/esm/package.json +0 -4
  85. package/src/MatchInput.ts +0 -303
  86. package/src/RouteGuard.ts +0 -217
  87. package/src/RouteMatch.ts +0 -104
package/src/Matcher.ts CHANGED
@@ -1,385 +1,1435 @@
1
- /**
2
- * @since 1.0.0
3
- */
1
+ import * as findMyWay from "find-my-way-ts";
2
+ import type * as Arr from "effect/Array";
3
+ import * as Cause from "effect/Cause";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Exit from "effect/Exit";
6
+ import { interrupt, isSuccess } from "effect/Exit";
7
+ import { dual, identity } from "effect/Function";
8
+ import * as Result from "effect/Result";
9
+ import * as Layer from "effect/Layer";
10
+ import * as Option from "effect/Option";
11
+ import { type Pipeable, pipeArguments } from "effect/Pipeable";
12
+ import * as Schema from "effect/Schema";
13
+ import { makeFormatterDefault } from "effect/SchemaIssue";
14
+ import * as Scope from "effect/Scope";
15
+ import * as ServiceMap from "effect/ServiceMap";
16
+ import * as Stream from "effect/Stream";
17
+ import type { ExcludeTag, ExtractTag, NoInfer, Tags } from "effect/Types";
18
+ import { exit } from "@typed/fx/Fx";
19
+ import { mapEffect } from "@typed/fx/Fx/combinators/mapEffect";
20
+ import { provideServices } from "@typed/fx/Fx/combinators/provide";
21
+ import { skipRepeats } from "@typed/fx/Fx/combinators/skipRepeats";
22
+ import { switchMap } from "@typed/fx/Fx/combinators/switchMap";
23
+ import { unwrap } from "@typed/fx/Fx/combinators/unwrap";
24
+ import { fromEffect, never } from "@typed/fx/Fx/constructors/fromEffect";
25
+ import { succeed } from "@typed/fx/Fx/constructors/succeed";
26
+ import type * as Fx from "@typed/fx/Fx/Fx";
27
+ import { fromStream } from "@typed/fx/Fx/stream";
28
+ import { isFx } from "@typed/fx/Fx/TypeId";
29
+ import { RefSubject } from "@typed/fx/RefSubject";
30
+ import { CurrentPath, Navigation } from "@typed/navigation/Navigation";
31
+ import type { MatchAst, RouteAst } from "./AST.js";
32
+ import * as AST from "./AST.js";
33
+ import { CurrentRoute } from "./CurrentRoute.js";
34
+ import { Join, make as makeRoute, type Route } from "./Route.js";
35
+ import type { Router } from "./Router.js";
4
36
 
5
- import * as Fx from "@typed/fx/Fx"
6
- import * as Match from "@typed/fx/Match"
7
- import type * as RefSubject from "@typed/fx/RefSubject"
8
- import type { Navigation } from "@typed/navigation"
9
- import { CurrentPath, isRedirectError, navigate, RedirectError } from "@typed/navigation"
10
- import type * as Route from "@typed/route"
11
- import * as Data from "effect/Data"
12
- import * as Effect from "effect/Effect"
13
- import { dual, pipe } from "effect/Function"
14
- import * as Option from "effect/Option"
15
- import { type Pipeable, pipeArguments } from "effect/Pipeable"
16
- import { hasProperty } from "effect/Predicate"
17
- import type * as Scope from "effect/Scope"
18
- import * as Unify from "effect/Unify"
19
- import type { CurrentRoute } from "./CurrentRoute.js"
20
- import { makeHref } from "./CurrentRoute.js"
21
- import type { MatchInput } from "./MatchInput.js"
22
- import * as RouteMatch from "./RouteMatch.js"
37
+ export type Layout<Params, A, E, R, B, E2, R2> = (
38
+ params: LayoutParams<Params, A, E, R>,
39
+ ) => Fx.Fx<B, E2, R2>;
23
40
 
24
- /**
25
- * @since 1.0.0
26
- */
27
- export const RouteMatcherTypeId = Symbol.for("@typed/router/RouteMatcher")
41
+ export type LayoutParams<Params, A, E, R> = {
42
+ readonly params: RefSubject.RefSubject<Params>;
43
+ readonly content: Fx.Fx<A, E, R>;
44
+ };
28
45
 
29
- /**
30
- * @since 1.0.0
31
- */
32
- export type RouteMatcherTypeId = typeof RouteMatcherTypeId
46
+ export type CatchHandler<E, A, E2, R2> = (
47
+ cause: RefSubject.RefSubject<Cause.Cause<E>>,
48
+ ) => Fx.Fx<A, E2, R2>;
33
49
 
34
- /**
35
- * @since 1.0.0
36
- */
37
- export interface RouteMatcher<Matches extends RouteMatch.RouteMatch.Any> extends Pipeable {
38
- readonly [RouteMatcherTypeId]: RouteMatcherTypeId
50
+ export type AnyLayer =
51
+ | Layer.Layer<any, any, any>
52
+ | Layer.Layer<never, any, any>
53
+ | Layer.Layer<any, never, any>
54
+ | Layer.Layer<any, any, never>
55
+ | Layer.Layer<never, never, never>
56
+ | Layer.Layer<any, never, never>
57
+ | Layer.Layer<never, any, never>
58
+ | Layer.Layer<never, never, any>;
39
59
 
40
- readonly matches: ReadonlyArray<Matches>
60
+ type AnyServiceMap = ServiceMap.ServiceMap<any> | ServiceMap.ServiceMap<never>;
61
+ type AnyDependency = AnyLayer | AnyServiceMap;
62
+ type AnyLayout = Layout<any, any, any, any, any, any, any>;
63
+ type AnyCatch = CatchHandler<any, any, any, any>;
64
+ type AnyGuard = GuardType<any, any, any, any>;
65
+ type AnyMatchHandler = (params: RefSubject.RefSubject<any>) => Fx.Fx<any, any, any>;
41
66
 
42
- readonly add: <I extends RouteMatch.RouteMatch.Any>(match: I) => RouteMatcher<Matches | I>
67
+ type DependencyProvided<D> =
68
+ D extends Layer.Layer<infer Provided, any, any>
69
+ ? Provided
70
+ : D extends ServiceMap.ServiceMap<infer Provided>
71
+ ? Provided
72
+ : 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;
43
75
 
44
- readonly match: <I extends MatchInput.Any, A, E, R>(
45
- input: I,
46
- match: (ref: RefSubject.RefSubject<MatchInput.Success<I>>) => Fx.Fx<A, E, R>
47
- ) => RouteMatcher<
48
- | Matches
49
- | RouteMatch.RouteMatch<
50
- MatchInput.Route<I>,
51
- MatchInput.Success<I>,
52
- MatchInput.Error<I>,
53
- MatchInput.Context<I>,
54
- A,
55
- E,
56
- R
57
- >
58
- >
76
+ type LayerSuccess<L> = L extends Layer.Layer<infer Provided, any, any> ? Provided : never;
77
+ type LayerError<L> = L extends Layer.Layer<any, infer E, any> ? E : never;
78
+ type LayerServices<L> = L extends Layer.Layer<any, any, infer R> ? R : never;
59
79
 
60
- readonly switch: <I extends MatchInput.Any, A, E, R>(
61
- input: I,
62
- match: (ref: MatchInput.Success<I>) => Fx.Fx<A, E, R>
63
- ) => RouteMatcher<
64
- | Matches
65
- | RouteMatch.RouteMatch<
66
- MatchInput.Route<I>,
67
- MatchInput.Success<I>,
68
- MatchInput.Error<I>,
69
- MatchInput.Context<I>,
70
- A,
71
- E,
72
- R | Scope.Scope
73
- >
74
- >
80
+ export type GuardType<I, O, E = never, R = never> = (
81
+ input: I,
82
+ ) => Effect.Effect<Option.Option<O>, E, R>;
83
+ export interface AsGuard<I, O, E = never, R = never> {
84
+ readonly asGuard: () => GuardType<I, O, E, R>;
85
+ }
86
+ export type GuardInput<I, O, E = never, R = never> = GuardType<I, O, E, R> | AsGuard<I, O, E, R>;
75
87
 
76
- readonly effect: <I extends MatchInput.Any, A, E, R>(
77
- input: I,
78
- match: (ref: MatchInput.Success<I>) => Effect.Effect<A, E, R>
79
- ) => RouteMatcher<
80
- | Matches
81
- | RouteMatch.RouteMatch<
82
- MatchInput.Route<I>,
83
- MatchInput.Success<I>,
84
- MatchInput.Error<I>,
85
- MatchInput.Context<I>,
86
- A,
87
- E,
88
- R | Scope.Scope
89
- >
90
- >
88
+ type GuardOutput<G> =
89
+ G extends GuardType<any, infer O, any, any>
90
+ ? O
91
+ : G extends AsGuard<any, infer O, any, any>
92
+ ? O
93
+ : never;
94
+ type GuardError<G> =
95
+ G extends GuardType<any, any, infer E, any>
96
+ ? E
97
+ : G extends AsGuard<any, any, infer E, any>
98
+ ? E
99
+ : never;
100
+ type GuardServices<G> =
101
+ G extends GuardType<any, any, any, infer R>
102
+ ? R
103
+ : G extends AsGuard<any, any, any, infer R>
104
+ ? R
105
+ : never;
91
106
 
92
- readonly to: <I extends MatchInput.Any, B>(
93
- input: I,
94
- f: (value: MatchInput.Success<I>) => B
95
- ) => RouteMatcher<
96
- | Matches
97
- | RouteMatch.RouteMatch<
98
- MatchInput.Route<I>,
99
- MatchInput.Success<I>,
100
- MatchInput.Error<I>,
101
- MatchInput.Context<I>,
102
- B,
103
- never,
104
- Scope.Scope
105
- >
106
- >
107
+ type MatchOptions<Rt extends Route.Any, B, E2, R2, D, LB, LE2, LR2, C> = {
108
+ readonly route: Rt;
109
+ readonly handler:
110
+ | MatchHandlerReturnValue<B, E2, R2>
111
+ | ((params: RefSubject.RefSubject<Route.Type<Rt>>) => MatchHandlerReturnValue<B, E2, R2>);
112
+ readonly dependencies?: D;
113
+ readonly layout?: Layout<Route.Type<Rt>, B, E2, R2, LB, LE2, LR2>;
114
+ readonly catch?: C;
115
+ };
116
+
117
+ type MatchHandlerReturnValue<A, E, R> =
118
+ | Fx.Fx<A, E, R>
119
+ | Stream.Stream<A, E, R>
120
+ | Effect.Effect<A, E, R>
121
+ | A;
122
+
123
+ type MatchHandlerOptions<Params, B, E2, R2, D, LB, LE2, LR2, C> = {
124
+ readonly handler:
125
+ | MatchHandlerReturnValue<B, E2, R2>
126
+ | ((params: RefSubject.RefSubject<Params>) => MatchHandlerReturnValue<B, E2, R2>);
127
+ readonly dependencies?: D;
128
+ readonly layout?: Layout<Params, B, E2, R2, LB, LE2, LR2>;
129
+ readonly catch?: C;
130
+ };
131
+
132
+ type ApplyDependencies<E, R, D> =
133
+ D extends ReadonlyArray<infer Dep>
134
+ ? {
135
+ readonly e: E | DependencyError<Dep>;
136
+ readonly r: Exclude<R, DependencyProvided<Dep>> | DependencyRequirements<Dep>;
137
+ }
138
+ : { readonly e: E; readonly r: R };
139
+
140
+ type ApplyCatch<A, E, R, C> =
141
+ C extends CatchHandler<any, infer CA, infer CE, infer CR>
142
+ ? { readonly a: A | CA; readonly e: CE; readonly r: R | CR }
143
+ : { readonly a: A; readonly e: E; readonly r: R };
144
+
145
+ type ComputeMatchResult<E2, R2, D, LB, LE2, LR2, C, GE, GR> = ApplyCatch<
146
+ LB,
147
+ ApplyDependencies<E2 | GE | LE2, R2 | GR | LR2, D>["e"],
148
+ ApplyDependencies<E2 | GE | LE2, R2 | GR | LR2, D>["r"],
149
+ C
150
+ >;
151
+
152
+ export interface Matcher<A, E = never, R = never> extends Pipeable {
153
+ readonly cases: ReadonlyArray<MatchAst>;
154
+
155
+ // Overload 1: match(route, handler) - function handler (must be first for inference)
156
+ match<Rt extends Route.Any, B, E2 = never, R2 = never>(
157
+ route: Rt,
158
+ handler: (params: RefSubject.RefSubject<Route.Type<Rt>>) => MatchHandlerReturnValue<B, E2, R2>,
159
+ ): Matcher<A | B, E | E2, R | R2 | Scope.Scope>;
160
+
161
+ // Overload 2: match(route, effectLike) - Fx/Effect/Stream handler
162
+ match<Rt extends Route.Any, B, E2 = never, R2 = never>(
163
+ route: Rt,
164
+ handler: Fx.Fx<B, E2, R2> | Effect.Effect<B, E2, R2> | Stream.Stream<B, E2, R2>,
165
+ ): Matcher<A | B, E | E2, R | R2 | Scope.Scope>;
166
+
167
+ // Overload 3: match(route, options) - route with options object
168
+ match<
169
+ Rt extends Route.Any,
170
+ B,
171
+ E2 = never,
172
+ R2 = never,
173
+ D extends ReadonlyArray<AnyDependency> | undefined = undefined,
174
+ LB = B,
175
+ LE2 = never,
176
+ LR2 = never,
177
+ C extends CatchHandler<any, any, any, any> | undefined = undefined,
178
+ >(
179
+ route: Rt,
180
+ options: MatchHandlerOptions<Route.Type<Rt>, B, E2, R2, D, LB, LE2, LR2, C>,
181
+ ): Matcher<
182
+ A | ComputeMatchResult<E2, R2, D, LB, LE2, LR2, C, never, never>["a"],
183
+ E | ComputeMatchResult<E2, R2, D, LB, LE2, LR2, C, never, never>["e"],
184
+ R | ComputeMatchResult<E2, R2, D, LB, LE2, LR2, C, never, never>["r"] | Scope.Scope
185
+ >;
186
+
187
+ // Overload 4: match(route, value) - direct value handler (last for 2-arg form)
188
+ match<Rt extends Route.Any, const B>(route: Rt, handler: B): Matcher<A | B, E, R | Scope.Scope>;
189
+
190
+ // Overload 5: match(route, guard, handler) - guard with function handler (must be before value)
191
+ match<
192
+ Rt extends Route.Any,
193
+ G extends GuardInput<Route.Type<Rt>, any, any, any>,
194
+ B,
195
+ E2 = never,
196
+ R2 = never,
197
+ >(
198
+ route: Rt,
199
+ guard: G,
200
+ handler: (params: RefSubject.RefSubject<GuardOutput<G>>) => MatchHandlerReturnValue<B, E2, R2>,
201
+ ): Matcher<A | B, E | E2 | GuardError<G>, R | R2 | GuardServices<G> | Scope.Scope>;
202
+
203
+ // Overload 6: match(route, guard, effectLike) - guard with Fx/Effect/Stream handler
204
+ match<
205
+ Rt extends Route.Any,
206
+ G extends GuardInput<Route.Type<Rt>, any, any, any>,
207
+ B,
208
+ E2 = never,
209
+ R2 = never,
210
+ >(
211
+ route: Rt,
212
+ guard: G,
213
+ handler: Fx.Fx<B, E2, R2> | Effect.Effect<B, E2, R2> | Stream.Stream<B, E2, R2>,
214
+ ): Matcher<A | B, E | E2 | GuardError<G>, R | R2 | GuardServices<G> | Scope.Scope>;
215
+
216
+ // Overload 7: match(route, guard, options) - route with guard and options object
217
+ match<
218
+ Rt extends Route.Any,
219
+ G extends GuardInput<Route.Type<Rt>, any, any, any>,
220
+ B,
221
+ E2 = never,
222
+ R2 = never,
223
+ D extends ReadonlyArray<AnyDependency> | undefined = undefined,
224
+ LB = B,
225
+ LE2 = never,
226
+ LR2 = never,
227
+ C extends CatchHandler<any, any, any, any> | undefined = undefined,
228
+ >(
229
+ route: Rt,
230
+ guard: G,
231
+ options: MatchHandlerOptions<GuardOutput<G>, B, E2, R2, D, LB, LE2, LR2, C>,
232
+ ): Matcher<
233
+ A | ComputeMatchResult<E2, R2, D, LB, LE2, LR2, C, GuardError<G>, GuardServices<G>>["a"],
234
+ E | ComputeMatchResult<E2, R2, D, LB, LE2, LR2, C, GuardError<G>, GuardServices<G>>["e"],
235
+ | R
236
+ | ComputeMatchResult<E2, R2, D, LB, LE2, LR2, C, GuardError<G>, GuardServices<G>>["r"]
237
+ | Scope.Scope
238
+ >;
239
+
240
+ // Overload 8: match(route, guard, value) - guard with value handler (last for 3-arg form)
241
+ match<Rt extends Route.Any, G extends GuardInput<Route.Type<Rt>, any, any, any>, B>(
242
+ route: Rt,
243
+ guard: G,
244
+ handler: B,
245
+ ): Matcher<A | B, E | GuardError<G>, R | GuardServices<G> | Scope.Scope>;
246
+
247
+ // Overload 9: match(fullOptions) - full options object including route
248
+ match<
249
+ Rt extends Route.Any,
250
+ B,
251
+ E2 = never,
252
+ R2 = never,
253
+ D extends ReadonlyArray<AnyDependency> | undefined = undefined,
254
+ LB = B,
255
+ LE2 = never,
256
+ LR2 = never,
257
+ C extends CatchHandler<any, any, any, any> | undefined = undefined,
258
+ >(
259
+ options: MatchOptions<Rt, B, E2, R2, D, LB, LE2, LR2, C>,
260
+ ): Matcher<
261
+ A | ComputeMatchResult<E2, R2, D, LB, LE2, LR2, C, never, never>["a"],
262
+ E | ComputeMatchResult<E2, R2, D, LB, LE2, LR2, C, never, never>["e"],
263
+ R | ComputeMatchResult<E2, R2, D, LB, LE2, LR2, C, never, never>["r"] | Scope.Scope
264
+ >;
265
+
266
+ readonly prefix: <Rt extends Route.Any>(route: Rt) => Matcher<A, E, R>;
267
+
268
+ readonly provide: <Layers extends readonly [AnyLayer, ...AnyLayer[]]>(
269
+ ...layers: Layers
270
+ ) => Matcher<
271
+ A,
272
+ E | LayerError<Layers[number]>,
273
+ Exclude<R, LayerSuccess<Layers[number]>> | LayerServices<Layers[number]>
274
+ >;
275
+
276
+ readonly provideService: <Id, S>(
277
+ tag: ServiceMap.Service<Id, S>,
278
+ service: S,
279
+ ) => Matcher<A, E, Exclude<R, Id>>;
280
+
281
+ readonly provideServices: <R2>(
282
+ services: ServiceMap.ServiceMap<R2>,
283
+ ) => Matcher<A, E, Exclude<R, R2>>;
284
+
285
+ readonly catchCause: <B, E2, R2>(f: CatchHandler<E, B, E2, R2>) => Matcher<A | B, E2, R | R2>;
286
+
287
+ readonly catch: <B, E2, R2>(f: (e: E) => Fx.Fx<B, E2, R2>) => Matcher<A | B, E2, R | R2>;
288
+
289
+ readonly catchTag: <const K extends Tags<E> | Arr.NonEmptyReadonlyArray<Tags<E>>, B, E2, R2>(
290
+ tag: K,
291
+ f: (
292
+ e: ExtractTag<NoInfer<E>, K extends Arr.NonEmptyReadonlyArray<string> ? K[number] : K>,
293
+ ) => Fx.Fx<B, E2, R2>,
294
+ ) => Matcher<
295
+ A | B,
296
+ E2 | ExcludeTag<E, K extends Arr.NonEmptyReadonlyArray<string> ? K[number] : K>,
297
+ R | R2
298
+ >;
299
+
300
+ readonly layout: <B, E2, R2>(
301
+ layout: Layout<any, A, E, R, B, E2, R2>,
302
+ ) => Matcher<B, E | E2, R | R2>;
303
+
304
+ /** Merge this matcher with one or more others. Combined matcher matches all routes; each matcher's layouts/provide apply only to its own routes. */
305
+ readonly merge: <const Others extends ReadonlyArray<Matcher.Any>>(
306
+ ...others: Others
307
+ ) => Matcher<
308
+ A | Matcher.MergeSuccess<Others>,
309
+ E | Matcher.MergeError<Others>,
310
+ R | Matcher.MergeServices<Others>
311
+ >;
107
312
  }
108
313
 
109
- /**
110
- * @since 1.0.0
111
- */
112
- export namespace RouteMatcher {
113
- /**
114
- * @since 1.0.0
115
- */
116
- export type Any = RouteMatcher<RouteMatch.RouteMatch.Any>
117
-
118
- /**
119
- * @since 1.0.0
120
- */
121
- export type Context<T> = T extends RouteMatcher<infer Matches> ? RouteMatch.RouteMatch.Context<Matches> : never
314
+ export declare namespace Matcher {
315
+ export type Any =
316
+ | Matcher<any, any, any>
317
+ | Matcher<any, never, any>
318
+ | Matcher<any, any, never>
319
+ | Matcher<any, never, never>;
320
+ export type Success<T> = [T] extends [Matcher<infer A, infer _E, infer _R>] ? A : never;
321
+ export type Error<T> = [T] extends [Matcher<infer _A, infer E, infer _R>] ? E : never;
322
+ export type Services<T> = [T] extends [Matcher<infer _A, infer _E, infer R>] ? R : never;
323
+
324
+ /** Union of Success types from each matcher in a tuple. */
325
+ export type MergeSuccess<Matchers extends ReadonlyArray<Matcher.Any>> = Success<Matchers[number]>;
326
+ /** Union of Error types from each matcher in a tuple. */
327
+ export type MergeError<Matchers extends ReadonlyArray<Matcher.Any>> = Error<Matchers[number]>;
328
+ /** Union of Services types from each matcher in a tuple. */
329
+ export type MergeServices<Matchers extends ReadonlyArray<Matcher.Any>> = Services<
330
+ Matchers[number]
331
+ >;
122
332
  }
123
333
 
124
- class RouteMatcherImpl<Matches extends RouteMatch.RouteMatch.Any> implements RouteMatcher<Matches> {
125
- readonly [RouteMatcherTypeId]: RouteMatcherTypeId = RouteMatcherTypeId
334
+ export type MatchHandler<Params, A, E, R> =
335
+ | Fx.Fx<A, E, R>
336
+ | Stream.Stream<A, E, R>
337
+ | Effect.Effect<A, E, R>
338
+ | A
339
+ | ((
340
+ params: RefSubject.RefSubject<Params>,
341
+ ) => Fx.Fx<A, E, R> | Stream.Stream<A, E, R> | Effect.Effect<A, E, R> | A);
126
342
 
127
- constructor(readonly matches: ReadonlyArray<Matches>) {
128
- this.add = this.add.bind(this)
129
- this.match = this.match.bind(this)
130
- this.switch = this.switch.bind(this)
131
- this.effect = this.effect.bind(this)
132
- this.to = this.to.bind(this)
343
+ type MatchHandlerFn<Params, A, E, R> = (
344
+ params: RefSubject.RefSubject<Params>,
345
+ ) => Fx.Fx<A, E, R> | Stream.Stream<A, E, R> | Effect.Effect<A, E, R> | A;
346
+
347
+ function isMatchHandlerFn<Params, A, E, R>(
348
+ handler: MatchHandler<Params, A, E, R>,
349
+ ): handler is MatchHandlerFn<Params, A, E, R> {
350
+ return typeof handler === "function";
351
+ }
352
+
353
+ function isHandlerOptions(value: unknown): value is { readonly handler: unknown } {
354
+ return typeof value === "object" && value !== null && "handler" in value;
355
+ }
356
+
357
+ // Monomorphic shape - all properties always present for V8 optimization
358
+ type ParsedMatch = {
359
+ readonly route: Route.Any;
360
+ readonly handler: unknown;
361
+ readonly guard: AnyGuard | undefined;
362
+ readonly layout: AnyLayout | undefined;
363
+ readonly catchFn: AnyCatch | undefined;
364
+ readonly dependencies: ReadonlyArray<AnyDependency> | undefined;
365
+ };
366
+
367
+ function parseMatchArgs(args: [unknown, ...Array<unknown>]): ParsedMatch {
368
+ const [first, second, third] = args;
369
+
370
+ // Single arg: full options object (Overload 9)
371
+ if (second === undefined) {
372
+ const opts = first as MatchOptions<Route.Any, any, any, any, any, any, any, any, any>;
373
+ return {
374
+ route: opts.route,
375
+ handler: opts.handler,
376
+ guard: undefined,
377
+ layout: opts.layout as AnyLayout | undefined,
378
+ catchFn: opts.catch as AnyCatch | undefined,
379
+ dependencies: opts.dependencies as ReadonlyArray<AnyDependency> | undefined,
380
+ };
133
381
  }
134
382
 
135
- add<I extends RouteMatch.RouteMatch.Any>(match: I): RouteMatcher<Matches | I> {
136
- return new RouteMatcherImpl([...this.matches, match])
383
+ // Two args
384
+ if (third === undefined) {
385
+ if (isHandlerOptions(second)) {
386
+ // Overload 3: match(route, options)
387
+ const opts = second as MatchHandlerOptions<any, any, any, any, any, any, any, any, any>;
388
+ return {
389
+ route: first as Route.Any,
390
+ handler: opts.handler,
391
+ guard: undefined,
392
+ layout: opts.layout as AnyLayout | undefined,
393
+ catchFn: opts.catch as AnyCatch | undefined,
394
+ dependencies: opts.dependencies as ReadonlyArray<AnyDependency> | undefined,
395
+ };
396
+ }
397
+ // Overloads 1, 2, 4: match(route, handler)
398
+ return {
399
+ route: first as Route.Any,
400
+ handler: second,
401
+ guard: undefined,
402
+ layout: undefined,
403
+ catchFn: undefined,
404
+ dependencies: undefined,
405
+ };
137
406
  }
138
407
 
139
- match<I extends MatchInput.Any, A, E, R>(
140
- input: I,
141
- match: (ref: RefSubject.RefSubject<MatchInput.Success<I>>) => Fx.Fx<A, E, R>
142
- ) {
143
- return this.add(RouteMatch.fromInput(input, match))
408
+ // Three args
409
+ if (isHandlerOptions(third)) {
410
+ // Overload 7: match(route, guard, options)
411
+ const opts = third as MatchHandlerOptions<any, any, any, any, any, any, any, any, any>;
412
+ return {
413
+ route: first as Route.Any,
414
+ handler: opts.handler,
415
+ guard: second as AnyGuard,
416
+ layout: opts.layout as AnyLayout | undefined,
417
+ catchFn: opts.catch as AnyCatch | undefined,
418
+ dependencies: opts.dependencies as ReadonlyArray<AnyDependency> | undefined,
419
+ };
144
420
  }
145
421
 
146
- switch<I extends MatchInput.Any, A, E, R>(
147
- input: I,
148
- match: (ref: MatchInput.Success<I>) => Fx.Fx<A, E, R>
149
- ) {
150
- return this.match(input, Fx.switchMap(match))
422
+ // Overloads 5, 6, 8: match(route, guard, handler)
423
+ return {
424
+ route: first as Route.Any,
425
+ handler: third,
426
+ guard: second as AnyGuard,
427
+ layout: undefined,
428
+ catchFn: undefined,
429
+ dependencies: undefined,
430
+ };
431
+ }
432
+
433
+ class MatcherImpl<A, E, R> implements Matcher<A, E, R> {
434
+ readonly cases: ReadonlyArray<MatchAst>;
435
+ constructor(cases: ReadonlyArray<MatchAst>) {
436
+ this.cases = cases;
437
+ this.match = this.match.bind(this);
438
+ this.catch = this.catch.bind(this);
439
+ this.catchTag = this.catchTag.bind(this);
440
+ this.layout = this.layout.bind(this);
441
+ this.provide = this.provide.bind(this);
442
+ this.provideService = this.provideService.bind(this);
151
443
  }
152
444
 
153
- effect<I extends MatchInput.Any, A, E, R>(
154
- input: I,
155
- match: (ref: MatchInput.Success<I>) => Effect.Effect<A, E, R>
156
- ) {
157
- return this.match(input, Fx.switchMapEffect(match))
445
+ // Implementation overloads for type inference - use simplified return types
446
+ match<Rt extends Route.Any, B, E2 = never, R2 = never>(
447
+ route: Rt,
448
+ handler: (params: RefSubject.RefSubject<Route.Type<Rt>>) => MatchHandlerReturnValue<B, E2, R2>,
449
+ ): Matcher<A | B, E | E2, R | R2 | Scope.Scope>;
450
+ match<Rt extends Route.Any, B, E2 = never, R2 = never>(
451
+ route: Rt,
452
+ handler: Fx.Fx<B, E2, R2> | Effect.Effect<B, E2, R2> | Stream.Stream<B, E2, R2>,
453
+ ): Matcher<A | B, E | E2, R | R2 | Scope.Scope>;
454
+ match<
455
+ Rt extends Route.Any,
456
+ B,
457
+ E2 = never,
458
+ R2 = never,
459
+ D extends ReadonlyArray<AnyDependency> | undefined = undefined,
460
+ LB = B,
461
+ LE2 = never,
462
+ LR2 = never,
463
+ C extends CatchHandler<any, any, any, any> | undefined = undefined,
464
+ >(
465
+ route: Rt,
466
+ options: MatchHandlerOptions<Route.Type<Rt>, B, E2, R2, D, LB, LE2, LR2, C>,
467
+ ): Matcher<
468
+ A | ComputeMatchResult<E2, R2, D, LB, LE2, LR2, C, never, never>["a"],
469
+ E | ComputeMatchResult<E2, R2, D, LB, LE2, LR2, C, never, never>["e"],
470
+ R | ComputeMatchResult<E2, R2, D, LB, LE2, LR2, C, never, never>["r"] | Scope.Scope
471
+ >;
472
+ match<Rt extends Route.Any, B>(route: Rt, handler: B): Matcher<A | B, E, R | Scope.Scope>;
473
+ match<Rt extends Route.Any, G extends GuardInput<Route.Type<Rt>, any, any, any>, B, E2, R2>(
474
+ route: Rt,
475
+ guard: G,
476
+ handler: (params: RefSubject.RefSubject<GuardOutput<G>>) => MatchHandlerReturnValue<B, E2, R2>,
477
+ ): Matcher<A | B, E | E2 | GuardError<G>, R | R2 | GuardServices<G> | Scope.Scope>;
478
+ match<Rt extends Route.Any, G extends GuardInput<Route.Type<Rt>, any, any, any>, B, E2, R2>(
479
+ route: Rt,
480
+ guard: G,
481
+ handler: Fx.Fx<B, E2, R2> | Effect.Effect<B, E2, R2> | Stream.Stream<B, E2, R2>,
482
+ ): Matcher<A | B, E | E2 | GuardError<G>, R | R2 | GuardServices<G> | Scope.Scope>;
483
+ match<
484
+ Rt extends Route.Any,
485
+ G extends GuardInput<Route.Type<Rt>, any, any, any>,
486
+ B,
487
+ E2 = never,
488
+ R2 = never,
489
+ D extends ReadonlyArray<AnyDependency> | undefined = undefined,
490
+ LB = B,
491
+ LE2 = never,
492
+ LR2 = never,
493
+ C extends CatchHandler<any, any, any, any> | undefined = undefined,
494
+ >(
495
+ route: Rt,
496
+ guard: G,
497
+ options: MatchHandlerOptions<GuardOutput<G>, B, E2, R2, D, LB, LE2, LR2, C>,
498
+ ): Matcher<
499
+ A | ComputeMatchResult<E2, R2, D, LB, LE2, LR2, C, GuardError<G>, GuardServices<G>>["a"],
500
+ E | ComputeMatchResult<E2, R2, D, LB, LE2, LR2, C, GuardError<G>, GuardServices<G>>["e"],
501
+ | R
502
+ | ComputeMatchResult<E2, R2, D, LB, LE2, LR2, C, GuardError<G>, GuardServices<G>>["r"]
503
+ | Scope.Scope
504
+ >;
505
+ match<Rt extends Route.Any, G extends GuardInput<Route.Type<Rt>, any, any, any>, B>(
506
+ route: Rt,
507
+ guard: G,
508
+ handler: B,
509
+ ): Matcher<A | B, E | GuardError<G>, R | GuardServices<G> | Scope.Scope>;
510
+ match<
511
+ Rt extends Route.Any,
512
+ B,
513
+ E2 = never,
514
+ R2 = never,
515
+ D extends ReadonlyArray<AnyDependency> | undefined = undefined,
516
+ LB = B,
517
+ LE2 = never,
518
+ LR2 = never,
519
+ C extends CatchHandler<any, any, any, any> | undefined = undefined,
520
+ >(
521
+ options: MatchOptions<Rt, B, E2, R2, D, LB, LE2, LR2, C>,
522
+ ): Matcher<
523
+ A | ComputeMatchResult<E2, R2, D, LB, LE2, LR2, C, never, never>["a"],
524
+ E | ComputeMatchResult<E2, R2, D, LB, LE2, LR2, C, never, never>["e"],
525
+ R | ComputeMatchResult<E2, R2, D, LB, LE2, LR2, C, never, never>["r"] | Scope.Scope
526
+ >;
527
+ match(...args: [unknown, ...Array<unknown>]): Matcher<any, any, any> {
528
+ const parsed = parseMatchArgs(args);
529
+ const normalizedGuard =
530
+ parsed.guard !== undefined
531
+ ? getGuard(parsed.guard as GuardInput<any, any, any, any>)
532
+ : defaultGuard();
533
+
534
+ const routeAst = AST.route(
535
+ parsed.route.ast,
536
+ parsed.handler as MatchHandler<any, any, any, any>,
537
+ normalizedGuard,
538
+ );
539
+
540
+ let matches: ReadonlyArray<MatchAst> = [routeAst];
541
+ if (parsed.layout !== undefined) {
542
+ matches = [AST.layout(matches, parsed.layout)];
543
+ }
544
+ if (parsed.catchFn !== undefined) {
545
+ matches = [AST.catchCause(matches, parsed.catchFn)];
546
+ }
547
+ if (parsed.dependencies !== undefined && parsed.dependencies.length > 0) {
548
+ matches = [AST.layer(matches, normalizeDependencies(parsed.dependencies))];
549
+ }
550
+
551
+ return new MatcherImpl([...this.cases, ...matches]);
158
552
  }
159
553
 
160
- to<I extends MatchInput.Any, B>(
161
- input: I,
162
- f: (value: MatchInput.Success<I>) => B
163
- ) {
164
- return this.match(input, Fx.map(f))
554
+ prefix<Rt extends Route.Any>(route: Rt): Matcher<A, E, R> {
555
+ return new MatcherImpl<A, E, R>([AST.prefixed(this.cases, route.ast)]);
556
+ }
557
+
558
+ provide<Layers extends readonly [AnyLayer, ...AnyLayer[]]>(
559
+ ...layers: Layers
560
+ ): Matcher<
561
+ A,
562
+ E | LayerError<Layers[number]>,
563
+ Exclude<R, LayerSuccess<Layers[number]>> | LayerServices<Layers[number]>
564
+ > {
565
+ return new MatcherImpl([AST.layer(this.cases, layers)]) as Matcher<
566
+ A,
567
+ E | LayerError<Layers[number]>,
568
+ Exclude<R, LayerSuccess<Layers[number]>> | LayerServices<Layers[number]>
569
+ >;
570
+ }
571
+
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));
574
+ }
575
+
576
+ provideServices<R2>(services: ServiceMap.ServiceMap<R2>): Matcher<A, E, Exclude<R, R2>> {
577
+ return this.provide(Layer.succeedServices(services));
578
+ }
579
+
580
+ catchCause<B, E2, R2>(f: CatchHandler<E, B, E2, R2>): Matcher<A | B, E2, R | R2> {
581
+ return new MatcherImpl<A | B, E2, R | R2>([AST.catchCause(this.cases, f as AnyCatch)]);
582
+ }
583
+
584
+ catch<B, E2, R2>(f: (e: E) => Fx.Fx<B, E2, R2>): Matcher<A | B, E2, R | R2> {
585
+ return this.catchCause((causeRef) =>
586
+ unwrap(
587
+ Effect.gen(function* () {
588
+ const cause = yield* causeRef;
589
+ const result = Cause.findFail(cause);
590
+ if (Result.isFailure(result)) {
591
+ return fromEffect(Effect.failCause(result.failure));
592
+ }
593
+ return f(result.success.error);
594
+ }),
595
+ ),
596
+ );
597
+ }
598
+
599
+ catchTag<const K extends Tags<E> | Arr.NonEmptyReadonlyArray<Tags<E>>, B, E2, R2>(
600
+ tag: K,
601
+ f: (
602
+ e: ExtractTag<NoInfer<E>, K extends Arr.NonEmptyReadonlyArray<string> ? K[number] : K>,
603
+ ) => Fx.Fx<B, E2, R2>,
604
+ ): Matcher<
605
+ A | B,
606
+ E2 | ExcludeTag<E, K extends Arr.NonEmptyReadonlyArray<string> ? K[number] : K>,
607
+ R | R2
608
+ > {
609
+ const rethrow = (cause: Cause.Cause<E>) =>
610
+ fromEffect(Effect.failCause(cause)) as Fx.Fx<
611
+ B,
612
+ E2 | ExcludeTag<E, K extends Arr.NonEmptyReadonlyArray<string> ? K[number] : K>,
613
+ R2
614
+ >;
615
+
616
+ return new MatcherImpl<
617
+ A | B,
618
+ E2 | ExcludeTag<E, K extends Arr.NonEmptyReadonlyArray<string> ? K[number] : K>,
619
+ R | R2
620
+ >([
621
+ AST.catchCause(this.cases, (causeRef) =>
622
+ unwrap(
623
+ Effect.gen(function* () {
624
+ const cause = yield* causeRef;
625
+ const result = Cause.findFail(cause);
626
+ if (Result.isFailure(result)) {
627
+ return rethrow(cause);
628
+ }
629
+ if (matchesTag(tag, result.success.error)) {
630
+ return f(result.success.error);
631
+ }
632
+ return rethrow(cause);
633
+ }),
634
+ ),
635
+ ),
636
+ ]);
637
+ }
638
+
639
+ layout<B, E2, R2>(layout: Layout<any, A, E, R, B, E2, R2>): Matcher<B, E | E2, R | R2> {
640
+ return new MatcherImpl<B, E | E2, R | R2>([
641
+ AST.layout(this.cases, layout as AnyLayout),
642
+ ]) as Matcher<B, E | E2, R | R2>;
643
+ }
644
+
645
+ merge<const Others extends ReadonlyArray<Matcher.Any>>(
646
+ ...others: Others
647
+ ): Matcher<
648
+ A | Matcher.MergeSuccess<Others>,
649
+ E | Matcher.MergeError<Others>,
650
+ R | Matcher.MergeServices<Others>
651
+ > {
652
+ const allCases = [...this.cases, ...others.flatMap((m) => m.cases)];
653
+ return new MatcherImpl(allCases) as Matcher<
654
+ A | Matcher.MergeSuccess<Others>,
655
+ E | Matcher.MergeError<Others>,
656
+ R | Matcher.MergeServices<Others>
657
+ >;
165
658
  }
166
659
 
167
660
  pipe() {
168
- return pipeArguments(this, arguments)
661
+ return pipeArguments(this, arguments);
169
662
  }
170
663
  }
171
664
 
172
- /**
173
- * @since 1.0.0
174
- */
175
- export function make<Matches extends RouteMatch.RouteMatch.Any>(
176
- matches: ReadonlyArray<Matches>
177
- ): RouteMatcher<Matches> {
178
- return new RouteMatcherImpl(matches)
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);
179
670
  }
180
671
 
181
- /**
182
- * @since 1.0.0
183
- */
184
- export const empty: RouteMatcher<never> = make<never>([])
185
-
186
- const { effect, match, switch: switch_, to } = empty
187
-
188
- export {
189
- /**
190
- * @since 1.0.0
191
- */
192
- effect,
193
- /**
194
- * @since 1.0.0
195
- */
196
- match,
197
- /**
198
- * @since 1.0.0
199
- */
200
- switch_ as switch,
201
- /**
202
- * @since 1.0.0
203
- */
204
- to
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);
205
679
  }
206
680
 
207
- /**
208
- * @since 1.0.0
209
- */
210
- export const catchRedirectError = <A, E, R>(
211
- fx: Fx.Fx<A, E | RedirectError, R>
212
- ): Fx.Fx<A, Exclude<E, RedirectError>, R | Scope.Scope | Navigation> =>
213
- Fx.filterMapErrorEffect(
214
- fx,
215
- Unify.unify((_) =>
216
- isRedirectError(_)
217
- ? Effect.as(Effect.forkScoped(Effect.ignoreLogged(navigate(_.path, _.options))), Option.none())
218
- : Effect.succeedSome(_ as Exclude<typeof _, RedirectError>)
219
- )
220
- )
681
+ export const empty: Matcher<never> = new MatcherImpl([]);
682
+ export const match = empty.match.bind(empty);
221
683
 
222
684
  /**
223
- * @since 1.0.0
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.
224
687
  */
225
- export const notFound: {
226
- <A, E, R>(
227
- onNotFound: Fx.Fx<A, E, R>
228
- ): <Matches extends RouteMatch.RouteMatch.Any>(router: RouteMatcher<Matches>) => Fx.Fx<
229
- A | RouteMatch.RouteMatch.Success<Matches>,
230
- Exclude<E | RouteMatch.RouteMatch.Error<Matches>, RedirectError>,
231
- Scope.Scope | Navigation | R | RouteMatch.RouteMatch.Context<Matches>
232
- >
233
-
234
- <Matches extends RouteMatch.RouteMatch.Any, A, E, R>(
235
- router: RouteMatcher<Matches>,
236
- onNotFound: Fx.Fx<A, E, R>
237
- ): Fx.Fx<
238
- A | RouteMatch.RouteMatch.Success<Matches>,
239
- Exclude<E | RouteMatch.RouteMatch.Error<Matches>, RedirectError>,
240
- Scope.Scope | Navigation | R | RouteMatch.RouteMatch.Context<Matches>
241
- >
242
- } = dual(2, function notFound<Matches extends RouteMatch.RouteMatch.Any, A, E, R>(
243
- router: RouteMatcher<Matches>,
244
- onNotFound: Fx.Fx<A, E, R>
245
- ): Fx.Fx<
246
- A | RouteMatch.RouteMatch.Success<Matches>,
247
- Exclude<E | RouteMatch.RouteMatch.Error<Matches>, RedirectError>,
248
- R | Navigation | RouteMatch.RouteMatch.Context<Matches> | Scope.Scope
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>
249
694
  > {
250
- let matcher = Match.value(CurrentPath) as Match.ValueMatcher<
251
- string,
252
- RouteMatch.RouteMatch.Success<Matches>,
253
- RouteMatch.RouteMatch.Error<Matches>,
254
- RouteMatch.RouteMatch.Context<Matches> | Navigation | Scope.Scope
255
- >
256
-
257
- for (const match of router.matches) {
258
- matcher = matcher.when(match.guard, match.match)
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
+ >;
259
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
+ }) {}
260
735
 
261
- return catchRedirectError(matcher.getOrElse(() => onNotFound))
262
- })
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
+ }) {}
263
748
 
264
749
  /**
265
- * @since 1.0.0
750
+ * @internal
266
751
  */
267
- export const notFoundWith: {
268
- <A, E, R>(
269
- onNotFound: Effect.Effect<A, E, R>
270
- ): <Matches extends RouteMatch.RouteMatch.Any>(router: RouteMatcher<Matches>) => Fx.Fx<
271
- A | RouteMatch.RouteMatch.Success<Matches>,
272
- Exclude<E | RouteMatch.RouteMatch.Error<Matches>, RedirectError>,
273
- R | Navigation | RouteMatch.RouteMatch.Context<Matches> | Scope.Scope
274
- >
275
-
276
- <Matches extends RouteMatch.RouteMatch.Any, A, E, R>(
277
- router: RouteMatcher<Matches>,
278
- onNotFound: Effect.Effect<A, E, R>
279
- ): Fx.Fx<
280
- A | RouteMatch.RouteMatch.Success<Matches>,
281
- Exclude<E | RouteMatch.RouteMatch.Error<Matches>, RedirectError>,
282
- R | Navigation | RouteMatch.RouteMatch.Context<Matches> | Scope.Scope
283
- >
284
- } = dual(2, function notFoundWith<Matches extends RouteMatch.RouteMatch.Any, A, E, R>(
285
- router: RouteMatcher<Matches>,
286
- onNotFound: Effect.Effect<A, E, R>
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,
287
764
  ): Fx.Fx<
288
- A | RouteMatch.RouteMatch.Success<Matches>,
289
- Exclude<E | RouteMatch.RouteMatch.Error<Matches>, RedirectError>,
290
- R | Navigation | RouteMatch.RouteMatch.Context<Matches> | Scope.Scope
765
+ Matcher.Success<M>,
766
+ Matcher.Error<M> | RouteNotFound | RouteDecodeError | RouteGuardError,
767
+ Matcher.Services<M> | Router | CurrentRoute | Scope.Scope
291
768
  > {
292
- return notFound(router, Fx.fromEffect(onNotFound))
293
- })
769
+ return unwrap(
770
+ Effect.gen(function* () {
771
+ const fiberId = yield* Effect.fiberId;
772
+ const rootScope = yield* Effect.scope;
773
+ const current = yield* CurrentRoute;
774
+ const prefixed = matcher.prefix(current.route);
775
+ const entries = compile(prefixed.cases);
776
+ const router = findMyWay.make<ReadonlyArray<CompiledEntry>>({
777
+ ignoreTrailingSlash: true,
778
+ caseSensitive: false,
779
+ });
780
+ const handlersByPath = new Map<string, Array<CompiledEntry>>();
781
+ const memoMap = yield* Layer.makeMemoMap;
782
+ const layerManager = makeLayerManager(memoMap, rootScope, fiberId);
783
+ const layoutManager = makeLayoutManager(rootScope, fiberId);
784
+ const catchManager = makeCatchManager(rootScope, fiberId);
785
+
786
+ for (const entry of entries) {
787
+ const path = entry.route.path;
788
+ const existing = handlersByPath.get(path);
789
+ if (existing !== undefined) {
790
+ existing.push(entry);
791
+ } else {
792
+ const list: Array<CompiledEntry> = [entry];
793
+ handlersByPath.set(path, list);
794
+ router.all(path, list);
795
+ }
796
+ }
797
+
798
+ let currentState: {
799
+ entry: CompiledEntry;
800
+ params: RefSubject.RefSubject<any>;
801
+ fx: Fx.Fx<Matcher.Success<M>, Matcher.Error<M>, Matcher.Services<M> | Scope.Scope | Router>;
802
+ scope: Scope.Closeable;
803
+ } | null = null;
804
+
805
+ return CurrentPath.pipe(
806
+ mapEffect(
807
+ Effect.fn(function* (path) {
808
+ const result = router.find("GET", path);
809
+ if (result === undefined) return yield* new RouteNotFound({ path });
810
+
811
+ const input = { ...result.params, ...result.searchParams };
812
+ const entries = result.handler;
813
+ const guardCauses: Array<Cause.Cause<any>> = [];
814
+ let matchedEntry: CompiledEntry | undefined = undefined;
815
+ let matchedParams: any = undefined;
816
+ let matchedPrepared:
817
+ | {
818
+ services: AnyServiceMap;
819
+ commit: Effect.Effect<void>;
820
+ rollback: Effect.Effect<void>;
821
+ }
822
+ | undefined = undefined;
823
+
824
+ for (const entry of entries) {
825
+ const params = yield* Effect.mapErrorEager(
826
+ entry.decode(input),
827
+ (cause) =>
828
+ new RouteDecodeError({ path, cause: makeFormatterDefault()(cause.issue) }),
829
+ );
830
+
831
+ const prepared = yield* layerManager.prepare(entry.layers);
832
+ const guardExit = yield* entry
833
+ .guard(params)
834
+ .pipe(Effect.provideServices(prepared.services), Effect.exit);
835
+
836
+ if (Exit.isFailure(guardExit)) {
837
+ guardCauses.push(guardExit.cause);
838
+ yield* prepared.rollback;
839
+ continue;
840
+ }
841
+
842
+ if (Option.isNone(guardExit.value)) {
843
+ yield* prepared.rollback;
844
+ continue;
845
+ }
846
+
847
+ matchedEntry = entry;
848
+ matchedParams = guardExit.value.value;
849
+ matchedPrepared = prepared;
850
+ break;
851
+ }
852
+
853
+ if (matchedEntry === undefined || matchedPrepared === undefined) {
854
+ return yield* new RouteGuardError({ path, causes: guardCauses });
855
+ }
856
+
857
+ yield* matchedPrepared.commit;
858
+
859
+ if (currentState !== null && currentState.entry === matchedEntry) {
860
+ yield* RefSubject.set(currentState.params, matchedParams);
861
+ yield* layoutManager.updateParams(matchedEntry.layouts, matchedParams);
862
+ return currentState.fx;
863
+ }
864
+
865
+ if (currentState !== null) {
866
+ yield* Scope.close(currentState.scope, interrupt(fiberId));
867
+ currentState = null;
868
+ }
869
+
870
+ const scope = yield* Scope.fork(rootScope);
871
+ const paramsRef = yield* RefSubject.make(matchedParams).pipe(Scope.provide(scope));
872
+
873
+ const preparedServices = matchedPrepared.services as ServiceMap.ServiceMap<any>;
874
+ const handlerServices = ServiceMap.merge(
875
+ preparedServices,
876
+ ServiceMap.make(Scope.Scope, scope),
877
+ );
878
+
879
+ const handlerFx = matchedEntry
880
+ .handler(paramsRef)
881
+ .pipe(provideServices(handlerServices));
882
+ const withLayouts = yield* layoutManager.apply(
883
+ matchedEntry.layouts,
884
+ matchedParams,
885
+ handlerFx,
886
+ preparedServices,
887
+ );
888
+ const withCatches = yield* catchManager.apply(
889
+ matchedEntry.catches,
890
+ withLayouts,
891
+ preparedServices,
892
+ );
893
+ const fx = withCatches;
894
+
895
+ currentState = {
896
+ entry: matchedEntry,
897
+ params: paramsRef,
898
+ scope,
899
+ fx,
900
+ };
901
+
902
+ return currentState.fx;
903
+ }),
904
+ ),
905
+ skipRepeats,
906
+ switchMap(identity),
907
+ );
908
+ }),
909
+ );
910
+ }
911
+
912
+ type InputSucces<T> = [Matcher.Success<T> | Fx.Fx.Success<T>] extends [infer A] ? A : never;
913
+ type InputError<T> = [Matcher.Error<T> | Fx.Fx.Error<T>] extends [infer E] ? E : never;
914
+ type InputServices<T> = [Matcher.Services<T> | Fx.Fx.Services<T>] extends [infer R] ? R : never;
915
+
916
+ export const catchCause: {
917
+ <I extends Fx.Fx.Any | Matcher.Any, B, E2 = never, R2 = never>(
918
+ f: (
919
+ cause: RefSubject.RefSubject<
920
+ Cause.Cause<InputError<I> | RouteNotFound | RouteDecodeError | RouteGuardError>
921
+ >,
922
+ ) => Fx.Fx<B, E2, R2>,
923
+ ): (input: I) => Fx.Fx<InputSucces<I> | B, E2, InputServices<I> | R2 | Router | Scope.Scope>;
924
+
925
+ <I extends Fx.Fx.Any | Matcher.Any, B, E2 = never, R2 = never>(
926
+ input: I,
927
+ f: (
928
+ cause: RefSubject.RefSubject<
929
+ Cause.Cause<InputError<I> | RouteNotFound | RouteDecodeError | RouteGuardError>
930
+ >,
931
+ ) => Fx.Fx<B, E2, R2>,
932
+ ): Fx.Fx<InputSucces<I> | B, E2, InputServices<I> | R2 | Router | Scope.Scope>;
933
+ } = dual(
934
+ 2,
935
+ <A, E, R, B, E2, R2>(
936
+ input: Fx.Fx<A, E, R> | Matcher<A, E, R>,
937
+ f: (
938
+ cause: RefSubject.RefSubject<
939
+ Cause.Cause<E | RouteNotFound | RouteDecodeError | RouteGuardError>
940
+ >,
941
+ ) => Fx.Fx<B, E2, R2>,
942
+ ): Fx.Fx<A | B, E2, R | R2 | Router | Scope.Scope> => {
943
+ const eff = Effect.gen(function* () {
944
+ const fiberId = yield* Effect.fiberId;
945
+ const rootScope = yield* Effect.scope;
946
+ const fx = isFx(input) ? input : run(input);
947
+ const manager = makeCatchManager(rootScope, fiberId);
948
+ const result = yield* manager.apply(
949
+ [f],
950
+ fx,
951
+ ServiceMap.empty() as ServiceMap.ServiceMap<any>,
952
+ );
953
+ return result as Fx.Fx<A | B, E2, R | R2 | Router | Scope.Scope>;
954
+ });
955
+ return unwrap(eff);
956
+ },
957
+ );
958
+
959
+ export const catch_: {
960
+ <I extends Fx.Fx.Any | Matcher.Any, B, E2, R2>(
961
+ f: (e: InputError<I>) => Fx.Fx<B, E2, R2>,
962
+ ): (input: I) => Fx.Fx<InputSucces<I> | B, E2, InputServices<I> | R2 | Router | Scope.Scope>;
963
+ } = dual(
964
+ 2,
965
+ <I extends Fx.Fx.Any | Matcher.Any, B, E2, R2>(
966
+ input: I,
967
+ f: (e: InputError<I>) => Fx.Fx<B, E2, R2>,
968
+ ): Fx.Fx<InputSucces<I> | B, E2, InputServices<I> | R2 | Router | Scope.Scope> =>
969
+ catchCause(input, (causeRef) =>
970
+ unwrap(
971
+ Effect.gen(function* () {
972
+ const cause = yield* causeRef;
973
+ const result = Cause.findFail(cause);
974
+ if (Result.isFailure(result)) {
975
+ return fromEffect(Effect.failCause(result.failure));
976
+ }
977
+ return f(result.success.error as InputError<I>);
978
+ }),
979
+ ),
980
+ ),
981
+ );
982
+
983
+ export { catch_ as catch };
984
+
985
+ export const catchTag: {
986
+ <
987
+ I extends Fx.Fx.Any | Matcher.Any,
988
+ const K extends Tags<E> | Arr.NonEmptyReadonlyArray<Tags<E>>,
989
+ E,
990
+ B,
991
+ E2,
992
+ R2,
993
+ >(
994
+ k: K,
995
+ f: (e: InputError<I>) => Fx.Fx<B, E2, R2>,
996
+ ): (input: I) => Fx.Fx<InputSucces<I> | B, E2, InputServices<I> | R2 | Router | Scope.Scope>;
997
+
998
+ <
999
+ I extends Fx.Fx.Any | Matcher.Any,
1000
+ const K extends Tags<InputError<I>> | Arr.NonEmptyReadonlyArray<Tags<InputError<I>>>,
1001
+ B,
1002
+ E2,
1003
+ R2,
1004
+ >(
1005
+ input: I,
1006
+ k: K,
1007
+ f: (
1008
+ e: ExtractTag<InputError<I>, K extends Arr.NonEmptyReadonlyArray<string> ? K[number] : K>,
1009
+ ) => Fx.Fx<B, E2, R2>,
1010
+ ): Fx.Fx<
1011
+ InputSucces<I> | B,
1012
+ E2 | ExcludeTag<InputError<I>, K extends Arr.NonEmptyReadonlyArray<string> ? K[number] : K>,
1013
+ InputServices<I> | R2 | Router | Scope.Scope
1014
+ >;
1015
+ } = dual(
1016
+ 3,
1017
+ <
1018
+ I extends Fx.Fx.Any | Matcher.Any,
1019
+ const K extends Tags<InputError<I>> | Arr.NonEmptyReadonlyArray<Tags<InputError<I>>>,
1020
+ B,
1021
+ E2,
1022
+ R2,
1023
+ >(
1024
+ input: I,
1025
+ k: K,
1026
+ f: (e: InputError<I>) => Fx.Fx<B, E2, R2>,
1027
+ ): Fx.Fx<InputSucces<I> | B, E2, InputServices<I> | R2 | Router | Scope.Scope> =>
1028
+ catchCause(input, (causeRef) =>
1029
+ unwrap(
1030
+ Effect.gen(function* () {
1031
+ const cause = yield* causeRef;
1032
+ const result = Cause.findFail(cause);
1033
+ if (Result.isFailure(result)) {
1034
+ return fromEffect(Effect.failCause(result.failure));
1035
+ }
1036
+ if (matchesTag(k, result.success.error)) {
1037
+ return f(
1038
+ result.success.error as ExtractTag<
1039
+ InputError<I>,
1040
+ K extends Arr.NonEmptyReadonlyArray<string> ? K[number] : K
1041
+ >,
1042
+ );
1043
+ }
1044
+ return fromEffect(Effect.fail(result.success.error as E2));
1045
+ }),
1046
+ ),
1047
+ ),
1048
+ );
1049
+
1050
+ export const redirectTo =
1051
+ (path: string) =>
1052
+ <I extends Fx.Fx.Any | Matcher.Any>(
1053
+ input: I,
1054
+ ): Fx.Fx<InputSucces<I>, never, Router | Scope.Scope | InputServices<I>> =>
1055
+ catchCause(input, (_) =>
1056
+ Navigation.navigate(path).pipe(
1057
+ Effect.matchCause({
1058
+ onFailure: () => never,
1059
+ onSuccess: () => never,
1060
+ }),
1061
+ unwrap,
1062
+ ),
1063
+ );
1064
+
1065
+ const hasTag = (u: unknown): u is { readonly _tag: string } =>
1066
+ typeof u === "object" &&
1067
+ u !== null &&
1068
+ "_tag" in u &&
1069
+ typeof (u as Record<string, unknown>)["_tag"] === "string";
1070
+
1071
+ const matchesTag = <E, K extends string>(
1072
+ tag: K | Arr.NonEmptyReadonlyArray<K>,
1073
+ error: E,
1074
+ ): error is ExtractTag<E, K> => {
1075
+ if (!hasTag(error)) return false;
1076
+ if (typeof tag === "string") return error._tag === tag;
1077
+ return tag.some((t) => t === error._tag);
1078
+ };
1079
+
1080
+ function isServiceMap(dep: AnyDependency): dep is AnyServiceMap {
1081
+ return !Layer.isLayer(dep);
1082
+ }
1083
+
1084
+ function toSingleLayer(dep: AnyDependency): AnyLayer {
1085
+ if (isServiceMap(dep)) return Layer.succeedServices(dep);
1086
+ return dep;
1087
+ }
1088
+
1089
+ function normalizeDependencies(
1090
+ dependencies: ReadonlyArray<AnyDependency>,
1091
+ ): ReadonlyArray<AnyLayer> {
1092
+ return dependencies.map(toSingleLayer);
1093
+ }
294
1094
 
295
1095
  /**
296
- * @since 1.0.0
1096
+ * Normalize dependency input (ServiceMap | Layer | Array of either) into a single Layer.
1097
+ * Use with `.provide(normalizeDependencyInput(deps))`.
297
1098
  */
298
- export function isRouteMatcher<M extends RouteMatch.RouteMatch.Any = RouteMatch.RouteMatch.Any>(
299
- value: unknown
300
- ): value is RouteMatcher<M> {
301
- return hasProperty(value, RouteMatcherTypeId)
1099
+ export function normalizeDependencyInput(
1100
+ input: AnyDependency | ReadonlyArray<AnyDependency>,
1101
+ ): AnyLayer {
1102
+ const arr = Array.isArray(input) ? input : [input];
1103
+ const layers = normalizeDependencies(arr);
1104
+ return mergeLayers(layers);
1105
+ }
1106
+
1107
+ function getGuard<I, O, E, R>(guard: GuardInput<I, O, E, R>): GuardType<I, O, E, R> {
1108
+ return "asGuard" in guard ? guard.asGuard() : guard;
1109
+ }
1110
+
1111
+ function defaultGuard<A>(): GuardType<A, A> {
1112
+ return Effect.succeedSome;
1113
+ }
1114
+
1115
+ function mergeLayers(layers: ReadonlyArray<AnyLayer>): AnyLayer {
1116
+ if (layers.length === 0) return Layer.empty;
1117
+ if (layers.length === 1) return layers[0];
1118
+ let current = layers[0];
1119
+ for (let i = 1; i < layers.length; i++) {
1120
+ current = Layer.merge(current, layers[i]);
1121
+ }
1122
+ return current;
302
1123
  }
303
1124
 
304
1125
  /**
305
- * @since 1.0.0
1126
+ * @internal
306
1127
  */
307
- export const redirectWith: {
308
- <E, R>(
309
- effect: Effect.Effect<string, E, R>
310
- ): <Matches extends RouteMatch.RouteMatch.Any>(router: RouteMatcher<Matches>) => Fx.Fx<
311
- RouteMatch.RouteMatch.Success<Matches>,
312
- Exclude<E | RouteMatch.RouteMatch.Error<Matches>, RedirectError>,
313
- Navigation | Scope.Scope | R | RouteMatch.RouteMatch.Context<Matches>
314
- >
315
-
316
- <Matches extends RouteMatch.RouteMatch.Any, E, R>(
317
- router: RouteMatcher<Matches>,
318
- effect: Effect.Effect<string, E, R>
319
- ): Fx.Fx<
320
- RouteMatch.RouteMatch.Success<Matches>,
321
- Exclude<E | RouteMatch.RouteMatch.Error<Matches>, RedirectError>,
322
- Navigation | Scope.Scope | R | RouteMatch.RouteMatch.Context<Matches>
323
- >
324
- } = dual(2, function redirectWith<Matches extends RouteMatch.RouteMatch.Any, E, R>(
325
- router: RouteMatcher<Matches>,
326
- effect: Effect.Effect<string, E, R>
327
- ) {
328
- return notFoundWith(router, Effect.flatMap(effect, (path) => new RedirectError({ path })))
329
- })
1128
+ export function compile(cases: ReadonlyArray<MatchAst>): ReadonlyArray<CompiledEntry> {
1129
+ const entries: Array<CompiledEntry> = [];
1130
+
1131
+ const visit = (
1132
+ matches: ReadonlyArray<MatchAst>,
1133
+ context: {
1134
+ readonly layers: ReadonlyArray<AnyLayer>;
1135
+ readonly layouts: ReadonlyArray<AnyLayout>;
1136
+ readonly catches: ReadonlyArray<AnyCatch>;
1137
+ readonly prefixes: ReadonlyArray<RouteAst>;
1138
+ },
1139
+ ): void => {
1140
+ for (const match of matches) {
1141
+ switch (match.type) {
1142
+ case "route": {
1143
+ const baseRoute = makeRoute(match.route);
1144
+ const prefixedRoute = applyPrefixes(baseRoute, context.prefixes);
1145
+ entries.push({
1146
+ route: prefixedRoute,
1147
+ guard: getGuard(match.guard as GuardInput<any, any, any, any>),
1148
+ handler: normalizeHandler(match.handler),
1149
+ layers: context.layers,
1150
+ layouts: context.layouts,
1151
+ catches: context.catches,
1152
+ decode: Schema.decodeUnknownEffect(prefixedRoute.paramsSchema),
1153
+ });
1154
+ break;
1155
+ }
1156
+ case "layer": {
1157
+ const merged = mergeLayers(match.deps);
1158
+ visit(match.matches, {
1159
+ ...context,
1160
+ layers: [...context.layers, merged],
1161
+ });
1162
+ break;
1163
+ }
1164
+ case "layout": {
1165
+ visit(match.matches, {
1166
+ ...context,
1167
+ layouts: [...context.layouts, match.layout as AnyLayout],
1168
+ });
1169
+ break;
1170
+ }
1171
+ case "prefixed": {
1172
+ visit(match.matches, {
1173
+ ...context,
1174
+ prefixes: [...context.prefixes, match.prefix],
1175
+ });
1176
+ break;
1177
+ }
1178
+ case "catch": {
1179
+ visit(match.matches, {
1180
+ ...context,
1181
+ catches: [...context.catches, match.f as AnyCatch],
1182
+ });
1183
+ break;
1184
+ }
1185
+ }
1186
+ }
1187
+ };
1188
+
1189
+ visit(cases, { layers: [], layouts: [], catches: [], prefixes: [] });
1190
+ return entries;
1191
+ }
1192
+
1193
+ function applyPrefixes(route: Route.Any, prefixes: ReadonlyArray<RouteAst>): Route.Any {
1194
+ if (prefixes.length === 0) return route;
1195
+ const prefixRoutes = prefixes.map((prefix) => makeRoute(prefix));
1196
+ return Join(...prefixRoutes, route);
1197
+ }
1198
+
1199
+ // Parallel scope cleanup helper
1200
+ const closeScopes = (scopes: Iterable<Scope.Closeable>, fiberId: number) =>
1201
+ Effect.forEach(scopes, (scope) => Scope.close(scope, interrupt(fiberId)), {
1202
+ concurrency: "unbounded",
1203
+ discard: true,
1204
+ });
330
1205
 
331
1206
  /**
332
- * @since 1.0.0
1207
+ * @internal
333
1208
  */
334
- export const redirectTo: {
335
- <R extends Route.Route.Any>(
336
- route: R,
337
- ...params: Route.Route.ParamsList<R>
338
- ): <Matches extends RouteMatch.RouteMatch.Any>(router: RouteMatcher<Matches>) => Fx.Fx<
339
- RouteMatch.RouteMatch.Success<Matches>,
340
- RedirectRouteMatchError<R> | Exclude<RouteMatch.RouteMatch.Error<Matches>, RedirectError>,
341
- Scope.Scope | Navigation | CurrentRoute | RouteMatch.RouteMatch.Context<Matches>
342
- >
343
-
344
- <Matches extends RouteMatch.RouteMatch.Any, R extends Route.Route.Any>(
345
- router: RouteMatcher<Matches>,
346
- route: R,
347
- ...params: Route.Route.ParamsList<R>
348
- ): Fx.Fx<
349
- RouteMatch.RouteMatch.Success<Matches>,
350
- RedirectRouteMatchError<R> | Exclude<RouteMatch.RouteMatch.Error<Matches>, RedirectError>,
351
- Scope.Scope | Navigation | CurrentRoute | RouteMatch.RouteMatch.Context<Matches>
352
- >
353
- } = dual(
354
- (args) => isRouteMatcher(args[0]),
355
- function redirect<Matches extends RouteMatch.RouteMatch.Any, R extends Route.Route.Any>(
356
- router: RouteMatcher<Matches>,
357
- route: R,
358
- ...params: Route.Route.ParamsList<R>
359
- ): Fx.Fx<
360
- RouteMatch.RouteMatch.Success<Matches>,
361
- RedirectRouteMatchError<R> | Exclude<RouteMatch.RouteMatch.Error<Matches>, RedirectError>,
362
- Scope.Scope | Navigation | CurrentRoute | RouteMatch.RouteMatch.Context<Matches>
363
- > {
364
- return redirectWith(
365
- router,
366
- pipe(
367
- Effect.catchTag(
368
- makeHref<R>(route, ...params),
369
- "NoSuchElementException",
370
- () => Effect.fail(new RedirectRouteMatchError<R>(route, (params[0] || {}) as Route.Route.Params<R>))
371
- ),
372
- Effect.flatMap((path) => new RedirectError({ path }))
373
- )
374
- )
375
- }
376
- )
1209
+ export function makeLayerManager(memoMap: Layer.MemoMap, rootScope: Scope.Scope, fiberId: number) {
1210
+ const states = new Map<AnyLayer, { scope: Scope.Closeable; services: AnyServiceMap }>();
1211
+ let order: ReadonlyArray<AnyLayer> = [];
1212
+ let cachedDesiredSet: Set<AnyLayer> | undefined = undefined;
1213
+ let cachedOrder: ReadonlyArray<AnyLayer> | undefined = undefined;
1214
+
1215
+ const prepare = (desired: ReadonlyArray<AnyLayer>) =>
1216
+ Effect.gen(function* () {
1217
+ const desiredSet =
1218
+ cachedOrder === desired
1219
+ ? cachedDesiredSet!
1220
+ : ((cachedDesiredSet = new Set(desired)), (cachedOrder = desired), cachedDesiredSet);
1221
+ const removed = order.filter((layer) => !desiredSet.has(layer));
1222
+ const added: Array<AnyLayer> = [];
1223
+ let services = ServiceMap.empty();
1224
+
1225
+ for (const layer of desired) {
1226
+ const existing = states.get(layer);
1227
+ if (existing) {
1228
+ services = ServiceMap.merge(services, existing.services);
1229
+ continue;
1230
+ }
1231
+
1232
+ const scope = yield* Scope.fork(rootScope);
1233
+ const buildExit = yield* Layer.buildWithMemoMap(layer, memoMap, scope).pipe(
1234
+ Effect.provideServices(services),
1235
+ Effect.exit,
1236
+ );
1237
+
1238
+ if (Exit.isFailure(buildExit)) {
1239
+ for (let i = added.length - 1; i >= 0; i--) {
1240
+ const addedLayer = added[i];
1241
+ const addedState = states.get(addedLayer);
1242
+ if (addedState) {
1243
+ states.delete(addedLayer);
1244
+ yield* Scope.close(addedState.scope, interrupt(fiberId));
1245
+ }
1246
+ }
1247
+ yield* Scope.close(scope, buildExit);
1248
+ return yield* Effect.failCause(buildExit.cause);
1249
+ }
1250
+
1251
+ const servicesForLayer = buildExit.value;
1252
+ services = ServiceMap.merge(services, servicesForLayer);
1253
+ states.set(layer, { scope, services: servicesForLayer });
1254
+ added.push(layer);
1255
+ }
1256
+
1257
+ const commit = Effect.gen(function* () {
1258
+ for (let i = removed.length - 1; i >= 0; i--) {
1259
+ const layer = removed[i];
1260
+ const state = states.get(layer);
1261
+ if (state) {
1262
+ states.delete(layer);
1263
+ yield* Scope.close(state.scope, interrupt(fiberId));
1264
+ }
1265
+ }
1266
+ order = desired;
1267
+ });
1268
+
1269
+ const rollback = Effect.gen(function* () {
1270
+ for (let i = added.length - 1; i >= 0; i--) {
1271
+ const layer = added[i];
1272
+ const state = states.get(layer);
1273
+ if (state) {
1274
+ states.delete(layer);
1275
+ yield* Scope.close(state.scope, interrupt(fiberId));
1276
+ }
1277
+ }
1278
+ });
1279
+
1280
+ return { services, commit, rollback };
1281
+ });
1282
+
1283
+ return { prepare };
1284
+ }
377
1285
 
378
1286
  /**
379
- * @since 1.0.0
1287
+ * @internal
380
1288
  */
381
- export class RedirectRouteMatchError<R extends Route.Route.Any> extends Data.TaggedError("RedirectRouteMatchError") {
382
- constructor(readonly route: R, readonly params: Route.Route.Params<R>) {
383
- super()
384
- }
1289
+ export function makeLayoutManager(rootScope: Scope.Scope, fiberId: number) {
1290
+ const states = new Map<
1291
+ AnyLayout,
1292
+ {
1293
+ params: RefSubject.RefSubject<any>;
1294
+ content: RefSubject.RefSubject<Fx.Fx<any, any, any>>;
1295
+ fx: Fx.Fx<any, any, any>;
1296
+ scope: Scope.Closeable;
1297
+ }
1298
+ >();
1299
+ let active: ReadonlyArray<AnyLayout> = [];
1300
+
1301
+ const removeUnused = (layouts: ReadonlyArray<AnyLayout>) =>
1302
+ Effect.gen(function* () {
1303
+ const next = new Set(layouts);
1304
+ const removed = active.filter((layout) => !next.has(layout));
1305
+ const scopes = removed.map((layout) => {
1306
+ const state = states.get(layout)!;
1307
+ states.delete(layout);
1308
+ return state.scope;
1309
+ });
1310
+ yield* closeScopes(scopes, fiberId);
1311
+ active = layouts;
1312
+ });
1313
+
1314
+ const apply = (
1315
+ layouts: ReadonlyArray<AnyLayout>,
1316
+ paramsValue: any,
1317
+ inner: Fx.Fx<any, any, any>,
1318
+ services: ServiceMap.ServiceMap<any>,
1319
+ ) =>
1320
+ Effect.gen(function* () {
1321
+ let current = inner;
1322
+ for (let i = layouts.length - 1; i >= 0; i--) {
1323
+ const layout = layouts[i];
1324
+ const state = states.get(layout);
1325
+ if (state === undefined) {
1326
+ const scope = yield* Scope.fork(rootScope);
1327
+ const params = yield* RefSubject.make(paramsValue).pipe(Scope.provide(scope));
1328
+ const content = yield* RefSubject.make<Fx.Fx<any, any, any>>(Effect.succeed(current), {
1329
+ eq: (left, right) => left === right,
1330
+ }).pipe(Scope.provide(scope));
1331
+ const fx = layout({ params, content: content.pipe(switchMap(identity)) }).pipe(
1332
+ provideServices(ServiceMap.merge(services, ServiceMap.make(Scope.Scope, scope))),
1333
+ );
1334
+ states.set(layout, { params, content, fx, scope });
1335
+ current = fx;
1336
+ } else {
1337
+ yield* RefSubject.set(state.params, paramsValue);
1338
+ // @effect-diagnostics-next-line floatingEffect:off
1339
+ yield* RefSubject.set(state.content, current);
1340
+ current = state.fx;
1341
+ }
1342
+ }
1343
+ yield* removeUnused(layouts);
1344
+ return current;
1345
+ });
1346
+
1347
+ const updateParams = (layouts: ReadonlyArray<AnyLayout>, paramsValue: any) =>
1348
+ Effect.forEach(
1349
+ layouts,
1350
+ (layout) => {
1351
+ const state = states.get(layout);
1352
+ return state !== undefined ? RefSubject.set(state.params, paramsValue) : Effect.void;
1353
+ },
1354
+ { discard: true },
1355
+ );
1356
+
1357
+ return { apply, updateParams };
1358
+ }
1359
+
1360
+ /**
1361
+ * @internal
1362
+ */
1363
+ export function makeCatchManager(rootScope: Scope.Scope, fiberId: number) {
1364
+ const states = new Map<
1365
+ AnyCatch,
1366
+ {
1367
+ causes: RefSubject.RefSubject<Cause.Cause<any>>;
1368
+ content: RefSubject.RefSubject<Fx.Fx<any, any, any>>;
1369
+ fx: Fx.Fx<any, any, any>;
1370
+ scope: Scope.Closeable;
1371
+ }
1372
+ >();
1373
+ let active: ReadonlyArray<AnyCatch> = [];
1374
+
1375
+ const removeUnused = (catches: ReadonlyArray<AnyCatch>) =>
1376
+ Effect.gen(function* () {
1377
+ const next = new Set(catches);
1378
+ const removed = active.filter((c) => !next.has(c));
1379
+ const scopes = removed.map((c) => {
1380
+ const state = states.get(c)!;
1381
+ states.delete(c);
1382
+ return state.scope;
1383
+ });
1384
+ yield* closeScopes(scopes, fiberId);
1385
+ active = catches;
1386
+ });
1387
+
1388
+ const apply = (
1389
+ catches: ReadonlyArray<AnyCatch>,
1390
+ inner: Fx.Fx<any, any, any>,
1391
+ services: ServiceMap.ServiceMap<any>,
1392
+ ) =>
1393
+ Effect.gen(function* () {
1394
+ let current = inner;
1395
+ for (let i = catches.length - 1; i >= 0; i--) {
1396
+ const catcher = catches[i];
1397
+ const state = states.get(catcher);
1398
+ if (state === undefined) {
1399
+ const scope = yield* Scope.fork(rootScope);
1400
+ const causes = yield* RefSubject.make<Cause.Cause<any>>(Cause.fail(undefined)).pipe(
1401
+ Scope.provide(scope),
1402
+ );
1403
+ const content = yield* RefSubject.make<Fx.Fx<any, any, any>>(Effect.succeed(current), {
1404
+ eq: (left, right) => left === right,
1405
+ }).pipe(Scope.provide(scope));
1406
+ const fallback = catcher(causes).pipe(
1407
+ provideServices(ServiceMap.merge(services, ServiceMap.make(Scope.Scope, scope))),
1408
+ );
1409
+ const fx = content.pipe(
1410
+ switchMap(identity),
1411
+ exit,
1412
+ mapEffect(
1413
+ Effect.fn(function* (e) {
1414
+ if (isSuccess(e)) return succeed(e.value);
1415
+ yield* RefSubject.set(causes, e.cause);
1416
+ return fallback;
1417
+ }),
1418
+ ),
1419
+ skipRepeats,
1420
+ switchMap(identity),
1421
+ );
1422
+ states.set(catcher, { causes, content, fx, scope });
1423
+ current = fx;
1424
+ } else {
1425
+ // @effect-diagnostics-next-line floatingEffect:off
1426
+ yield* RefSubject.set(state.content, current);
1427
+ current = state.fx;
1428
+ }
1429
+ }
1430
+ yield* removeUnused(catches);
1431
+ return current;
1432
+ });
1433
+
1434
+ return { apply };
385
1435
  }