ui-router-effect 0.2.0 → 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
@@ -15,16 +15,17 @@ npm install ui-router-effect effect
15
15
  ## Features
16
16
 
17
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.
19
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.
20
21
 
21
22
  ## Usage
22
23
 
23
24
  ```typescript
24
- import { Effect, Stream } from 'effect';
25
+ import { Effect, Stream, SubscriptionRef } from 'effect';
25
26
  import { routeRef } from 'ui-router-effect';
26
27
 
27
- const names = routeRef(router).changes.pipe(
28
+ const names = SubscriptionRef.changes(routeRef(router)).pipe(
28
29
  Stream.map((route) => route.current?.name),
29
30
  Stream.changes,
30
31
  );
@@ -32,6 +33,25 @@ const names = routeRef(router).changes.pipe(
32
33
  Effect.runFork(Stream.runForEach(names, (name) => Effect.log(name)));
33
34
  ```
34
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
+
35
55
  ## Dependencies
36
56
 
37
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,4 @@
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";
3
4
  export { type RouteTypes, type TypedRouteSnapshot } from "./route-types.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","mappings":"AAAA,SAAS,gBAAgB;AAEzB,cAAc,eAAe,qBAAqB;AAElD,cAAc,iBAAiB,0BAA0B","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"]}
@@ -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.2.0",
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,5 @@
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';
@@ -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
+ }