@real-router/memory-plugin 0.4.5 → 0.4.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@real-router/memory-plugin",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "type": "commonjs",
5
5
  "description": "In-memory history engine for Real-Router — non-browser environments and benchmarks",
6
6
  "main": "./dist/cjs/index.js",
@@ -18,8 +18,7 @@
18
18
  }
19
19
  },
20
20
  "files": [
21
- "dist",
22
- "src"
21
+ "dist"
23
22
  ],
24
23
  "repository": {
25
24
  "type": "git",
@@ -45,7 +44,7 @@
45
44
  "homepage": "https://github.com/greydragon888/real-router",
46
45
  "sideEffects": false,
47
46
  "dependencies": {
48
- "@real-router/core": "^0.56.0",
47
+ "@real-router/core": "^0.57.0",
49
48
  "@real-router/types": "^0.36.0"
50
49
  },
51
50
  "scripts": {
package/src/factory.ts DELETED
@@ -1,34 +0,0 @@
1
- import { getPluginApi } from "@real-router/core/api";
2
-
3
- import { MemoryPlugin } from "./plugin";
4
-
5
- import type { MemoryPluginOptions } from "./types";
6
- import type { PluginFactory, Plugin, Router } from "@real-router/core";
7
-
8
- export function memoryPluginFactory(
9
- options: MemoryPluginOptions = {},
10
- ): PluginFactory {
11
- if (options.maxHistoryLength !== undefined) {
12
- const length = options.maxHistoryLength;
13
-
14
- if (
15
- typeof length !== "number" ||
16
- !Number.isFinite(length) ||
17
- !Number.isInteger(length) ||
18
- length < 0
19
- ) {
20
- throw new TypeError(
21
- `[memory-plugin] Invalid maxHistoryLength: expected non-negative integer, got ${String(length)}.`,
22
- );
23
- }
24
- }
25
-
26
- const frozenOptions: MemoryPluginOptions = Object.freeze({ ...options });
27
-
28
- return (router): Plugin => {
29
- const api = getPluginApi(router);
30
- const plugin = new MemoryPlugin(router as Router, api, frozenOptions);
31
-
32
- return plugin.getPlugin();
33
- };
34
- }
package/src/index.ts DELETED
@@ -1,23 +0,0 @@
1
- export { memoryPluginFactory } from "./factory";
2
-
3
- export type {
4
- MemoryPluginOptions,
5
- MemoryContext,
6
- MemoryDirection,
7
- } from "./types";
8
-
9
- declare module "@real-router/types" {
10
- interface StateContext {
11
- memory?: import("./types").MemoryContext;
12
- }
13
- }
14
-
15
- declare module "@real-router/core" {
16
- interface Router {
17
- back: () => void;
18
- forward: () => void;
19
- go: (delta: number) => void;
20
- canGoBack: () => boolean;
21
- canGoForward: () => boolean;
22
- }
23
- }
package/src/plugin.ts DELETED
@@ -1,188 +0,0 @@
1
- import type {
2
- MemoryContext,
3
- MemoryDirection,
4
- MemoryPluginOptions,
5
- } from "./types";
6
- import type {
7
- NavigationOptions,
8
- Plugin,
9
- Router,
10
- State,
11
- } from "@real-router/core";
12
- import type { PluginApi } from "@real-router/core/api";
13
-
14
- const DEFAULT_MAX_HISTORY = 1000;
15
-
16
- /** @internal — instantiated by `memoryPluginFactory`; not part of the public API. */
17
- export class MemoryPlugin {
18
- readonly #router: Router;
19
- readonly #api: PluginApi;
20
- readonly #maxHistory: number;
21
- // Stored entries are full State snapshots (#561). Snapshot semantics for
22
- // back/forward replay: api.navigateToState commits the stored State as-is,
23
- // immune to post-recording route mutations (routes.update / routes.replace
24
- // changing defaultParams or meta) and to non-idempotent dynamic
25
- // forwardFn / buildPath interceptors. Activation guards still run at
26
- // replay time — that is where current-world-state checks belong, not in
27
- // the navigation pipeline.
28
- readonly #entries: State[] = [];
29
- readonly #removeExtensions: () => void;
30
- readonly #claim: {
31
- write: (state: State, value: MemoryContext) => void;
32
- release: () => void;
33
- };
34
- #index = -1;
35
- #navigatingFromHistory = false;
36
- #pendingDirection: MemoryDirection = "navigate";
37
- #goGeneration = 0;
38
- #disposed = false;
39
-
40
- constructor(router: Router, api: PluginApi, options: MemoryPluginOptions) {
41
- this.#router = router;
42
- this.#api = api;
43
- this.#maxHistory = options.maxHistoryLength ?? DEFAULT_MAX_HISTORY;
44
- this.#claim = api.claimContextNamespace("memory");
45
-
46
- this.#removeExtensions = api.extendRouter({
47
- back: () => {
48
- this.#go(-1);
49
- },
50
- forward: () => {
51
- this.#go(1);
52
- },
53
- go: (delta: number) => {
54
- this.#go(delta);
55
- },
56
- canGoBack: () => this.#index > 0,
57
- canGoForward: () => this.#index < this.#entries.length - 1,
58
- });
59
- }
60
-
61
- getPlugin(): Plugin {
62
- return {
63
- onTransitionSuccess: (
64
- toState: State,
65
- _fromState: State | undefined,
66
- opts: NavigationOptions,
67
- ) => {
68
- if (this.#navigatingFromHistory) {
69
- this.#writeMemoryContext(toState, this.#pendingDirection);
70
-
71
- return;
72
- }
73
-
74
- if (opts.replace && this.#index >= 0) {
75
- this.#entries[this.#index] = toState;
76
- } else {
77
- this.#entries.length = this.#index + 1;
78
- this.#entries.push(toState);
79
- this.#index = this.#entries.length - 1;
80
-
81
- if (this.#maxHistory > 0 && this.#entries.length > this.#maxHistory) {
82
- const overflow = this.#entries.length - this.#maxHistory;
83
-
84
- this.#entries.splice(0, overflow);
85
- this.#index = Math.max(0, this.#index - overflow);
86
- }
87
- }
88
-
89
- this.#writeMemoryContext(toState, "navigate");
90
- },
91
-
92
- onStop: () => {
93
- // Bump generation so any in-flight #go settler observes a mismatch
94
- // and skips its revert / flag reset — writing into cleared state
95
- // would otherwise leave #index pointing into an empty #entries (#505).
96
- this.#goGeneration++;
97
- this.#clear();
98
- },
99
-
100
- teardown: () => {
101
- /* v8 ignore next 3 -- @preserve: core's unsubscribe() already guards via `unsubscribed` flag; this idempotency check covers router.dispose() + unsubscribe() ordering edge cases */
102
- if (this.#disposed) {
103
- return;
104
- }
105
-
106
- this.#disposed = true;
107
- // Same generation bump as onStop — pre-teardown in-flight #go settlers
108
- // must not write into a released plugin (#505).
109
- this.#goGeneration++;
110
- this.#removeExtensions();
111
- this.#claim.release();
112
- this.#clear();
113
- },
114
- };
115
- }
116
-
117
- #writeMemoryContext(toState: State, direction: MemoryDirection): void {
118
- this.#claim.write(toState, { direction, historyIndex: this.#index });
119
- }
120
-
121
- #go(delta: number): void {
122
- if (!Number.isInteger(delta) || delta === 0) {
123
- return;
124
- }
125
-
126
- const targetIndex = this.#index + delta;
127
-
128
- if (targetIndex < 0 || targetIndex >= this.#entries.length) {
129
- return;
130
- }
131
-
132
- const entry = this.#entries[targetIndex];
133
- const currentState = this.#router.getState();
134
-
135
- if (entry.path === currentState?.path) {
136
- // Short-circuit: landing on an entry whose path matches the current
137
- // state skips api.navigateToState. Still rewrite state.context.memory
138
- // so subscribers see the new historyIndex + direction — otherwise
139
- // UI animation driven by `direction` sees a stale "navigate" value
140
- // and `state.context.memory.historyIndex` diverges from `#index`
141
- // until the next full transition (#508).
142
- this.#index = targetIndex;
143
- this.#writeMemoryContext(currentState, delta > 0 ? "forward" : "back");
144
-
145
- return;
146
- }
147
-
148
- const previousIndex = this.#index;
149
- const generation = ++this.#goGeneration;
150
-
151
- this.#pendingDirection = delta > 0 ? "forward" : "back";
152
- this.#navigatingFromHistory = true;
153
- this.#index = targetIndex;
154
-
155
- // navigateToState commits the stored snapshot verbatim — same primitive
156
- // every URL-driven flow uses (start, popstate, navigate-event). Skips
157
- // forwardState + buildPath re-resolution and their interceptors; route
158
- // mutations between record and replay do not retroactively change what
159
- // back/forward commits (#561).
160
- void this.#api.navigateToState(entry, { replace: true }).then(
161
- () => {
162
- if (this.#goGeneration === generation) {
163
- this.#navigatingFromHistory = false;
164
- }
165
- },
166
- () => {
167
- if (this.#goGeneration === generation) {
168
- this.#index = previousIndex;
169
- this.#navigatingFromHistory = false;
170
- }
171
- },
172
- );
173
- }
174
-
175
- #clear(): void {
176
- this.#entries.length = 0;
177
- this.#index = -1;
178
- // Reset transient #go state as well: if #clear runs while a #go is in
179
- // flight, the reject-handler skips (generation mismatch) and would
180
- // otherwise leave #navigatingFromHistory stuck at true — the next
181
- // onTransitionSuccess after restart would take the history-restore
182
- // branch and silently skip pushing a new entry. Both fields are
183
- // "current #go intent", not persistent history, so resetting them on
184
- // clear is always correct (#505).
185
- this.#navigatingFromHistory = false;
186
- this.#pendingDirection = "navigate";
187
- }
188
- }
package/src/types.ts DELETED
@@ -1,21 +0,0 @@
1
- export type MemoryDirection = "back" | "forward" | "navigate";
2
-
3
- export interface MemoryContext {
4
- readonly direction: MemoryDirection;
5
- readonly historyIndex: number;
6
- }
7
-
8
- export interface MemoryPluginOptions {
9
- /**
10
- * Maximum number of entries retained in the in-memory history stack.
11
- *
12
- * @description
13
- * When set, the oldest entries are dropped once the stack grows past this
14
- * length. The sentinel value `0` disables trimming (unlimited). Negatives,
15
- * `NaN`, `±Infinity`, and fractional numbers are rejected at factory time
16
- * with a `TypeError`.
17
- *
18
- * @default 1000
19
- */
20
- maxHistoryLength?: number;
21
- }