@typed/router 0.32.0 → 1.0.0-beta.0

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 (88) hide show
  1. package/README.md +111 -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 +191 -0
  9. package/dist/Matcher.d.ts.map +1 -0
  10. package/dist/Matcher.js +597 -0
  11. package/dist/Parser.d.ts +96 -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 +25 -70
  30. package/src/AST.ts +166 -0
  31. package/src/CurrentRoute.ts +30 -331
  32. package/src/Matcher.test.ts +476 -0
  33. package/src/Matcher.ts +1269 -328
  34. package/src/Parser.ts +282 -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 +31 -0
  40. package/src/Uri.ts +214 -0
  41. package/src/index.ts +4 -28
  42. package/tsconfig.json +6 -0
  43. package/CurrentRoute/package.json +0 -6
  44. package/LICENSE +0 -21
  45. package/MatchInput/package.json +0 -6
  46. package/Matcher/package.json +0 -6
  47. package/RouteGuard/package.json +0 -6
  48. package/RouteMatch/package.json +0 -6
  49. package/dist/cjs/CurrentRoute.js +0 -170
  50. package/dist/cjs/CurrentRoute.js.map +0 -1
  51. package/dist/cjs/MatchInput.js +0 -96
  52. package/dist/cjs/MatchInput.js.map +0 -1
  53. package/dist/cjs/Matcher.js +0 -138
  54. package/dist/cjs/Matcher.js.map +0 -1
  55. package/dist/cjs/RouteGuard.js +0 -78
  56. package/dist/cjs/RouteGuard.js.map +0 -1
  57. package/dist/cjs/RouteMatch.js +0 -49
  58. package/dist/cjs/RouteMatch.js.map +0 -1
  59. package/dist/cjs/index.js +0 -53
  60. package/dist/cjs/index.js.map +0 -1
  61. package/dist/dts/CurrentRoute.d.ts +0 -94
  62. package/dist/dts/CurrentRoute.d.ts.map +0 -1
  63. package/dist/dts/MatchInput.d.ts +0 -143
  64. package/dist/dts/MatchInput.d.ts.map +0 -1
  65. package/dist/dts/Matcher.d.ts +0 -121
  66. package/dist/dts/Matcher.d.ts.map +0 -1
  67. package/dist/dts/RouteGuard.d.ts +0 -94
  68. package/dist/dts/RouteGuard.d.ts.map +0 -1
  69. package/dist/dts/RouteMatch.d.ts +0 -50
  70. package/dist/dts/RouteMatch.d.ts.map +0 -1
  71. package/dist/dts/index.d.ts +0 -24
  72. package/dist/dts/index.d.ts.map +0 -1
  73. package/dist/esm/CurrentRoute.js +0 -152
  74. package/dist/esm/CurrentRoute.js.map +0 -1
  75. package/dist/esm/MatchInput.js +0 -79
  76. package/dist/esm/MatchInput.js.map +0 -1
  77. package/dist/esm/Matcher.js +0 -130
  78. package/dist/esm/Matcher.js.map +0 -1
  79. package/dist/esm/RouteGuard.js +0 -57
  80. package/dist/esm/RouteGuard.js.map +0 -1
  81. package/dist/esm/RouteMatch.js +0 -29
  82. package/dist/esm/RouteMatch.js.map +0 -1
  83. package/dist/esm/index.js +0 -24
  84. package/dist/esm/index.js.map +0 -1
  85. package/dist/esm/package.json +0 -4
  86. package/src/MatchInput.ts +0 -303
  87. package/src/RouteGuard.ts +0 -217
  88. package/src/RouteMatch.ts +0 -104
package/src/Matcher.ts CHANGED
@@ -1,385 +1,1326 @@
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>;
107
303
  }
108
304
 
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
305
+ export declare namespace Matcher {
306
+ export type Any =
307
+ | Matcher<any, any, any>
308
+ | Matcher<any, never, any>
309
+ | Matcher<any, any, never>
310
+ | Matcher<any, never, never>;
311
+ export type Success<T> = [T] extends [Matcher<infer A, infer _E, infer _R>] ? A : never;
312
+ export type Error<T> = [T] extends [Matcher<infer _A, infer E, infer _R>] ? E : never;
313
+ export type Services<T> = [T] extends [Matcher<infer _A, infer _E, infer R>] ? R : never;
314
+ }
315
+
316
+ export type MatchHandler<Params, A, E, R> =
317
+ | Fx.Fx<A, E, R>
318
+ | Stream.Stream<A, E, R>
319
+ | Effect.Effect<A, E, R>
320
+ | A
321
+ | ((
322
+ params: RefSubject.RefSubject<Params>,
323
+ ) => Fx.Fx<A, E, R> | Stream.Stream<A, E, R> | Effect.Effect<A, E, R> | A);
324
+
325
+ type MatchHandlerFn<Params, A, E, R> = (
326
+ params: RefSubject.RefSubject<Params>,
327
+ ) => Fx.Fx<A, E, R> | Stream.Stream<A, E, R> | Effect.Effect<A, E, R> | A;
328
+
329
+ function isMatchHandlerFn<Params, A, E, R>(
330
+ handler: MatchHandler<Params, A, E, R>,
331
+ ): handler is MatchHandlerFn<Params, A, E, R> {
332
+ return typeof handler === "function";
122
333
  }
123
334
 
124
- class RouteMatcherImpl<Matches extends RouteMatch.RouteMatch.Any> implements RouteMatcher<Matches> {
125
- readonly [RouteMatcherTypeId]: RouteMatcherTypeId = RouteMatcherTypeId
335
+ function isHandlerOptions(value: unknown): value is { readonly handler: unknown } {
336
+ return typeof value === "object" && value !== null && "handler" in value;
337
+ }
338
+
339
+ // Monomorphic shape - all properties always present for V8 optimization
340
+ type ParsedMatch = {
341
+ readonly route: Route.Any;
342
+ readonly handler: unknown;
343
+ readonly guard: AnyGuard | undefined;
344
+ readonly layout: AnyLayout | undefined;
345
+ readonly catchFn: AnyCatch | undefined;
346
+ readonly dependencies: ReadonlyArray<AnyDependency> | undefined;
347
+ };
348
+
349
+ function parseMatchArgs(args: [unknown, ...Array<unknown>]): ParsedMatch {
350
+ const [first, second, third] = args;
126
351
 
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)
352
+ // Single arg: full options object (Overload 9)
353
+ if (second === undefined) {
354
+ const opts = first as MatchOptions<Route.Any, any, any, any, any, any, any, any, any>;
355
+ return {
356
+ route: opts.route,
357
+ handler: opts.handler,
358
+ guard: undefined,
359
+ layout: opts.layout as AnyLayout | undefined,
360
+ catchFn: opts.catch as AnyCatch | undefined,
361
+ dependencies: opts.dependencies as ReadonlyArray<AnyDependency> | undefined,
362
+ };
133
363
  }
134
364
 
135
- add<I extends RouteMatch.RouteMatch.Any>(match: I): RouteMatcher<Matches | I> {
136
- return new RouteMatcherImpl([...this.matches, match])
365
+ // Two args
366
+ if (third === undefined) {
367
+ if (isHandlerOptions(second)) {
368
+ // Overload 3: match(route, options)
369
+ const opts = second as MatchHandlerOptions<any, any, any, any, any, any, any, any, any>;
370
+ return {
371
+ route: first as Route.Any,
372
+ handler: opts.handler,
373
+ guard: undefined,
374
+ layout: opts.layout as AnyLayout | undefined,
375
+ catchFn: opts.catch as AnyCatch | undefined,
376
+ dependencies: opts.dependencies as ReadonlyArray<AnyDependency> | undefined,
377
+ };
378
+ }
379
+ // Overloads 1, 2, 4: match(route, handler)
380
+ return {
381
+ route: first as Route.Any,
382
+ handler: second,
383
+ guard: undefined,
384
+ layout: undefined,
385
+ catchFn: undefined,
386
+ dependencies: undefined,
387
+ };
137
388
  }
138
389
 
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))
390
+ // Three args
391
+ if (isHandlerOptions(third)) {
392
+ // Overload 7: match(route, guard, options)
393
+ const opts = third as MatchHandlerOptions<any, any, any, any, any, any, any, any, any>;
394
+ return {
395
+ route: first as Route.Any,
396
+ handler: opts.handler,
397
+ guard: second as AnyGuard,
398
+ layout: opts.layout as AnyLayout | undefined,
399
+ catchFn: opts.catch as AnyCatch | undefined,
400
+ dependencies: opts.dependencies as ReadonlyArray<AnyDependency> | undefined,
401
+ };
144
402
  }
145
403
 
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))
404
+ // Overloads 5, 6, 8: match(route, guard, handler)
405
+ return {
406
+ route: first as Route.Any,
407
+ handler: third,
408
+ guard: second as AnyGuard,
409
+ layout: undefined,
410
+ catchFn: undefined,
411
+ dependencies: undefined,
412
+ };
413
+ }
414
+
415
+ class MatcherImpl<A, E, R> implements Matcher<A, E, R> {
416
+ readonly cases: ReadonlyArray<MatchAst>;
417
+ constructor(cases: ReadonlyArray<MatchAst>) {
418
+ this.cases = cases;
419
+ this.match = this.match.bind(this);
420
+ this.catch = this.catch.bind(this);
421
+ this.catchTag = this.catchTag.bind(this);
422
+ this.layout = this.layout.bind(this);
423
+ this.provide = this.provide.bind(this);
424
+ this.provideService = this.provideService.bind(this);
151
425
  }
152
426
 
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))
427
+ // Implementation overloads for type inference - use simplified return types
428
+ match<Rt extends Route.Any, B, E2 = never, R2 = never>(
429
+ route: Rt,
430
+ handler: (params: RefSubject.RefSubject<Route.Type<Rt>>) => MatchHandlerReturnValue<B, E2, R2>,
431
+ ): Matcher<A | B, E | E2, R | R2 | Scope.Scope>;
432
+ match<Rt extends Route.Any, B, E2 = never, R2 = never>(
433
+ route: Rt,
434
+ handler: Fx.Fx<B, E2, R2> | Effect.Effect<B, E2, R2> | Stream.Stream<B, E2, R2>,
435
+ ): Matcher<A | B, E | E2, R | R2 | Scope.Scope>;
436
+ match<
437
+ Rt extends Route.Any,
438
+ B,
439
+ E2 = never,
440
+ R2 = never,
441
+ D extends ReadonlyArray<AnyDependency> | undefined = undefined,
442
+ >(
443
+ route: Rt,
444
+ options: MatchHandlerOptions<Route.Type<Rt>, B, E2, R2, D, B, never, never, undefined>,
445
+ ): Matcher<
446
+ A | B,
447
+ E | E2 | DependencyError<D extends ReadonlyArray<infer Dep> ? Dep : never>,
448
+ R | R2 | Scope.Scope
449
+ >;
450
+ match<Rt extends Route.Any, B>(route: Rt, handler: B): Matcher<A | B, E, R | Scope.Scope>;
451
+ match<Rt extends Route.Any, G extends GuardInput<Route.Type<Rt>, any, any, any>, B, E2, R2>(
452
+ route: Rt,
453
+ guard: G,
454
+ handler: (params: RefSubject.RefSubject<GuardOutput<G>>) => MatchHandlerReturnValue<B, E2, R2>,
455
+ ): Matcher<A | B, E | E2 | GuardError<G>, R | R2 | GuardServices<G> | Scope.Scope>;
456
+ match<Rt extends Route.Any, G extends GuardInput<Route.Type<Rt>, any, any, any>, B, E2, R2>(
457
+ route: Rt,
458
+ guard: G,
459
+ handler: Fx.Fx<B, E2, R2> | Effect.Effect<B, E2, R2> | Stream.Stream<B, E2, R2>,
460
+ ): Matcher<A | B, E | E2 | GuardError<G>, R | R2 | GuardServices<G> | Scope.Scope>;
461
+ match<
462
+ Rt extends Route.Any,
463
+ G extends GuardInput<Route.Type<Rt>, any, any, any>,
464
+ B,
465
+ E2 = never,
466
+ R2 = never,
467
+ D extends ReadonlyArray<AnyDependency> | undefined = undefined,
468
+ >(
469
+ route: Rt,
470
+ guard: G,
471
+ options: MatchHandlerOptions<GuardOutput<G>, B, E2, R2, D, B, never, never, undefined>,
472
+ ): Matcher<
473
+ A | B,
474
+ E | E2 | GuardError<G> | DependencyError<D extends ReadonlyArray<infer Dep> ? Dep : never>,
475
+ R | R2 | GuardServices<G> | Scope.Scope
476
+ >;
477
+ match<Rt extends Route.Any, G extends GuardInput<Route.Type<Rt>, any, any, any>, B>(
478
+ route: Rt,
479
+ guard: G,
480
+ handler: B,
481
+ ): Matcher<A | B, E | GuardError<G>, R | GuardServices<G> | Scope.Scope>;
482
+ match<
483
+ Rt extends Route.Any,
484
+ B,
485
+ E2 = never,
486
+ R2 = never,
487
+ D extends ReadonlyArray<AnyDependency> | undefined = undefined,
488
+ >(
489
+ options: MatchOptions<Rt, B, E2, R2, D, B, never, never, undefined>,
490
+ ): Matcher<
491
+ A | B,
492
+ E | E2 | DependencyError<D extends ReadonlyArray<infer Dep> ? Dep : never>,
493
+ R | R2 | Scope.Scope
494
+ >;
495
+ match(...args: [unknown, ...Array<unknown>]): Matcher<any, any, any> {
496
+ const parsed = parseMatchArgs(args);
497
+ const normalizedGuard =
498
+ parsed.guard !== undefined
499
+ ? getGuard(parsed.guard as GuardInput<any, any, any, any>)
500
+ : defaultGuard();
501
+
502
+ const routeAst = AST.route(
503
+ parsed.route.ast,
504
+ parsed.handler as MatchHandler<any, any, any, any>,
505
+ normalizedGuard,
506
+ );
507
+
508
+ let matches: ReadonlyArray<MatchAst> = [routeAst];
509
+ if (parsed.layout !== undefined) {
510
+ matches = [AST.layout(matches, parsed.layout)];
511
+ }
512
+ if (parsed.catchFn !== undefined) {
513
+ matches = [AST.catchCause(matches, parsed.catchFn)];
514
+ }
515
+ if (parsed.dependencies !== undefined && parsed.dependencies.length > 0) {
516
+ matches = [AST.layer(matches, normalizeDependencies(parsed.dependencies))];
517
+ }
518
+
519
+ return new MatcherImpl([...this.cases, ...matches]);
158
520
  }
159
521
 
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))
522
+ prefix<Rt extends Route.Any>(route: Rt): Matcher<A, E, R> {
523
+ return new MatcherImpl<A, E, R>([AST.prefixed(this.cases, route.ast)]);
524
+ }
525
+
526
+ provide<Layers extends readonly [AnyLayer, ...AnyLayer[]]>(
527
+ ...layers: Layers
528
+ ): Matcher<
529
+ A,
530
+ E | LayerError<Layers[number]>,
531
+ Exclude<R, LayerSuccess<Layers[number]>> | LayerServices<Layers[number]>
532
+ > {
533
+ return new MatcherImpl([AST.layer(this.cases, layers)]) as Matcher<
534
+ A,
535
+ E | LayerError<Layers[number]>,
536
+ Exclude<R, LayerSuccess<Layers[number]>> | LayerServices<Layers[number]>
537
+ >;
538
+ }
539
+
540
+ provideService<Id, S>(tag: ServiceMap.Service<Id, S>, service: S): Matcher<A, E, Exclude<R, Id>> {
541
+ return this.provideServices(ServiceMap.make(tag, service));
542
+ }
543
+
544
+ provideServices<R2>(services: ServiceMap.ServiceMap<R2>): Matcher<A, E, Exclude<R, R2>> {
545
+ return this.provide(Layer.succeedServices(services));
546
+ }
547
+
548
+ catchCause<B, E2, R2>(f: CatchHandler<E, B, E2, R2>): Matcher<A | B, E2, R | R2> {
549
+ return new MatcherImpl<A | B, E2, R | R2>([AST.catchCause(this.cases, f as AnyCatch)]);
550
+ }
551
+
552
+ catch<B, E2, R2>(f: (e: E) => Fx.Fx<B, E2, R2>): Matcher<A | B, E2, R | R2> {
553
+ return this.catchCause((causeRef) =>
554
+ unwrap(
555
+ Effect.gen(function* () {
556
+ const cause = yield* causeRef;
557
+ const result = Cause.findFail(cause);
558
+ if (Result.isFailure(result)) {
559
+ return fromEffect(Effect.failCause(result.failure));
560
+ }
561
+ return f(result.success.error);
562
+ }),
563
+ ),
564
+ );
565
+ }
566
+
567
+ catchTag<const K extends Tags<E> | Arr.NonEmptyReadonlyArray<Tags<E>>, B, E2, R2>(
568
+ tag: K,
569
+ f: (
570
+ e: ExtractTag<NoInfer<E>, K extends Arr.NonEmptyReadonlyArray<string> ? K[number] : K>,
571
+ ) => Fx.Fx<B, E2, R2>,
572
+ ): Matcher<
573
+ A | B,
574
+ E2 | ExcludeTag<E, K extends Arr.NonEmptyReadonlyArray<string> ? K[number] : K>,
575
+ R | R2
576
+ > {
577
+ const rethrow = (cause: Cause.Cause<E>) =>
578
+ fromEffect(Effect.failCause(cause)) as Fx.Fx<
579
+ B,
580
+ E2 | ExcludeTag<E, K extends Arr.NonEmptyReadonlyArray<string> ? K[number] : K>,
581
+ R2
582
+ >;
583
+
584
+ return new MatcherImpl<
585
+ A | B,
586
+ E2 | ExcludeTag<E, K extends Arr.NonEmptyReadonlyArray<string> ? K[number] : K>,
587
+ R | R2
588
+ >([
589
+ AST.catchCause(this.cases, (causeRef) =>
590
+ unwrap(
591
+ Effect.gen(function* () {
592
+ const cause = yield* causeRef;
593
+ const result = Cause.findFail(cause);
594
+ if (Result.isFailure(result)) {
595
+ return rethrow(cause);
596
+ }
597
+ if (matchesTag(tag, result.success.error)) {
598
+ return f(result.success.error);
599
+ }
600
+ return rethrow(cause);
601
+ }),
602
+ ),
603
+ ),
604
+ ]);
605
+ }
606
+
607
+ layout<B, E2, R2>(layout: Layout<any, A, E, R, B, E2, R2>): Matcher<B, E | E2, R | R2> {
608
+ return new MatcherImpl<B, E | E2, R | R2>([
609
+ AST.layout(this.cases, layout as AnyLayout),
610
+ ]) as Matcher<B, E | E2, R | R2>;
165
611
  }
166
612
 
167
613
  pipe() {
168
- return pipeArguments(this, arguments)
614
+ return pipeArguments(this, arguments);
169
615
  }
170
616
  }
171
617
 
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)
618
+ function normalizeHandler<Params, B, E2 = never, R2 = never>(
619
+ handler: MatchHandler<Params, B, E2, R2>,
620
+ ): (params: RefSubject.RefSubject<Params>) => Fx.Fx<B, E2, R2> {
621
+ if (isMatchHandlerFn(handler)) return (params) => toFx(handler(params));
622
+ return () => toFx(handler);
179
623
  }
180
624
 
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
625
+ function toFx<A, E, R>(
626
+ value: Fx.Fx<A, E, R> | Stream.Stream<A, E, R> | Effect.Effect<A, E, R> | A,
627
+ ): Fx.Fx<A, E, R> {
628
+ if (isFx(value)) return value;
629
+ if (Stream.isStream(value)) return fromStream(value);
630
+ if (Effect.isEffect(value)) return fromEffect(value);
631
+ return succeed(value);
205
632
  }
206
633
 
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
- )
634
+ export const empty: Matcher<never> = new MatcherImpl([]);
635
+ export const match = empty.match.bind(empty);
636
+
637
+ export class RouteGuardError extends Schema.ErrorClass<RouteGuardError>(
638
+ "@typed/router/RouteGuardError",
639
+ )({
640
+ _tag: Schema.tag("RouteGuardError"),
641
+ path: Schema.String,
642
+ causes: Schema.Array(Schema.Unknown),
643
+ }) {}
644
+
645
+ export class RouteNotFound extends Schema.ErrorClass<RouteNotFound>("@typed/router/RouteNotFound")({
646
+ _tag: Schema.tag("RouteNotFound"),
647
+ path: Schema.String,
648
+ }) {}
649
+
650
+ export class RouteDecodeError extends Schema.ErrorClass<RouteDecodeError>(
651
+ "@typed/router/RouteDecodeError",
652
+ )({
653
+ _tag: Schema.tag("RouteDecodeError"),
654
+ path: Schema.String,
655
+ cause: Schema.String,
656
+ }) {}
221
657
 
222
658
  /**
223
- * @since 1.0.0
659
+ * @internal
224
660
  */
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>
661
+ export type CompiledEntry = {
662
+ readonly route: Route.Any;
663
+ readonly guard: AnyGuard;
664
+ readonly handler: AnyMatchHandler;
665
+ readonly layers: ReadonlyArray<AnyLayer>;
666
+ readonly layouts: ReadonlyArray<AnyLayout>;
667
+ readonly catches: ReadonlyArray<AnyCatch>;
668
+ readonly decode: (input: unknown) => Effect.Effect<any, Schema.SchemaError, any>;
669
+ };
670
+
671
+ export function run<M extends Matcher.Any>(
672
+ matcher: M,
245
673
  ): 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
674
+ Matcher.Success<M>,
675
+ Matcher.Error<M> | RouteNotFound | RouteDecodeError | RouteGuardError,
676
+ Matcher.Services<M> | Router | CurrentRoute | Scope.Scope
249
677
  > {
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)
259
- }
678
+ return unwrap(
679
+ Effect.gen(function* () {
680
+ const fiberId = yield* Effect.fiberId;
681
+ const rootScope = yield* Effect.scope;
682
+ const current = yield* CurrentRoute;
683
+ const prefixed = matcher.prefix(current.route);
684
+ const entries = compile(prefixed.cases);
685
+ const router = findMyWay.make<ReadonlyArray<CompiledEntry>>({
686
+ ignoreTrailingSlash: true,
687
+ caseSensitive: false,
688
+ });
689
+ const handlersByPath = new Map<string, Array<CompiledEntry>>();
690
+ const memoMap = yield* Layer.makeMemoMap;
691
+ const layerManager = makeLayerManager(memoMap, rootScope, fiberId);
692
+ const layoutManager = makeLayoutManager(rootScope, fiberId);
693
+ const catchManager = makeCatchManager(rootScope, fiberId);
260
694
 
261
- return catchRedirectError(matcher.getOrElse(() => onNotFound))
262
- })
695
+ for (const entry of entries) {
696
+ const path = entry.route.path;
697
+ const existing = handlersByPath.get(path);
698
+ if (existing !== undefined) {
699
+ existing.push(entry);
700
+ } else {
701
+ const list: Array<CompiledEntry> = [entry];
702
+ handlersByPath.set(path, list);
703
+ router.all(path, list);
704
+ }
705
+ }
263
706
 
264
- /**
265
- * @since 1.0.0
266
- */
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>
707
+ let currentState: {
708
+ entry: CompiledEntry;
709
+ params: RefSubject.RefSubject<any>;
710
+ fx: Fx.Fx<Matcher.Success<M>, Matcher.Error<M>, Matcher.Services<M> | Scope.Scope | Router>;
711
+ scope: Scope.Closeable;
712
+ } | null = null;
713
+
714
+ return CurrentPath.pipe(
715
+ mapEffect(
716
+ Effect.fn(function* (path) {
717
+ const result = router.find("GET", path);
718
+ if (result === undefined) return yield* new RouteNotFound({ path });
719
+
720
+ const input = { ...result.params, ...result.searchParams };
721
+ const entries = result.handler;
722
+ const guardCauses: Array<Cause.Cause<any>> = [];
723
+ let matchedEntry: CompiledEntry | undefined = undefined;
724
+ let matchedParams: any = undefined;
725
+ let matchedPrepared:
726
+ | {
727
+ services: AnyServiceMap;
728
+ commit: Effect.Effect<void>;
729
+ rollback: Effect.Effect<void>;
730
+ }
731
+ | undefined = undefined;
732
+
733
+ for (const entry of entries) {
734
+ const params = yield* Effect.mapErrorEager(
735
+ entry.decode(input),
736
+ (cause) =>
737
+ new RouteDecodeError({ path, cause: makeFormatterDefault()(cause.issue) }),
738
+ );
739
+
740
+ const prepared = yield* layerManager.prepare(entry.layers);
741
+ const guardExit = yield* entry
742
+ .guard(params)
743
+ .pipe(Effect.provideServices(prepared.services), Effect.exit);
744
+
745
+ if (Exit.isFailure(guardExit)) {
746
+ guardCauses.push(guardExit.cause);
747
+ yield* prepared.rollback;
748
+ continue;
749
+ }
750
+
751
+ if (Option.isNone(guardExit.value)) {
752
+ yield* prepared.rollback;
753
+ continue;
754
+ }
755
+
756
+ matchedEntry = entry;
757
+ matchedParams = guardExit.value.value;
758
+ matchedPrepared = prepared;
759
+ break;
760
+ }
761
+
762
+ if (matchedEntry === undefined || matchedPrepared === undefined) {
763
+ return yield* new RouteGuardError({ path, causes: guardCauses });
764
+ }
765
+
766
+ yield* matchedPrepared.commit;
767
+
768
+ if (currentState !== null && currentState.entry === matchedEntry) {
769
+ yield* RefSubject.set(currentState.params, matchedParams);
770
+ yield* layoutManager.updateParams(matchedEntry.layouts, matchedParams);
771
+ return currentState.fx;
772
+ }
773
+
774
+ if (currentState !== null) {
775
+ yield* Scope.close(currentState.scope, interrupt(fiberId));
776
+ currentState = null;
777
+ }
778
+
779
+ const scope = yield* Scope.fork(rootScope);
780
+ const paramsRef = yield* RefSubject.make(matchedParams).pipe(Scope.provide(scope));
781
+
782
+ const preparedServices = matchedPrepared.services as ServiceMap.ServiceMap<any>;
783
+ const handlerServices = ServiceMap.merge(
784
+ preparedServices,
785
+ ServiceMap.make(Scope.Scope, scope),
786
+ );
787
+
788
+ const handlerFx = matchedEntry
789
+ .handler(paramsRef)
790
+ .pipe(provideServices(handlerServices));
791
+ const withLayouts = yield* layoutManager.apply(
792
+ matchedEntry.layouts,
793
+ matchedParams,
794
+ handlerFx,
795
+ preparedServices,
796
+ );
797
+ const withCatches = yield* catchManager.apply(
798
+ matchedEntry.catches,
799
+ withLayouts,
800
+ preparedServices,
801
+ );
802
+ const fx = withCatches;
803
+
804
+ currentState = {
805
+ entry: matchedEntry,
806
+ params: paramsRef,
807
+ scope,
808
+ fx,
809
+ };
810
+
811
+ return currentState.fx;
812
+ }),
813
+ ),
814
+ skipRepeats,
815
+ switchMap(identity),
816
+ );
817
+ }),
818
+ );
819
+ }
820
+
821
+ type InputSucces<T> = [Matcher.Success<T> | Fx.Fx.Success<T>] extends [infer A] ? A : never;
822
+ type InputError<T> = [Matcher.Error<T> | Fx.Fx.Error<T>] extends [infer E] ? E : never;
823
+ type InputServices<T> = [Matcher.Services<T> | Fx.Fx.Services<T>] extends [infer R] ? R : never;
824
+
825
+ export const catchCause: {
826
+ <I extends Fx.Fx.Any | Matcher.Any, B, E2 = never, R2 = never>(
827
+ f: (
828
+ cause: RefSubject.RefSubject<
829
+ Cause.Cause<InputError<I> | RouteNotFound | RouteDecodeError | RouteGuardError>
830
+ >,
831
+ ) => Fx.Fx<B, E2, R2>,
832
+ ): (input: I) => Fx.Fx<InputSucces<I> | B, E2, InputServices<I> | R2 | Router | Scope.Scope>;
833
+
834
+ <I extends Fx.Fx.Any | Matcher.Any, B, E2 = never, R2 = never>(
835
+ input: I,
836
+ f: (
837
+ cause: RefSubject.RefSubject<
838
+ Cause.Cause<InputError<I> | RouteNotFound | RouteDecodeError | RouteGuardError>
839
+ >,
840
+ ) => Fx.Fx<B, E2, R2>,
841
+ ): Fx.Fx<InputSucces<I> | B, E2, InputServices<I> | R2 | Router | Scope.Scope>;
842
+ } = dual(
843
+ 2,
844
+ <A, E, R, B, E2, R2>(
845
+ input: Fx.Fx<A, E, R> | Matcher<A, E, R>,
846
+ f: (
847
+ cause: RefSubject.RefSubject<
848
+ Cause.Cause<E | RouteNotFound | RouteDecodeError | RouteGuardError>
849
+ >,
850
+ ) => Fx.Fx<B, E2, R2>,
851
+ ): Fx.Fx<A | B, E2, R | R2 | Router | Scope.Scope> => {
852
+ const eff = Effect.gen(function* () {
853
+ const fiberId = yield* Effect.fiberId;
854
+ const rootScope = yield* Effect.scope;
855
+ const fx = isFx(input) ? input : run(input);
856
+ const manager = makeCatchManager(rootScope, fiberId);
857
+ const result = yield* manager.apply(
858
+ [f],
859
+ fx,
860
+ ServiceMap.empty() as ServiceMap.ServiceMap<any>,
861
+ );
862
+ return result as Fx.Fx<A | B, E2, R | R2 | Router | Scope.Scope>;
863
+ });
864
+ return unwrap(eff);
865
+ },
866
+ );
867
+
868
+ export const catch_: {
869
+ <I extends Fx.Fx.Any | Matcher.Any, B, E2, R2>(
870
+ f: (e: InputError<I>) => Fx.Fx<B, E2, R2>,
871
+ ): (input: I) => Fx.Fx<InputSucces<I> | B, E2, InputServices<I> | R2 | Router | Scope.Scope>;
872
+ } = dual(
873
+ 2,
874
+ <I extends Fx.Fx.Any | Matcher.Any, B, E2, R2>(
875
+ input: I,
876
+ f: (e: InputError<I>) => Fx.Fx<B, E2, R2>,
877
+ ): Fx.Fx<InputSucces<I> | B, E2, InputServices<I> | R2 | Router | Scope.Scope> =>
878
+ catchCause(input, (causeRef) =>
879
+ unwrap(
880
+ Effect.gen(function* () {
881
+ const cause = yield* causeRef;
882
+ const result = Cause.findFail(cause);
883
+ if (Result.isFailure(result)) {
884
+ return fromEffect(Effect.failCause(result.failure));
885
+ }
886
+ return f(result.success.error as InputError<I>);
887
+ }),
888
+ ),
889
+ ),
890
+ );
891
+
892
+ export { catch_ as catch };
893
+
894
+ export const catchTag: {
895
+ <
896
+ I extends Fx.Fx.Any | Matcher.Any,
897
+ const K extends Tags<E> | Arr.NonEmptyReadonlyArray<Tags<E>>,
898
+ E,
899
+ B,
900
+ E2,
901
+ R2,
902
+ >(
903
+ k: K,
904
+ f: (e: InputError<I>) => Fx.Fx<B, E2, R2>,
905
+ ): (input: I) => Fx.Fx<InputSucces<I> | B, E2, InputServices<I> | R2 | Router | Scope.Scope>;
906
+
907
+ <
908
+ I extends Fx.Fx.Any | Matcher.Any,
909
+ const K extends Tags<InputError<I>> | Arr.NonEmptyReadonlyArray<Tags<InputError<I>>>,
910
+ B,
911
+ E2,
912
+ R2,
913
+ >(
914
+ input: I,
915
+ k: K,
916
+ f: (
917
+ e: ExtractTag<InputError<I>, K extends Arr.NonEmptyReadonlyArray<string> ? K[number] : K>,
918
+ ) => Fx.Fx<B, E2, R2>,
279
919
  ): 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>
287
- ): 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
291
- > {
292
- return notFound(router, Fx.fromEffect(onNotFound))
293
- })
920
+ InputSucces<I> | B,
921
+ E2 | ExcludeTag<InputError<I>, K extends Arr.NonEmptyReadonlyArray<string> ? K[number] : K>,
922
+ InputServices<I> | R2 | Router | Scope.Scope
923
+ >;
924
+ } = dual(
925
+ 3,
926
+ <
927
+ I extends Fx.Fx.Any | Matcher.Any,
928
+ const K extends Tags<InputError<I>> | Arr.NonEmptyReadonlyArray<Tags<InputError<I>>>,
929
+ B,
930
+ E2,
931
+ R2,
932
+ >(
933
+ input: I,
934
+ k: K,
935
+ f: (e: InputError<I>) => Fx.Fx<B, E2, R2>,
936
+ ): Fx.Fx<InputSucces<I> | B, E2, InputServices<I> | R2 | Router | Scope.Scope> =>
937
+ catchCause(input, (causeRef) =>
938
+ unwrap(
939
+ Effect.gen(function* () {
940
+ const cause = yield* causeRef;
941
+ const result = Cause.findFail(cause);
942
+ if (Result.isFailure(result)) {
943
+ return fromEffect(Effect.failCause(result.failure));
944
+ }
945
+ if (matchesTag(k, result.success.error)) {
946
+ return f(
947
+ result.success.error as ExtractTag<
948
+ InputError<I>,
949
+ K extends Arr.NonEmptyReadonlyArray<string> ? K[number] : K
950
+ >,
951
+ );
952
+ }
953
+ return fromEffect(Effect.fail(result.success.error as E2));
954
+ }),
955
+ ),
956
+ ),
957
+ );
958
+
959
+ export const redirectTo =
960
+ (path: string) =>
961
+ <I extends Fx.Fx.Any | Matcher.Any>(
962
+ input: I,
963
+ ): Fx.Fx<InputSucces<I>, never, Router | Scope.Scope | InputServices<I>> =>
964
+ catchCause(input, (_) =>
965
+ Navigation.navigate(path).pipe(
966
+ Effect.matchCause({
967
+ onFailure: () => never,
968
+ onSuccess: () => never,
969
+ }),
970
+ unwrap,
971
+ ),
972
+ );
973
+
974
+ const hasTag = (u: unknown): u is { readonly _tag: string } =>
975
+ typeof u === "object" &&
976
+ u !== null &&
977
+ "_tag" in u &&
978
+ typeof (u as Record<string, unknown>)["_tag"] === "string";
979
+
980
+ const matchesTag = <E, K extends string>(
981
+ tag: K | Arr.NonEmptyReadonlyArray<K>,
982
+ error: E,
983
+ ): error is ExtractTag<E, K> => {
984
+ if (!hasTag(error)) return false;
985
+ if (typeof tag === "string") return error._tag === tag;
986
+ return tag.some((t) => t === error._tag);
987
+ };
988
+
989
+ function normalizeDependencies(
990
+ dependencies: ReadonlyArray<AnyDependency>,
991
+ ): ReadonlyArray<AnyLayer> {
992
+ return dependencies.map((dep) =>
993
+ Layer.isLayer(dep)
994
+ ? dep
995
+ : (Layer.succeedServices(dep as ServiceMap.ServiceMap<any>) as AnyLayer),
996
+ );
997
+ }
998
+
999
+ function getGuard<I, O, E, R>(guard: GuardInput<I, O, E, R>): GuardType<I, O, E, R> {
1000
+ return "asGuard" in guard ? guard.asGuard() : guard;
1001
+ }
1002
+
1003
+ function defaultGuard<A>(): GuardType<A, A> {
1004
+ return Effect.succeedSome;
1005
+ }
1006
+
1007
+ function mergeLayers(layers: ReadonlyArray<AnyLayer>): AnyLayer {
1008
+ if (layers.length === 1) return layers[0];
1009
+ let current = layers[0];
1010
+ for (let i = 1; i < layers.length; i++) {
1011
+ current = Layer.merge(current, layers[i]);
1012
+ }
1013
+ return current;
1014
+ }
294
1015
 
295
1016
  /**
296
- * @since 1.0.0
1017
+ * @internal
297
1018
  */
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)
1019
+ export function compile(cases: ReadonlyArray<MatchAst>): ReadonlyArray<CompiledEntry> {
1020
+ const entries: Array<CompiledEntry> = [];
1021
+
1022
+ const visit = (
1023
+ matches: ReadonlyArray<MatchAst>,
1024
+ context: {
1025
+ readonly layers: ReadonlyArray<AnyLayer>;
1026
+ readonly layouts: ReadonlyArray<AnyLayout>;
1027
+ readonly catches: ReadonlyArray<AnyCatch>;
1028
+ readonly prefixes: ReadonlyArray<RouteAst>;
1029
+ },
1030
+ ): void => {
1031
+ for (const match of matches) {
1032
+ switch (match.type) {
1033
+ case "route": {
1034
+ const baseRoute = makeRoute(match.route);
1035
+ const prefixedRoute = applyPrefixes(baseRoute, context.prefixes);
1036
+ entries.push({
1037
+ route: prefixedRoute,
1038
+ guard: getGuard(match.guard as GuardInput<any, any, any, any>),
1039
+ handler: normalizeHandler(match.handler),
1040
+ layers: context.layers,
1041
+ layouts: context.layouts,
1042
+ catches: context.catches,
1043
+ decode: Schema.decodeUnknownEffect(prefixedRoute.paramsSchema),
1044
+ });
1045
+ break;
1046
+ }
1047
+ case "layer": {
1048
+ const merged = mergeLayers(match.deps);
1049
+ visit(match.matches, {
1050
+ ...context,
1051
+ layers: [...context.layers, merged],
1052
+ });
1053
+ break;
1054
+ }
1055
+ case "layout": {
1056
+ visit(match.matches, {
1057
+ ...context,
1058
+ layouts: [...context.layouts, match.layout as AnyLayout],
1059
+ });
1060
+ break;
1061
+ }
1062
+ case "prefixed": {
1063
+ visit(match.matches, {
1064
+ ...context,
1065
+ prefixes: [...context.prefixes, match.prefix],
1066
+ });
1067
+ break;
1068
+ }
1069
+ case "catch": {
1070
+ visit(match.matches, {
1071
+ ...context,
1072
+ catches: [...context.catches, match.f as AnyCatch],
1073
+ });
1074
+ break;
1075
+ }
1076
+ }
1077
+ }
1078
+ };
1079
+
1080
+ visit(cases, { layers: [], layouts: [], catches: [], prefixes: [] });
1081
+ return entries;
302
1082
  }
303
1083
 
1084
+ function applyPrefixes(route: Route.Any, prefixes: ReadonlyArray<RouteAst>): Route.Any {
1085
+ if (prefixes.length === 0) return route;
1086
+ const prefixRoutes = prefixes.map((prefix) => makeRoute(prefix));
1087
+ return Join(...prefixRoutes, route);
1088
+ }
1089
+
1090
+ // Parallel scope cleanup helper
1091
+ const closeScopes = (scopes: Iterable<Scope.Closeable>, fiberId: number) =>
1092
+ Effect.forEach(scopes, (scope) => Scope.close(scope, interrupt(fiberId)), {
1093
+ concurrency: "unbounded",
1094
+ discard: true,
1095
+ });
1096
+
304
1097
  /**
305
- * @since 1.0.0
1098
+ * @internal
306
1099
  */
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
- })
1100
+ export function makeLayerManager(memoMap: Layer.MemoMap, rootScope: Scope.Scope, fiberId: number) {
1101
+ const states = new Map<AnyLayer, { scope: Scope.Closeable; services: AnyServiceMap }>();
1102
+ let order: ReadonlyArray<AnyLayer> = [];
1103
+ let cachedDesiredSet: Set<AnyLayer> | undefined = undefined;
1104
+ let cachedOrder: ReadonlyArray<AnyLayer> | undefined = undefined;
1105
+
1106
+ const prepare = (desired: ReadonlyArray<AnyLayer>) =>
1107
+ Effect.gen(function* () {
1108
+ const desiredSet =
1109
+ cachedOrder === desired
1110
+ ? cachedDesiredSet!
1111
+ : ((cachedDesiredSet = new Set(desired)), (cachedOrder = desired), cachedDesiredSet);
1112
+ const removed = order.filter((layer) => !desiredSet.has(layer));
1113
+ const added: Array<AnyLayer> = [];
1114
+ let services = ServiceMap.empty();
1115
+
1116
+ for (const layer of desired) {
1117
+ const existing = states.get(layer);
1118
+ if (existing) {
1119
+ services = ServiceMap.merge(services, existing.services);
1120
+ continue;
1121
+ }
1122
+
1123
+ const scope = yield* Scope.fork(rootScope);
1124
+ const buildExit = yield* Layer.buildWithMemoMap(layer, memoMap, scope).pipe(
1125
+ Effect.provideServices(services),
1126
+ Effect.exit,
1127
+ );
1128
+
1129
+ if (Exit.isFailure(buildExit)) {
1130
+ for (let i = added.length - 1; i >= 0; i--) {
1131
+ const addedLayer = added[i];
1132
+ const addedState = states.get(addedLayer);
1133
+ if (addedState) {
1134
+ states.delete(addedLayer);
1135
+ yield* Scope.close(addedState.scope, interrupt(fiberId));
1136
+ }
1137
+ }
1138
+ yield* Scope.close(scope, buildExit);
1139
+ return yield* Effect.failCause(buildExit.cause);
1140
+ }
1141
+
1142
+ const servicesForLayer = buildExit.value;
1143
+ services = ServiceMap.merge(services, servicesForLayer);
1144
+ states.set(layer, { scope, services: servicesForLayer });
1145
+ added.push(layer);
1146
+ }
1147
+
1148
+ const commit = Effect.gen(function* () {
1149
+ for (let i = removed.length - 1; i >= 0; i--) {
1150
+ const layer = removed[i];
1151
+ const state = states.get(layer);
1152
+ if (state) {
1153
+ states.delete(layer);
1154
+ yield* Scope.close(state.scope, interrupt(fiberId));
1155
+ }
1156
+ }
1157
+ order = desired;
1158
+ });
1159
+
1160
+ const rollback = Effect.gen(function* () {
1161
+ for (let i = added.length - 1; i >= 0; i--) {
1162
+ const layer = added[i];
1163
+ const state = states.get(layer);
1164
+ if (state) {
1165
+ states.delete(layer);
1166
+ yield* Scope.close(state.scope, interrupt(fiberId));
1167
+ }
1168
+ }
1169
+ });
1170
+
1171
+ return { services, commit, rollback };
1172
+ });
1173
+
1174
+ return { prepare };
1175
+ }
330
1176
 
331
1177
  /**
332
- * @since 1.0.0
1178
+ * @internal
333
1179
  */
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
- )
1180
+ export function makeLayoutManager(rootScope: Scope.Scope, fiberId: number) {
1181
+ const states = new Map<
1182
+ AnyLayout,
1183
+ {
1184
+ params: RefSubject.RefSubject<any>;
1185
+ content: RefSubject.RefSubject<Fx.Fx<any, any, any>>;
1186
+ fx: Fx.Fx<any, any, any>;
1187
+ scope: Scope.Closeable;
1188
+ }
1189
+ >();
1190
+ let active: ReadonlyArray<AnyLayout> = [];
1191
+
1192
+ const removeUnused = (layouts: ReadonlyArray<AnyLayout>) =>
1193
+ Effect.gen(function* () {
1194
+ const next = new Set(layouts);
1195
+ const removed = active.filter((layout) => !next.has(layout));
1196
+ const scopes = removed.map((layout) => {
1197
+ const state = states.get(layout)!;
1198
+ states.delete(layout);
1199
+ return state.scope;
1200
+ });
1201
+ yield* closeScopes(scopes, fiberId);
1202
+ active = layouts;
1203
+ });
1204
+
1205
+ const apply = (
1206
+ layouts: ReadonlyArray<AnyLayout>,
1207
+ paramsValue: any,
1208
+ inner: Fx.Fx<any, any, any>,
1209
+ services: ServiceMap.ServiceMap<any>,
1210
+ ) =>
1211
+ Effect.gen(function* () {
1212
+ let current = inner;
1213
+ for (let i = layouts.length - 1; i >= 0; i--) {
1214
+ const layout = layouts[i];
1215
+ const state = states.get(layout);
1216
+ if (state === undefined) {
1217
+ const scope = yield* Scope.fork(rootScope);
1218
+ const params = yield* RefSubject.make(paramsValue).pipe(Scope.provide(scope));
1219
+ const content = yield* RefSubject.make<Fx.Fx<any, any, any>>(Effect.succeed(current), {
1220
+ eq: (left, right) => left === right,
1221
+ }).pipe(Scope.provide(scope));
1222
+ const fx = layout({ params, content: content.pipe(switchMap(identity)) }).pipe(
1223
+ provideServices(ServiceMap.merge(services, ServiceMap.make(Scope.Scope, scope))),
1224
+ );
1225
+ states.set(layout, { params, content, fx, scope });
1226
+ current = fx;
1227
+ } else {
1228
+ yield* RefSubject.set(state.params, paramsValue);
1229
+ // @effect-diagnostics-next-line floatingEffect:off
1230
+ yield* RefSubject.set(state.content, current);
1231
+ current = state.fx;
1232
+ }
1233
+ }
1234
+ yield* removeUnused(layouts);
1235
+ return current;
1236
+ });
1237
+
1238
+ const updateParams = (layouts: ReadonlyArray<AnyLayout>, paramsValue: any) =>
1239
+ Effect.forEach(
1240
+ layouts,
1241
+ (layout) => {
1242
+ const state = states.get(layout);
1243
+ return state !== undefined ? RefSubject.set(state.params, paramsValue) : Effect.void;
1244
+ },
1245
+ { discard: true },
1246
+ );
1247
+
1248
+ return { apply, updateParams };
1249
+ }
377
1250
 
378
1251
  /**
379
- * @since 1.0.0
1252
+ * @internal
380
1253
  */
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
- }
1254
+ export function makeCatchManager(rootScope: Scope.Scope, fiberId: number) {
1255
+ const states = new Map<
1256
+ AnyCatch,
1257
+ {
1258
+ causes: RefSubject.RefSubject<Cause.Cause<any>>;
1259
+ content: RefSubject.RefSubject<Fx.Fx<any, any, any>>;
1260
+ fx: Fx.Fx<any, any, any>;
1261
+ scope: Scope.Closeable;
1262
+ }
1263
+ >();
1264
+ let active: ReadonlyArray<AnyCatch> = [];
1265
+
1266
+ const removeUnused = (catches: ReadonlyArray<AnyCatch>) =>
1267
+ Effect.gen(function* () {
1268
+ const next = new Set(catches);
1269
+ const removed = active.filter((c) => !next.has(c));
1270
+ const scopes = removed.map((c) => {
1271
+ const state = states.get(c)!;
1272
+ states.delete(c);
1273
+ return state.scope;
1274
+ });
1275
+ yield* closeScopes(scopes, fiberId);
1276
+ active = catches;
1277
+ });
1278
+
1279
+ const apply = (
1280
+ catches: ReadonlyArray<AnyCatch>,
1281
+ inner: Fx.Fx<any, any, any>,
1282
+ services: ServiceMap.ServiceMap<any>,
1283
+ ) =>
1284
+ Effect.gen(function* () {
1285
+ let current = inner;
1286
+ for (let i = catches.length - 1; i >= 0; i--) {
1287
+ const catcher = catches[i];
1288
+ const state = states.get(catcher);
1289
+ if (state === undefined) {
1290
+ const scope = yield* Scope.fork(rootScope);
1291
+ const causes = yield* RefSubject.make<Cause.Cause<any>>(Cause.fail(undefined)).pipe(
1292
+ Scope.provide(scope),
1293
+ );
1294
+ const content = yield* RefSubject.make<Fx.Fx<any, any, any>>(Effect.succeed(current), {
1295
+ eq: (left, right) => left === right,
1296
+ }).pipe(Scope.provide(scope));
1297
+ const fallback = catcher(causes).pipe(
1298
+ provideServices(ServiceMap.merge(services, ServiceMap.make(Scope.Scope, scope))),
1299
+ );
1300
+ const fx = content.pipe(
1301
+ switchMap(identity),
1302
+ exit,
1303
+ mapEffect(
1304
+ Effect.fn(function* (e) {
1305
+ if (isSuccess(e)) return succeed(e.value);
1306
+ yield* RefSubject.set(causes, e.cause);
1307
+ return fallback;
1308
+ }),
1309
+ ),
1310
+ skipRepeats,
1311
+ switchMap(identity),
1312
+ );
1313
+ states.set(catcher, { causes, content, fx, scope });
1314
+ current = fx;
1315
+ } else {
1316
+ // @effect-diagnostics-next-line floatingEffect:off
1317
+ yield* RefSubject.set(state.content, current);
1318
+ current = state.fx;
1319
+ }
1320
+ }
1321
+ yield* removeUnused(catches);
1322
+ return current;
1323
+ });
1324
+
1325
+ return { apply };
385
1326
  }