@arkade-os/sdk 0.4.0-next.3 → 0.4.0-next.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.
@@ -4,15 +4,111 @@ exports.__resetServiceWorkerManager = void 0;
4
4
  exports.setupServiceWorkerOnce = setupServiceWorkerOnce;
5
5
  exports.getActiveServiceWorker = getActiveServiceWorker;
6
6
  const registrations = new Map();
7
+ let handshakes = new WeakSet();
7
8
  function ensureServiceWorkerSupport() {
8
9
  if (!("serviceWorker" in navigator)) {
9
10
  throw new Error("Service workers are not supported in this browser");
10
11
  }
11
12
  }
12
- function registerOnce(path) {
13
+ function debugLog(debug, ...args) {
14
+ if (debug) {
15
+ // eslint-disable-next-line no-console
16
+ console.debug(...args);
17
+ }
18
+ }
19
+ function normalizeOptions(pathOrOptions) {
20
+ if (typeof pathOrOptions === "string") {
21
+ return {
22
+ path: pathOrOptions,
23
+ updateViaCache: "none",
24
+ autoReload: true,
25
+ debug: false,
26
+ activationTimeoutMs: 10000,
27
+ };
28
+ }
29
+ return {
30
+ path: pathOrOptions.path,
31
+ updateViaCache: pathOrOptions.updateViaCache ?? "none",
32
+ autoReload: pathOrOptions.autoReload ?? true,
33
+ onNeedRefresh: pathOrOptions.onNeedRefresh,
34
+ onUpdated: pathOrOptions.onUpdated,
35
+ debug: pathOrOptions.debug ?? false,
36
+ activationTimeoutMs: pathOrOptions.activationTimeoutMs ?? 10000,
37
+ };
38
+ }
39
+ function sendSkipWaiting(worker, debug) {
40
+ if (!worker)
41
+ return;
42
+ try {
43
+ worker.postMessage({ type: "SKIP_WAITING" });
44
+ debugLog(debug, "Sent SKIP_WAITING to waiting service worker");
45
+ }
46
+ catch (error) {
47
+ console.warn("Failed to post SKIP_WAITING to service worker", error);
48
+ }
49
+ }
50
+ function attachUpdateHandlers(registration, options) {
51
+ // Guard: only the first caller per registration attaches handlers.
52
+ // Subsequent calls with different options are silently ignored.
53
+ if (handshakes.has(registration))
54
+ return;
55
+ handshakes.add(registration);
56
+ const { autoReload, onNeedRefresh, onUpdated, activationTimeoutMs, debug } = options;
57
+ let reloadTriggered = false;
58
+ const maybeReload = () => {
59
+ if (reloadTriggered)
60
+ return;
61
+ reloadTriggered = true;
62
+ debugLog(debug, "Service worker controller change detected");
63
+ onUpdated?.();
64
+ if (autoReload &&
65
+ typeof window !== "undefined" &&
66
+ typeof window.location?.reload === "function") {
67
+ window.location.reload();
68
+ }
69
+ };
70
+ const handleWaiting = (worker) => {
71
+ if (!worker)
72
+ return;
73
+ onNeedRefresh?.();
74
+ sendSkipWaiting(worker, debug);
75
+ if (activationTimeoutMs > 0 && typeof window !== "undefined") {
76
+ window.setTimeout(() => {
77
+ if (registration.waiting) {
78
+ debugLog(debug, "Waiting worker still pending; re-sending SKIP_WAITING");
79
+ sendSkipWaiting(registration.waiting, debug);
80
+ registration
81
+ .update()
82
+ .catch(() => debugLog(debug, "Service worker update retry failed (timeout path)"));
83
+ }
84
+ }, activationTimeoutMs);
85
+ }
86
+ };
87
+ // Handle an already waiting worker at startup.
88
+ if (registration.waiting) {
89
+ handleWaiting(registration.waiting);
90
+ }
91
+ // Listen for newly installed workers becoming waiting.
92
+ registration.addEventListener("updatefound", () => {
93
+ const installing = registration.installing;
94
+ if (!installing)
95
+ return;
96
+ installing.addEventListener("statechange", () => {
97
+ if (installing.state === "installed") {
98
+ handleWaiting(registration.waiting);
99
+ }
100
+ });
101
+ });
102
+ // Reload (or callback) once the new controller takes over.
103
+ navigator.serviceWorker.addEventListener("controllerchange", maybeReload, {
104
+ once: true,
105
+ });
106
+ }
107
+ function registerOnce(options) {
108
+ const { path, updateViaCache } = options;
13
109
  if (!registrations.has(path)) {
14
110
  const registrationPromise = navigator.serviceWorker
15
- .register(path)
111
+ .register(path, { updateViaCache })
16
112
  .then(async (registration) => {
17
113
  try {
18
114
  await registration.update();
@@ -29,18 +125,23 @@ function registerOnce(path) {
29
125
  });
30
126
  registrations.set(path, registrationPromise);
31
127
  }
32
- return registrations.get(path);
128
+ return registrations.get(path).then((registration) => {
129
+ attachUpdateHandlers(registration, options);
130
+ return registration;
131
+ });
33
132
  }
34
133
  /**
35
- * Registers a service worker for the given path only once and caches the
36
- * registration promise for subsequent calls.
134
+ * Registers a service worker for the given path only once, attaches an
135
+ * update/activation handshake (SKIP_WAITING + controllerchange reload), and
136
+ * caches the registration promise for subsequent calls.
37
137
  *
38
- * @param path - Service worker script path to register.
138
+ * @param pathOrOptions - Service worker script path or a configuration object.
39
139
  * @throws if service workers are not supported or registration fails.
40
140
  */
41
- async function setupServiceWorkerOnce(path) {
141
+ async function setupServiceWorkerOnce(pathOrOptions) {
42
142
  ensureServiceWorkerSupport();
43
- return registerOnce(path);
143
+ const options = normalizeOptions(pathOrOptions);
144
+ return registerOnce(options);
44
145
  }
45
146
  /**
46
147
  * Returns an active service worker instance, optionally ensuring a specific
@@ -51,9 +152,8 @@ async function setupServiceWorkerOnce(path) {
51
152
  */
52
153
  async function getActiveServiceWorker(path) {
53
154
  ensureServiceWorkerSupport();
54
- // Avoid mixing registrations when a specific script path is provided.
55
155
  const registration = path
56
- ? await registerOnce(path)
156
+ ? await registerOnce(normalizeOptions(path))
57
157
  : await navigator.serviceWorker.ready;
58
158
  let serviceWorker = registration.active ||
59
159
  registration.waiting ||
@@ -78,5 +178,6 @@ async function getActiveServiceWorker(path) {
78
178
  */
79
179
  const __resetServiceWorkerManager = () => {
80
180
  registrations.clear();
181
+ handshakes = new WeakSet();
81
182
  };
82
183
  exports.__resetServiceWorkerManager = __resetServiceWorkerManager;
@@ -1,13 +1,109 @@
1
1
  const registrations = new Map();
2
+ let handshakes = new WeakSet();
2
3
  function ensureServiceWorkerSupport() {
3
4
  if (!("serviceWorker" in navigator)) {
4
5
  throw new Error("Service workers are not supported in this browser");
5
6
  }
6
7
  }
7
- function registerOnce(path) {
8
+ function debugLog(debug, ...args) {
9
+ if (debug) {
10
+ // eslint-disable-next-line no-console
11
+ console.debug(...args);
12
+ }
13
+ }
14
+ function normalizeOptions(pathOrOptions) {
15
+ if (typeof pathOrOptions === "string") {
16
+ return {
17
+ path: pathOrOptions,
18
+ updateViaCache: "none",
19
+ autoReload: true,
20
+ debug: false,
21
+ activationTimeoutMs: 10000,
22
+ };
23
+ }
24
+ return {
25
+ path: pathOrOptions.path,
26
+ updateViaCache: pathOrOptions.updateViaCache ?? "none",
27
+ autoReload: pathOrOptions.autoReload ?? true,
28
+ onNeedRefresh: pathOrOptions.onNeedRefresh,
29
+ onUpdated: pathOrOptions.onUpdated,
30
+ debug: pathOrOptions.debug ?? false,
31
+ activationTimeoutMs: pathOrOptions.activationTimeoutMs ?? 10000,
32
+ };
33
+ }
34
+ function sendSkipWaiting(worker, debug) {
35
+ if (!worker)
36
+ return;
37
+ try {
38
+ worker.postMessage({ type: "SKIP_WAITING" });
39
+ debugLog(debug, "Sent SKIP_WAITING to waiting service worker");
40
+ }
41
+ catch (error) {
42
+ console.warn("Failed to post SKIP_WAITING to service worker", error);
43
+ }
44
+ }
45
+ function attachUpdateHandlers(registration, options) {
46
+ // Guard: only the first caller per registration attaches handlers.
47
+ // Subsequent calls with different options are silently ignored.
48
+ if (handshakes.has(registration))
49
+ return;
50
+ handshakes.add(registration);
51
+ const { autoReload, onNeedRefresh, onUpdated, activationTimeoutMs, debug } = options;
52
+ let reloadTriggered = false;
53
+ const maybeReload = () => {
54
+ if (reloadTriggered)
55
+ return;
56
+ reloadTriggered = true;
57
+ debugLog(debug, "Service worker controller change detected");
58
+ onUpdated?.();
59
+ if (autoReload &&
60
+ typeof window !== "undefined" &&
61
+ typeof window.location?.reload === "function") {
62
+ window.location.reload();
63
+ }
64
+ };
65
+ const handleWaiting = (worker) => {
66
+ if (!worker)
67
+ return;
68
+ onNeedRefresh?.();
69
+ sendSkipWaiting(worker, debug);
70
+ if (activationTimeoutMs > 0 && typeof window !== "undefined") {
71
+ window.setTimeout(() => {
72
+ if (registration.waiting) {
73
+ debugLog(debug, "Waiting worker still pending; re-sending SKIP_WAITING");
74
+ sendSkipWaiting(registration.waiting, debug);
75
+ registration
76
+ .update()
77
+ .catch(() => debugLog(debug, "Service worker update retry failed (timeout path)"));
78
+ }
79
+ }, activationTimeoutMs);
80
+ }
81
+ };
82
+ // Handle an already waiting worker at startup.
83
+ if (registration.waiting) {
84
+ handleWaiting(registration.waiting);
85
+ }
86
+ // Listen for newly installed workers becoming waiting.
87
+ registration.addEventListener("updatefound", () => {
88
+ const installing = registration.installing;
89
+ if (!installing)
90
+ return;
91
+ installing.addEventListener("statechange", () => {
92
+ if (installing.state === "installed") {
93
+ handleWaiting(registration.waiting);
94
+ }
95
+ });
96
+ });
97
+ // Reload (or callback) once the new controller takes over.
98
+ navigator.serviceWorker.addEventListener("controllerchange", maybeReload, {
99
+ once: true,
100
+ });
101
+ }
102
+ function registerOnce(options) {
103
+ const { path, updateViaCache } = options;
8
104
  if (!registrations.has(path)) {
9
105
  const registrationPromise = navigator.serviceWorker
10
- .register(path)
106
+ .register(path, { updateViaCache })
11
107
  .then(async (registration) => {
12
108
  try {
13
109
  await registration.update();
@@ -24,18 +120,23 @@ function registerOnce(path) {
24
120
  });
25
121
  registrations.set(path, registrationPromise);
26
122
  }
27
- return registrations.get(path);
123
+ return registrations.get(path).then((registration) => {
124
+ attachUpdateHandlers(registration, options);
125
+ return registration;
126
+ });
28
127
  }
29
128
  /**
30
- * Registers a service worker for the given path only once and caches the
31
- * registration promise for subsequent calls.
129
+ * Registers a service worker for the given path only once, attaches an
130
+ * update/activation handshake (SKIP_WAITING + controllerchange reload), and
131
+ * caches the registration promise for subsequent calls.
32
132
  *
33
- * @param path - Service worker script path to register.
133
+ * @param pathOrOptions - Service worker script path or a configuration object.
34
134
  * @throws if service workers are not supported or registration fails.
35
135
  */
36
- export async function setupServiceWorkerOnce(path) {
136
+ export async function setupServiceWorkerOnce(pathOrOptions) {
37
137
  ensureServiceWorkerSupport();
38
- return registerOnce(path);
138
+ const options = normalizeOptions(pathOrOptions);
139
+ return registerOnce(options);
39
140
  }
40
141
  /**
41
142
  * Returns an active service worker instance, optionally ensuring a specific
@@ -46,9 +147,8 @@ export async function setupServiceWorkerOnce(path) {
46
147
  */
47
148
  export async function getActiveServiceWorker(path) {
48
149
  ensureServiceWorkerSupport();
49
- // Avoid mixing registrations when a specific script path is provided.
50
150
  const registration = path
51
- ? await registerOnce(path)
151
+ ? await registerOnce(normalizeOptions(path))
52
152
  : await navigator.serviceWorker.ready;
53
153
  let serviceWorker = registration.active ||
54
154
  registration.waiting ||
@@ -73,4 +173,5 @@ export async function getActiveServiceWorker(path) {
73
173
  */
74
174
  export const __resetServiceWorkerManager = () => {
75
175
  registrations.clear();
176
+ handshakes = new WeakSet();
76
177
  };
@@ -1,11 +1,21 @@
1
+ type SetupServiceWorkerOptions = {
2
+ path: string;
3
+ updateViaCache?: ServiceWorkerUpdateViaCache;
4
+ autoReload?: boolean;
5
+ onNeedRefresh?: () => void;
6
+ onUpdated?: () => void;
7
+ debug?: boolean;
8
+ activationTimeoutMs?: number;
9
+ };
1
10
  /**
2
- * Registers a service worker for the given path only once and caches the
3
- * registration promise for subsequent calls.
11
+ * Registers a service worker for the given path only once, attaches an
12
+ * update/activation handshake (SKIP_WAITING + controllerchange reload), and
13
+ * caches the registration promise for subsequent calls.
4
14
  *
5
- * @param path - Service worker script path to register.
15
+ * @param pathOrOptions - Service worker script path or a configuration object.
6
16
  * @throws if service workers are not supported or registration fails.
7
17
  */
8
- export declare function setupServiceWorkerOnce(path: string): Promise<ServiceWorkerRegistration>;
18
+ export declare function setupServiceWorkerOnce(pathOrOptions: string | SetupServiceWorkerOptions): Promise<ServiceWorkerRegistration>;
9
19
  /**
10
20
  * Returns an active service worker instance, optionally ensuring a specific
11
21
  * script path is registered before resolving.
@@ -19,3 +29,4 @@ export declare function getActiveServiceWorker(path?: string): Promise<ServiceWo
19
29
  * Intended for tests to reset state between runs.
20
30
  */
21
31
  export declare const __resetServiceWorkerManager: () => void;
32
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arkade-os/sdk",
3
- "version": "0.4.0-next.3",
3
+ "version": "0.4.0-next.4",
4
4
  "description": "Bitcoin wallet SDK with Taproot and Ark integration",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",