@real-router/memory-plugin 0.0.1
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 +122 -0
- package/dist/cjs/index.d.ts +23 -0
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/esm/index.d.mts +23 -0
- package/dist/esm/index.mjs +2 -0
- package/dist/esm/index.mjs.map +1 -0
- package/package.json +60 -0
- package/src/factory.ts +29 -0
- package/src/index.ts +13 -0
- package/src/plugin.ts +128 -0
- package/src/types.ts +11 -0
package/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# @real-router/memory-plugin
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@real-router/memory-plugin)
|
|
4
|
+
[](https://www.npmjs.com/package/@real-router/memory-plugin)
|
|
5
|
+
[](https://bundlejs.com/?q=@real-router/memory-plugin&treeshake=[*])
|
|
6
|
+
[](../../LICENSE)
|
|
7
|
+
|
|
8
|
+
> In-memory history stack for [Real-Router](https://github.com/greydragon888/real-router). Back/forward/go navigation without browser History API.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @real-router/memory-plugin
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**Peer dependency:** `@real-router/core`
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { createRouter } from "@real-router/core";
|
|
22
|
+
import { memoryPluginFactory } from "@real-router/memory-plugin";
|
|
23
|
+
|
|
24
|
+
const router = createRouter([
|
|
25
|
+
{ name: "home", path: "/" },
|
|
26
|
+
{ name: "users", path: "/users/:id" },
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
router.usePlugin(memoryPluginFactory());
|
|
30
|
+
await router.start("/");
|
|
31
|
+
|
|
32
|
+
await router.navigate("users", { id: "1" });
|
|
33
|
+
await router.navigate("users", { id: "2" });
|
|
34
|
+
|
|
35
|
+
router.back(); // navigate to users/1
|
|
36
|
+
router.forward(); // navigate to users/2
|
|
37
|
+
router.go(-2); // navigate to home
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Options
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
router.usePlugin(
|
|
44
|
+
memoryPluginFactory({
|
|
45
|
+
maxHistoryLength: 50, // Keep at most 50 entries
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
| Option | Type | Default | Description |
|
|
51
|
+
| ------------------ | -------- | ------- | ------------------------------------------------------------- |
|
|
52
|
+
| `maxHistoryLength` | `number` | `1000` | Maximum entries. `0` = unlimited, negative throws `TypeError` |
|
|
53
|
+
|
|
54
|
+
## Router Extensions
|
|
55
|
+
|
|
56
|
+
The plugin extends the router instance with five methods via [`extendRouter()`](https://github.com/greydragon888/real-router/wiki/plugin-architecture):
|
|
57
|
+
|
|
58
|
+
| Method | Returns | Description |
|
|
59
|
+
| ---------------- | --------- | --------------------------------------------------------------- |
|
|
60
|
+
| `back()` | `void` | Navigate to the previous history entry |
|
|
61
|
+
| `forward()` | `void` | Navigate to the next history entry |
|
|
62
|
+
| `go(delta)` | `void` | Navigate by `delta` steps (negative = back, positive = forward) |
|
|
63
|
+
| `canGoBack()` | `boolean` | `true` if there is a previous entry |
|
|
64
|
+
| `canGoForward()` | `boolean` | `true` if there is a next entry |
|
|
65
|
+
|
|
66
|
+
All navigation methods are fire-and-forget (`void`). To detect completion, subscribe to state changes before calling.
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
router.subscribe(({ route }) => {
|
|
70
|
+
console.log("navigated to", route.name);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
router.back();
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Guards can block back/forward navigation. If a guard rejects, the history index stays unchanged and `canGoBack()`/`canGoForward()` continue to reflect the actual position.
|
|
77
|
+
|
|
78
|
+
## Use Cases
|
|
79
|
+
|
|
80
|
+
### React Native / non-browser environments
|
|
81
|
+
|
|
82
|
+
No `window.history` required. Drop in `memoryPluginFactory()` and get full back/forward support anywhere JavaScript runs.
|
|
83
|
+
|
|
84
|
+
### Testing and benchmarks
|
|
85
|
+
|
|
86
|
+
Deterministic navigation without browser globals. Start the router at any path, navigate programmatically, assert state.
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
const router = createRouter(routes);
|
|
90
|
+
router.usePlugin(memoryPluginFactory());
|
|
91
|
+
await router.start("/");
|
|
92
|
+
|
|
93
|
+
await router.navigate("dashboard");
|
|
94
|
+
expect(router.canGoBack()).toBe(true);
|
|
95
|
+
|
|
96
|
+
router.back();
|
|
97
|
+
// wait for state change, then assert
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### SSR navigation simulation
|
|
101
|
+
|
|
102
|
+
Simulate multi-step user flows on the server without a browser environment.
|
|
103
|
+
|
|
104
|
+
## Documentation
|
|
105
|
+
|
|
106
|
+
Full documentation: [Wiki — memory-plugin](https://github.com/greydragon888/real-router/wiki/memory-plugin)
|
|
107
|
+
|
|
108
|
+
## Related Packages
|
|
109
|
+
|
|
110
|
+
| Package | Description |
|
|
111
|
+
| ---------------------------------------------------------------------------------------- | -------------------------------------- |
|
|
112
|
+
| [@real-router/core](https://www.npmjs.com/package/@real-router/core) | Core router (required peer dependency) |
|
|
113
|
+
| [@real-router/browser-plugin](https://www.npmjs.com/package/@real-router/browser-plugin) | Browser History API integration |
|
|
114
|
+
| [@real-router/hash-plugin](https://www.npmjs.com/package/@real-router/hash-plugin) | Hash-based routing (`#/path`) |
|
|
115
|
+
|
|
116
|
+
## Contributing
|
|
117
|
+
|
|
118
|
+
See [contributing guidelines](../../CONTRIBUTING.md) for development setup and PR process.
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
[MIT](../../LICENSE) © [Oleg Ivanov](https://github.com/greydragon888)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { PluginFactory } from "@real-router/core";
|
|
2
|
+
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
interface MemoryPluginOptions {
|
|
5
|
+
maxHistoryLength?: number;
|
|
6
|
+
}
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/factory.d.ts
|
|
9
|
+
declare function memoryPluginFactory(options?: MemoryPluginOptions): PluginFactory;
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/index.d.ts
|
|
12
|
+
declare module "@real-router/core" {
|
|
13
|
+
interface Router {
|
|
14
|
+
back: () => void;
|
|
15
|
+
forward: () => void;
|
|
16
|
+
go: (delta: number) => void;
|
|
17
|
+
canGoBack: () => boolean;
|
|
18
|
+
canGoForward: () => boolean;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
export { type MemoryPluginOptions, memoryPluginFactory };
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require(`@real-router/core/api`);var t=class{#e;#t;#n=[];#r;#i=-1;#a=!1;#o=0;constructor(e,t,n){this.#e=e,this.#t=n.maxHistoryLength??1e3,this.#r=t.extendRouter({back:()=>{this.#s(-1)},forward:()=>{this.#s(1)},go:e=>{this.#s(e)},canGoBack:()=>this.#i>0,canGoForward:()=>this.#i<this.#n.length-1})}getPlugin(){return{onTransitionSuccess:(e,t,n)=>{if(this.#a)return;let r={name:e.name,params:e.params,path:e.path};if(n.replace&&this.#i>=0)this.#n[this.#i]=r;else if(this.#n.splice(this.#i+1),this.#n.push(r),this.#i=this.#n.length-1,this.#t>0&&this.#n.length>this.#t){let e=this.#n.length-this.#t;this.#n.splice(0,e),this.#i=Math.max(0,this.#i-e)}},onStop:()=>{this.#c()},teardown:()=>{this.#r(),this.#c()}}}#s(e){if(e===0)return;let t=this.#i+e;if(t<0||t>=this.#n.length)return;let n=this.#n[t],r=this.#e.getState();if(n.path===r?.path){this.#i=t;return}let i=this.#i,a=++this.#o;this.#a=!0,this.#i=t,this.#e.navigate(n.name,n.params,{replace:!0}).catch(()=>{this.#o===a&&(this.#i=i)}).finally(()=>{this.#o===a&&(this.#a=!1)})}#c(){this.#n.length=0,this.#i=-1}};function n(n={}){if(n.maxHistoryLength!==void 0&&(typeof n.maxHistoryLength!=`number`||n.maxHistoryLength<0))throw TypeError(`[memory-plugin] Invalid maxHistoryLength: expected non-negative number, got ${String(n.maxHistoryLength)}.`);let r=Object.freeze({...n});return n=>new t(n,(0,e.getPluginApi)(n),r).getPlugin()}exports.memoryPluginFactory=n;
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["#router","#maxHistory","#entries","#removeExtensions","#go","#index","#navigatingFromHistory","#clear","#goGeneration"],"sources":["../../src/plugin.ts","../../src/factory.ts"],"sourcesContent":["import type { HistoryEntry, MemoryPluginOptions } from \"./types\";\nimport type {\n NavigationOptions,\n Plugin,\n Router,\n State,\n} from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\nconst DEFAULT_MAX_HISTORY = 1000;\n\nexport class MemoryPlugin {\n readonly #router: Router;\n readonly #maxHistory: number;\n readonly #entries: HistoryEntry[] = [];\n readonly #removeExtensions: () => void;\n #index = -1;\n #navigatingFromHistory = false;\n #goGeneration = 0;\n\n constructor(router: Router, api: PluginApi, options: MemoryPluginOptions) {\n this.#router = router;\n this.#maxHistory = options.maxHistoryLength ?? DEFAULT_MAX_HISTORY;\n\n this.#removeExtensions = api.extendRouter({\n back: () => {\n this.#go(-1);\n },\n forward: () => {\n this.#go(1);\n },\n go: (delta: number) => {\n this.#go(delta);\n },\n canGoBack: () => this.#index > 0,\n canGoForward: () => this.#index < this.#entries.length - 1,\n });\n }\n\n getPlugin(): Plugin {\n return {\n onTransitionSuccess: (\n toState: State,\n _fromState: State | undefined,\n opts: NavigationOptions,\n ) => {\n if (this.#navigatingFromHistory) {\n return;\n }\n\n const entry: HistoryEntry = {\n name: toState.name,\n params: toState.params,\n path: toState.path,\n };\n\n if (opts.replace && this.#index >= 0) {\n this.#entries[this.#index] = entry;\n } else {\n this.#entries.splice(this.#index + 1);\n this.#entries.push(entry);\n this.#index = this.#entries.length - 1;\n\n if (this.#maxHistory > 0 && this.#entries.length > this.#maxHistory) {\n const overflow = this.#entries.length - this.#maxHistory;\n\n this.#entries.splice(0, overflow);\n this.#index = Math.max(0, this.#index - overflow);\n }\n }\n },\n\n onStop: () => {\n this.#clear();\n },\n\n teardown: () => {\n this.#removeExtensions();\n this.#clear();\n },\n };\n }\n\n #go(delta: number): void {\n if (delta === 0) {\n return;\n }\n\n const targetIndex = this.#index + delta;\n\n if (targetIndex < 0 || targetIndex >= this.#entries.length) {\n return;\n }\n\n const entry = this.#entries[targetIndex];\n const currentState = this.#router.getState();\n\n if (entry.path === currentState?.path) {\n this.#index = targetIndex;\n\n return;\n }\n\n const previousIndex = this.#index;\n const generation = ++this.#goGeneration;\n\n this.#navigatingFromHistory = true;\n this.#index = targetIndex;\n\n void this.#router\n .navigate(entry.name, entry.params, { replace: true })\n .catch(() => {\n if (this.#goGeneration === generation) {\n this.#index = previousIndex;\n }\n })\n .finally(() => {\n if (this.#goGeneration === generation) {\n this.#navigatingFromHistory = false;\n }\n });\n }\n\n #clear(): void {\n this.#entries.length = 0;\n this.#index = -1;\n }\n}\n","import { getPluginApi } from \"@real-router/core/api\";\n\nimport { MemoryPlugin } from \"./plugin\";\n\nimport type { MemoryPluginOptions } from \"./types\";\nimport type { PluginFactory, Plugin, Router } from \"@real-router/core\";\n\nexport function memoryPluginFactory(\n options: MemoryPluginOptions = {},\n): PluginFactory {\n if (\n options.maxHistoryLength !== undefined &&\n (typeof options.maxHistoryLength !== \"number\" ||\n options.maxHistoryLength < 0)\n ) {\n throw new TypeError(\n `[memory-plugin] Invalid maxHistoryLength: expected non-negative number, got ${String(options.maxHistoryLength)}.`,\n );\n }\n\n const frozenOptions: MemoryPluginOptions = Object.freeze({ ...options });\n\n return (router): Plugin => {\n const api = getPluginApi(router);\n const plugin = new MemoryPlugin(router as Router, api, frozenOptions);\n\n return plugin.getPlugin();\n };\n}\n"],"mappings":"0GAWA,IAAa,EAAb,KAA0B,CACxB,GACA,GACA,GAAoC,EAAE,CACtC,GACA,GAAS,GACT,GAAyB,GACzB,GAAgB,EAEhB,YAAY,EAAgB,EAAgB,EAA8B,CACxE,MAAA,EAAe,EACf,MAAA,EAAmB,EAAQ,kBAAoB,IAE/C,MAAA,EAAyB,EAAI,aAAa,CACxC,SAAY,CACV,MAAA,EAAS,GAAG,EAEd,YAAe,CACb,MAAA,EAAS,EAAE,EAEb,GAAK,GAAkB,CACrB,MAAA,EAAS,EAAM,EAEjB,cAAiB,MAAA,EAAc,EAC/B,iBAAoB,MAAA,EAAc,MAAA,EAAc,OAAS,EAC1D,CAAC,CAGJ,WAAoB,CAClB,MAAO,CACL,qBACE,EACA,EACA,IACG,CACH,GAAI,MAAA,EACF,OAGF,IAAM,EAAsB,CAC1B,KAAM,EAAQ,KACd,OAAQ,EAAQ,OAChB,KAAM,EAAQ,KACf,CAED,GAAI,EAAK,SAAW,MAAA,GAAe,EACjC,MAAA,EAAc,MAAA,GAAe,UAE7B,MAAA,EAAc,OAAO,MAAA,EAAc,EAAE,CACrC,MAAA,EAAc,KAAK,EAAM,CACzB,MAAA,EAAc,MAAA,EAAc,OAAS,EAEjC,MAAA,EAAmB,GAAK,MAAA,EAAc,OAAS,MAAA,EAAkB,CACnE,IAAM,EAAW,MAAA,EAAc,OAAS,MAAA,EAExC,MAAA,EAAc,OAAO,EAAG,EAAS,CACjC,MAAA,EAAc,KAAK,IAAI,EAAG,MAAA,EAAc,EAAS,GAKvD,WAAc,CACZ,MAAA,GAAa,EAGf,aAAgB,CACd,MAAA,GAAwB,CACxB,MAAA,GAAa,EAEhB,CAGH,GAAI,EAAqB,CACvB,GAAI,IAAU,EACZ,OAGF,IAAM,EAAc,MAAA,EAAc,EAElC,GAAI,EAAc,GAAK,GAAe,MAAA,EAAc,OAClD,OAGF,IAAM,EAAQ,MAAA,EAAc,GACtB,EAAe,MAAA,EAAa,UAAU,CAE5C,GAAI,EAAM,OAAS,GAAc,KAAM,CACrC,MAAA,EAAc,EAEd,OAGF,IAAM,EAAgB,MAAA,EAChB,EAAa,EAAE,MAAA,EAErB,MAAA,EAA8B,GAC9B,MAAA,EAAc,EAET,MAAA,EACF,SAAS,EAAM,KAAM,EAAM,OAAQ,CAAE,QAAS,GAAM,CAAC,CACrD,UAAY,CACP,MAAA,IAAuB,IACzB,MAAA,EAAc,IAEhB,CACD,YAAc,CACT,MAAA,IAAuB,IACzB,MAAA,EAA8B,KAEhC,CAGN,IAAe,CACb,MAAA,EAAc,OAAS,EACvB,MAAA,EAAc,KCtHlB,SAAgB,EACd,EAA+B,EAAE,CAClB,CACf,GACE,EAAQ,mBAAqB,IAAA,KAC5B,OAAO,EAAQ,kBAAqB,UACnC,EAAQ,iBAAmB,GAE7B,MAAU,UACR,+EAA+E,OAAO,EAAQ,iBAAiB,CAAC,GACjH,CAGH,IAAM,EAAqC,OAAO,OAAO,CAAE,GAAG,EAAS,CAAC,CAExE,MAAQ,IAES,IAAI,EAAa,GAAA,EAAA,EAAA,cADP,EAAO,CACuB,EAAc,CAEvD,WAAW"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { PluginFactory } from "@real-router/core";
|
|
2
|
+
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
interface MemoryPluginOptions {
|
|
5
|
+
maxHistoryLength?: number;
|
|
6
|
+
}
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/factory.d.ts
|
|
9
|
+
declare function memoryPluginFactory(options?: MemoryPluginOptions): PluginFactory;
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/index.d.ts
|
|
12
|
+
declare module "@real-router/core" {
|
|
13
|
+
interface Router {
|
|
14
|
+
back: () => void;
|
|
15
|
+
forward: () => void;
|
|
16
|
+
go: (delta: number) => void;
|
|
17
|
+
canGoBack: () => boolean;
|
|
18
|
+
canGoForward: () => boolean;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
export { type MemoryPluginOptions, memoryPluginFactory };
|
|
23
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{getPluginApi as e}from"@real-router/core/api";var t=class{#e;#t;#n=[];#r;#i=-1;#a=!1;#o=0;constructor(e,t,n){this.#e=e,this.#t=n.maxHistoryLength??1e3,this.#r=t.extendRouter({back:()=>{this.#s(-1)},forward:()=>{this.#s(1)},go:e=>{this.#s(e)},canGoBack:()=>this.#i>0,canGoForward:()=>this.#i<this.#n.length-1})}getPlugin(){return{onTransitionSuccess:(e,t,n)=>{if(this.#a)return;let r={name:e.name,params:e.params,path:e.path};if(n.replace&&this.#i>=0)this.#n[this.#i]=r;else if(this.#n.splice(this.#i+1),this.#n.push(r),this.#i=this.#n.length-1,this.#t>0&&this.#n.length>this.#t){let e=this.#n.length-this.#t;this.#n.splice(0,e),this.#i=Math.max(0,this.#i-e)}},onStop:()=>{this.#c()},teardown:()=>{this.#r(),this.#c()}}}#s(e){if(e===0)return;let t=this.#i+e;if(t<0||t>=this.#n.length)return;let n=this.#n[t],r=this.#e.getState();if(n.path===r?.path){this.#i=t;return}let i=this.#i,a=++this.#o;this.#a=!0,this.#i=t,this.#e.navigate(n.name,n.params,{replace:!0}).catch(()=>{this.#o===a&&(this.#i=i)}).finally(()=>{this.#o===a&&(this.#a=!1)})}#c(){this.#n.length=0,this.#i=-1}};function n(n={}){if(n.maxHistoryLength!==void 0&&(typeof n.maxHistoryLength!=`number`||n.maxHistoryLength<0))throw TypeError(`[memory-plugin] Invalid maxHistoryLength: expected non-negative number, got ${String(n.maxHistoryLength)}.`);let r=Object.freeze({...n});return n=>new t(n,e(n),r).getPlugin()}export{n as memoryPluginFactory};
|
|
2
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["#router","#maxHistory","#entries","#removeExtensions","#go","#index","#navigatingFromHistory","#clear","#goGeneration"],"sources":["../../src/plugin.ts","../../src/factory.ts"],"sourcesContent":["import type { HistoryEntry, MemoryPluginOptions } from \"./types\";\nimport type {\n NavigationOptions,\n Plugin,\n Router,\n State,\n} from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\nconst DEFAULT_MAX_HISTORY = 1000;\n\nexport class MemoryPlugin {\n readonly #router: Router;\n readonly #maxHistory: number;\n readonly #entries: HistoryEntry[] = [];\n readonly #removeExtensions: () => void;\n #index = -1;\n #navigatingFromHistory = false;\n #goGeneration = 0;\n\n constructor(router: Router, api: PluginApi, options: MemoryPluginOptions) {\n this.#router = router;\n this.#maxHistory = options.maxHistoryLength ?? DEFAULT_MAX_HISTORY;\n\n this.#removeExtensions = api.extendRouter({\n back: () => {\n this.#go(-1);\n },\n forward: () => {\n this.#go(1);\n },\n go: (delta: number) => {\n this.#go(delta);\n },\n canGoBack: () => this.#index > 0,\n canGoForward: () => this.#index < this.#entries.length - 1,\n });\n }\n\n getPlugin(): Plugin {\n return {\n onTransitionSuccess: (\n toState: State,\n _fromState: State | undefined,\n opts: NavigationOptions,\n ) => {\n if (this.#navigatingFromHistory) {\n return;\n }\n\n const entry: HistoryEntry = {\n name: toState.name,\n params: toState.params,\n path: toState.path,\n };\n\n if (opts.replace && this.#index >= 0) {\n this.#entries[this.#index] = entry;\n } else {\n this.#entries.splice(this.#index + 1);\n this.#entries.push(entry);\n this.#index = this.#entries.length - 1;\n\n if (this.#maxHistory > 0 && this.#entries.length > this.#maxHistory) {\n const overflow = this.#entries.length - this.#maxHistory;\n\n this.#entries.splice(0, overflow);\n this.#index = Math.max(0, this.#index - overflow);\n }\n }\n },\n\n onStop: () => {\n this.#clear();\n },\n\n teardown: () => {\n this.#removeExtensions();\n this.#clear();\n },\n };\n }\n\n #go(delta: number): void {\n if (delta === 0) {\n return;\n }\n\n const targetIndex = this.#index + delta;\n\n if (targetIndex < 0 || targetIndex >= this.#entries.length) {\n return;\n }\n\n const entry = this.#entries[targetIndex];\n const currentState = this.#router.getState();\n\n if (entry.path === currentState?.path) {\n this.#index = targetIndex;\n\n return;\n }\n\n const previousIndex = this.#index;\n const generation = ++this.#goGeneration;\n\n this.#navigatingFromHistory = true;\n this.#index = targetIndex;\n\n void this.#router\n .navigate(entry.name, entry.params, { replace: true })\n .catch(() => {\n if (this.#goGeneration === generation) {\n this.#index = previousIndex;\n }\n })\n .finally(() => {\n if (this.#goGeneration === generation) {\n this.#navigatingFromHistory = false;\n }\n });\n }\n\n #clear(): void {\n this.#entries.length = 0;\n this.#index = -1;\n }\n}\n","import { getPluginApi } from \"@real-router/core/api\";\n\nimport { MemoryPlugin } from \"./plugin\";\n\nimport type { MemoryPluginOptions } from \"./types\";\nimport type { PluginFactory, Plugin, Router } from \"@real-router/core\";\n\nexport function memoryPluginFactory(\n options: MemoryPluginOptions = {},\n): PluginFactory {\n if (\n options.maxHistoryLength !== undefined &&\n (typeof options.maxHistoryLength !== \"number\" ||\n options.maxHistoryLength < 0)\n ) {\n throw new TypeError(\n `[memory-plugin] Invalid maxHistoryLength: expected non-negative number, got ${String(options.maxHistoryLength)}.`,\n );\n }\n\n const frozenOptions: MemoryPluginOptions = Object.freeze({ ...options });\n\n return (router): Plugin => {\n const api = getPluginApi(router);\n const plugin = new MemoryPlugin(router as Router, api, frozenOptions);\n\n return plugin.getPlugin();\n };\n}\n"],"mappings":"qDAWA,IAAa,EAAb,KAA0B,CACxB,GACA,GACA,GAAoC,EAAE,CACtC,GACA,GAAS,GACT,GAAyB,GACzB,GAAgB,EAEhB,YAAY,EAAgB,EAAgB,EAA8B,CACxE,MAAA,EAAe,EACf,MAAA,EAAmB,EAAQ,kBAAoB,IAE/C,MAAA,EAAyB,EAAI,aAAa,CACxC,SAAY,CACV,MAAA,EAAS,GAAG,EAEd,YAAe,CACb,MAAA,EAAS,EAAE,EAEb,GAAK,GAAkB,CACrB,MAAA,EAAS,EAAM,EAEjB,cAAiB,MAAA,EAAc,EAC/B,iBAAoB,MAAA,EAAc,MAAA,EAAc,OAAS,EAC1D,CAAC,CAGJ,WAAoB,CAClB,MAAO,CACL,qBACE,EACA,EACA,IACG,CACH,GAAI,MAAA,EACF,OAGF,IAAM,EAAsB,CAC1B,KAAM,EAAQ,KACd,OAAQ,EAAQ,OAChB,KAAM,EAAQ,KACf,CAED,GAAI,EAAK,SAAW,MAAA,GAAe,EACjC,MAAA,EAAc,MAAA,GAAe,UAE7B,MAAA,EAAc,OAAO,MAAA,EAAc,EAAE,CACrC,MAAA,EAAc,KAAK,EAAM,CACzB,MAAA,EAAc,MAAA,EAAc,OAAS,EAEjC,MAAA,EAAmB,GAAK,MAAA,EAAc,OAAS,MAAA,EAAkB,CACnE,IAAM,EAAW,MAAA,EAAc,OAAS,MAAA,EAExC,MAAA,EAAc,OAAO,EAAG,EAAS,CACjC,MAAA,EAAc,KAAK,IAAI,EAAG,MAAA,EAAc,EAAS,GAKvD,WAAc,CACZ,MAAA,GAAa,EAGf,aAAgB,CACd,MAAA,GAAwB,CACxB,MAAA,GAAa,EAEhB,CAGH,GAAI,EAAqB,CACvB,GAAI,IAAU,EACZ,OAGF,IAAM,EAAc,MAAA,EAAc,EAElC,GAAI,EAAc,GAAK,GAAe,MAAA,EAAc,OAClD,OAGF,IAAM,EAAQ,MAAA,EAAc,GACtB,EAAe,MAAA,EAAa,UAAU,CAE5C,GAAI,EAAM,OAAS,GAAc,KAAM,CACrC,MAAA,EAAc,EAEd,OAGF,IAAM,EAAgB,MAAA,EAChB,EAAa,EAAE,MAAA,EAErB,MAAA,EAA8B,GAC9B,MAAA,EAAc,EAET,MAAA,EACF,SAAS,EAAM,KAAM,EAAM,OAAQ,CAAE,QAAS,GAAM,CAAC,CACrD,UAAY,CACP,MAAA,IAAuB,IACzB,MAAA,EAAc,IAEhB,CACD,YAAc,CACT,MAAA,IAAuB,IACzB,MAAA,EAA8B,KAEhC,CAGN,IAAe,CACb,MAAA,EAAc,OAAS,EACvB,MAAA,EAAc,KCtHlB,SAAgB,EACd,EAA+B,EAAE,CAClB,CACf,GACE,EAAQ,mBAAqB,IAAA,KAC5B,OAAO,EAAQ,kBAAqB,UACnC,EAAQ,iBAAmB,GAE7B,MAAU,UACR,+EAA+E,OAAO,EAAQ,iBAAiB,CAAC,GACjH,CAGH,IAAM,EAAqC,OAAO,OAAO,CAAE,GAAG,EAAS,CAAC,CAExE,MAAQ,IAES,IAAI,EAAa,EADpB,EAAa,EAAO,CACuB,EAAc,CAEvD,WAAW"}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@real-router/memory-plugin",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "commonjs",
|
|
5
|
+
"description": "In-memory history engine for Real-Router — non-browser environments and benchmarks",
|
|
6
|
+
"main": "./dist/cjs/index.js",
|
|
7
|
+
"module": "./dist/esm/index.mjs",
|
|
8
|
+
"types": "./dist/esm/index.d.mts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"development": "./src/index.ts",
|
|
12
|
+
"types": {
|
|
13
|
+
"import": "./dist/esm/index.d.mts",
|
|
14
|
+
"require": "./dist/cjs/index.d.ts"
|
|
15
|
+
},
|
|
16
|
+
"import": "./dist/esm/index.mjs",
|
|
17
|
+
"require": "./dist/cjs/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"src"
|
|
23
|
+
],
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/greydragon888/real-router.git"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"real-router",
|
|
30
|
+
"memory",
|
|
31
|
+
"history",
|
|
32
|
+
"in-memory",
|
|
33
|
+
"react-native",
|
|
34
|
+
"benchmark"
|
|
35
|
+
],
|
|
36
|
+
"author": {
|
|
37
|
+
"name": "Oleg Ivanov",
|
|
38
|
+
"email": "greydragon888@gmail.com",
|
|
39
|
+
"url": "https://github.com/greydragon888"
|
|
40
|
+
},
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/greydragon888/real-router/issues"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/greydragon888/real-router",
|
|
46
|
+
"scripts": {
|
|
47
|
+
"test": "vitest",
|
|
48
|
+
"test:properties": "vitest --config vitest.config.properties.mts --run",
|
|
49
|
+
"build": "tsdown --config-loader unrun",
|
|
50
|
+
"type-check": "tsc --noEmit",
|
|
51
|
+
"lint": "eslint --cache --ext .ts src/ tests/ --fix --max-warnings 0",
|
|
52
|
+
"lint:package": "publint",
|
|
53
|
+
"lint:types": "attw --pack .",
|
|
54
|
+
"build:dist-only": "tsdown --config-loader unrun"
|
|
55
|
+
},
|
|
56
|
+
"sideEffects": false,
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@real-router/core": "workspace:^"
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/factory.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
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 (
|
|
12
|
+
options.maxHistoryLength !== undefined &&
|
|
13
|
+
(typeof options.maxHistoryLength !== "number" ||
|
|
14
|
+
options.maxHistoryLength < 0)
|
|
15
|
+
) {
|
|
16
|
+
throw new TypeError(
|
|
17
|
+
`[memory-plugin] Invalid maxHistoryLength: expected non-negative number, got ${String(options.maxHistoryLength)}.`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const frozenOptions: MemoryPluginOptions = Object.freeze({ ...options });
|
|
22
|
+
|
|
23
|
+
return (router): Plugin => {
|
|
24
|
+
const api = getPluginApi(router);
|
|
25
|
+
const plugin = new MemoryPlugin(router as Router, api, frozenOptions);
|
|
26
|
+
|
|
27
|
+
return plugin.getPlugin();
|
|
28
|
+
};
|
|
29
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { memoryPluginFactory } from "./factory";
|
|
2
|
+
|
|
3
|
+
export type { MemoryPluginOptions } from "./types";
|
|
4
|
+
|
|
5
|
+
declare module "@real-router/core" {
|
|
6
|
+
interface Router {
|
|
7
|
+
back: () => void;
|
|
8
|
+
forward: () => void;
|
|
9
|
+
go: (delta: number) => void;
|
|
10
|
+
canGoBack: () => boolean;
|
|
11
|
+
canGoForward: () => boolean;
|
|
12
|
+
}
|
|
13
|
+
}
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { HistoryEntry, MemoryPluginOptions } from "./types";
|
|
2
|
+
import type {
|
|
3
|
+
NavigationOptions,
|
|
4
|
+
Plugin,
|
|
5
|
+
Router,
|
|
6
|
+
State,
|
|
7
|
+
} from "@real-router/core";
|
|
8
|
+
import type { PluginApi } from "@real-router/core/api";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_MAX_HISTORY = 1000;
|
|
11
|
+
|
|
12
|
+
export class MemoryPlugin {
|
|
13
|
+
readonly #router: Router;
|
|
14
|
+
readonly #maxHistory: number;
|
|
15
|
+
readonly #entries: HistoryEntry[] = [];
|
|
16
|
+
readonly #removeExtensions: () => void;
|
|
17
|
+
#index = -1;
|
|
18
|
+
#navigatingFromHistory = false;
|
|
19
|
+
#goGeneration = 0;
|
|
20
|
+
|
|
21
|
+
constructor(router: Router, api: PluginApi, options: MemoryPluginOptions) {
|
|
22
|
+
this.#router = router;
|
|
23
|
+
this.#maxHistory = options.maxHistoryLength ?? DEFAULT_MAX_HISTORY;
|
|
24
|
+
|
|
25
|
+
this.#removeExtensions = api.extendRouter({
|
|
26
|
+
back: () => {
|
|
27
|
+
this.#go(-1);
|
|
28
|
+
},
|
|
29
|
+
forward: () => {
|
|
30
|
+
this.#go(1);
|
|
31
|
+
},
|
|
32
|
+
go: (delta: number) => {
|
|
33
|
+
this.#go(delta);
|
|
34
|
+
},
|
|
35
|
+
canGoBack: () => this.#index > 0,
|
|
36
|
+
canGoForward: () => this.#index < this.#entries.length - 1,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getPlugin(): Plugin {
|
|
41
|
+
return {
|
|
42
|
+
onTransitionSuccess: (
|
|
43
|
+
toState: State,
|
|
44
|
+
_fromState: State | undefined,
|
|
45
|
+
opts: NavigationOptions,
|
|
46
|
+
) => {
|
|
47
|
+
if (this.#navigatingFromHistory) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const entry: HistoryEntry = {
|
|
52
|
+
name: toState.name,
|
|
53
|
+
params: toState.params,
|
|
54
|
+
path: toState.path,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
if (opts.replace && this.#index >= 0) {
|
|
58
|
+
this.#entries[this.#index] = entry;
|
|
59
|
+
} else {
|
|
60
|
+
this.#entries.splice(this.#index + 1);
|
|
61
|
+
this.#entries.push(entry);
|
|
62
|
+
this.#index = this.#entries.length - 1;
|
|
63
|
+
|
|
64
|
+
if (this.#maxHistory > 0 && this.#entries.length > this.#maxHistory) {
|
|
65
|
+
const overflow = this.#entries.length - this.#maxHistory;
|
|
66
|
+
|
|
67
|
+
this.#entries.splice(0, overflow);
|
|
68
|
+
this.#index = Math.max(0, this.#index - overflow);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
onStop: () => {
|
|
74
|
+
this.#clear();
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
teardown: () => {
|
|
78
|
+
this.#removeExtensions();
|
|
79
|
+
this.#clear();
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#go(delta: number): void {
|
|
85
|
+
if (delta === 0) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const targetIndex = this.#index + delta;
|
|
90
|
+
|
|
91
|
+
if (targetIndex < 0 || targetIndex >= this.#entries.length) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const entry = this.#entries[targetIndex];
|
|
96
|
+
const currentState = this.#router.getState();
|
|
97
|
+
|
|
98
|
+
if (entry.path === currentState?.path) {
|
|
99
|
+
this.#index = targetIndex;
|
|
100
|
+
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const previousIndex = this.#index;
|
|
105
|
+
const generation = ++this.#goGeneration;
|
|
106
|
+
|
|
107
|
+
this.#navigatingFromHistory = true;
|
|
108
|
+
this.#index = targetIndex;
|
|
109
|
+
|
|
110
|
+
void this.#router
|
|
111
|
+
.navigate(entry.name, entry.params, { replace: true })
|
|
112
|
+
.catch(() => {
|
|
113
|
+
if (this.#goGeneration === generation) {
|
|
114
|
+
this.#index = previousIndex;
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
.finally(() => {
|
|
118
|
+
if (this.#goGeneration === generation) {
|
|
119
|
+
this.#navigatingFromHistory = false;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#clear(): void {
|
|
125
|
+
this.#entries.length = 0;
|
|
126
|
+
this.#index = -1;
|
|
127
|
+
}
|
|
128
|
+
}
|
package/src/types.ts
ADDED