@real-router/memory-plugin 0.4.7 → 0.4.9

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/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;#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;
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.#s=!1,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}).catch(()=>{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","#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
+ {"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 // Consume the flag on observing the commit, not in a later microtask.\n // Core commits navigateToState synchronously (optimistic-sync), so a\n // navigate() fired in the SAME tick as back()/forward()/go() would\n // otherwise still see the flag set — the .then reset is a microtask\n // that has not run yet — and be swallowed as a phantom history-restore\n // (no push, stale direction/historyIndex, orphan forward leg) (#807).\n this.#navigatingFromHistory = false;\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 this.#api.navigateToState(entry, { replace: true }).catch(() => {\n // Reject only: guard block, ROUTE_NOT_FOUND, or cancellation by a newer\n // navigation. onTransitionSuccess never fired, so the flag was not\n // consumed there — revert the optimistic index and clear the flag here. A\n // successful navigateToState always emits onTransitionSuccess (which now\n // resets the flag), so no resolve handler is needed. The generation guard\n // skips a superseded #go whose optimistic target a newer #go has already\n // overtaken — it must not revert the newer call's index or flag (#505).\n if (this.#goGeneration === generation) {\n this.#index = previousIndex;\n this.#navigatingFromHistory = false;\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":"0GAgBA,IAAa,EAAb,KAA0B,CACxB,GACA,GACA,GAQA,GAA6B,CAAC,EAC9B,GACA,GAIA,GAAS,GACT,GAAyB,GACzB,GAAqC,WACrC,GAAgB,EAChB,GAAY,GAEZ,YAAY,EAAgB,EAAgB,EAA8B,CACxE,KAAKA,GAAU,EACf,KAAKC,GAAO,EACZ,KAAKC,GAAc,EAAQ,kBAAoB,IAC/C,KAAKG,GAAS,EAAI,sBAAsB,QAAQ,EAEhD,KAAKD,GAAoB,EAAI,aAAa,CACxC,SAAY,CACV,KAAKE,GAAI,EAAE,CACb,EACA,YAAe,CACb,KAAKA,GAAI,CAAC,CACZ,EACA,GAAK,GAAkB,CACrB,KAAKA,GAAI,CAAK,CAChB,EACA,cAAiB,KAAKC,GAAS,EAC/B,iBAAoB,KAAKA,GAAS,KAAKJ,GAAS,OAAS,CAC3D,CAAC,CACH,CAEA,WAAoB,CAClB,MAAO,CACL,qBACE,EACA,EACA,IACG,CACH,GAAI,KAAKK,GAAwB,CAO/B,KAAKA,GAAyB,GAC9B,KAAKC,GAAoB,EAAS,KAAKC,EAAiB,EAExD,MACF,CAEA,GAAI,EAAK,SAAW,KAAKH,IAAU,EACjC,KAAKJ,GAAS,KAAKI,IAAU,OAM7B,GAJA,KAAKJ,GAAS,OAAS,KAAKI,GAAS,EACrC,KAAKJ,GAAS,KAAK,CAAO,EAC1B,KAAKI,GAAS,KAAKJ,GAAS,OAAS,EAEjC,KAAKD,GAAc,GAAK,KAAKC,GAAS,OAAS,KAAKD,GAAa,CACnE,IAAM,EAAW,KAAKC,GAAS,OAAS,KAAKD,GAE7C,KAAKC,GAAS,OAAO,EAAG,CAAQ,EAChC,KAAKI,GAAS,KAAK,IAAI,EAAG,KAAKA,GAAS,CAAQ,CAClD,CAGF,KAAKE,GAAoB,EAAS,UAAU,CAC9C,EAEA,WAAc,CAIZ,KAAKE,KACL,KAAKC,GAAO,CACd,EAEA,aAAgB,CAEV,KAAKC,KAIT,KAAKA,GAAY,GAGjB,KAAKF,KACL,KAAKP,GAAkB,EACvB,KAAKC,GAAO,QAAQ,EACpB,KAAKO,GAAO,EACd,CACF,CACF,CAEA,GAAoB,EAAgB,EAAkC,CACpE,KAAKP,GAAO,MAAM,EAAS,CAAE,YAAW,aAAc,KAAKE,EAAO,CAAC,CACrE,CAEA,GAAI,EAAqB,CACvB,GAAI,CAAC,OAAO,UAAU,CAAK,GAAK,IAAU,EACxC,OAGF,IAAM,EAAc,KAAKA,GAAS,EAElC,GAAI,EAAc,GAAK,GAAe,KAAKJ,GAAS,OAClD,OAGF,IAAM,EAAQ,KAAKA,GAAS,GACtB,EAAe,KAAKH,GAAQ,SAAS,EAE3C,GAAI,EAAM,OAAS,GAAc,KAAM,CAOrC,KAAKO,GAAS,EACd,KAAKE,GAAoB,EAAc,EAAQ,EAAI,UAAY,MAAM,EAErE,MACF,CAEA,IAAM,EAAgB,KAAKF,GACrB,EAAa,EAAE,KAAKI,GAE1B,KAAKD,GAAoB,EAAQ,EAAI,UAAY,OACjD,KAAKF,GAAyB,GAC9B,KAAKD,GAAS,EAOd,KAAKN,GAAK,gBAAgB,EAAO,CAAE,QAAS,EAAK,CAAC,CAAC,CAAC,UAAY,CAQ1D,KAAKU,KAAkB,IACzB,KAAKJ,GAAS,EACd,KAAKC,GAAyB,GAElC,CAAC,CACH,CAEA,IAAe,CACb,KAAKL,GAAS,OAAS,EACvB,KAAKI,GAAS,GAQd,KAAKC,GAAyB,GAC9B,KAAKE,GAAoB,UAC3B,CACF,EC3LA,SAAgB,EACd,EAA+B,CAAC,EACjB,CACf,GAAI,EAAQ,mBAAqB,IAAA,GAAW,CAC1C,IAAM,EAAS,EAAQ,iBAEvB,GACE,OAAO,GAAW,UAClB,CAAC,OAAO,SAAS,CAAM,GACvB,CAAC,OAAO,UAAU,CAAM,GACxB,EAAS,EAET,MAAU,UACR,gFAAgF,OAAO,CAAM,EAAE,EACjG,CAEJ,CAEA,IAAM,EAAqC,OAAO,OAAO,CAAE,GAAG,CAAQ,CAAC,EAEvE,MAAQ,IAIC,IAFY,EAAa,GAAA,EAAA,EAAA,aAAA,CADP,CAC2B,EAAG,CAE3C,CAAC,CAAC,UAAU,CAE5B"}
@@ -1,2 +1,2 @@
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};
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.#s=!1,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}).catch(()=>{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","#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
+ {"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 // Consume the flag on observing the commit, not in a later microtask.\n // Core commits navigateToState synchronously (optimistic-sync), so a\n // navigate() fired in the SAME tick as back()/forward()/go() would\n // otherwise still see the flag set — the .then reset is a microtask\n // that has not run yet — and be swallowed as a phantom history-restore\n // (no push, stale direction/historyIndex, orphan forward leg) (#807).\n this.#navigatingFromHistory = false;\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 this.#api.navigateToState(entry, { replace: true }).catch(() => {\n // Reject only: guard block, ROUTE_NOT_FOUND, or cancellation by a newer\n // navigation. onTransitionSuccess never fired, so the flag was not\n // consumed there — revert the optimistic index and clear the flag here. A\n // successful navigateToState always emits onTransitionSuccess (which now\n // resets the flag), so no resolve handler is needed. The generation guard\n // skips a superseded #go whose optimistic target a newer #go has already\n // overtaken — it must not revert the newer call's index or flag (#505).\n if (this.#goGeneration === generation) {\n this.#index = previousIndex;\n this.#navigatingFromHistory = false;\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.4.7",
3
+ "version": "0.4.9",
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",
@@ -44,7 +44,7 @@
44
44
  "homepage": "https://github.com/greydragon888/real-router",
45
45
  "sideEffects": false,
46
46
  "dependencies": {
47
- "@real-router/core": "^0.58.0",
47
+ "@real-router/core": "^0.59.0",
48
48
  "@real-router/types": "^0.36.0"
49
49
  },
50
50
  "scripts": {