@real-router/memory-plugin 0.3.3 → 0.3.5

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.
@@ -3,8 +3,8 @@ import { PluginFactory } from "@real-router/core";
3
3
  //#region src/types.d.ts
4
4
  type MemoryDirection = "back" | "forward" | "navigate";
5
5
  interface MemoryContext {
6
- direction: MemoryDirection;
7
- historyIndex: number;
6
+ readonly direction: MemoryDirection;
7
+ readonly historyIndex: number;
8
8
  }
9
9
  interface MemoryPluginOptions {
10
10
  /**
@@ -1 +1 @@
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;EAPU;AAE3B;;;;;;;;;AAKA;EAYE,gBAAA;AAAA;;;iBCdc,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
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/types.ts","../../src/factory.ts","../../src/index.ts"],"mappings":";;;KAEY,eAAA;AAAA,UAEK,aAAA;EAAA,SACN,SAAA,EAAW,eAAA;EAAA,SACX,YAAA;AAAA;AAAA,UAGM,mBAAA;EAPU;AAE3B;;;;;;;;;AAKA;EAYE,gBAAA;AAAA;;;iBCdc,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;#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;
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.#c++,this.#f()},teardown:()=>{this.#l||(this.#l=!0,this.#c++,this.#r(),this.#i.release(),this.#f())}}}#u(e,t){this.#i.write(e,{direction:t,historyIndex:this.#a})}#d(e){if(!Number.isInteger(e)||e===0)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,this.#u(r,e>0?`forward`:`back`);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}).then(()=>{this.#c===a&&(this.#o=!1)},()=>{this.#c===a&&(this.#a=i,this.#o=!1)})}#f(){this.#n.length=0,this.#a=-1,this.#o=!1,this.#s=`navigate`}};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","#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
+ {"version":3,"file":"index.js","names":["#router","#maxHistory","#entries","#removeExtensions","#claim","#go","#index","#navigatingFromHistory","#writeMemoryContext","#pendingDirection","#goGeneration","#clear","#disposed"],"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 // Bump generation so any in-flight #go settler observes a mismatch\n // and skips its revert / flag reset — writing into cleared state\n // would otherwise leave #index pointing into an empty #entries (#505).\n this.#goGeneration++;\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 // Same generation bump as onStop — pre-teardown in-flight #go settlers\n // must not write into a released plugin (#505).\n this.#goGeneration++;\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(toState, { direction, historyIndex: this.#index });\n }\n\n #go(delta: number): void {\n if (!Number.isInteger(delta) || 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 // Short-circuit: landing on an entry whose path matches the current\n // state skips router.navigate(). Still rewrite state.context.memory\n // so subscribers see the new historyIndex + direction — otherwise\n // UI animation driven by `direction` sees a stale \"navigate\" value\n // and `state.context.memory.historyIndex` diverges from `#index`\n // until the next full transition (#508).\n this.#index = targetIndex;\n this.#writeMemoryContext(currentState, delta > 0 ? \"forward\" : \"back\");\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 .then(\n () => {\n if (this.#goGeneration === generation) {\n this.#navigatingFromHistory = false;\n }\n },\n () => {\n if (this.#goGeneration === generation) {\n this.#index = previousIndex;\n this.#navigatingFromHistory = false;\n }\n },\n );\n }\n\n #clear(): void {\n this.#entries.length = 0;\n this.#index = -1;\n // Reset transient #go state as well: if #clear runs while a #go is in\n // flight, the reject-handler skips (generation mismatch) and would\n // otherwise leave #navigatingFromHistory stuck at true — the next\n // onTransitionSuccess after restart would take the history-restore\n // branch and silently skip pushing a new entry. Both fields are\n // \"current #go intent\", not persistent history, so resetting them on\n // clear is always correct (#505).\n this.#navigatingFromHistory = false;\n this.#pendingDirection = \"navigate\";\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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"}
@@ -3,8 +3,8 @@ import { PluginFactory } from "@real-router/core";
3
3
  //#region src/types.d.ts
4
4
  type MemoryDirection = "back" | "forward" | "navigate";
5
5
  interface MemoryContext {
6
- direction: MemoryDirection;
7
- historyIndex: number;
6
+ readonly direction: MemoryDirection;
7
+ readonly historyIndex: number;
8
8
  }
9
9
  interface MemoryPluginOptions {
10
10
  /**
@@ -1 +1 @@
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;EAPU;AAE3B;;;;;;;;;AAKA;EAYE,gBAAA;AAAA;;;iBCdc,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
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/types.ts","../../src/factory.ts","../../src/index.ts"],"mappings":";;;KAEY,eAAA;AAAA,UAEK,aAAA;EAAA,SACN,SAAA,EAAW,eAAA;EAAA,SACX,YAAA;AAAA;AAAA,UAGM,mBAAA;EAPU;AAE3B;;;;;;;;;AAKA;EAYE,gBAAA;AAAA;;;iBCdc,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;#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};
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.#c++,this.#f()},teardown:()=>{this.#l||(this.#l=!0,this.#c++,this.#r(),this.#i.release(),this.#f())}}}#u(e,t){this.#i.write(e,{direction:t,historyIndex:this.#a})}#d(e){if(!Number.isInteger(e)||e===0)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,this.#u(r,e>0?`forward`:`back`);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}).then(()=>{this.#c===a&&(this.#o=!1)},()=>{this.#c===a&&(this.#a=i,this.#o=!1)})}#f(){this.#n.length=0,this.#a=-1,this.#o=!1,this.#s=`navigate`}};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","#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
+ {"version":3,"file":"index.mjs","names":["#router","#maxHistory","#entries","#removeExtensions","#claim","#go","#index","#navigatingFromHistory","#writeMemoryContext","#pendingDirection","#goGeneration","#clear","#disposed"],"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 // Bump generation so any in-flight #go settler observes a mismatch\n // and skips its revert / flag reset — writing into cleared state\n // would otherwise leave #index pointing into an empty #entries (#505).\n this.#goGeneration++;\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 // Same generation bump as onStop — pre-teardown in-flight #go settlers\n // must not write into a released plugin (#505).\n this.#goGeneration++;\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(toState, { direction, historyIndex: this.#index });\n }\n\n #go(delta: number): void {\n if (!Number.isInteger(delta) || 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 // Short-circuit: landing on an entry whose path matches the current\n // state skips router.navigate(). Still rewrite state.context.memory\n // so subscribers see the new historyIndex + direction — otherwise\n // UI animation driven by `direction` sees a stale \"navigate\" value\n // and `state.context.memory.historyIndex` diverges from `#index`\n // until the next full transition (#508).\n this.#index = targetIndex;\n this.#writeMemoryContext(currentState, delta > 0 ? \"forward\" : \"back\");\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 .then(\n () => {\n if (this.#goGeneration === generation) {\n this.#navigatingFromHistory = false;\n }\n },\n () => {\n if (this.#goGeneration === generation) {\n this.#index = previousIndex;\n this.#navigatingFromHistory = false;\n }\n },\n );\n }\n\n #clear(): void {\n this.#entries.length = 0;\n this.#index = -1;\n // Reset transient #go state as well: if #clear runs while a #go is in\n // flight, the reject-handler skips (generation mismatch) and would\n // otherwise leave #navigatingFromHistory stuck at true — the next\n // onTransitionSuccess after restart would take the history-restore\n // branch and silently skip pushing a new entry. Both fields are\n // \"current #go intent\", not persistent history, so resetting them on\n // clear is always correct (#505).\n this.#navigatingFromHistory = false;\n this.#pendingDirection = \"navigate\";\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.3.3",
3
+ "version": "0.3.5",
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",
package/src/plugin.ts CHANGED
@@ -88,6 +88,10 @@ export class MemoryPlugin {
88
88
  },
89
89
 
90
90
  onStop: () => {
91
+ // Bump generation so any in-flight #go settler observes a mismatch
92
+ // and skips its revert / flag reset — writing into cleared state
93
+ // would otherwise leave #index pointing into an empty #entries (#505).
94
+ this.#goGeneration++;
91
95
  this.#clear();
92
96
  },
93
97
 
@@ -98,6 +102,9 @@ export class MemoryPlugin {
98
102
  }
99
103
 
100
104
  this.#disposed = true;
105
+ // Same generation bump as onStop — pre-teardown in-flight #go settlers
106
+ // must not write into a released plugin (#505).
107
+ this.#goGeneration++;
101
108
  this.#removeExtensions();
102
109
  this.#claim.release();
103
110
  this.#clear();
@@ -106,14 +113,11 @@ export class MemoryPlugin {
106
113
  }
107
114
 
108
115
  #writeMemoryContext(toState: State, direction: MemoryDirection): void {
109
- this.#claim.write(
110
- toState,
111
- Object.freeze({ direction, historyIndex: this.#index }),
112
- );
116
+ this.#claim.write(toState, { direction, historyIndex: this.#index });
113
117
  }
114
118
 
115
119
  #go(delta: number): void {
116
- if (delta === 0 || !Number.isFinite(delta) || !Number.isInteger(delta)) {
120
+ if (!Number.isInteger(delta) || delta === 0) {
117
121
  return;
118
122
  }
119
123
 
@@ -127,7 +131,14 @@ export class MemoryPlugin {
127
131
  const currentState = this.#router.getState();
128
132
 
129
133
  if (entry.path === currentState?.path) {
134
+ // Short-circuit: landing on an entry whose path matches the current
135
+ // state skips router.navigate(). Still rewrite state.context.memory
136
+ // so subscribers see the new historyIndex + direction — otherwise
137
+ // UI animation driven by `direction` sees a stale "navigate" value
138
+ // and `state.context.memory.historyIndex` diverges from `#index`
139
+ // until the next full transition (#508).
130
140
  this.#index = targetIndex;
141
+ this.#writeMemoryContext(currentState, delta > 0 ? "forward" : "back");
131
142
 
132
143
  return;
133
144
  }
@@ -141,20 +152,32 @@ export class MemoryPlugin {
141
152
 
142
153
  void this.#router
143
154
  .navigate(entry.name, entry.params, { replace: true })
144
- .catch(() => {
145
- if (this.#goGeneration === generation) {
146
- this.#index = previousIndex;
147
- }
148
- })
149
- .finally(() => {
150
- if (this.#goGeneration === generation) {
151
- this.#navigatingFromHistory = false;
152
- }
153
- });
155
+ .then(
156
+ () => {
157
+ if (this.#goGeneration === generation) {
158
+ this.#navigatingFromHistory = false;
159
+ }
160
+ },
161
+ () => {
162
+ if (this.#goGeneration === generation) {
163
+ this.#index = previousIndex;
164
+ this.#navigatingFromHistory = false;
165
+ }
166
+ },
167
+ );
154
168
  }
155
169
 
156
170
  #clear(): void {
157
171
  this.#entries.length = 0;
158
172
  this.#index = -1;
173
+ // Reset transient #go state as well: if #clear runs while a #go is in
174
+ // flight, the reject-handler skips (generation mismatch) and would
175
+ // otherwise leave #navigatingFromHistory stuck at true — the next
176
+ // onTransitionSuccess after restart would take the history-restore
177
+ // branch and silently skip pushing a new entry. Both fields are
178
+ // "current #go intent", not persistent history, so resetting them on
179
+ // clear is always correct (#505).
180
+ this.#navigatingFromHistory = false;
181
+ this.#pendingDirection = "navigate";
159
182
  }
160
183
  }
package/src/types.ts CHANGED
@@ -3,8 +3,8 @@ import type { Params } from "@real-router/core";
3
3
  export type MemoryDirection = "back" | "forward" | "navigate";
4
4
 
5
5
  export interface MemoryContext {
6
- direction: MemoryDirection;
7
- historyIndex: number;
6
+ readonly direction: MemoryDirection;
7
+ readonly historyIndex: number;
8
8
  }
9
9
 
10
10
  export interface MemoryPluginOptions {