@ovineko/spa-guard 0.0.1-alpha-24 → 0.0.1-alpha-26

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/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # @ovineko/spa-guard
2
+
3
+ [![npm](https://img.shields.io/npm/v/@ovineko/spa-guard)](https://www.npmjs.com/package/@ovineko/spa-guard)
4
+ [![license](https://img.shields.io/npm/l/@ovineko/spa-guard)](./LICENSE)
5
+
6
+ Core runtime for spa-guard — chunk load error handling, version checking, spinner, i18n, and event schema for SPAs.
7
+
8
+ ## Install
9
+
10
+ ```sh
11
+ npm install @ovineko/spa-guard
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ Call `recommendedSetup` once when your app boots. It dismisses the loading spinner, starts version checking, and returns a cleanup function.
17
+
18
+ ```ts
19
+ import { recommendedSetup } from "@ovineko/spa-guard/runtime";
20
+
21
+ const cleanup = recommendedSetup();
22
+ // optionally call cleanup() to stop background checks
23
+ ```
24
+
25
+ Disable version checking:
26
+
27
+ ```ts
28
+ const cleanup = recommendedSetup({ versionCheck: false });
29
+ ```
30
+
31
+ ## API
32
+
33
+ ### `@ovineko/spa-guard` (common)
34
+
35
+ - `listen` — subscribe to spa-guard events
36
+ - `events` — event type constants
37
+ - `options` — runtime options helpers
38
+ - `disableDefaultRetry` / `enableDefaultRetry` / `isDefaultRetryEnabled` — control default retry behaviour
39
+ - `BeaconError` — error class for beacon failures
40
+ - `ForceRetryError` — error class to force a retry
41
+
42
+ ### `@ovineko/spa-guard/runtime`
43
+
44
+ - `recommendedSetup(options?)` — enable recommended runtime features; returns cleanup function
45
+ - `RecommendedSetupOptions` — `{ versionCheck?: boolean }`
46
+ - `startVersionCheck` / `stopVersionCheck` — manual version check control
47
+ - `getState` / `subscribeToState` — runtime state access
48
+ - `SpaGuardState` — state type
49
+ - `showSpinner` / `dismissSpinner` / `getSpinnerHtml` — spinner helpers
50
+ - `setTranslations` — override i18n strings
51
+ - `ForceRetryError`
52
+
53
+ ### `@ovineko/spa-guard/schema`
54
+
55
+ Schemas for spa-guard configuration.
56
+
57
+ ### `@ovineko/spa-guard/i18n`
58
+
59
+ Built-in translation strings.
60
+
61
+ ### `@ovineko/spa-guard/runtime/debug`
62
+
63
+ Debug helpers for testing error scenarios.
64
+
65
+ ## Related packages
66
+
67
+ - [@ovineko/spa-guard-react](../react/README.md) — `lazyWithRetry` and React error boundary
68
+ - [@ovineko/spa-guard-react-router](../react-router/README.md) — React Router error boundary
69
+ - [@ovineko/spa-guard-vite](../vite/README.md) — Vite plugin (injects runtime script)
70
+ - [@ovineko/spa-guard-node](../node/README.md) — Node.js cache and builder API
71
+ - [@ovineko/spa-guard-fastify](../fastify/README.md) — Fastify plugin
72
+ - [@ovineko/spa-guard-eslint](../eslint/README.md) — ESLint rules
73
+
74
+ ## License
75
+
76
+ MIT
package/dist/_internal.js CHANGED
@@ -1,14 +1,14 @@
1
- import {
2
- SPINNER_ID,
3
- defaultSpinnerSvg,
4
- extractVersionFromHtml
5
- } from "./chunk-UF7QAEI7.js";
6
1
  import {
7
2
  createLogger,
8
3
  isChunkError,
9
4
  listenInternal,
10
5
  serializeError
11
- } from "./chunk-DUR4QNQE.js";
6
+ } from "./chunk-ZVYB2746.js";
7
+ import {
8
+ SPINNER_ID,
9
+ defaultSpinnerSvg,
10
+ extractVersionFromHtml
11
+ } from "./chunk-Z75UPJWV.js";
12
12
  import {
13
13
  dispatchAsyncRuntimeError,
14
14
  dispatchChunkLoadError,
@@ -17,13 +17,13 @@ import {
17
17
  dispatchNetworkTimeout,
18
18
  dispatchSyncRuntimeError,
19
19
  dispatchUnhandledRejection
20
- } from "./chunk-H4DWKO5I.js";
20
+ } from "./chunk-XFILAQ7P.js";
21
21
  import {
22
22
  attemptReload,
23
23
  sendBeacon,
24
24
  shouldForceRetry,
25
25
  shouldIgnoreMessages
26
- } from "./chunk-VZCUDNEG.js";
26
+ } from "./chunk-CSN7MQGX.js";
27
27
  import {
28
28
  applyI18n,
29
29
  debugSyncErrorEventType,
@@ -38,7 +38,7 @@ import {
38
38
  isDefaultRetryEnabled,
39
39
  optionsWindowKey,
40
40
  subscribe
41
- } from "./chunk-5DE7AKYB.js";
41
+ } from "./chunk-X6E7KUDK.js";
42
42
  import "./chunk-MLKGABMK.js";
43
43
 
44
44
  // src/common/handleErrorWithSpaGuard.ts
@@ -18,7 +18,7 @@ import {
18
18
  setLastReloadTime,
19
19
  setLastRetryResetInfo,
20
20
  shouldResetRetryCycle
21
- } from "./chunk-5DE7AKYB.js";
21
+ } from "./chunk-X6E7KUDK.js";
22
22
 
23
23
  // src/common/shouldIgnore.ts
24
24
  var shouldIgnoreMessages = (messages) => {
@@ -3,7 +3,7 @@ import {
3
3
  getRetryAttemptFromUrl,
4
4
  getRetryStateFromUrl,
5
5
  subscribe
6
- } from "./chunk-5DE7AKYB.js";
6
+ } from "./chunk-X6E7KUDK.js";
7
7
 
8
8
  // src/runtime/state.ts
9
9
  var getInitialStateFromUrl = () => {
@@ -13,6 +13,7 @@ var initializedKey = /* @__PURE__ */ Symbol.for(`${name}:initialized`);
13
13
  var loggerWindowKey = /* @__PURE__ */ Symbol.for(`${name}:logger`);
14
14
  var RETRY_ID_PARAM = "spaGuardRetryId";
15
15
  var RETRY_ATTEMPT_PARAM = "spaGuardRetryAttempt";
16
+ var versionCheckStateWindowKey = /* @__PURE__ */ Symbol.for(`${name}:version-check-state`);
16
17
  var debugSyncErrorEventType = "spa-guard:debug-sync-error";
17
18
 
18
19
  // src/common/events/internal.ts
@@ -141,6 +142,7 @@ __export(options_exports, {
141
142
  });
142
143
  var defaultOptions = {
143
144
  checkVersion: {
145
+ cache: "no-store",
144
146
  interval: 3e5,
145
147
  mode: "html",
146
148
  onUpdate: "reload"
@@ -418,6 +420,7 @@ export {
418
420
  optionsWindowKey,
419
421
  RETRY_ID_PARAM,
420
422
  RETRY_ATTEMPT_PARAM,
423
+ versionCheckStateWindowKey,
421
424
  debugSyncErrorEventType,
422
425
  setLogger,
423
426
  getLogger,
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  showFallbackUI
3
- } from "./chunk-VZCUDNEG.js";
3
+ } from "./chunk-CSN7MQGX.js";
4
4
  import {
5
5
  ForceRetryError,
6
6
  debugSyncErrorEventType,
7
7
  emitEvent,
8
8
  getOptions
9
- } from "./chunk-5DE7AKYB.js";
9
+ } from "./chunk-X6E7KUDK.js";
10
10
 
11
11
  // src/runtime/debug/errorDispatchers.ts
12
12
  function dispatchAsyncRuntimeError() {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  defaultSpinnerHtml,
3
3
  getOptions
4
- } from "./chunk-5DE7AKYB.js";
4
+ } from "./chunk-X6E7KUDK.js";
5
5
 
6
6
  // src/common/parseVersion.ts
7
7
  function extractVersionFromHtml(html) {
@@ -3,7 +3,7 @@ import {
3
3
  sendBeacon,
4
4
  shouldForceRetry,
5
5
  shouldIgnoreMessages
6
- } from "./chunk-VZCUDNEG.js";
6
+ } from "./chunk-CSN7MQGX.js";
7
7
  import {
8
8
  getLogger,
9
9
  getOptions,
@@ -13,7 +13,7 @@ import {
13
13
  markInitialized,
14
14
  setLogger,
15
15
  updateRetryStateInUrl
16
- } from "./chunk-5DE7AKYB.js";
16
+ } from "./chunk-X6E7KUDK.js";
17
17
 
18
18
  // src/common/isChunkError.ts
19
19
  var isChunkError = (error) => {
@@ -5,4 +5,5 @@ export declare const initializedKey: unique symbol;
5
5
  export declare const loggerWindowKey: unique symbol;
6
6
  export declare const RETRY_ID_PARAM = "spaGuardRetryId";
7
7
  export declare const RETRY_ATTEMPT_PARAM = "spaGuardRetryAttempt";
8
+ export declare const versionCheckStateWindowKey: unique symbol;
8
9
  export declare const debugSyncErrorEventType = "spa-guard:debug-sync-error";
@@ -2,8 +2,8 @@ import {
2
2
  createLogger,
3
3
  listenInternal,
4
4
  serializeError
5
- } from "../chunk-DUR4QNQE.js";
6
- import "../chunk-VZCUDNEG.js";
5
+ } from "../chunk-ZVYB2746.js";
6
+ import "../chunk-CSN7MQGX.js";
7
7
  import {
8
8
  ForceRetryError,
9
9
  disableDefaultRetry,
@@ -12,7 +12,7 @@ import {
12
12
  isDefaultRetryEnabled,
13
13
  options_exports,
14
14
  subscribe
15
- } from "../chunk-5DE7AKYB.js";
15
+ } from "../chunk-X6E7KUDK.js";
16
16
  import {
17
17
  __export
18
18
  } from "../chunk-MLKGABMK.js";
@@ -11,6 +11,13 @@ export interface Options {
11
11
  * and dispatches a `spa-guard:version-change` CustomEvent on the window.
12
12
  */
13
13
  checkVersion?: {
14
+ /**
15
+ * Cache mode for the fetch request used to check the version.
16
+ * - "no-store": Bypass the HTTP cache entirely (default).
17
+ * - "no-cache": Revalidate with the server before using cached response.
18
+ * @default "no-store"
19
+ */
20
+ cache?: "no-cache" | "no-store";
14
21
  /**
15
22
  * Endpoint URL for JSON mode version checking.
16
23
  * Required when mode is "json".
@@ -7,14 +7,14 @@ import {
7
7
  dispatchRetryExhausted,
8
8
  dispatchSyncRuntimeError,
9
9
  dispatchUnhandledRejection
10
- } from "../../chunk-H4DWKO5I.js";
10
+ } from "../../chunk-XFILAQ7P.js";
11
+ import "../../chunk-CSN7MQGX.js";
11
12
  import {
12
13
  subscribeToState
13
- } from "../../chunk-CQ5IVYGC.js";
14
- import "../../chunk-VZCUDNEG.js";
14
+ } from "../../chunk-T72DERME.js";
15
15
  import {
16
16
  subscribe
17
- } from "../../chunk-5DE7AKYB.js";
17
+ } from "../../chunk-X6E7KUDK.js";
18
18
  import "../../chunk-MLKGABMK.js";
19
19
 
20
20
  // src/runtime/debug/index.ts
@@ -3,29 +3,41 @@ import {
3
3
  extractVersionFromHtml,
4
4
  getSpinnerHtml,
5
5
  showSpinner
6
- } from "../chunk-UF7QAEI7.js";
6
+ } from "../chunk-Z75UPJWV.js";
7
7
  import {
8
8
  getState,
9
9
  subscribeToState
10
- } from "../chunk-CQ5IVYGC.js";
10
+ } from "../chunk-T72DERME.js";
11
11
  import {
12
12
  ForceRetryError,
13
13
  getLogger,
14
14
  getOptions,
15
- setTranslations
16
- } from "../chunk-5DE7AKYB.js";
15
+ setTranslations,
16
+ versionCheckStateWindowKey
17
+ } from "../chunk-X6E7KUDK.js";
17
18
  import "../chunk-MLKGABMK.js";
18
19
 
19
20
  // src/common/checkVersion.ts
20
- var versionCheckInterval = null;
21
- var versionCheckTimeout = null;
22
- var lastKnownVersion = null;
23
- var lastCheckTimestamp = null;
24
- var visibilityHandler = null;
25
- var focusHandler = null;
26
- var blurHandler = null;
27
- var checkInProgress = false;
28
- var runEpoch = 0;
21
+ var createInitialState = () => ({
22
+ blurHandler: null,
23
+ checkInProgress: false,
24
+ focusHandler: null,
25
+ lastCheckTimestamp: null,
26
+ lastKnownVersion: null,
27
+ runEpoch: 0,
28
+ versionCheckInterval: null,
29
+ versionCheckTimeout: null,
30
+ visibilityHandler: null
31
+ });
32
+ if (globalThis.window && !globalThis.window[versionCheckStateWindowKey]) {
33
+ globalThis.window[versionCheckStateWindowKey] = createInitialState();
34
+ }
35
+ var getState2 = () => {
36
+ if (globalThis.window === void 0) {
37
+ return createInitialState();
38
+ }
39
+ return globalThis.window[versionCheckStateWindowKey] ?? (globalThis.window[versionCheckStateWindowKey] = createInitialState());
40
+ };
29
41
  var fetchJsonVersion = async () => {
30
42
  const endpoint = getOptions().checkVersion?.endpoint;
31
43
  if (!endpoint) {
@@ -33,7 +45,7 @@ var fetchJsonVersion = async () => {
33
45
  return null;
34
46
  }
35
47
  const response = await fetch(endpoint, {
36
- cache: "no-store",
48
+ cache: getOptions().checkVersion?.cache ?? "no-store",
37
49
  headers: { Accept: "application/json" }
38
50
  });
39
51
  if (!response.ok) {
@@ -51,7 +63,7 @@ var fetchHtmlVersion = async () => {
51
63
  url.search = "";
52
64
  url.hash = "";
53
65
  const response = await fetch(url.toString(), {
54
- cache: "no-store",
66
+ cache: getOptions().checkVersion?.cache ?? "no-store",
55
67
  headers: { Accept: "text/html" }
56
68
  });
57
69
  if (!response.ok) {
@@ -83,44 +95,47 @@ var onVersionChange = (oldVersion, latestVersion) => {
83
95
  }
84
96
  };
85
97
  var checkVersionOnce = async (mode) => {
86
- if (checkInProgress) {
98
+ const s = getState2();
99
+ if (s.checkInProgress) {
87
100
  return;
88
101
  }
89
- checkInProgress = true;
90
- const epochAtStart = runEpoch;
102
+ s.checkInProgress = true;
103
+ const epochAtStart = s.runEpoch;
91
104
  try {
92
105
  const remoteVersion = await fetchRemoteVersion(mode);
93
- if (epochAtStart !== runEpoch) {
106
+ if (epochAtStart !== s.runEpoch) {
94
107
  return;
95
108
  }
96
- if (remoteVersion && remoteVersion !== lastKnownVersion) {
97
- const oldVersion = lastKnownVersion;
98
- lastKnownVersion = remoteVersion;
109
+ if (remoteVersion && remoteVersion !== s.lastKnownVersion) {
110
+ const oldVersion = s.lastKnownVersion;
111
+ s.lastKnownVersion = remoteVersion;
99
112
  onVersionChange(oldVersion, remoteVersion);
100
113
  }
101
114
  } catch (error) {
102
115
  getLogger()?.versionCheckFailed(error);
103
116
  } finally {
104
- if (epochAtStart === runEpoch) {
105
- checkInProgress = false;
117
+ if (epochAtStart === s.runEpoch) {
118
+ s.checkInProgress = false;
106
119
  }
107
120
  }
108
121
  };
109
122
  var startPolling = (mode, interval) => {
123
+ const s = getState2();
110
124
  clearTimers();
111
- versionCheckInterval = setInterval(async () => {
112
- lastCheckTimestamp = Date.now();
125
+ s.versionCheckInterval = setInterval(async () => {
126
+ s.lastCheckTimestamp = Date.now();
113
127
  await checkVersionOnce(mode);
114
128
  }, interval);
115
129
  };
116
130
  var clearTimers = () => {
117
- if (versionCheckInterval !== null) {
118
- clearInterval(versionCheckInterval);
119
- versionCheckInterval = null;
131
+ const s = getState2();
132
+ if (s.versionCheckInterval !== null) {
133
+ clearInterval(s.versionCheckInterval);
134
+ s.versionCheckInterval = null;
120
135
  }
121
- if (versionCheckTimeout !== null) {
122
- clearTimeout(versionCheckTimeout);
123
- versionCheckTimeout = null;
136
+ if (s.versionCheckTimeout !== null) {
137
+ clearTimeout(s.versionCheckTimeout);
138
+ s.versionCheckTimeout = null;
124
139
  }
125
140
  };
126
141
  var handleVisibilityHidden = () => {
@@ -128,22 +143,23 @@ var handleVisibilityHidden = () => {
128
143
  getLogger()?.versionCheckPaused();
129
144
  };
130
145
  var handleResume = (mode, interval) => {
131
- if (versionCheckInterval !== null || versionCheckTimeout !== null) {
146
+ const s = getState2();
147
+ if (s.versionCheckInterval !== null || s.versionCheckTimeout !== null) {
132
148
  return;
133
149
  }
134
- const elapsed = Date.now() - (lastCheckTimestamp ?? 0);
150
+ const elapsed = Date.now() - (s.lastCheckTimestamp ?? 0);
135
151
  if (elapsed >= interval) {
136
152
  getLogger()?.versionCheckResumedImmediate();
137
- lastCheckTimestamp = Date.now();
153
+ s.lastCheckTimestamp = Date.now();
138
154
  void checkVersionOnce(mode);
139
155
  startPolling(mode, interval);
140
156
  return;
141
157
  }
142
158
  getLogger()?.versionCheckResumed();
143
159
  const remaining = interval - elapsed;
144
- versionCheckTimeout = setTimeout(() => {
145
- versionCheckTimeout = null;
146
- lastCheckTimestamp = Date.now();
160
+ s.versionCheckTimeout = setTimeout(() => {
161
+ s.versionCheckTimeout = null;
162
+ s.lastCheckTimestamp = Date.now();
147
163
  void checkVersionOnce(mode);
148
164
  startPolling(mode, interval);
149
165
  }, remaining);
@@ -157,57 +173,62 @@ var startVersionCheck = () => {
157
173
  getLogger()?.versionCheckDisabled();
158
174
  return;
159
175
  }
160
- if (versionCheckInterval !== null || visibilityHandler !== null) {
176
+ const s = getState2();
177
+ if (s.versionCheckInterval !== null || s.visibilityHandler !== null) {
161
178
  getLogger()?.versionCheckAlreadyRunning();
162
179
  return;
163
180
  }
164
- runEpoch++;
165
- lastKnownVersion = options.version;
181
+ s.runEpoch++;
182
+ s.lastKnownVersion = options.version;
166
183
  const interval = options.checkVersion?.interval ?? 3e5;
167
184
  const mode = options.checkVersion?.mode ?? "html";
168
- getLogger()?.versionCheckStarted(mode, interval, lastKnownVersion);
185
+ getLogger()?.versionCheckStarted(mode, interval, s.lastKnownVersion);
169
186
  const isTabVisible = document.visibilityState === "visible";
170
187
  const isWindowFocused = document.hasFocus();
171
188
  if (isTabVisible && isWindowFocused) {
172
- lastCheckTimestamp = Date.now();
189
+ s.lastCheckTimestamp = Date.now();
173
190
  startPolling(mode, interval);
174
191
  } else {
175
- lastCheckTimestamp = 0;
192
+ s.lastCheckTimestamp = 0;
176
193
  getLogger()?.versionCheckPaused();
177
194
  }
178
- visibilityHandler = () => {
195
+ s.visibilityHandler = () => {
179
196
  if (document.visibilityState === "hidden") {
180
197
  handleVisibilityHidden();
181
198
  } else {
182
199
  handleResume(mode, interval);
183
200
  }
184
201
  };
185
- focusHandler = () => {
202
+ s.focusHandler = () => {
186
203
  handleResume(mode, interval);
187
204
  };
188
- blurHandler = () => {
205
+ s.blurHandler = () => {
189
206
  handleVisibilityHidden();
190
207
  };
191
- document.addEventListener("visibilitychange", visibilityHandler);
192
- globalThis.addEventListener("focus", focusHandler);
193
- globalThis.addEventListener("blur", blurHandler);
208
+ document.addEventListener("visibilitychange", s.visibilityHandler);
209
+ globalThis.addEventListener("focus", s.focusHandler);
210
+ globalThis.addEventListener("blur", s.blurHandler);
194
211
  };
195
212
  var stopVersionCheck = () => {
196
- runEpoch++;
197
- checkInProgress = false;
198
- const wasRunning = versionCheckInterval !== null || versionCheckTimeout !== null || visibilityHandler !== null;
213
+ if (globalThis.window === void 0) {
214
+ return;
215
+ }
216
+ const s = getState2();
217
+ s.runEpoch++;
218
+ s.checkInProgress = false;
219
+ const wasRunning = s.versionCheckInterval !== null || s.versionCheckTimeout !== null || s.visibilityHandler !== null;
199
220
  clearTimers();
200
- if (visibilityHandler !== null) {
201
- document.removeEventListener("visibilitychange", visibilityHandler);
202
- visibilityHandler = null;
221
+ if (s.visibilityHandler !== null) {
222
+ document.removeEventListener("visibilitychange", s.visibilityHandler);
223
+ s.visibilityHandler = null;
203
224
  }
204
- if (focusHandler !== null) {
205
- globalThis.removeEventListener("focus", focusHandler);
206
- focusHandler = null;
225
+ if (s.focusHandler !== null) {
226
+ globalThis.removeEventListener("focus", s.focusHandler);
227
+ s.focusHandler = null;
207
228
  }
208
- if (blurHandler !== null) {
209
- globalThis.removeEventListener("blur", blurHandler);
210
- blurHandler = null;
229
+ if (s.blurHandler !== null) {
230
+ globalThis.removeEventListener("blur", s.blurHandler);
231
+ s.blurHandler = null;
211
232
  }
212
233
  if (wasRunning) {
213
234
  getLogger()?.versionCheckStopped();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ovineko/spa-guard",
3
- "version": "0.0.1-alpha-24",
3
+ "version": "0.0.1-alpha-26",
4
4
  "description": "Chunk load error handling for SPAs — core runtime, error handling, schema, i18n",
5
5
  "keywords": [
6
6
  "spa",