@real-router/memory-plugin 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
@@ -32,6 +32,8 @@ await router.start("/");
32
32
  await router.navigate("users", { id: "1" });
33
33
  await router.navigate("users", { id: "2" });
34
34
 
35
+ // Fire-and-forget: each call schedules a navigation and returns immediately.
36
+ // Subscribe to state changes to detect completion.
35
37
  router.back(); // navigate to users/1
36
38
  router.forward(); // navigate to users/2
37
39
  router.go(-2); // navigate to home
@@ -1,6 +1,11 @@
1
1
  import { PluginFactory } from "@real-router/core";
2
2
 
3
3
  //#region src/types.d.ts
4
+ type MemoryDirection = "back" | "forward" | "navigate";
5
+ interface MemoryContext {
6
+ direction: MemoryDirection;
7
+ historyIndex: number;
8
+ }
4
9
  interface MemoryPluginOptions {
5
10
  maxHistoryLength?: number;
6
11
  }
@@ -9,6 +14,11 @@ interface MemoryPluginOptions {
9
14
  declare function memoryPluginFactory(options?: MemoryPluginOptions): PluginFactory;
10
15
  //#endregion
11
16
  //#region src/index.d.ts
17
+ declare module "@real-router/types" {
18
+ interface StateContext {
19
+ memory?: MemoryContext;
20
+ }
21
+ }
12
22
  declare module "@real-router/core" {
13
23
  interface Router {
14
24
  back: () => void;
@@ -19,5 +29,5 @@ declare module "@real-router/core" {
19
29
  }
20
30
  } //# sourceMappingURL=index.d.ts.map
21
31
  //#endregion
22
- export { type MemoryPluginOptions, memoryPluginFactory };
32
+ export { type MemoryContext, type MemoryDirection, type MemoryPluginOptions, memoryPluginFactory };
23
33
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/types.ts","../../src/factory.ts","../../src/index.ts"],"mappings":";;;UAEiB,mBAAA;EACf,gBAAA;AAAA;;;iBCIc,mBAAA,CACd,OAAA,GAAS,mBAAA,GACR,aAAA;;;;YCJS,MAAA;IACR,IAAA;IACA,OAAA;IACA,EAAA,GAAK,KAAA;IACL,SAAA;IACA,YAAA;EAAA;AAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/types.ts","../../src/factory.ts","../../src/index.ts"],"mappings":";;;KAEY,eAAA;AAAA,UAEK,aAAA;EACf,SAAA,EAAW,eAAA;EACX,YAAA;AAAA;AAAA,UAGe,mBAAA;EACf,gBAAA;AAAA;;;iBCHc,mBAAA,CACd,OAAA,GAAS,mBAAA,GACR,aAAA;;;;YCAS,YAAA;IACR,MAAA,GAJa,aAAA;EAAA;AAAA;AAAA;EAAA,UASL,MAAA;IACR,IAAA;IACA,OAAA;IACA,EAAA,GAAK,KAAA;IACL,SAAA;IACA,YAAA;EAAA;AAAA"}
package/dist/cjs/index.js CHANGED
@@ -1,2 +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;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require(`@real-router/core/api`);var t=class{#e;#t;#n=[];#r;#i;#a=-1;#o=!1;#s=`navigate`;#c=0;#l=!1;constructor(e,t,n){this.#e=e,this.#t=n.maxHistoryLength??1e3,this.#i=t.claimContextNamespace(`memory`),this.#r=t.extendRouter({back:()=>{this.#d(-1)},forward:()=>{this.#d(1)},go:e=>{this.#d(e)},canGoBack:()=>this.#a>0,canGoForward:()=>this.#a<this.#n.length-1})}getPlugin(){return{onTransitionSuccess:(e,t,n)=>{if(this.#o){this.#u(e,this.#s);return}let r={name:e.name,params:e.params,path:e.path};if(n.replace&&this.#a>=0)this.#n[this.#a]=r;else if(this.#n.length=this.#a+1,this.#n.push(r),this.#a=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.#a=Math.max(0,this.#a-e)}this.#u(e,`navigate`)},onStop:()=>{this.#f()},teardown:()=>{this.#l||(this.#l=!0,this.#r(),this.#i.release(),this.#f())}}}#u(e,t){this.#i.write(e,Object.freeze({direction:t,historyIndex:this.#a}))}#d(e){if(e===0||!Number.isFinite(e)||!Number.isInteger(e))return;let t=this.#a+e;if(t<0||t>=this.#n.length)return;let n=this.#n[t],r=this.#e.getState();if(n.path===r?.path){this.#a=t;return}let i=this.#a,a=++this.#c;this.#s=e>0?`forward`:`back`,this.#o=!0,this.#a=t,this.#e.navigate(n.name,n.params,{replace:!0}).catch(()=>{this.#c===a&&(this.#a=i)}).finally(()=>{this.#c===a&&(this.#o=!1)})}#f(){this.#n.length=0,this.#a=-1}};function n(n={}){if(n.maxHistoryLength!==void 0){let e=n.maxHistoryLength;if(typeof e!=`number`||!Number.isFinite(e)||!Number.isInteger(e)||e<0)throw TypeError(`[memory-plugin] Invalid maxHistoryLength: expected non-negative integer, got ${String(e)}.`)}let r=Object.freeze({...n});return n=>new t(n,(0,e.getPluginApi)(n),r).getPlugin()}exports.memoryPluginFactory=n;
2
2
  //# sourceMappingURL=index.js.map
@@ -1 +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"}
1
+ {"version":3,"file":"index.js","names":["#router","#maxHistory","#entries","#removeExtensions","#claim","#go","#index","#navigatingFromHistory","#writeMemoryContext","#pendingDirection","#clear","#disposed","#goGeneration"],"sources":["../../src/plugin.ts","../../src/factory.ts"],"sourcesContent":["import type {\n HistoryEntry,\n MemoryContext,\n MemoryDirection,\n MemoryPluginOptions,\n} 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\n/** @internal — instantiated by `memoryPluginFactory`; not part of the public API. */\nexport class MemoryPlugin {\n readonly #router: Router;\n readonly #maxHistory: number;\n readonly #entries: HistoryEntry[] = [];\n readonly #removeExtensions: () => void;\n readonly #claim: {\n write: (state: State, value: MemoryContext) => void;\n release: () => void;\n };\n #index = -1;\n #navigatingFromHistory = false;\n #pendingDirection: MemoryDirection = \"navigate\";\n #goGeneration = 0;\n #disposed = false;\n\n constructor(router: Router, api: PluginApi, options: MemoryPluginOptions) {\n this.#router = router;\n this.#maxHistory = options.maxHistoryLength ?? DEFAULT_MAX_HISTORY;\n this.#claim = api.claimContextNamespace(\"memory\");\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 this.#writeMemoryContext(toState, this.#pendingDirection);\n\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.length = 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 this.#writeMemoryContext(toState, \"navigate\");\n },\n\n onStop: () => {\n this.#clear();\n },\n\n teardown: () => {\n /* v8 ignore next 3 -- @preserve: core's unsubscribe() already guards via `unsubscribed` flag; this idempotency check covers router.dispose() + unsubscribe() ordering edge cases */\n if (this.#disposed) {\n return;\n }\n\n this.#disposed = true;\n this.#removeExtensions();\n this.#claim.release();\n this.#clear();\n },\n };\n }\n\n #writeMemoryContext(toState: State, direction: MemoryDirection): void {\n this.#claim.write(\n toState,\n Object.freeze({ direction, historyIndex: this.#index }),\n );\n }\n\n #go(delta: number): void {\n if (delta === 0 || !Number.isFinite(delta) || !Number.isInteger(delta)) {\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.#pendingDirection = delta > 0 ? \"forward\" : \"back\";\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 (options.maxHistoryLength !== undefined) {\n const length = options.maxHistoryLength;\n\n if (\n typeof length !== \"number\" ||\n !Number.isFinite(length) ||\n !Number.isInteger(length) ||\n length < 0\n ) {\n throw new TypeError(\n `[memory-plugin] Invalid maxHistoryLength: expected non-negative integer, got ${String(length)}.`,\n );\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":"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"}
@@ -1,6 +1,11 @@
1
1
  import { PluginFactory } from "@real-router/core";
2
2
 
3
3
  //#region src/types.d.ts
4
+ type MemoryDirection = "back" | "forward" | "navigate";
5
+ interface MemoryContext {
6
+ direction: MemoryDirection;
7
+ historyIndex: number;
8
+ }
4
9
  interface MemoryPluginOptions {
5
10
  maxHistoryLength?: number;
6
11
  }
@@ -9,6 +14,11 @@ interface MemoryPluginOptions {
9
14
  declare function memoryPluginFactory(options?: MemoryPluginOptions): PluginFactory;
10
15
  //#endregion
11
16
  //#region src/index.d.ts
17
+ declare module "@real-router/types" {
18
+ interface StateContext {
19
+ memory?: MemoryContext;
20
+ }
21
+ }
12
22
  declare module "@real-router/core" {
13
23
  interface Router {
14
24
  back: () => void;
@@ -19,5 +29,5 @@ declare module "@real-router/core" {
19
29
  }
20
30
  } //# sourceMappingURL=index.d.ts.map
21
31
  //#endregion
22
- export { type MemoryPluginOptions, memoryPluginFactory };
32
+ export { type MemoryContext, type MemoryDirection, type MemoryPluginOptions, memoryPluginFactory };
23
33
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/types.ts","../../src/factory.ts","../../src/index.ts"],"mappings":";;;UAEiB,mBAAA;EACf,gBAAA;AAAA;;;iBCIc,mBAAA,CACd,OAAA,GAAS,mBAAA,GACR,aAAA;;;;YCJS,MAAA;IACR,IAAA;IACA,OAAA;IACA,EAAA,GAAK,KAAA;IACL,SAAA;IACA,YAAA;EAAA;AAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/types.ts","../../src/factory.ts","../../src/index.ts"],"mappings":";;;KAEY,eAAA;AAAA,UAEK,aAAA;EACf,SAAA,EAAW,eAAA;EACX,YAAA;AAAA;AAAA,UAGe,mBAAA;EACf,gBAAA;AAAA;;;iBCHc,mBAAA,CACd,OAAA,GAAS,mBAAA,GACR,aAAA;;;;YCAS,YAAA;IACR,MAAA,GAJa,aAAA;EAAA;AAAA;AAAA;EAAA,UASL,MAAA;IACR,IAAA;IACA,OAAA;IACA,EAAA,GAAK,KAAA;IACL,SAAA;IACA,YAAA;EAAA;AAAA"}
@@ -1,2 +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};
1
+ import{getPluginApi as e}from"@real-router/core/api";var t=class{#e;#t;#n=[];#r;#i;#a=-1;#o=!1;#s=`navigate`;#c=0;#l=!1;constructor(e,t,n){this.#e=e,this.#t=n.maxHistoryLength??1e3,this.#i=t.claimContextNamespace(`memory`),this.#r=t.extendRouter({back:()=>{this.#d(-1)},forward:()=>{this.#d(1)},go:e=>{this.#d(e)},canGoBack:()=>this.#a>0,canGoForward:()=>this.#a<this.#n.length-1})}getPlugin(){return{onTransitionSuccess:(e,t,n)=>{if(this.#o){this.#u(e,this.#s);return}let r={name:e.name,params:e.params,path:e.path};if(n.replace&&this.#a>=0)this.#n[this.#a]=r;else if(this.#n.length=this.#a+1,this.#n.push(r),this.#a=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.#a=Math.max(0,this.#a-e)}this.#u(e,`navigate`)},onStop:()=>{this.#f()},teardown:()=>{this.#l||(this.#l=!0,this.#r(),this.#i.release(),this.#f())}}}#u(e,t){this.#i.write(e,Object.freeze({direction:t,historyIndex:this.#a}))}#d(e){if(e===0||!Number.isFinite(e)||!Number.isInteger(e))return;let t=this.#a+e;if(t<0||t>=this.#n.length)return;let n=this.#n[t],r=this.#e.getState();if(n.path===r?.path){this.#a=t;return}let i=this.#a,a=++this.#c;this.#s=e>0?`forward`:`back`,this.#o=!0,this.#a=t,this.#e.navigate(n.name,n.params,{replace:!0}).catch(()=>{this.#c===a&&(this.#a=i)}).finally(()=>{this.#c===a&&(this.#o=!1)})}#f(){this.#n.length=0,this.#a=-1}};function n(n={}){if(n.maxHistoryLength!==void 0){let e=n.maxHistoryLength;if(typeof e!=`number`||!Number.isFinite(e)||!Number.isInteger(e)||e<0)throw TypeError(`[memory-plugin] Invalid maxHistoryLength: expected non-negative integer, got ${String(e)}.`)}let r=Object.freeze({...n});return n=>new t(n,e(n),r).getPlugin()}export{n as memoryPluginFactory};
2
2
  //# sourceMappingURL=index.mjs.map
@@ -1 +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"}
1
+ {"version":3,"file":"index.mjs","names":["#router","#maxHistory","#entries","#removeExtensions","#claim","#go","#index","#navigatingFromHistory","#writeMemoryContext","#pendingDirection","#clear","#disposed","#goGeneration"],"sources":["../../src/plugin.ts","../../src/factory.ts"],"sourcesContent":["import type {\n HistoryEntry,\n MemoryContext,\n MemoryDirection,\n MemoryPluginOptions,\n} 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\n/** @internal — instantiated by `memoryPluginFactory`; not part of the public API. */\nexport class MemoryPlugin {\n readonly #router: Router;\n readonly #maxHistory: number;\n readonly #entries: HistoryEntry[] = [];\n readonly #removeExtensions: () => void;\n readonly #claim: {\n write: (state: State, value: MemoryContext) => void;\n release: () => void;\n };\n #index = -1;\n #navigatingFromHistory = false;\n #pendingDirection: MemoryDirection = \"navigate\";\n #goGeneration = 0;\n #disposed = false;\n\n constructor(router: Router, api: PluginApi, options: MemoryPluginOptions) {\n this.#router = router;\n this.#maxHistory = options.maxHistoryLength ?? DEFAULT_MAX_HISTORY;\n this.#claim = api.claimContextNamespace(\"memory\");\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 this.#writeMemoryContext(toState, this.#pendingDirection);\n\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.length = 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 this.#writeMemoryContext(toState, \"navigate\");\n },\n\n onStop: () => {\n this.#clear();\n },\n\n teardown: () => {\n /* v8 ignore next 3 -- @preserve: core's unsubscribe() already guards via `unsubscribed` flag; this idempotency check covers router.dispose() + unsubscribe() ordering edge cases */\n if (this.#disposed) {\n return;\n }\n\n this.#disposed = true;\n this.#removeExtensions();\n this.#claim.release();\n this.#clear();\n },\n };\n }\n\n #writeMemoryContext(toState: State, direction: MemoryDirection): void {\n this.#claim.write(\n toState,\n Object.freeze({ direction, historyIndex: this.#index }),\n );\n }\n\n #go(delta: number): void {\n if (delta === 0 || !Number.isFinite(delta) || !Number.isInteger(delta)) {\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.#pendingDirection = delta > 0 ? \"forward\" : \"back\";\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 (options.maxHistoryLength !== undefined) {\n const length = options.maxHistoryLength;\n\n if (\n typeof length !== \"number\" ||\n !Number.isFinite(length) ||\n !Number.isInteger(length) ||\n length < 0\n ) {\n throw new TypeError(\n `[memory-plugin] Invalid maxHistoryLength: expected non-negative integer, got ${String(length)}.`,\n );\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":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@real-router/memory-plugin",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
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",
@@ -45,15 +45,17 @@
45
45
  "homepage": "https://github.com/greydragon888/real-router",
46
46
  "sideEffects": false,
47
47
  "dependencies": {
48
- "@real-router/core": "^0.47.0"
48
+ "@real-router/core": "^0.48.0",
49
+ "@real-router/types": "^0.34.0"
49
50
  },
50
51
  "scripts": {
51
52
  "test": "vitest",
52
53
  "test:properties": "vitest --config vitest.config.properties.mts --run",
53
- "build": "tsdown --config-loader unrun",
54
+ "test:stress": "vitest --config vitest.config.stress.mts --run",
54
55
  "type-check": "tsc --noEmit",
55
56
  "lint": "eslint --cache --ext .ts src/ tests/ --fix --max-warnings 0",
56
57
  "lint:package": "publint",
57
- "lint:types": "attw --pack ."
58
+ "lint:types": "attw --pack .",
59
+ "bundle": "tsdown --config-loader unrun"
58
60
  }
59
61
  }
package/src/factory.ts CHANGED
@@ -8,14 +8,19 @@ import type { PluginFactory, Plugin, Router } from "@real-router/core";
8
8
  export function memoryPluginFactory(
9
9
  options: MemoryPluginOptions = {},
10
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
- );
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
+ }
19
24
  }
20
25
 
21
26
  const frozenOptions: MemoryPluginOptions = Object.freeze({ ...options });
package/src/index.ts CHANGED
@@ -1,6 +1,16 @@
1
1
  export { memoryPluginFactory } from "./factory";
2
2
 
3
- export type { MemoryPluginOptions } from "./types";
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
+ }
4
14
 
5
15
  declare module "@real-router/core" {
6
16
  interface Router {
package/src/plugin.ts CHANGED
@@ -1,4 +1,9 @@
1
- import type { HistoryEntry, MemoryPluginOptions } from "./types";
1
+ import type {
2
+ HistoryEntry,
3
+ MemoryContext,
4
+ MemoryDirection,
5
+ MemoryPluginOptions,
6
+ } from "./types";
2
7
  import type {
3
8
  NavigationOptions,
4
9
  Plugin,
@@ -9,18 +14,26 @@ import type { PluginApi } from "@real-router/core/api";
9
14
 
10
15
  const DEFAULT_MAX_HISTORY = 1000;
11
16
 
17
+ /** @internal — instantiated by `memoryPluginFactory`; not part of the public API. */
12
18
  export class MemoryPlugin {
13
19
  readonly #router: Router;
14
20
  readonly #maxHistory: number;
15
21
  readonly #entries: HistoryEntry[] = [];
16
22
  readonly #removeExtensions: () => void;
23
+ readonly #claim: {
24
+ write: (state: State, value: MemoryContext) => void;
25
+ release: () => void;
26
+ };
17
27
  #index = -1;
18
28
  #navigatingFromHistory = false;
29
+ #pendingDirection: MemoryDirection = "navigate";
19
30
  #goGeneration = 0;
31
+ #disposed = false;
20
32
 
21
33
  constructor(router: Router, api: PluginApi, options: MemoryPluginOptions) {
22
34
  this.#router = router;
23
35
  this.#maxHistory = options.maxHistoryLength ?? DEFAULT_MAX_HISTORY;
36
+ this.#claim = api.claimContextNamespace("memory");
24
37
 
25
38
  this.#removeExtensions = api.extendRouter({
26
39
  back: () => {
@@ -45,6 +58,8 @@ export class MemoryPlugin {
45
58
  opts: NavigationOptions,
46
59
  ) => {
47
60
  if (this.#navigatingFromHistory) {
61
+ this.#writeMemoryContext(toState, this.#pendingDirection);
62
+
48
63
  return;
49
64
  }
50
65
 
@@ -57,7 +72,7 @@ export class MemoryPlugin {
57
72
  if (opts.replace && this.#index >= 0) {
58
73
  this.#entries[this.#index] = entry;
59
74
  } else {
60
- this.#entries.splice(this.#index + 1);
75
+ this.#entries.length = this.#index + 1;
61
76
  this.#entries.push(entry);
62
77
  this.#index = this.#entries.length - 1;
63
78
 
@@ -68,6 +83,8 @@ export class MemoryPlugin {
68
83
  this.#index = Math.max(0, this.#index - overflow);
69
84
  }
70
85
  }
86
+
87
+ this.#writeMemoryContext(toState, "navigate");
71
88
  },
72
89
 
73
90
  onStop: () => {
@@ -75,14 +92,28 @@ export class MemoryPlugin {
75
92
  },
76
93
 
77
94
  teardown: () => {
95
+ /* v8 ignore next 3 -- @preserve: core's unsubscribe() already guards via `unsubscribed` flag; this idempotency check covers router.dispose() + unsubscribe() ordering edge cases */
96
+ if (this.#disposed) {
97
+ return;
98
+ }
99
+
100
+ this.#disposed = true;
78
101
  this.#removeExtensions();
102
+ this.#claim.release();
79
103
  this.#clear();
80
104
  },
81
105
  };
82
106
  }
83
107
 
108
+ #writeMemoryContext(toState: State, direction: MemoryDirection): void {
109
+ this.#claim.write(
110
+ toState,
111
+ Object.freeze({ direction, historyIndex: this.#index }),
112
+ );
113
+ }
114
+
84
115
  #go(delta: number): void {
85
- if (delta === 0) {
116
+ if (delta === 0 || !Number.isFinite(delta) || !Number.isInteger(delta)) {
86
117
  return;
87
118
  }
88
119
 
@@ -104,6 +135,7 @@ export class MemoryPlugin {
104
135
  const previousIndex = this.#index;
105
136
  const generation = ++this.#goGeneration;
106
137
 
138
+ this.#pendingDirection = delta > 0 ? "forward" : "back";
107
139
  this.#navigatingFromHistory = true;
108
140
  this.#index = targetIndex;
109
141
 
package/src/types.ts CHANGED
@@ -1,5 +1,12 @@
1
1
  import type { Params } from "@real-router/core";
2
2
 
3
+ export type MemoryDirection = "back" | "forward" | "navigate";
4
+
5
+ export interface MemoryContext {
6
+ direction: MemoryDirection;
7
+ historyIndex: number;
8
+ }
9
+
3
10
  export interface MemoryPluginOptions {
4
11
  maxHistoryLength?: number;
5
12
  }