ui-router-effect 0.1.2 → 0.3.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.
package/README.md CHANGED
@@ -14,16 +14,18 @@ npm install ui-router-effect effect
14
14
 
15
15
  ## Features
16
16
 
17
- - **`routeRef(router)`**: a `SubscriptionRef<RouteSnapshot>` of the current state, params and transition. There is one ref, and one `onSuccess` hook, per router, attached lazily on first use. Every value it holds is a settled route.
17
+ - **`routeRef(router)`**: a `SubscriptionRef<TypedRouteSnapshot>` of the current state, params and transition. There is one ref, and one `onSuccess` hook, per router, attached lazily on first use. Every value it holds is a settled route.
18
+ - **`pendingRef(router)`** and **`errorRef(router)`**: `SubscriptionRef`s of the newest transition, kept out of `RouteSnapshot`. Two refs, and one `onStart` hook, per router, attached lazily on first use of either. `pendingRef` holds the transition still in flight, cleared when that transition settles, however it settles, unless a newer one has started. `errorRef` holds that transition's `RejectType.ERROR` rejection, cleared when the next transition starts.
18
19
  - **`RouteSnapshot`** and **`snapshotRoute(router, transition?)`**: the value the ref holds. Its `includes()` evaluates `StateService.includes` against the snapshot, not the live router.
20
+ - **`RouteTypes`**: an empty interface an app or a registry fills in by module augmentation. Its `snapshot` member types the ref's value, so a snapshot whose `includes(name)` is a type predicate narrows `current` and `params`. Empty, the ref holds a `RouteSnapshot`. `snapshotRoute` stays untyped.
19
21
 
20
22
  ## Usage
21
23
 
22
24
  ```typescript
23
- import { Effect, Stream } from 'effect';
25
+ import { Effect, Stream, SubscriptionRef } from 'effect';
24
26
  import { routeRef } from 'ui-router-effect';
25
27
 
26
- const names = routeRef(router).changes.pipe(
28
+ const names = SubscriptionRef.changes(routeRef(router)).pipe(
27
29
  Stream.map((route) => route.current?.name),
28
30
  Stream.changes,
29
31
  );
@@ -31,6 +33,25 @@ const names = routeRef(router).changes.pipe(
31
33
  Effect.runFork(Stream.runForEach(names, (name) => Effect.log(name)));
32
34
  ```
33
35
 
36
+ ### Loading and error state
37
+
38
+ ```ts
39
+ import { Effect, Stream, SubscriptionRef } from 'effect';
40
+ import { errorRef, pendingRef } from 'ui-router-effect';
41
+
42
+ const status = Stream.zipLatestWith(
43
+ SubscriptionRef.changes(pendingRef(router)),
44
+ SubscriptionRef.changes(errorRef(router)),
45
+ (pending, error) => (pending ? 'loading' : error ? 'error' : 'idle'),
46
+ ).pipe(Stream.changes);
47
+
48
+ Effect.runFork(
49
+ Stream.runForEach(status, (value) =>
50
+ Effect.sync(() => (document.body.dataset.routeStatus = value)),
51
+ ),
52
+ );
53
+ ```
54
+
34
55
  ## Dependencies
35
56
 
36
57
  `@uirouter/core` and `effect` are peer dependencies. `lit` and `lit-ui-router` are not, and the `bundleProbe` claim in `package.json` holds the entry free of them.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export { errorRef, pendingRef } from "./transition-refs.js";
1
2
  export { routeRef } from "./route-ref.js";
2
3
  export { type RouteSnapshot, snapshotRoute } from "./shared/route-snapshot.js";
4
+ export { type RouteTypes, type TypedRouteSnapshot } from "./route-types.js";
3
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","mappings":"AAAA,SAAS,gBAAgB;AAEzB,cAAc,eAAe,qBAAqB","names":[],"ignoreList":[],"sources":["../src/index.ts"]}
1
+ {"version":3,"file":"index.d.ts","mappings":"AAAA,SAAS,UAAU,kBAAkB;AAErC,SAAS,gBAAgB;AAEzB,cAAc,eAAe,qBAAqB;AAElD,cAAc,iBAAiB,0BAA0B","names":[],"ignoreList":[],"sources":["../src/index.ts"]}
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ export { errorRef, pendingRef } from "./transition-refs.js";
1
2
  export { routeRef } from "./route-ref.js";
2
3
  export { snapshotRoute } from "./shared/route-snapshot.js";
3
4
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","mappings":"AAAA,SAAS,gBAAgB;AAEzB,SAA6B,qBAAqB","names":[],"ignoreList":[],"sources":["../src/index.ts"]}
1
+ {"version":3,"file":"index.js","mappings":"AAAA,SAAS,UAAU,kBAAkB;AAErC,SAAS,gBAAgB;AAEzB,SAA6B,qBAAqB","names":[],"ignoreList":[],"sources":["../src/index.ts"]}
@@ -1,6 +1,6 @@
1
1
  import { SubscriptionRef } from "effect";
2
2
  import { UIRouter } from "@uirouter/core";
3
- import { RouteSnapshot } from "./shared/route-snapshot.js";
3
+ import type { TypedRouteSnapshot } from "./route-types.js";
4
4
  /**
5
5
  * The route ref for a router — one per router instance, attached lazily.
6
6
  *
@@ -8,6 +8,7 @@ import { RouteSnapshot } from "./shared/route-snapshot.js";
8
8
  * ref and registers the single `onSuccess` hook that feeds it, and both live
9
9
  * as long as the router does. `onSuccess` is the only hook a later transition
10
10
  * cannot supersede, so every value the ref ever holds is a settled route.
11
+ * Its snapshot is typed by `RouteTypes` when a registry fills it in.
11
12
  */
12
- export declare function routeRef(router: UIRouter): SubscriptionRef.SubscriptionRef<RouteSnapshot>;
13
+ export declare function routeRef(router: UIRouter): SubscriptionRef.SubscriptionRef<TypedRouteSnapshot>;
13
14
  //# sourceMappingURL=route-ref.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"route-ref.d.ts","mappings":"AAAA,SAAiB,uBAAuB;AACxC,SAAS,gBAAgB;AAEzB,SAAS,qBAAoC;;;;;;;;;AAe7C,OAAO,iBAAS,SACd,QAAQ,WACP,gBAAgB,gBAAgB","names":[],"ignoreList":[],"sources":["../src/route-ref.ts"]}
1
+ {"version":3,"file":"route-ref.d.ts","mappings":"AAAA,SAAiB,uBAAuB;AACxC,SAAS,gBAAgB;AAIzB,cAAc,0BAA0B;;;;;;;;;;AAgBxC,OAAO,iBAAS,SACd,QAAQ,WACP,gBAAgB,gBAAgB","names":[],"ignoreList":[],"sources":["../src/route-ref.ts"]}
@@ -1 +1 @@
1
- {"version":3,"file":"route-ref.js","mappings":"AAAA,SAAS,QAAQ,uBAAuB;AAGxC,SAAwB,qBAAqB;AAE7C,MAAM,YAAY,IAAI,QAGpB;AAUF,OAAO,SAAS,SACd,QACgD;CAChD,MAAM,WAAW,UAAU,IAAI,MAAM;CAErC,IAAI,UAAU,OAAO;CACrB,MAAM,MAAM,OAAO,QAAQ,gBAAgB,KAAK,cAAc,MAAM,CAAC,CAAC;CACtE,UAAU,IAAI,QAAQ,GAAG;CACzB,OAAO,kBAAkB,UAAU,CAAC,IAAI,eAAe;EACrD,OAAO,QAAQ,gBAAgB,IAAI,KAAK,cAAc,QAAQ,UAAU,CAAC,CAAC;CAC5E,CAAC;CAED,OAAO;AACT","names":[],"ignoreList":[],"sources":["../src/route-ref.ts"]}
1
+ {"version":3,"file":"route-ref.js","mappings":"AAAA,SAAS,QAAQ,uBAAuB;AAGxC,SAAS,qBAAqB;AAI9B,MAAM,YAAY,IAAI,QAGpB;AAWF,OAAO,SAAS,SACd,QACqD;CACrD,MAAM,WAAW,UAAU,IAAI,MAAM;CAErC,IAAI,UAAU,OAAO;CACrB,MAAM,MAAM,OAAO,QAAQ,gBAAgB,KAAK,cAAc,MAAM,CAAC,CAAC;CACtE,UAAU,IAAI,QAAQ,GAAG;CACzB,OAAO,kBAAkB,UAAU,CAAC,IAAI,eAAe;EACrD,OAAO,QAAQ,gBAAgB,IAAI,KAAK,cAAc,QAAQ,UAAU,CAAC,CAAC;CAC5E,CAAC;CAED,OAAO;AACT","names":[],"ignoreList":[],"sources":["../src/route-ref.ts"]}
@@ -0,0 +1,34 @@
1
+ import type { RouteSnapshot } from "./shared/route-snapshot.js";
2
+ /**
3
+ * Typed route snapshots, filled in by module augmentation. Empty, the
4
+ * snapshot is {@link RouteSnapshot}, as it always has been; nothing here
5
+ * emits JavaScript.
6
+ *
7
+ * An app or a registry such as `ui-router-registry` fills it in. Its member:
8
+ *
9
+ * - `snapshot`: the type `routeRef`'s ref holds, a typed view of
10
+ * {@link RouteSnapshot} whose `includes(name)` narrows `current` and
11
+ * `params`.
12
+ *
13
+ * ```ts
14
+ * declare module 'ui-router-effect' {
15
+ * interface RouteTypes {
16
+ * snapshot: AppRouteSnapshot;
17
+ * }
18
+ * }
19
+ * ```
20
+ *
21
+ * @category types
22
+ */
23
+ export interface RouteTypes {}
24
+ /**
25
+ * The route snapshot as {@link RouteTypes} types it: its `snapshot` member,
26
+ * or {@link RouteSnapshot} until it is filled in.
27
+ *
28
+ * @category types
29
+ */
30
+ export type TypedRouteSnapshot = RouteTypes extends {
31
+ /** The filled-in snapshot type. */
32
+ snapshot: infer S;
33
+ } ? S : RouteSnapshot;
34
+ //# sourceMappingURL=route-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-types.d.ts","mappings":"AAAA,cAAc,qBAAqB;;;;;;;;;;;;;;;;;;;;;;AAwBnC,iBAAiB,WAAW,CAAC;;;;;;;AAQ7B,YAAY,qBAAqB,mBAAmB;;CAElD,gBAAgB;AAClB,IACI,IACA","names":[],"ignoreList":[],"sources":["../src/route-types.ts"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=route-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-types.js","mappings":"","names":[],"ignoreList":[],"sources":["../src/route-types.ts"]}
@@ -0,0 +1,33 @@
1
+ import { Rejection, Transition, UIRouter } from "@uirouter/core";
2
+ /** The router's newest transition: in flight, or how it last failed. */
3
+ export interface TransitionStatus {
4
+ /** The newest transition still in flight; `undefined` when none is. */
5
+ readonly pending: Transition | undefined;
6
+ /**
7
+ * The newest transition's `RejectType.ERROR` rejection; `undefined` while
8
+ * one is in flight, after a success, and after any other rejection.
9
+ */
10
+ readonly error: Rejection | undefined;
11
+ }
12
+ /**
13
+ * Mirrors the status of the router's newest transition into `set`, one
14
+ * whole {@link TransitionStatus} per change: `{ pending: transition }` when
15
+ * it starts, then `{ pending: undefined, error }` once it settles, however it
16
+ * settles. `error` is the transition's `error()` only for a
17
+ * `RejectType.ERROR` rejection; superseded, redirected, aborted and ignored
18
+ * transitions leave it `undefined`. Invalid transitions are rejected before
19
+ * `onStart`, so they never reach the tracker.
20
+ *
21
+ * `onStart` records each transition as the newest. Its promise settles the
22
+ * status only while it is still the newest, so a superseded transition never
23
+ * clears or fails its successor. `onFinish` would miss failures: it never
24
+ * fires for a transition that errors or is aborted.
25
+ *
26
+ * A transition already running when this attaches is picked up too, so a
27
+ * mirror created mid-navigation reads it as pending.
28
+ *
29
+ * @returns the `onStart` hook's deregistration function. A transition
30
+ * already pending still settles when it does.
31
+ */
32
+ export declare function trackTransitionStatus(router: UIRouter, set: (status: TransitionStatus) => void): () => void;
33
+ //# sourceMappingURL=transition-status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transition-status.d.ts","mappings":"AAAA,SAAS,WAAuB,YAAY,gBAAgB;;AAG5D,iBAAiB,iBAAiB;;UAEvB,SAAS;;;;;UAKT,OAAO;AAClB;;;;;;;;;;;;;;;;;;;;;AAsBA,OAAO,iBAAS,sBACd,QAAQ,UACR,MAAM,QAAQ","names":[],"ignoreList":[],"sources":["../../../shared/transition-status/src/transition-status.ts"],"sourcesContent":["import { Rejection, RejectType, Transition, UIRouter } from '@uirouter/core';\n\n/** The router's newest transition: in flight, or how it last failed. */\nexport interface TransitionStatus {\n /** The newest transition still in flight; `undefined` when none is. */\n readonly pending: Transition | undefined;\n /**\n * The newest transition's `RejectType.ERROR` rejection; `undefined` while\n * one is in flight, after a success, and after any other rejection.\n */\n readonly error: Rejection | undefined;\n}\n\n/**\n * Mirrors the status of the router's newest transition into `set`, one\n * whole {@link TransitionStatus} per change: `{ pending: transition }` when\n * it starts, then `{ pending: undefined, error }` once it settles, however it\n * settles. `error` is the transition's `error()` only for a\n * `RejectType.ERROR` rejection; superseded, redirected, aborted and ignored\n * transitions leave it `undefined`. Invalid transitions are rejected before\n * `onStart`, so they never reach the tracker.\n *\n * `onStart` records each transition as the newest. Its promise settles the\n * status only while it is still the newest, so a superseded transition never\n * clears or fails its successor. `onFinish` would miss failures: it never\n * fires for a transition that errors or is aborted.\n *\n * A transition already running when this attaches is picked up too, so a\n * mirror created mid-navigation reads it as pending.\n *\n * @returns the `onStart` hook's deregistration function. A transition\n * already pending still settles when it does.\n */\nexport function trackTransitionStatus(\n router: UIRouter,\n set: (status: TransitionStatus) => void,\n): () => void {\n let newest: Transition | undefined;\n\n const start = (transition: Transition): void => {\n newest = transition;\n set({ pending: transition, error: undefined });\n\n const settle = (): void => {\n if (newest !== transition) return;\n newest = undefined;\n // SAFETY: `error()` is undefined after a success; core types it as `Rejection`.\n const error = transition.error() as Rejection | undefined;\n set({\n pending: undefined,\n error: error?.type === RejectType.ERROR ? error : undefined,\n });\n };\n\n // Handled here, the rejection still reaches the transition's own callers and onError hooks.\n void transition.promise.then(settle, settle);\n };\n\n const running = router.globals.transition;\n\n // SAFETY: `success` stays undefined until the transition settles; core types it as `boolean`.\n if (running && (running.success as boolean | undefined) === undefined) {\n start(running);\n }\n\n // SAFETY: `onStart` returns its deregistration function, typed only as `Function`.\n return router.transitionService.onStart({}, start) as () => void;\n}\n"]}
@@ -0,0 +1,27 @@
1
+ import { RejectType } from "@uirouter/core";
2
+ export function trackTransitionStatus(router, set) {
3
+ let newest;
4
+ const start = (transition) => {
5
+ newest = transition;
6
+ set({
7
+ pending: transition,
8
+ error: undefined
9
+ });
10
+ const settle = () => {
11
+ if (newest !== transition) return;
12
+ newest = undefined;
13
+ const error = transition.error();
14
+ set({
15
+ pending: undefined,
16
+ error: error?.type === RejectType.ERROR ? error : undefined
17
+ });
18
+ };
19
+ void transition.promise.then(settle, settle);
20
+ };
21
+ const running = router.globals.transition;
22
+ if (running && running.success === undefined) {
23
+ start(running);
24
+ }
25
+ return router.transitionService.onStart({}, start);
26
+ }
27
+ //# sourceMappingURL=transition-status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transition-status.js","mappings":"AAAA,SAAoB,kBAAwC;AAiC5D,OAAO,SAAS,sBACd,QACA,KACY;CACZ,IAAI;CAEJ,MAAM,SAAS,eAAiC;EAC9C,SAAS;EACT,IAAI;GAAE,SAAS;GAAY,OAAO;EAAU,CAAC;EAE7C,MAAM,eAAqB;GACzB,IAAI,WAAW,YAAY;GAC3B,SAAS;GAET,MAAM,QAAQ,WAAW,MAAM;GAC/B,IAAI;IACF,SAAS;IACT,OAAO,OAAO,SAAS,WAAW,QAAQ,QAAQ;GACpD,CAAC;EACH;EAGA,KAAK,WAAW,QAAQ,KAAK,QAAQ,MAAM;CAC7C;CAEA,MAAM,UAAU,OAAO,QAAQ;CAG/B,IAAI,WAAY,QAAQ,YAAoC,WAAW;EACrE,MAAM,OAAO;CACf;CAGA,OAAO,OAAO,kBAAkB,QAAQ,CAAC,GAAG,KAAK;AACnD","names":[],"ignoreList":[],"sources":["../../../shared/transition-status/src/transition-status.ts"],"sourcesContent":["import { Rejection, RejectType, Transition, UIRouter } from '@uirouter/core';\n\n/** The router's newest transition: in flight, or how it last failed. */\nexport interface TransitionStatus {\n /** The newest transition still in flight; `undefined` when none is. */\n readonly pending: Transition | undefined;\n /**\n * The newest transition's `RejectType.ERROR` rejection; `undefined` while\n * one is in flight, after a success, and after any other rejection.\n */\n readonly error: Rejection | undefined;\n}\n\n/**\n * Mirrors the status of the router's newest transition into `set`, one\n * whole {@link TransitionStatus} per change: `{ pending: transition }` when\n * it starts, then `{ pending: undefined, error }` once it settles, however it\n * settles. `error` is the transition's `error()` only for a\n * `RejectType.ERROR` rejection; superseded, redirected, aborted and ignored\n * transitions leave it `undefined`. Invalid transitions are rejected before\n * `onStart`, so they never reach the tracker.\n *\n * `onStart` records each transition as the newest. Its promise settles the\n * status only while it is still the newest, so a superseded transition never\n * clears or fails its successor. `onFinish` would miss failures: it never\n * fires for a transition that errors or is aborted.\n *\n * A transition already running when this attaches is picked up too, so a\n * mirror created mid-navigation reads it as pending.\n *\n * @returns the `onStart` hook's deregistration function. A transition\n * already pending still settles when it does.\n */\nexport function trackTransitionStatus(\n router: UIRouter,\n set: (status: TransitionStatus) => void,\n): () => void {\n let newest: Transition | undefined;\n\n const start = (transition: Transition): void => {\n newest = transition;\n set({ pending: transition, error: undefined });\n\n const settle = (): void => {\n if (newest !== transition) return;\n newest = undefined;\n // SAFETY: `error()` is undefined after a success; core types it as `Rejection`.\n const error = transition.error() as Rejection | undefined;\n set({\n pending: undefined,\n error: error?.type === RejectType.ERROR ? error : undefined,\n });\n };\n\n // Handled here, the rejection still reaches the transition's own callers and onError hooks.\n void transition.promise.then(settle, settle);\n };\n\n const running = router.globals.transition;\n\n // SAFETY: `success` stays undefined until the transition settles; core types it as `boolean`.\n if (running && (running.success as boolean | undefined) === undefined) {\n start(running);\n }\n\n // SAFETY: `onStart` returns its deregistration function, typed only as `Function`.\n return router.transitionService.onStart({}, start) as () => void;\n}\n"]}
@@ -0,0 +1,28 @@
1
+ import { SubscriptionRef } from "effect";
2
+ import { Rejection, Transition, UIRouter } from "@uirouter/core";
3
+ /**
4
+ * The pending-transition ref for a router — one per router instance,
5
+ * attached lazily.
6
+ *
7
+ * It holds the newest transition still in flight, and `undefined` when none
8
+ * is. The first caller of this or {@link errorRef} creates both refs and
9
+ * registers the single `onStart` hook that feeds them; the transition's
10
+ * promise clears it when that transition settles, whether it succeeds,
11
+ * errors or is aborted, unless a newer one has started. A transition already
12
+ * running on first use counts as pending. Kept out of `RouteSnapshot`, whose
13
+ * every value is a settled route.
14
+ */
15
+ export declare function pendingRef(router: UIRouter): SubscriptionRef.SubscriptionRef<Transition | undefined>;
16
+ /**
17
+ * The failed-transition ref for a router — one per router instance,
18
+ * attached lazily, sharing {@link pendingRef}'s `onStart` hook.
19
+ *
20
+ * It holds the newest transition's failure as core's `Rejection`, and
21
+ * `undefined` otherwise. Only a `RejectType.ERROR` rejection, such as a
22
+ * resolve that throws, sets it; superseded, redirected, aborted and ignored
23
+ * transitions leave it `undefined`, as does a success. The next transition
24
+ * clears it as it starts, and a superseded transition that fails late never
25
+ * sets it. Kept out of `RouteSnapshot`.
26
+ */
27
+ export declare function errorRef(router: UIRouter): SubscriptionRef.SubscriptionRef<Rejection | undefined>;
28
+ //# sourceMappingURL=transition-refs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transition-refs.d.ts","mappings":"AAAA,SAAiB,uBAAuB;AACxC,SAAS,WAAW,YAAY,gBAAgB;;;;;;;;;;;;;AA6DhD,OAAO,iBAAS,WACd,QAAQ,WACP,gBAAgB,gBAAgB;;;;;;;;;;;;AAenC,OAAO,iBAAS,SACd,QAAQ,WACP,gBAAgB,gBAAgB","names":[],"ignoreList":[],"sources":["../src/transition-refs.ts"]}
@@ -0,0 +1,35 @@
1
+ import { Effect, SubscriptionRef } from "effect";
2
+ import { trackTransitionStatus } from "./shared/transition-status.js";
3
+ const transitionRefs = new WeakMap();
4
+ function refsFor(router) {
5
+ const existing = transitionRefs.get(router);
6
+ if (existing) return existing;
7
+ const refs = {
8
+ pending: Effect.runSync(SubscriptionRef.make(undefined)),
9
+ error: Effect.runSync(SubscriptionRef.make(undefined))
10
+ };
11
+ let error;
12
+ const setError = (next) => {
13
+ if (next === error) return;
14
+ error = next;
15
+ Effect.runSync(SubscriptionRef.set(refs.error, next));
16
+ };
17
+ transitionRefs.set(router, refs);
18
+ trackTransitionStatus(router, (status) => {
19
+ if (status.pending) {
20
+ Effect.runSync(SubscriptionRef.set(refs.pending, status.pending));
21
+ setError(status.error);
22
+ } else {
23
+ setError(status.error);
24
+ Effect.runSync(SubscriptionRef.set(refs.pending, undefined));
25
+ }
26
+ });
27
+ return refs;
28
+ }
29
+ export function pendingRef(router) {
30
+ return refsFor(router).pending;
31
+ }
32
+ export function errorRef(router) {
33
+ return refsFor(router).error;
34
+ }
35
+ //# sourceMappingURL=transition-refs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transition-refs.js","mappings":"AAAA,SAAS,QAAQ,uBAAuB;AAGxC,SAAS,6BAA6B;AAOtC,MAAM,iBAAiB,IAAI,QAAkC;AAG7D,SAAS,QAAQ,QAAkC;CACjD,MAAM,WAAW,eAAe,IAAI,MAAM;CAE1C,IAAI,UAAU,OAAO;CAErB,MAAM,OAAuB;EAC3B,SAAS,OAAO,QACd,gBAAgB,KAA6B,SAAS,CACxD;EACA,OAAO,OAAO,QACZ,gBAAgB,KAA4B,SAAS,CACvD;CACF;CAEA,IAAI;CAEJ,MAAM,YAAY,SAAsC;EACtD,IAAI,SAAS,OAAO;EACpB,QAAQ;EACR,OAAO,QAAQ,gBAAgB,IAAI,KAAK,OAAO,IAAI,CAAC;CACtD;CAEA,eAAe,IAAI,QAAQ,IAAI;CAE/B,sBAAsB,SAAS,WAAW;EACxC,IAAI,OAAO,SAAS;GAClB,OAAO,QAAQ,gBAAgB,IAAI,KAAK,SAAS,OAAO,OAAO,CAAC;GAChE,SAAS,OAAO,KAAK;EACvB,OAAO;GACL,SAAS,OAAO,KAAK;GACrB,OAAO,QAAQ,gBAAgB,IAAI,KAAK,SAAS,SAAS,CAAC;EAC7D;CACF,CAAC;CAED,OAAO;AACT;AAcA,OAAO,SAAS,WACd,QACyD;CACzD,OAAO,QAAQ,MAAM,CAAC,CAAC;AACzB;AAaA,OAAO,SAAS,SACd,QACwD;CACxD,OAAO,QAAQ,MAAM,CAAC,CAAC;AACzB","names":[],"ignoreList":[],"sources":["../src/transition-refs.ts"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ui-router-effect",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "Effect bindings for UI-Router: a framework-agnostic SubscriptionRef of the router's current state",
5
5
  "homepage": "https://lit-ui-router.dev/packages/ui-router-effect",
6
6
  "bugs": {
package/src/index.ts CHANGED
@@ -1,3 +1,7 @@
1
+ export { errorRef, pendingRef } from './transition-refs.js';
2
+
1
3
  export { routeRef } from './route-ref.js';
2
4
 
3
5
  export { type RouteSnapshot, snapshotRoute } from '@repo/route-snapshot';
6
+
7
+ export { type RouteTypes, type TypedRouteSnapshot } from './route-types.js';
package/src/route-ref.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  import { Effect, SubscriptionRef } from 'effect';
2
2
  import { UIRouter } from '@uirouter/core';
3
3
 
4
- import { RouteSnapshot, snapshotRoute } from '@repo/route-snapshot';
4
+ import { snapshotRoute } from '@repo/route-snapshot';
5
+
6
+ import type { TypedRouteSnapshot } from './route-types.js';
5
7
 
6
8
  const routeRefs = new WeakMap<
7
9
  UIRouter,
8
- SubscriptionRef.SubscriptionRef<RouteSnapshot>
10
+ SubscriptionRef.SubscriptionRef<TypedRouteSnapshot>
9
11
  >();
10
12
 
11
13
  /**
@@ -15,10 +17,11 @@ const routeRefs = new WeakMap<
15
17
  * ref and registers the single `onSuccess` hook that feeds it, and both live
16
18
  * as long as the router does. `onSuccess` is the only hook a later transition
17
19
  * cannot supersede, so every value the ref ever holds is a settled route.
20
+ * Its snapshot is typed by `RouteTypes` when a registry fills it in.
18
21
  */
19
22
  export function routeRef(
20
23
  router: UIRouter,
21
- ): SubscriptionRef.SubscriptionRef<RouteSnapshot> {
24
+ ): SubscriptionRef.SubscriptionRef<TypedRouteSnapshot> {
22
25
  const existing = routeRefs.get(router);
23
26
 
24
27
  if (existing) return existing;
@@ -0,0 +1,38 @@
1
+ import type { RouteSnapshot } from '@repo/route-snapshot';
2
+
3
+ /**
4
+ * Typed route snapshots, filled in by module augmentation. Empty, the
5
+ * snapshot is {@link RouteSnapshot}, as it always has been; nothing here
6
+ * emits JavaScript.
7
+ *
8
+ * An app or a registry such as `ui-router-registry` fills it in. Its member:
9
+ *
10
+ * - `snapshot`: the type `routeRef`'s ref holds, a typed view of
11
+ * {@link RouteSnapshot} whose `includes(name)` narrows `current` and
12
+ * `params`.
13
+ *
14
+ * ```ts
15
+ * declare module 'ui-router-effect' {
16
+ * interface RouteTypes {
17
+ * snapshot: AppRouteSnapshot;
18
+ * }
19
+ * }
20
+ * ```
21
+ *
22
+ * @category types
23
+ */
24
+ // oxlint-disable-next-line typescript/no-empty-interface, typescript/no-empty-object-type -- filled in by module augmentation
25
+ export interface RouteTypes {}
26
+
27
+ /**
28
+ * The route snapshot as {@link RouteTypes} types it: its `snapshot` member,
29
+ * or {@link RouteSnapshot} until it is filled in.
30
+ *
31
+ * @category types
32
+ */
33
+ export type TypedRouteSnapshot = RouteTypes extends {
34
+ /** The filled-in snapshot type. */
35
+ snapshot: infer S;
36
+ }
37
+ ? S
38
+ : RouteSnapshot;
@@ -0,0 +1,84 @@
1
+ import { Effect, SubscriptionRef } from 'effect';
2
+ import { Rejection, Transition, UIRouter } from '@uirouter/core';
3
+
4
+ import { trackTransitionStatus } from '@repo/transition-status';
5
+
6
+ interface TransitionRefs {
7
+ readonly pending: SubscriptionRef.SubscriptionRef<Transition | undefined>;
8
+ readonly error: SubscriptionRef.SubscriptionRef<Rejection | undefined>;
9
+ }
10
+
11
+ const transitionRefs = new WeakMap<UIRouter, TransitionRefs>();
12
+
13
+ /** Both refs for a router, fed by one `onStart` hook registered on first use. */
14
+ function refsFor(router: UIRouter): TransitionRefs {
15
+ const existing = transitionRefs.get(router);
16
+
17
+ if (existing) return existing;
18
+
19
+ const refs: TransitionRefs = {
20
+ pending: Effect.runSync(
21
+ SubscriptionRef.make<Transition | undefined>(undefined),
22
+ ),
23
+ error: Effect.runSync(
24
+ SubscriptionRef.make<Rejection | undefined>(undefined),
25
+ ),
26
+ };
27
+
28
+ let error: Rejection | undefined;
29
+
30
+ const setError = (next: Rejection | undefined): void => {
31
+ if (next === error) return;
32
+ error = next;
33
+ Effect.runSync(SubscriptionRef.set(refs.error, next));
34
+ };
35
+
36
+ transitionRefs.set(router, refs);
37
+ // `pending` is set before `error` clears, and `error` before `pending` clears, so no read between the two looks idle.
38
+ trackTransitionStatus(router, (status) => {
39
+ if (status.pending) {
40
+ Effect.runSync(SubscriptionRef.set(refs.pending, status.pending));
41
+ setError(status.error);
42
+ } else {
43
+ setError(status.error);
44
+ Effect.runSync(SubscriptionRef.set(refs.pending, undefined));
45
+ }
46
+ });
47
+
48
+ return refs;
49
+ }
50
+
51
+ /**
52
+ * The pending-transition ref for a router — one per router instance,
53
+ * attached lazily.
54
+ *
55
+ * It holds the newest transition still in flight, and `undefined` when none
56
+ * is. The first caller of this or {@link errorRef} creates both refs and
57
+ * registers the single `onStart` hook that feeds them; the transition's
58
+ * promise clears it when that transition settles, whether it succeeds,
59
+ * errors or is aborted, unless a newer one has started. A transition already
60
+ * running on first use counts as pending. Kept out of `RouteSnapshot`, whose
61
+ * every value is a settled route.
62
+ */
63
+ export function pendingRef(
64
+ router: UIRouter,
65
+ ): SubscriptionRef.SubscriptionRef<Transition | undefined> {
66
+ return refsFor(router).pending;
67
+ }
68
+
69
+ /**
70
+ * The failed-transition ref for a router — one per router instance,
71
+ * attached lazily, sharing {@link pendingRef}'s `onStart` hook.
72
+ *
73
+ * It holds the newest transition's failure as core's `Rejection`, and
74
+ * `undefined` otherwise. Only a `RejectType.ERROR` rejection, such as a
75
+ * resolve that throws, sets it; superseded, redirected, aborted and ignored
76
+ * transitions leave it `undefined`, as does a success. The next transition
77
+ * clears it as it starts, and a superseded transition that fails late never
78
+ * sets it. Kept out of `RouteSnapshot`.
79
+ */
80
+ export function errorRef(
81
+ router: UIRouter,
82
+ ): SubscriptionRef.SubscriptionRef<Rejection | undefined> {
83
+ return refsFor(router).error;
84
+ }