@real-router/memory-plugin 0.3.5 → 0.4.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.
@@ -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;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
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/types.ts","../../src/factory.ts","../../src/index.ts"],"mappings":";;;KAAY,eAAA;AAAA,UAEK,aAAA;EAAA,SACN,SAAA,EAAW,eAAA;EAAA,SACX,YAAA;AAAA;AAAA,UAGM,mBAAA;EAPU;;AAE3B;;;;;;;;;EAiBE,gBAAA;AAAA;;;iBCZc,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.#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;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require(`@real-router/core/api`);var t=class{#e;#t;#n;#r=[];#i;#a;#o=-1;#s=!1;#c=`navigate`;#l=0;#u=!1;constructor(e,t,n){this.#e=e,this.#t=t,this.#n=n.maxHistoryLength??1e3,this.#a=t.claimContextNamespace(`memory`),this.#i=t.extendRouter({back:()=>{this.#f(-1)},forward:()=>{this.#f(1)},go:e=>{this.#f(e)},canGoBack:()=>this.#o>0,canGoForward:()=>this.#o<this.#r.length-1})}getPlugin(){return{onTransitionSuccess:(e,t,n)=>{if(this.#s){this.#d(e,this.#c);return}if(n.replace&&this.#o>=0)this.#r[this.#o]=e;else if(this.#r.length=this.#o+1,this.#r.push(e),this.#o=this.#r.length-1,this.#n>0&&this.#r.length>this.#n){let e=this.#r.length-this.#n;this.#r.splice(0,e),this.#o=Math.max(0,this.#o-e)}this.#d(e,`navigate`)},onStop:()=>{this.#l++,this.#p()},teardown:()=>{this.#u||(this.#u=!0,this.#l++,this.#i(),this.#a.release(),this.#p())}}}#d(e,t){this.#a.write(e,{direction:t,historyIndex:this.#o})}#f(e){if(!Number.isInteger(e)||e===0)return;let t=this.#o+e;if(t<0||t>=this.#r.length)return;let n=this.#r[t],r=this.#e.getState();if(n.path===r?.path){this.#o=t,this.#d(r,e>0?`forward`:`back`);return}let i=this.#o,a=++this.#l;this.#c=e>0?`forward`:`back`,this.#s=!0,this.#o=t,this.#t.navigateToState(n,{replace:!0}).then(()=>{this.#l===a&&(this.#s=!1)},()=>{this.#l===a&&(this.#o=i,this.#s=!1)})}#p(){this.#r.length=0,this.#o=-1,this.#s=!1,this.#c=`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","#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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"}
1
+ {"version":3,"file":"index.js","names":["#router","#api","#maxHistory","#entries","#removeExtensions","#claim","#go","#index","#navigatingFromHistory","#writeMemoryContext","#pendingDirection","#goGeneration","#clear","#disposed"],"sources":["../../src/plugin.ts","../../src/factory.ts"],"sourcesContent":["import type {\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 #api: PluginApi;\n readonly #maxHistory: number;\n // Stored entries are full State snapshots (#561). Snapshot semantics for\n // back/forward replay: api.navigateToState commits the stored State as-is,\n // immune to post-recording route mutations (routes.update / routes.replace\n // changing defaultParams or meta) and to non-idempotent dynamic\n // forwardFn / buildPath interceptors. Activation guards still run at\n // replay time — that is where current-world-state checks belong, not in\n // the navigation pipeline.\n readonly #entries: State[] = [];\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.#api = api;\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 if (opts.replace && this.#index >= 0) {\n this.#entries[this.#index] = toState;\n } else {\n this.#entries.length = this.#index + 1;\n this.#entries.push(toState);\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 api.navigateToState. 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 // navigateToState commits the stored snapshot verbatim — same primitive\n // every URL-driven flow uses (start, popstate, navigate-event). Skips\n // forwardState + buildPath re-resolution and their interceptors; route\n // mutations between record and replay do not retroactively change what\n // back/forward commits (#561).\n void this.#api.navigateToState(entry, { replace: true }).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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"}
@@ -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;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
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/types.ts","../../src/factory.ts","../../src/index.ts"],"mappings":";;;KAAY,eAAA;AAAA,UAEK,aAAA;EAAA,SACN,SAAA,EAAW,eAAA;EAAA,SACX,YAAA;AAAA;AAAA,UAGM,mBAAA;EAPU;;AAE3B;;;;;;;;;EAiBE,gBAAA;AAAA;;;iBCZc,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.#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};
1
+ import{getPluginApi as e}from"@real-router/core/api";var t=class{#e;#t;#n;#r=[];#i;#a;#o=-1;#s=!1;#c=`navigate`;#l=0;#u=!1;constructor(e,t,n){this.#e=e,this.#t=t,this.#n=n.maxHistoryLength??1e3,this.#a=t.claimContextNamespace(`memory`),this.#i=t.extendRouter({back:()=>{this.#f(-1)},forward:()=>{this.#f(1)},go:e=>{this.#f(e)},canGoBack:()=>this.#o>0,canGoForward:()=>this.#o<this.#r.length-1})}getPlugin(){return{onTransitionSuccess:(e,t,n)=>{if(this.#s){this.#d(e,this.#c);return}if(n.replace&&this.#o>=0)this.#r[this.#o]=e;else if(this.#r.length=this.#o+1,this.#r.push(e),this.#o=this.#r.length-1,this.#n>0&&this.#r.length>this.#n){let e=this.#r.length-this.#n;this.#r.splice(0,e),this.#o=Math.max(0,this.#o-e)}this.#d(e,`navigate`)},onStop:()=>{this.#l++,this.#p()},teardown:()=>{this.#u||(this.#u=!0,this.#l++,this.#i(),this.#a.release(),this.#p())}}}#d(e,t){this.#a.write(e,{direction:t,historyIndex:this.#o})}#f(e){if(!Number.isInteger(e)||e===0)return;let t=this.#o+e;if(t<0||t>=this.#r.length)return;let n=this.#r[t],r=this.#e.getState();if(n.path===r?.path){this.#o=t,this.#d(r,e>0?`forward`:`back`);return}let i=this.#o,a=++this.#l;this.#c=e>0?`forward`:`back`,this.#s=!0,this.#o=t,this.#t.navigateToState(n,{replace:!0}).then(()=>{this.#l===a&&(this.#s=!1)},()=>{this.#l===a&&(this.#o=i,this.#s=!1)})}#p(){this.#r.length=0,this.#o=-1,this.#s=!1,this.#c=`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","#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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"}
1
+ {"version":3,"file":"index.mjs","names":["#router","#api","#maxHistory","#entries","#removeExtensions","#claim","#go","#index","#navigatingFromHistory","#writeMemoryContext","#pendingDirection","#goGeneration","#clear","#disposed"],"sources":["../../src/plugin.ts","../../src/factory.ts"],"sourcesContent":["import type {\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 #api: PluginApi;\n readonly #maxHistory: number;\n // Stored entries are full State snapshots (#561). Snapshot semantics for\n // back/forward replay: api.navigateToState commits the stored State as-is,\n // immune to post-recording route mutations (routes.update / routes.replace\n // changing defaultParams or meta) and to non-idempotent dynamic\n // forwardFn / buildPath interceptors. Activation guards still run at\n // replay time — that is where current-world-state checks belong, not in\n // the navigation pipeline.\n readonly #entries: State[] = [];\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.#api = api;\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 if (opts.replace && this.#index >= 0) {\n this.#entries[this.#index] = toState;\n } else {\n this.#entries.length = this.#index + 1;\n this.#entries.push(toState);\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 api.navigateToState. 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 // navigateToState commits the stored snapshot verbatim — same primitive\n // every URL-driven flow uses (start, popstate, navigate-event). Skips\n // forwardState + buildPath re-resolution and their interceptors; route\n // mutations between record and replay do not retroactively change what\n // back/forward commits (#561).\n void this.#api.navigateToState(entry, { replace: true }).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.5",
3
+ "version": "0.4.1",
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,8 +45,8 @@
45
45
  "homepage": "https://github.com/greydragon888/real-router",
46
46
  "sideEffects": false,
47
47
  "dependencies": {
48
- "@real-router/core": "^0.50.0",
49
- "@real-router/types": "^0.34.1"
48
+ "@real-router/core": "^0.52.0",
49
+ "@real-router/types": "^0.35.0"
50
50
  },
51
51
  "scripts": {
52
52
  "test": "vitest",
package/src/plugin.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import type {
2
- HistoryEntry,
3
2
  MemoryContext,
4
3
  MemoryDirection,
5
4
  MemoryPluginOptions,
@@ -17,8 +16,16 @@ const DEFAULT_MAX_HISTORY = 1000;
17
16
  /** @internal — instantiated by `memoryPluginFactory`; not part of the public API. */
18
17
  export class MemoryPlugin {
19
18
  readonly #router: Router;
19
+ readonly #api: PluginApi;
20
20
  readonly #maxHistory: number;
21
- readonly #entries: HistoryEntry[] = [];
21
+ // Stored entries are full State snapshots (#561). Snapshot semantics for
22
+ // back/forward replay: api.navigateToState commits the stored State as-is,
23
+ // immune to post-recording route mutations (routes.update / routes.replace
24
+ // changing defaultParams or meta) and to non-idempotent dynamic
25
+ // forwardFn / buildPath interceptors. Activation guards still run at
26
+ // replay time — that is where current-world-state checks belong, not in
27
+ // the navigation pipeline.
28
+ readonly #entries: State[] = [];
22
29
  readonly #removeExtensions: () => void;
23
30
  readonly #claim: {
24
31
  write: (state: State, value: MemoryContext) => void;
@@ -32,6 +39,7 @@ export class MemoryPlugin {
32
39
 
33
40
  constructor(router: Router, api: PluginApi, options: MemoryPluginOptions) {
34
41
  this.#router = router;
42
+ this.#api = api;
35
43
  this.#maxHistory = options.maxHistoryLength ?? DEFAULT_MAX_HISTORY;
36
44
  this.#claim = api.claimContextNamespace("memory");
37
45
 
@@ -63,17 +71,11 @@ export class MemoryPlugin {
63
71
  return;
64
72
  }
65
73
 
66
- const entry: HistoryEntry = {
67
- name: toState.name,
68
- params: toState.params,
69
- path: toState.path,
70
- };
71
-
72
74
  if (opts.replace && this.#index >= 0) {
73
- this.#entries[this.#index] = entry;
75
+ this.#entries[this.#index] = toState;
74
76
  } else {
75
77
  this.#entries.length = this.#index + 1;
76
- this.#entries.push(entry);
78
+ this.#entries.push(toState);
77
79
  this.#index = this.#entries.length - 1;
78
80
 
79
81
  if (this.#maxHistory > 0 && this.#entries.length > this.#maxHistory) {
@@ -132,7 +134,7 @@ export class MemoryPlugin {
132
134
 
133
135
  if (entry.path === currentState?.path) {
134
136
  // Short-circuit: landing on an entry whose path matches the current
135
- // state skips router.navigate(). Still rewrite state.context.memory
137
+ // state skips api.navigateToState. Still rewrite state.context.memory
136
138
  // so subscribers see the new historyIndex + direction — otherwise
137
139
  // UI animation driven by `direction` sees a stale "navigate" value
138
140
  // and `state.context.memory.historyIndex` diverges from `#index`
@@ -150,21 +152,24 @@ export class MemoryPlugin {
150
152
  this.#navigatingFromHistory = true;
151
153
  this.#index = targetIndex;
152
154
 
153
- void this.#router
154
- .navigate(entry.name, entry.params, { replace: true })
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
- );
155
+ // navigateToState commits the stored snapshot verbatim — same primitive
156
+ // every URL-driven flow uses (start, popstate, navigate-event). Skips
157
+ // forwardState + buildPath re-resolution and their interceptors; route
158
+ // mutations between record and replay do not retroactively change what
159
+ // back/forward commits (#561).
160
+ void this.#api.navigateToState(entry, { replace: true }).then(
161
+ () => {
162
+ if (this.#goGeneration === generation) {
163
+ this.#navigatingFromHistory = false;
164
+ }
165
+ },
166
+ () => {
167
+ if (this.#goGeneration === generation) {
168
+ this.#index = previousIndex;
169
+ this.#navigatingFromHistory = false;
170
+ }
171
+ },
172
+ );
168
173
  }
169
174
 
170
175
  #clear(): void {
package/src/types.ts CHANGED
@@ -1,5 +1,3 @@
1
- import type { Params } from "@real-router/core";
2
-
3
1
  export type MemoryDirection = "back" | "forward" | "navigate";
4
2
 
5
3
  export interface MemoryContext {
@@ -21,9 +19,3 @@ export interface MemoryPluginOptions {
21
19
  */
22
20
  maxHistoryLength?: number;
23
21
  }
24
-
25
- export interface HistoryEntry {
26
- readonly name: string;
27
- readonly params: Params;
28
- readonly path: string;
29
- }