@real-router/sources 0.8.5 → 0.8.6
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/package.json +3 -4
- package/src/BaseSource.ts +0 -95
- package/src/canonicalJson.ts +0 -110
- package/src/computeSnapshot.ts +0 -44
- package/src/createActiveNameSelector.ts +0 -182
- package/src/createActiveRouteSource.ts +0 -223
- package/src/createDismissableError.ts +0 -108
- package/src/createErrorSource.ts +0 -111
- package/src/createRouteNodeSource.ts +0 -130
- package/src/createRouteSource.ts +0 -56
- package/src/createTransitionSource.ts +0 -192
- package/src/index.ts +0 -35
- package/src/internal/noopDestroy.ts +0 -19
- package/src/internal/readContextHash.ts +0 -32
- package/src/normalizeActiveOptions.ts +0 -41
- package/src/stabilizeState.ts +0 -75
- package/src/types.ts +0 -63
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { BaseSource } from "./BaseSource";
|
|
2
|
-
import { getErrorSource } from "./createErrorSource";
|
|
3
|
-
import { noopDestroy } from "./internal/noopDestroy.js";
|
|
4
|
-
|
|
5
|
-
import type { DismissableErrorSnapshot, RouterSource } from "./types.js";
|
|
6
|
-
import type { Router } from "@real-router/core";
|
|
7
|
-
|
|
8
|
-
const dismissableCache = new WeakMap<
|
|
9
|
-
Router,
|
|
10
|
-
RouterSource<DismissableErrorSnapshot>
|
|
11
|
-
>();
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Returns a per-router cached source that wraps `getErrorSource(router)` with
|
|
15
|
-
* an integrated "dismissed" version counter, exposing a single reactive
|
|
16
|
-
* snapshot `{ error, toRoute, fromRoute, version, resetError }`.
|
|
17
|
-
*
|
|
18
|
-
* Each `RouterErrorBoundary` in a framework adapter subscribes to this one
|
|
19
|
-
* source instead of re-implementing the `dismissedVersion` state pattern
|
|
20
|
-
* locally. The 6-copy duplicate across adapters collapses to this helper.
|
|
21
|
-
*
|
|
22
|
-
* **Semantics:**
|
|
23
|
-
* - `error` is non-null only when `underlying.version > dismissedVersion`.
|
|
24
|
-
* - `resetError()` sets `dismissedVersion = current underlying version`,
|
|
25
|
-
* immediately clearing `error` to `null` and notifying all listeners.
|
|
26
|
-
* - A subsequent `TRANSITION_ERROR` advances `version` beyond `dismissedVersion`,
|
|
27
|
-
* so `error` becomes non-null again — no additional plumbing needed.
|
|
28
|
-
*
|
|
29
|
-
* **Cached:** one instance per router. `destroy()` on the returned source is
|
|
30
|
-
* a no-op. Shared across all `RouterErrorBoundary` consumers.
|
|
31
|
-
*/
|
|
32
|
-
export function createDismissableError(
|
|
33
|
-
router: Router,
|
|
34
|
-
): RouterSource<DismissableErrorSnapshot> {
|
|
35
|
-
const cached = dismissableCache.get(router);
|
|
36
|
-
|
|
37
|
-
if (cached) {
|
|
38
|
-
return cached;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const errorSource = getErrorSource(router);
|
|
42
|
-
|
|
43
|
-
let dismissedVersion = -1;
|
|
44
|
-
// Hoisted up here so the `onFirstSubscribe` closure below can read/write
|
|
45
|
-
// it before `disconnect()`'s declaration. JS hoisting makes the original
|
|
46
|
-
// post-declaration order legal, but reading top-to-bottom is clearer.
|
|
47
|
-
let unsubFromError: (() => void) | null = null;
|
|
48
|
-
|
|
49
|
-
const buildDismissableSnapshot = (): DismissableErrorSnapshot => {
|
|
50
|
-
const snap = errorSource.getSnapshot();
|
|
51
|
-
const isDismissed = snap.version <= dismissedVersion;
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
error: isDismissed ? null : snap.error,
|
|
55
|
-
toRoute: isDismissed ? null : snap.toRoute,
|
|
56
|
-
fromRoute: isDismissed ? null : snap.fromRoute,
|
|
57
|
-
version: snap.version,
|
|
58
|
-
resetError,
|
|
59
|
-
};
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const source = new BaseSource<DismissableErrorSnapshot>(
|
|
63
|
-
buildDismissableSnapshot(),
|
|
64
|
-
{
|
|
65
|
-
onFirstSubscribe: () => {
|
|
66
|
-
unsubFromError = errorSource.subscribe(() => {
|
|
67
|
-
source.updateSnapshot(buildDismissableSnapshot());
|
|
68
|
-
});
|
|
69
|
-
},
|
|
70
|
-
onLastUnsubscribe: () => {
|
|
71
|
-
disconnect();
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
function resetError(): void {
|
|
77
|
-
const currentVersion = errorSource.getSnapshot().version;
|
|
78
|
-
|
|
79
|
-
// No-op guard: if we already dismissed at this version (or are even ahead
|
|
80
|
-
// of the live error stream), there's nothing to clear. Skipping prevents
|
|
81
|
-
// a redundant snapshot allocation + listener notification under tight
|
|
82
|
-
// resetError(); resetError() patterns — common when a RouterErrorBoundary
|
|
83
|
-
// user clicks "dismiss" while another dismiss is already in flight.
|
|
84
|
-
if (currentVersion <= dismissedVersion) {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
dismissedVersion = currentVersion;
|
|
89
|
-
source.updateSnapshot(buildDismissableSnapshot());
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function disconnect(): void {
|
|
93
|
-
const unsub = unsubFromError;
|
|
94
|
-
|
|
95
|
-
unsubFromError = null;
|
|
96
|
-
unsub?.();
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const wrapper: RouterSource<DismissableErrorSnapshot> = {
|
|
100
|
-
subscribe: source.subscribe,
|
|
101
|
-
getSnapshot: source.getSnapshot,
|
|
102
|
-
destroy: noopDestroy,
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
dismissableCache.set(router, wrapper);
|
|
106
|
-
|
|
107
|
-
return wrapper;
|
|
108
|
-
}
|
package/src/createErrorSource.ts
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { events } from "@real-router/core";
|
|
2
|
-
import { getPluginApi } from "@real-router/core/api";
|
|
3
|
-
|
|
4
|
-
import { BaseSource } from "./BaseSource";
|
|
5
|
-
import { noopDestroy } from "./internal/noopDestroy.js";
|
|
6
|
-
|
|
7
|
-
import type { RouterErrorSnapshot, RouterSource } from "./types.js";
|
|
8
|
-
import type { Router, State, RouterError } from "@real-router/core";
|
|
9
|
-
|
|
10
|
-
const INITIAL_SNAPSHOT: RouterErrorSnapshot = {
|
|
11
|
-
error: null,
|
|
12
|
-
toRoute: null,
|
|
13
|
-
fromRoute: null,
|
|
14
|
-
version: 0,
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const errorSourceCache = new WeakMap<
|
|
18
|
-
Router,
|
|
19
|
-
RouterSource<RouterErrorSnapshot>
|
|
20
|
-
>();
|
|
21
|
-
|
|
22
|
-
export function createErrorSource(
|
|
23
|
-
router: Router,
|
|
24
|
-
): RouterSource<RouterErrorSnapshot> {
|
|
25
|
-
let errorVersion = 0;
|
|
26
|
-
let hasError = false;
|
|
27
|
-
|
|
28
|
-
const source = new BaseSource(INITIAL_SNAPSHOT, {
|
|
29
|
-
onDestroy: () => {
|
|
30
|
-
for (const unsub of unsubs) {
|
|
31
|
-
unsub();
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const api = getPluginApi(router);
|
|
37
|
-
|
|
38
|
-
// Eager connection: subscribe to router events immediately
|
|
39
|
-
const unsubs = [
|
|
40
|
-
api.addEventListener(
|
|
41
|
-
events.TRANSITION_ERROR,
|
|
42
|
-
(
|
|
43
|
-
toState: State | undefined,
|
|
44
|
-
fromState: State | undefined,
|
|
45
|
-
err: RouterError,
|
|
46
|
-
) => {
|
|
47
|
-
errorVersion++;
|
|
48
|
-
hasError = true;
|
|
49
|
-
source.updateSnapshot({
|
|
50
|
-
error: err,
|
|
51
|
-
toRoute: toState ?? null,
|
|
52
|
-
/* v8 ignore next -- @preserve: fromState undefined only during start() error; unreachable via navigate() */
|
|
53
|
-
fromRoute: fromState ?? null,
|
|
54
|
-
version: errorVersion,
|
|
55
|
-
});
|
|
56
|
-
},
|
|
57
|
-
),
|
|
58
|
-
api.addEventListener(events.TRANSITION_SUCCESS, () => {
|
|
59
|
-
// Skip if no error — avoids unnecessary re-renders.
|
|
60
|
-
// BaseSource.updateSnapshot() always notifies listeners (new object = new ref),
|
|
61
|
-
// and useSyncExternalStore compares via Object.is().
|
|
62
|
-
if (hasError) {
|
|
63
|
-
hasError = false;
|
|
64
|
-
source.updateSnapshot({
|
|
65
|
-
error: null,
|
|
66
|
-
toRoute: null,
|
|
67
|
-
fromRoute: null,
|
|
68
|
-
version: errorVersion,
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
}),
|
|
72
|
-
];
|
|
73
|
-
|
|
74
|
-
return source;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Returns a per-router cached error source shared across all consumers.
|
|
79
|
-
*
|
|
80
|
-
* Safe to call destroy() — the cached source ignores external destroy() calls
|
|
81
|
-
* and lives until the router itself is garbage-collected (the WeakMap entry
|
|
82
|
-
* releases automatically).
|
|
83
|
-
*
|
|
84
|
-
* Use this in framework adapters (React/Preact/Solid/Vue/Svelte/Angular) to
|
|
85
|
-
* share a single ErrorSource instance across all mount/unmount cycles.
|
|
86
|
-
*
|
|
87
|
-
* For isolated/advanced use (ad-hoc, short-lived, per-owner teardown), call
|
|
88
|
-
* `createErrorSource(router)` directly — it returns a fresh instance with a
|
|
89
|
-
* working `destroy()`.
|
|
90
|
-
*/
|
|
91
|
-
export function getErrorSource(
|
|
92
|
-
router: Router,
|
|
93
|
-
): RouterSource<RouterErrorSnapshot> {
|
|
94
|
-
let cached = errorSourceCache.get(router);
|
|
95
|
-
|
|
96
|
-
if (!cached) {
|
|
97
|
-
const source = createErrorSource(router);
|
|
98
|
-
|
|
99
|
-
// Wrap with no-op destroy. The underlying source is shared across all
|
|
100
|
-
// consumers; letting any one consumer call destroy() would tear it down
|
|
101
|
-
// for the rest. The source lives as long as the router (WeakMap key).
|
|
102
|
-
cached = {
|
|
103
|
-
subscribe: source.subscribe,
|
|
104
|
-
getSnapshot: source.getSnapshot,
|
|
105
|
-
destroy: noopDestroy,
|
|
106
|
-
};
|
|
107
|
-
errorSourceCache.set(router, cached);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return cached;
|
|
111
|
-
}
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { BaseSource } from "./BaseSource";
|
|
2
|
-
import { computeSnapshot } from "./computeSnapshot.js";
|
|
3
|
-
import { noopDestroy } from "./internal/noopDestroy.js";
|
|
4
|
-
|
|
5
|
-
import type { RouteNodeSnapshot, RouterSource } from "./types.js";
|
|
6
|
-
import type { Router } from "@real-router/core";
|
|
7
|
-
|
|
8
|
-
const nodeSourceCache = new WeakMap<
|
|
9
|
-
Router,
|
|
10
|
-
Map<string, RouterSource<RouteNodeSnapshot>>
|
|
11
|
-
>();
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Creates a source scoped to a specific route node.
|
|
15
|
-
*
|
|
16
|
-
* **Per-router + per-nodeName cache:** repeated calls with the same
|
|
17
|
-
* `(router, nodeName)` return the same shared instance. `N` consumers
|
|
18
|
-
* calling `createRouteNodeSource(r, "users")` produce one router subscription
|
|
19
|
-
* shared across all of them.
|
|
20
|
-
*
|
|
21
|
-
* Uses a lazy-connection pattern: the router subscription is created when the
|
|
22
|
-
* first listener subscribes and removed when the last listener unsubscribes.
|
|
23
|
-
* This is compatible with React's useSyncExternalStore and Strict Mode.
|
|
24
|
-
*
|
|
25
|
-
* `destroy()` on the returned source is a no-op — the shared instance lives
|
|
26
|
-
* as long as the router itself (the WeakMap entry releases automatically on
|
|
27
|
-
* router GC). Callers that need an isolated instance with working teardown
|
|
28
|
-
* can use `buildRouteNodeSource` internally (not exported).
|
|
29
|
-
*/
|
|
30
|
-
export function createRouteNodeSource(
|
|
31
|
-
router: Router,
|
|
32
|
-
nodeName: string,
|
|
33
|
-
): RouterSource<RouteNodeSnapshot> {
|
|
34
|
-
let perRouter = nodeSourceCache.get(router);
|
|
35
|
-
|
|
36
|
-
if (!perRouter) {
|
|
37
|
-
perRouter = new Map();
|
|
38
|
-
nodeSourceCache.set(router, perRouter);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
let cached = perRouter.get(nodeName);
|
|
42
|
-
|
|
43
|
-
if (!cached) {
|
|
44
|
-
const source = buildRouteNodeSource(router, nodeName);
|
|
45
|
-
|
|
46
|
-
// Wrap with no-op destroy. The shared source lives with the router.
|
|
47
|
-
cached = {
|
|
48
|
-
subscribe: source.subscribe,
|
|
49
|
-
getSnapshot: source.getSnapshot,
|
|
50
|
-
destroy: noopDestroy,
|
|
51
|
-
};
|
|
52
|
-
perRouter.set(nodeName, cached);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return cached;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function buildRouteNodeSource(
|
|
59
|
-
router: Router,
|
|
60
|
-
nodeName: string,
|
|
61
|
-
): RouterSource<RouteNodeSnapshot> {
|
|
62
|
-
let routerUnsubscribe: (() => void) | null = null;
|
|
63
|
-
|
|
64
|
-
// Built once per cached source instance; safe — createRouteNodeSource is
|
|
65
|
-
// itself per-(router, nodeName) cached, so shouldUpdate is called once.
|
|
66
|
-
const shouldUpdate = router.shouldUpdateNode(nodeName);
|
|
67
|
-
|
|
68
|
-
const initialSnapshot: RouteNodeSnapshot = {
|
|
69
|
-
route: undefined,
|
|
70
|
-
previousRoute: undefined,
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const disconnect = (): void => {
|
|
74
|
-
const unsub = routerUnsubscribe;
|
|
75
|
-
|
|
76
|
-
routerUnsubscribe = null;
|
|
77
|
-
unsub?.();
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const source = new BaseSource<RouteNodeSnapshot>(
|
|
81
|
-
computeSnapshot(initialSnapshot, router, nodeName),
|
|
82
|
-
{
|
|
83
|
-
onFirstSubscribe: () => {
|
|
84
|
-
// Reconcile snapshot with current router state before connecting.
|
|
85
|
-
// Covers reconnection after Activity hide/show cycles where the
|
|
86
|
-
// source was disconnected and missed navigation events.
|
|
87
|
-
const reconciled = computeSnapshot(
|
|
88
|
-
source.getSnapshot(),
|
|
89
|
-
router,
|
|
90
|
-
nodeName,
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
if (!Object.is(reconciled, source.getSnapshot())) {
|
|
94
|
-
source.updateSnapshot(reconciled);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Connect to router on first subscription
|
|
98
|
-
routerUnsubscribe = router.subscribe((next) => {
|
|
99
|
-
if (!shouldUpdate(next.route, next.previousRoute)) {
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const newSnapshot = computeSnapshot(
|
|
104
|
-
source.getSnapshot(),
|
|
105
|
-
router,
|
|
106
|
-
nodeName,
|
|
107
|
-
next,
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
// computeSnapshot returns the SAME currentSnapshot reference when
|
|
111
|
-
// both route and previousRoute stabilize to prev — guard against
|
|
112
|
-
// emitting redundant updates to listeners (matters for signal-
|
|
113
|
-
// based adapters that re-run effects on every set).
|
|
114
|
-
/* v8 ignore next 3 -- @preserve: structurally unreachable after #605
|
|
115
|
-
— reload navs always return fresh refs via stabilizeState, and
|
|
116
|
-
within-node non-reload navs short-circuit at shouldUpdate. Guard
|
|
117
|
-
kept for defensive correctness against future stabilizer changes. */
|
|
118
|
-
if (Object.is(source.getSnapshot(), newSnapshot)) {
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
source.updateSnapshot(newSnapshot);
|
|
123
|
-
});
|
|
124
|
-
},
|
|
125
|
-
onLastUnsubscribe: disconnect,
|
|
126
|
-
},
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
return source;
|
|
130
|
-
}
|
package/src/createRouteSource.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { BaseSource } from "./BaseSource";
|
|
2
|
-
import { stabilizeState } from "./stabilizeState.js";
|
|
3
|
-
|
|
4
|
-
import type { RouteSnapshot, RouterSource } from "./types.js";
|
|
5
|
-
import type { Router } from "@real-router/core";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Creates a source for the full route state.
|
|
9
|
-
*
|
|
10
|
-
* Uses a lazy-connection pattern: the router subscription is created when the
|
|
11
|
-
* first listener subscribes and removed when the last listener unsubscribes.
|
|
12
|
-
* This is compatible with React's useSyncExternalStore and Strict Mode.
|
|
13
|
-
*/
|
|
14
|
-
export function createRouteSource(router: Router): RouterSource<RouteSnapshot> {
|
|
15
|
-
let routerUnsubscribe: (() => void) | null = null;
|
|
16
|
-
|
|
17
|
-
const disconnect = (): void => {
|
|
18
|
-
const unsub = routerUnsubscribe;
|
|
19
|
-
|
|
20
|
-
routerUnsubscribe = null;
|
|
21
|
-
unsub?.();
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const source = new BaseSource<RouteSnapshot>(
|
|
25
|
-
{
|
|
26
|
-
route: router.getState(),
|
|
27
|
-
previousRoute: undefined,
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
onFirstSubscribe: () => {
|
|
31
|
-
routerUnsubscribe = router.subscribe((next) => {
|
|
32
|
-
const prev = source.getSnapshot();
|
|
33
|
-
const newRoute = stabilizeState(prev.route, next.route);
|
|
34
|
-
const newPreviousRoute = stabilizeState(
|
|
35
|
-
prev.previousRoute,
|
|
36
|
-
next.previousRoute,
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
newRoute !== prev.route ||
|
|
41
|
-
newPreviousRoute !== prev.previousRoute
|
|
42
|
-
) {
|
|
43
|
-
source.updateSnapshot({
|
|
44
|
-
route: newRoute,
|
|
45
|
-
previousRoute: newPreviousRoute,
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
},
|
|
50
|
-
onLastUnsubscribe: disconnect,
|
|
51
|
-
onDestroy: disconnect,
|
|
52
|
-
},
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
return source;
|
|
56
|
-
}
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import { events } from "@real-router/core";
|
|
2
|
-
import { getPluginApi } from "@real-router/core/api";
|
|
3
|
-
|
|
4
|
-
import { BaseSource } from "./BaseSource";
|
|
5
|
-
import { noopDestroy } from "./internal/noopDestroy.js";
|
|
6
|
-
import { stabilizeState } from "./stabilizeState.js";
|
|
7
|
-
|
|
8
|
-
import type { RouterTransitionSnapshot, RouterSource } from "./types.js";
|
|
9
|
-
import type { Router, State } from "@real-router/core";
|
|
10
|
-
|
|
11
|
-
// Frozen so accidental consumer mutation (`source.getSnapshot().toRoute = X`)
|
|
12
|
-
// throws in strict mode. The singleton ref is shared across every IDLE state
|
|
13
|
-
// for the lifetime of the process — mutating it would corrupt the contract
|
|
14
|
-
// "all IDLE snapshots are the same object reference" relied on by every
|
|
15
|
-
// adapter's useSyncExternalStore equivalent.
|
|
16
|
-
const IDLE_SNAPSHOT: RouterTransitionSnapshot = Object.freeze({
|
|
17
|
-
isTransitioning: false,
|
|
18
|
-
isLeaveApproved: false,
|
|
19
|
-
toRoute: null,
|
|
20
|
-
fromRoute: null,
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
const transitionSourceCache = new WeakMap<
|
|
24
|
-
Router,
|
|
25
|
-
RouterSource<RouterTransitionSnapshot>
|
|
26
|
-
>();
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* @internal test-only export — returns the next snapshot for a TRANSITION_START
|
|
30
|
-
* payload, or `null` when the same-paths dedup guard should suppress the
|
|
31
|
-
* update. Exported so the (structurally-unreachable after #605) guard can be
|
|
32
|
-
* exercised by unit tests without resorting to private-API hacks.
|
|
33
|
-
*/
|
|
34
|
-
export function nextTransitionStartSnapshot(
|
|
35
|
-
prev: RouterTransitionSnapshot,
|
|
36
|
-
toState: State,
|
|
37
|
-
fromState: State | undefined,
|
|
38
|
-
): RouterTransitionSnapshot | null {
|
|
39
|
-
const newToRoute = stabilizeState(prev.toRoute, toState);
|
|
40
|
-
const newFromRoute = stabilizeState(prev.fromRoute, fromState ?? null);
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
prev.isTransitioning &&
|
|
44
|
-
newToRoute === prev.toRoute &&
|
|
45
|
-
newFromRoute === prev.fromRoute
|
|
46
|
-
) {
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
isTransitioning: true,
|
|
52
|
-
isLeaveApproved: false,
|
|
53
|
-
toRoute: newToRoute,
|
|
54
|
-
fromRoute: newFromRoute,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* @internal test-only export — analogous to {@link nextTransitionStartSnapshot}
|
|
60
|
-
* for the LEAVE_APPROVE payload. The guard is structurally unreachable in
|
|
61
|
-
* practice (router emits LEAVE_APPROVE exactly once per pipeline) but stays
|
|
62
|
-
* for plugin-driven re-entrant flows.
|
|
63
|
-
*/
|
|
64
|
-
export function nextLeaveApproveSnapshot(
|
|
65
|
-
prev: RouterTransitionSnapshot,
|
|
66
|
-
toState: State,
|
|
67
|
-
fromState: State | undefined,
|
|
68
|
-
): RouterTransitionSnapshot | null {
|
|
69
|
-
const newToRoute = stabilizeState(prev.toRoute, toState);
|
|
70
|
-
const newFromRoute = stabilizeState(prev.fromRoute, fromState ?? null);
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
prev.isLeaveApproved &&
|
|
74
|
-
newToRoute === prev.toRoute &&
|
|
75
|
-
newFromRoute === prev.fromRoute
|
|
76
|
-
) {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return {
|
|
81
|
-
isTransitioning: true,
|
|
82
|
-
isLeaveApproved: true,
|
|
83
|
-
toRoute: newToRoute,
|
|
84
|
-
fromRoute: newFromRoute,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function createTransitionSource(
|
|
89
|
-
router: Router,
|
|
90
|
-
): RouterSource<RouterTransitionSnapshot> {
|
|
91
|
-
const source = new BaseSource(IDLE_SNAPSHOT, {
|
|
92
|
-
onDestroy: () => {
|
|
93
|
-
for (const unsub of unsubs) {
|
|
94
|
-
unsub();
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
const api = getPluginApi(router);
|
|
100
|
-
|
|
101
|
-
const resetToIdle = (): void => {
|
|
102
|
-
source.updateSnapshot(IDLE_SNAPSHOT);
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
// Eager connection: subscribe to router events immediately
|
|
106
|
-
const unsubs = [
|
|
107
|
-
api.addEventListener(
|
|
108
|
-
events.TRANSITION_START,
|
|
109
|
-
(toState: State, fromState?: State) => {
|
|
110
|
-
// The same-paths dedup branch inside nextTransitionStartSnapshot is
|
|
111
|
-
// structurally unreachable after #605 (every router-emitted
|
|
112
|
-
// TRANSITION_START carries a fresh State per navigate()), but the
|
|
113
|
-
// helper is kept testable for future stabilizer changes — see the
|
|
114
|
-
// direct unit test in createTransitionSource.test.ts.
|
|
115
|
-
const next = nextTransitionStartSnapshot(
|
|
116
|
-
source.getSnapshot(),
|
|
117
|
-
toState,
|
|
118
|
-
fromState,
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
/* v8 ignore next 3 -- @preserve: dedup-skip branch unreachable through
|
|
122
|
-
normal router flow; covered directly via nextTransitionStartSnapshot
|
|
123
|
-
unit test. */
|
|
124
|
-
if (next === null) {
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
source.updateSnapshot(next);
|
|
129
|
-
},
|
|
130
|
-
),
|
|
131
|
-
api.addEventListener(
|
|
132
|
-
events.TRANSITION_LEAVE_APPROVE,
|
|
133
|
-
(toState: State, fromState?: State) => {
|
|
134
|
-
const next = nextLeaveApproveSnapshot(
|
|
135
|
-
source.getSnapshot(),
|
|
136
|
-
toState,
|
|
137
|
-
fromState,
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
/* v8 ignore next 3 -- @preserve: dedup-skip branch unreachable through
|
|
141
|
-
normal router flow (LEAVE_APPROVE fires once per pipeline); covered
|
|
142
|
-
directly via nextLeaveApproveSnapshot unit test. */
|
|
143
|
-
if (next === null) {
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
source.updateSnapshot(next);
|
|
148
|
-
},
|
|
149
|
-
),
|
|
150
|
-
api.addEventListener(events.TRANSITION_SUCCESS, resetToIdle),
|
|
151
|
-
api.addEventListener(events.TRANSITION_ERROR, resetToIdle),
|
|
152
|
-
api.addEventListener(events.TRANSITION_CANCEL, resetToIdle),
|
|
153
|
-
];
|
|
154
|
-
|
|
155
|
-
return source;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Returns a per-router cached transition source shared across all consumers.
|
|
160
|
-
*
|
|
161
|
-
* Safe to call destroy() — the cached source ignores external destroy() calls
|
|
162
|
-
* and lives until the router itself is garbage-collected (the WeakMap entry
|
|
163
|
-
* releases automatically).
|
|
164
|
-
*
|
|
165
|
-
* Use this in framework adapters (React/Preact/Solid/Vue/Svelte/Angular) to
|
|
166
|
-
* share a single TransitionSource instance across all mount/unmount cycles.
|
|
167
|
-
*
|
|
168
|
-
* For isolated/advanced use (ad-hoc, short-lived, per-owner teardown), call
|
|
169
|
-
* `createTransitionSource(router)` directly — it returns a fresh instance with
|
|
170
|
-
* a working `destroy()`.
|
|
171
|
-
*/
|
|
172
|
-
export function getTransitionSource(
|
|
173
|
-
router: Router,
|
|
174
|
-
): RouterSource<RouterTransitionSnapshot> {
|
|
175
|
-
let cached = transitionSourceCache.get(router);
|
|
176
|
-
|
|
177
|
-
if (!cached) {
|
|
178
|
-
const source = createTransitionSource(router);
|
|
179
|
-
|
|
180
|
-
// Wrap with no-op destroy. The underlying source is shared across all
|
|
181
|
-
// consumers; letting any one consumer call destroy() would tear it down
|
|
182
|
-
// for the rest. The source lives as long as the router (WeakMap key).
|
|
183
|
-
cached = {
|
|
184
|
-
subscribe: source.subscribe,
|
|
185
|
-
getSnapshot: source.getSnapshot,
|
|
186
|
-
destroy: noopDestroy,
|
|
187
|
-
};
|
|
188
|
-
transitionSourceCache.set(router, cached);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return cached;
|
|
192
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
export type {
|
|
2
|
-
RouterSource,
|
|
3
|
-
RouteSnapshot,
|
|
4
|
-
RouteNodeSnapshot,
|
|
5
|
-
ActiveRouteSourceOptions,
|
|
6
|
-
RouterTransitionSnapshot,
|
|
7
|
-
RouterErrorSnapshot,
|
|
8
|
-
DismissableErrorSnapshot,
|
|
9
|
-
} from "./types.js";
|
|
10
|
-
|
|
11
|
-
export { createRouteSource } from "./createRouteSource";
|
|
12
|
-
|
|
13
|
-
export { createRouteNodeSource } from "./createRouteNodeSource";
|
|
14
|
-
|
|
15
|
-
export { createActiveRouteSource } from "./createActiveRouteSource";
|
|
16
|
-
|
|
17
|
-
export {
|
|
18
|
-
createTransitionSource,
|
|
19
|
-
getTransitionSource,
|
|
20
|
-
} from "./createTransitionSource";
|
|
21
|
-
|
|
22
|
-
export { createErrorSource, getErrorSource } from "./createErrorSource";
|
|
23
|
-
|
|
24
|
-
export { createDismissableError } from "./createDismissableError";
|
|
25
|
-
|
|
26
|
-
export { createActiveNameSelector } from "./createActiveNameSelector";
|
|
27
|
-
|
|
28
|
-
export type { ActiveNameSelector } from "./createActiveNameSelector";
|
|
29
|
-
|
|
30
|
-
export {
|
|
31
|
-
DEFAULT_ACTIVE_OPTIONS,
|
|
32
|
-
normalizeActiveOptions,
|
|
33
|
-
} from "./normalizeActiveOptions";
|
|
34
|
-
|
|
35
|
-
export { canonicalJson } from "./canonicalJson";
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @internal
|
|
3
|
-
*
|
|
4
|
-
* Shared no-op `destroy()` for cached `RouterSource` wrappers.
|
|
5
|
-
*
|
|
6
|
-
* Cached factories (`getTransitionSource`, `getErrorSource`, `createDismissableError`,
|
|
7
|
-
* `createActiveNameSelector`, cache hit in `createRouteNodeSource`,
|
|
8
|
-
* `createActiveRouteSource`) return a wrapper whose `destroy()` is a no-op —
|
|
9
|
-
* the underlying source is shared across all consumers and lives as long as
|
|
10
|
-
* the router (WeakMap entry releases on router GC).
|
|
11
|
-
*
|
|
12
|
-
* One module-level function shared by every cached factory keeps the wrapper
|
|
13
|
-
* shape (`{ subscribe, getSnapshot, destroy: noopDestroy }`) byte-stable and
|
|
14
|
-
* eliminates the previous six copies. (Bundlers inline a stand-alone arrow
|
|
15
|
-
* just as readily, so the cost is purely on the maintenance side.)
|
|
16
|
-
*/
|
|
17
|
-
export function noopDestroy(): void {
|
|
18
|
-
// Shared cached source — external destroy() is a no-op.
|
|
19
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import type { State } from "@real-router/core";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @internal
|
|
5
|
-
*
|
|
6
|
-
* Reads the URL fragment published by `browser-plugin` / `navigation-plugin`
|
|
7
|
-
* on a router state. The plugins claim the `"url"` namespace via
|
|
8
|
-
* `state.context.url` and the `hash` field carries the **decoded** fragment.
|
|
9
|
-
*
|
|
10
|
-
* Returns:
|
|
11
|
-
* - `undefined` — no plugin claimed the `"url"` namespace (hash-plugin runtime,
|
|
12
|
-
* memory-plugin, SSR before hydration) OR the state itself is nullish;
|
|
13
|
-
* - `""` — the URL namespace exists but the fragment is empty
|
|
14
|
-
* (browser-plugin on a hash-less URL).
|
|
15
|
-
*
|
|
16
|
-
* Callers that need the "no namespace at all" branch (e.g. `stabilizeState`
|
|
17
|
-
* comparing cross-plugin transitions) read the raw `undefined`. Callers that
|
|
18
|
-
* collapse "no namespace" to "no hash" (e.g. `createActiveRouteSource`'s
|
|
19
|
-
* hash-equality check) coalesce with `?? ""` themselves.
|
|
20
|
-
*
|
|
21
|
-
* Centralising the context cast removes the previous duplicate definitions in
|
|
22
|
-
* `stabilizeState.ts` and `createActiveRouteSource.ts` that drifted in
|
|
23
|
-
* signature (state vs router) and default-value (undefined vs "") — both
|
|
24
|
-
* variants are reconstructible from this single helper at the callsite.
|
|
25
|
-
*/
|
|
26
|
-
export function readContextHash(
|
|
27
|
-
state: State | null | undefined,
|
|
28
|
-
): string | undefined {
|
|
29
|
-
const ctx = state?.context as { url?: { hash?: string } } | undefined;
|
|
30
|
-
|
|
31
|
-
return ctx?.url?.hash;
|
|
32
|
-
}
|