@invisibleloop/pulse 0.21.3 → 0.21.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.
- package/package.json +1 -1
- package/public/.pulse-ui-version +1 -1
- package/src/runtime/index.js +11 -3
- package/src/runtime/navigate.js +16 -0
- package/types/runtime.d.ts +3 -2
package/package.json
CHANGED
package/public/.pulse-ui-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.21.
|
|
1
|
+
0.21.5
|
package/src/runtime/index.js
CHANGED
|
@@ -169,7 +169,9 @@ export function mount(spec, el, serverState = {}, options = {}) {
|
|
|
169
169
|
// ---------------------------------------------------------------------------
|
|
170
170
|
|
|
171
171
|
function persist() {
|
|
172
|
-
|
|
172
|
+
// The key is per route, not per instance: once destroyed, a late write (an action resolving
|
|
173
|
+
// after navigation) could overwrite what the next page, on the same route, has persisted.
|
|
174
|
+
if (!persistKey || _destroyed) return
|
|
173
175
|
try {
|
|
174
176
|
const snapshot = {}
|
|
175
177
|
spec.persist.forEach(k => { snapshot[k] = state[k] })
|
|
@@ -184,6 +186,11 @@ export function mount(spec, el, serverState = {}, options = {}) {
|
|
|
184
186
|
let lastHtml = null
|
|
185
187
|
|
|
186
188
|
function render() {
|
|
189
|
+
// A destroyed instance never touches the DOM again. el now belongs to the page that replaced
|
|
190
|
+
// it, and an action that resolves late or a debounce timer that fires late would otherwise
|
|
191
|
+
// morph this page's view into that one (#172). State updates, toasts and store pushes from
|
|
192
|
+
// late work still go through; only the render (and persist(), below) is dropped.
|
|
193
|
+
if (_destroyed) return
|
|
187
194
|
let html
|
|
188
195
|
try {
|
|
189
196
|
html = resolveView(spec, state, getEffectiveServerState())
|
|
@@ -303,8 +310,9 @@ export function mount(spec, el, serverState = {}, options = {}) {
|
|
|
303
310
|
/** Force a re-render — useful after external state changes */
|
|
304
311
|
refresh: render,
|
|
305
312
|
|
|
306
|
-
/** Tear down — remove event listeners, unsubscribe from store, clear element
|
|
307
|
-
|
|
313
|
+
/** Tear down — remove event listeners, unsubscribe from store, clear element
|
|
314
|
+
* (pass { keepDom: true } to leave the element's content in place) */
|
|
315
|
+
destroy: (opts) => { _destroyed = true; _unsubStore?.(); _eventAbort.abort(); if (!opts?.keepDom) el.innerHTML = trustedHTML('') }
|
|
308
316
|
}
|
|
309
317
|
}
|
|
310
318
|
|
package/src/runtime/navigate.js
CHANGED
|
@@ -26,6 +26,17 @@ export function initNavigation(root, mountFn) {
|
|
|
26
26
|
let currentMount = null
|
|
27
27
|
let currentNavController = null
|
|
28
28
|
|
|
29
|
+
// The target page has no client bundle (a purely server-rendered page), so nothing will mount
|
|
30
|
+
// over the previous page's instance. Release it, or its listeners keep firing on the new DOM
|
|
31
|
+
// and a store change re-renders the old view into it (#171). Call it immediately before the
|
|
32
|
+
// new HTML replaces the old (never earlier: if the response is cut off first, the visitor
|
|
33
|
+
// keeps a working page). keepDom: the caller swaps the root's content itself.
|
|
34
|
+
function releaseCurrentMount() {
|
|
35
|
+
const mount = currentMount
|
|
36
|
+
currentMount = null // cleared first, so a throwing destroy() cannot leave a stale reference
|
|
37
|
+
mount?.destroy({ keepDom: true })
|
|
38
|
+
}
|
|
39
|
+
|
|
29
40
|
async function navigate(path, push) {
|
|
30
41
|
// Cancel any in-flight navigation before starting a new one
|
|
31
42
|
currentNavController?.abort()
|
|
@@ -63,6 +74,10 @@ export function initNavigation(root, mountFn) {
|
|
|
63
74
|
await applyScripts(msg.scripts)
|
|
64
75
|
|
|
65
76
|
} else if (msg.type === 'html') {
|
|
77
|
+
if (!hydratePath) {
|
|
78
|
+
if (controller.signal.aborted) return // superseded: leave the old page alone
|
|
79
|
+
releaseCurrentMount()
|
|
80
|
+
}
|
|
66
81
|
root.innerHTML = trustedHTML(msg.html)
|
|
67
82
|
// Index <pulse-deferred> placeholders so deferred chunks land in the right spot
|
|
68
83
|
for (const id of (msg.deferred || [])) {
|
|
@@ -126,6 +141,7 @@ export function initNavigation(root, mountFn) {
|
|
|
126
141
|
window.__updatePulseStore__(storeState)
|
|
127
142
|
}
|
|
128
143
|
|
|
144
|
+
if (!hydrate) releaseCurrentMount()
|
|
129
145
|
root.innerHTML = trustedHTML(html)
|
|
130
146
|
document.title = title || document.title
|
|
131
147
|
applyStyles(styles)
|
package/types/runtime.d.ts
CHANGED
|
@@ -27,8 +27,9 @@ export interface MountOptions {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export interface MountResult {
|
|
30
|
-
/**
|
|
31
|
-
|
|
30
|
+
/** Remove listeners and unsubscribe from the store, then clear the element.
|
|
31
|
+
* Pass `{ keepDom: true }` to leave the element's content in place. */
|
|
32
|
+
destroy(options?: { keepDom?: boolean }): void
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
/**
|