@kasperrt/smolter 0.3.2 → 0.3.4

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.
@@ -36,27 +36,24 @@ export function deploymentCheck(options: DeploymentOptions) {
36
36
  return pending;
37
37
  }
38
38
 
39
- async function reload(target: URL) {
40
- // If storage is unavailable, do not risk reloading the same cached document forever.
41
- const allowed = await Promise.resolve()
39
+ function reload(target: URL, current: () => boolean = () => true) {
40
+ // Validate and claim recovery in the same task: stale checks must not consume
41
+ // the marker or reload after a newer navigation or mutation has started.
42
+ return Promise.resolve()
42
43
  .then(() => {
43
- if (sessionStorage.getItem(key)) return false;
44
+ if (stopped || !current() || sessionStorage.getItem(key)) return false;
44
45
 
45
46
  sessionStorage.setItem(key, "1");
46
47
 
48
+ if (options.reload) options.reload(target);
49
+ if (!options.reload) {
50
+ if (target.href === location.href) location.reload();
51
+ if (target.href !== location.href) location.assign(target.href);
52
+ }
53
+
47
54
  return true;
48
55
  })
49
56
  .catch(() => false);
50
-
51
- if (allowed && !stopped) {
52
- if (options?.reload) options.reload(target);
53
- if (!options?.reload) {
54
- if (target.href === location.href) location.reload();
55
- if (target.href !== location.href) location.assign(target.href);
56
- }
57
- }
58
-
59
- return allowed && !stopped;
60
57
  }
61
58
 
62
59
  return {
@@ -20,18 +20,21 @@ export function watchDeployment(
20
20
 
21
21
  const check = deploymentCheck(options);
22
22
  const { changed, reload } = check;
23
+ const read = router.getSnapshot;
23
24
  const navigate = router.navigate;
24
25
  const dispose = router.dispose;
25
26
  let sequence = 0;
26
27
  let stopped = false;
27
28
  let replay = false;
28
- let destination = new URL(location.href);
29
+ const valid = (current: number) => current === sequence && !stopped;
30
+ const unsubscribe = router.subscribe(() => {
31
+ if (read().state === "submitting") sequence++;
32
+ });
29
33
 
30
34
  const guarded: NavigationRouter["navigate"] = async (href, operation) => {
31
35
  const target = new URL(href, router.getSnapshot().url);
32
36
  const currentURL = new URL(router.getSnapshot().url);
33
37
  const current = ++sequence;
34
- destination = target;
35
38
 
36
39
  if (
37
40
  target.origin === location.origin &&
@@ -39,7 +42,8 @@ export function watchDeployment(
39
42
  target.search !== currentURL.search) &&
40
43
  (await changed())
41
44
  ) {
42
- if (current !== sequence || stopped || (await reload(target))) return;
45
+ if (!valid(current) || (await reload(target, () => valid(current))))
46
+ return;
43
47
  }
44
48
 
45
49
  if (current === sequence && !stopped) await navigate(href, operation);
@@ -49,7 +53,8 @@ export function watchDeployment(
49
53
  if (replay) return;
50
54
 
51
55
  const target = new URL(location.href);
52
- const previous = new URL(router.getSnapshot().url);
56
+ const previous = new URL(read().url);
57
+ const current = ++sequence;
53
58
 
54
59
  if (
55
60
  target.pathname === previous.pathname &&
@@ -58,11 +63,10 @@ export function watchDeployment(
58
63
  return;
59
64
 
60
65
  event.stopImmediatePropagation();
61
- const current = ++sequence;
62
- destination = target;
63
66
  void changed().then(async (updated) => {
64
- if (stopped || current !== sequence) return;
65
- if (updated && (await reload(target))) return;
67
+ if (!valid(current)) return;
68
+ if (updated && (await reload(target, () => valid(current)))) return;
69
+ if (!valid(current)) return;
66
70
 
67
71
  replay = true;
68
72
  window.dispatchEvent(
@@ -73,12 +77,21 @@ export function watchDeployment(
73
77
  };
74
78
 
75
79
  const preloadError = () => {
76
- if (router.getSnapshot().state === "submitting") return;
80
+ // Intent prefetch is speculative: it must not reload the current page or
81
+ // consume the recovery marker before a click records its destination.
82
+ if (read().state !== "loading") return;
77
83
 
78
84
  const current = sequence;
79
- const target = destination;
85
+ const target = new URL(read().url);
80
86
  void changed().then((updated) => {
81
- if (updated && current === sequence && !stopped) return reload(target);
87
+ if (updated)
88
+ return reload(
89
+ target,
90
+ () =>
91
+ valid(current) &&
92
+ read().state !== "submitting" &&
93
+ read().url === target.href,
94
+ );
82
95
  });
83
96
  };
84
97
 
@@ -86,6 +99,7 @@ export function watchDeployment(
86
99
  stopped = true;
87
100
  sequence++;
88
101
  check.dispose();
102
+ unsubscribe();
89
103
  window.removeEventListener("popstate", pop, true);
90
104
  window.removeEventListener("vite:preloadError", preloadError);
91
105
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.4
4
+
5
+ - Cancel stale deployment checks when newer Back/Forward operations return to the current route, including hash changes.
6
+ - Recover missing action-redirect chunks at the committed destination, preserving query/hash and avoiding mutation replay.
7
+ - Invalidate pending chunk recovery when a mutation starts; validate recovery ownership before claiming the reload marker.
8
+ - Expand production-browser coverage for scoped application paths, separate CDN asset/manifest origins, overlapping navigation, Chromium offline/online recovery and history races. Manual and file-route HMR remain covered.
9
+
10
+ ## 0.3.3
11
+
12
+ - Ignore idle intent-prefetch chunk failures in deployment recovery so the next click retains its destination and reload marker.
13
+ - Keep active-navigation chunk recovery and latest-navigation wins behavior, with unit and production-browser regressions.
14
+
3
15
  ## 0.3.2
4
16
 
5
17
  - Configure transport endpoints consistently across browser handlers, file routes, Express and development middleware for multiple apps sharing one origin.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # smolter _
2
2
 
3
3
  <!-- bundlejs:start -->
4
- [![react gzip size](https://deno.bundlejs.com/?q=%40kasperrt%2Fsmolter%400.3.2%2Freact&treeshake=%5B*%5D&config=%7B%22compression%22%3A%22gzip%22%2C%22esbuild%22%3A%7B%22external%22%3A%5B%22react%22%2C%22react%2F*%22%5D%7D%7D&badge)](https://bundlejs.com/?q=%40kasperrt%2Fsmolter%400.3.2%2Freact&treeshake=%5B*%5D&config=%7B%22compression%22%3A%22gzip%22%2C%22esbuild%22%3A%7B%22external%22%3A%5B%22react%22%2C%22react%2F*%22%5D%7D%7D&bundle)
5
- [![preact gzip size](https://deno.bundlejs.com/?q=%40kasperrt%2Fsmolter%400.3.2%2Fpreact&treeshake=%5B*%5D&config=%7B%22compression%22%3A%22gzip%22%2C%22esbuild%22%3A%7B%22external%22%3A%5B%22preact%22%2C%22preact%2F*%22%5D%7D%7D&badge)](https://bundlejs.com/?q=%40kasperrt%2Fsmolter%400.3.2%2Fpreact&treeshake=%5B*%5D&config=%7B%22compression%22%3A%22gzip%22%2C%22esbuild%22%3A%7B%22external%22%3A%5B%22preact%22%2C%22preact%2F*%22%5D%7D%7D&bundle)
4
+ [![react gzip size](https://deno.bundlejs.com/?q=%40kasperrt%2Fsmolter%400.3.4%2Freact&treeshake=%5B*%5D&config=%7B%22compression%22%3A%22gzip%22%2C%22esbuild%22%3A%7B%22external%22%3A%5B%22react%22%2C%22react%2F*%22%5D%7D%7D&badge)](https://bundlejs.com/?q=%40kasperrt%2Fsmolter%400.3.4%2Freact&treeshake=%5B*%5D&config=%7B%22compression%22%3A%22gzip%22%2C%22esbuild%22%3A%7B%22external%22%3A%5B%22react%22%2C%22react%2F*%22%5D%7D%7D&bundle)
5
+ [![preact gzip size](https://deno.bundlejs.com/?q=%40kasperrt%2Fsmolter%400.3.4%2Fpreact&treeshake=%5B*%5D&config=%7B%22compression%22%3A%22gzip%22%2C%22esbuild%22%3A%7B%22external%22%3A%5B%22preact%22%2C%22preact%2F*%22%5D%7D%7D&badge)](https://bundlejs.com/?q=%40kasperrt%2Fsmolter%400.3.4%2Fpreact&treeshake=%5B*%5D&config=%7B%22compression%22%3A%22gzip%22%2C%22esbuild%22%3A%7B%22external%22%3A%5B%22preact%22%2C%22preact%2F*%22%5D%7D%7D&bundle)
6
6
  <!-- bundlejs:end -->
7
7
 
8
8
  A typed data router for React and Preact. **Zero runtime dependencies.**
@@ -605,10 +605,10 @@ a claim of identical features.
605
605
  | `Preact: routing-only named exports` | 1405 |
606
606
  | `@kasperrt/smolter/react` | 2948 |
607
607
  | `@kasperrt/smolter/preact` | 4842 |
608
- | `@kasperrt/smolter/react + @kasperrt/smolter/react/client` | 4046 |
609
- | `@kasperrt/smolter/preact + server transport` | 5886 |
610
- | `@kasperrt/smolter/preact + client + document (application without lazy routes)` | 8418 |
611
- | `@kasperrt/smolter/preact + client + document + lazy-route (file-routed application)` | 8704 |
608
+ | `@kasperrt/smolter/react + @kasperrt/smolter/react/client` | 4104 |
609
+ | `@kasperrt/smolter/preact + server transport` | 5950 |
610
+ | `@kasperrt/smolter/preact + client + document (application without lazy routes)` | 8478 |
611
+ | `@kasperrt/smolter/preact + client + document + lazy-route (file-routed application)` | 8761 |
612
612
  | `@kasperrt/smolter/react/data (optional resource API)` | 2962 |
613
613
  | `React: common routing/data exports` | 2761 |
614
614
  | `Preact: common routing/data exports` | 3046 |
@@ -1,6 +1,6 @@
1
1
  import type { DeploymentOptions } from "./deployment.ts";
2
2
  export declare function deploymentCheck(options: DeploymentOptions): {
3
3
  changed: () => Promise<boolean>;
4
- reload: (target: URL) => Promise<boolean>;
4
+ reload: (target: URL, current?: () => boolean) => Promise<boolean>;
5
5
  dispose(): void;
6
6
  };
@@ -23,20 +23,17 @@ function deploymentCheck(options) {
23
23
  });
24
24
  return pending;
25
25
  }
26
- async function reload(target) {
27
- const allowed = await Promise.resolve().then(() => {
28
- if (sessionStorage.getItem(key)) return false;
26
+ function reload(target, current = () => true) {
27
+ return Promise.resolve().then(() => {
28
+ if (stopped || !current() || sessionStorage.getItem(key)) return false;
29
29
  sessionStorage.setItem(key, "1");
30
- return true;
31
- }).catch(() => false);
32
- if (allowed && !stopped) {
33
- if (options?.reload) options.reload(target);
34
- if (!options?.reload) {
30
+ if (options.reload) options.reload(target);
31
+ if (!options.reload) {
35
32
  if (target.href === location.href) location.reload();
36
33
  if (target.href !== location.href) location.assign(target.href);
37
34
  }
38
- }
39
- return allowed && !stopped;
35
+ return true;
36
+ }).catch(() => false);
40
37
  }
41
38
  return {
42
39
  changed,
@@ -5,50 +5,54 @@ function watchDeployment(router, options) {
5
5
  if (!options || typeof window === "undefined") return () => {};
6
6
  const check = deploymentCheck(options);
7
7
  const { changed, reload } = check;
8
+ const read = router.getSnapshot;
8
9
  const navigate = router.navigate;
9
10
  const dispose = router.dispose;
10
11
  let sequence = 0;
11
12
  let stopped = false;
12
13
  let replay = false;
13
- let destination = new URL(location.href);
14
+ const valid = (current) => current === sequence && !stopped;
15
+ const unsubscribe = router.subscribe(() => {
16
+ if (read().state === "submitting") sequence++;
17
+ });
14
18
  const guarded = async (href, operation) => {
15
19
  const target = new URL(href, router.getSnapshot().url);
16
20
  const currentURL = new URL(router.getSnapshot().url);
17
21
  const current = ++sequence;
18
- destination = target;
19
22
  if (target.origin === location.origin && (target.pathname !== currentURL.pathname || target.search !== currentURL.search) && await changed()) {
20
- if (current !== sequence || stopped || await reload(target)) return;
23
+ if (!valid(current) || await reload(target, () => valid(current))) return;
21
24
  }
22
25
  if (current === sequence && !stopped) await navigate(href, operation);
23
26
  };
24
27
  const pop = (event) => {
25
28
  if (replay) return;
26
29
  const target = new URL(location.href);
27
- const previous = new URL(router.getSnapshot().url);
30
+ const previous = new URL(read().url);
31
+ const current = ++sequence;
28
32
  if (target.pathname === previous.pathname && target.search === previous.search) return;
29
33
  event.stopImmediatePropagation();
30
- const current = ++sequence;
31
- destination = target;
32
34
  changed().then(async (updated) => {
33
- if (stopped || current !== sequence) return;
34
- if (updated && await reload(target)) return;
35
+ if (!valid(current)) return;
36
+ if (updated && await reload(target, () => valid(current))) return;
37
+ if (!valid(current)) return;
35
38
  replay = true;
36
39
  window.dispatchEvent(new PopStateEvent("popstate", { state: event.state }));
37
40
  replay = false;
38
41
  });
39
42
  };
40
43
  const preloadError = () => {
41
- if (router.getSnapshot().state === "submitting") return;
44
+ if (read().state !== "loading") return;
42
45
  const current = sequence;
43
- const target = destination;
46
+ const target = new URL(read().url);
44
47
  changed().then((updated) => {
45
- if (updated && current === sequence && !stopped) return reload(target);
48
+ if (updated) return reload(target, () => valid(current) && read().state !== "submitting" && read().url === target.href);
46
49
  });
47
50
  };
48
51
  function cleanup() {
49
52
  stopped = true;
50
53
  sequence++;
51
54
  check.dispose();
55
+ unsubscribe();
52
56
  window.removeEventListener("popstate", pop, true);
53
57
  window.removeEventListener("vite:preloadError", preloadError);
54
58
  if (router.navigate === guarded) router.navigate = navigate;
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kasperrt/smolter",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "exports": {
5
5
  "./react": "./.generated/react/index.ts",
6
6
  "./preact": "./.generated/preact/index.ts",
package/package.json CHANGED
@@ -56,7 +56,7 @@
56
56
  "wouter": "3.13.0",
57
57
  "wouter-preact": "3.13.0"
58
58
  },
59
- "version": "0.3.2",
59
+ "version": "0.3.4",
60
60
  "description": "A tiny typed data router for React and Preact.",
61
61
  "sideEffects": false,
62
62
  "license": "MIT",