@arkade-os/sdk 0.4.0-next.7 → 0.4.0-next.8

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.
@@ -95,6 +95,9 @@ class ServiceWorkerReadonlyWallet {
95
95
  constructor(serviceWorker, identity, walletRepository, contractRepository, messageTag) {
96
96
  this.serviceWorker = serviceWorker;
97
97
  this.messageTag = messageTag;
98
+ this.initConfig = null;
99
+ this.initWalletPayload = null;
100
+ this.reinitPromise = null;
98
101
  this.identity = identity;
99
102
  this.walletRepository = walletRepository;
100
103
  this.contractRepository = contractRepository;
@@ -135,6 +138,16 @@ class ServiceWorkerReadonlyWallet {
135
138
  payload: initConfig,
136
139
  };
137
140
  await wallet.sendMessage(initMessage);
141
+ wallet.initConfig = {
142
+ wallet: initConfig.key,
143
+ arkServer: {
144
+ url: initConfig.arkServerUrl,
145
+ publicKey: initConfig.arkServerPublicKey,
146
+ },
147
+ delegatorUrl: initConfig.delegatorUrl,
148
+ };
149
+ wallet.initWalletPayload = initConfig;
150
+ wallet.messageBusTimeoutMs = options.messageBusTimeoutMs;
138
151
  return wallet;
139
152
  }
140
153
  /**
@@ -160,15 +173,17 @@ class ServiceWorkerReadonlyWallet {
160
173
  */
161
174
  static async setup(options) {
162
175
  // Register and setup the service worker
163
- const serviceWorker = await (0, utils_1.setupServiceWorker)(options.serviceWorkerPath);
176
+ const serviceWorker = await (0, utils_1.setupServiceWorker)({
177
+ path: options.serviceWorkerPath,
178
+ activationTimeoutMs: options.serviceWorkerActivationTimeoutMs,
179
+ });
164
180
  // Use the existing create method
165
181
  return await ServiceWorkerReadonlyWallet.create({
166
182
  ...options,
167
183
  serviceWorker,
168
184
  });
169
185
  }
170
- // send a message and wait for a response
171
- async sendMessage(request) {
186
+ sendMessageDirect(request) {
172
187
  return new Promise((resolve, reject) => {
173
188
  const cleanup = () => {
174
189
  clearTimeout(timeoutId);
@@ -195,6 +210,44 @@ class ServiceWorkerReadonlyWallet {
195
210
  this.serviceWorker.postMessage(request);
196
211
  });
197
212
  }
213
+ // send a message, retrying up to 2 times if the service worker was
214
+ // killed and restarted by the OS (mobile browsers do this aggressively)
215
+ async sendMessage(request) {
216
+ const maxRetries = 2;
217
+ for (let attempt = 0;; attempt++) {
218
+ try {
219
+ return await this.sendMessageDirect(request);
220
+ }
221
+ catch (error) {
222
+ const isNotInitialized = typeof error?.message === "string" &&
223
+ error.message.includes("MessageBus not initialized");
224
+ if (!isNotInitialized || attempt >= maxRetries) {
225
+ throw error;
226
+ }
227
+ await this.reinitialize();
228
+ }
229
+ }
230
+ }
231
+ async reinitialize() {
232
+ if (this.reinitPromise)
233
+ return this.reinitPromise;
234
+ this.reinitPromise = (async () => {
235
+ if (!this.initConfig || !this.initWalletPayload) {
236
+ throw new Error("Cannot re-initialize: missing configuration");
237
+ }
238
+ await initializeMessageBus(this.serviceWorker, this.initConfig, this.messageBusTimeoutMs);
239
+ const initMessage = {
240
+ tag: this.messageTag,
241
+ type: "INIT_WALLET",
242
+ id: (0, utils_2.getRandomId)(),
243
+ payload: this.initWalletPayload,
244
+ };
245
+ await this.sendMessageDirect(initMessage);
246
+ })().finally(() => {
247
+ this.reinitPromise = null;
248
+ });
249
+ return this.reinitPromise;
250
+ }
198
251
  async clear() {
199
252
  const message = {
200
253
  id: (0, utils_2.getRandomId)(),
@@ -554,6 +607,16 @@ class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
554
607
  };
555
608
  // Initialize the service worker
556
609
  await wallet.sendMessage(initMessage);
610
+ wallet.initConfig = {
611
+ wallet: initConfig.key,
612
+ arkServer: {
613
+ url: initConfig.arkServerUrl,
614
+ publicKey: initConfig.arkServerPublicKey,
615
+ },
616
+ delegatorUrl: initConfig.delegatorUrl,
617
+ };
618
+ wallet.initWalletPayload = initConfig;
619
+ wallet.messageBusTimeoutMs = options.messageBusTimeoutMs;
557
620
  return wallet;
558
621
  }
559
622
  /**
@@ -579,7 +642,10 @@ class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
579
642
  */
580
643
  static async setup(options) {
581
644
  // Register and setup the service worker
582
- const serviceWorker = await (0, utils_1.setupServiceWorker)(options.serviceWorkerPath);
645
+ const serviceWorker = await (0, utils_1.setupServiceWorker)({
646
+ path: options.serviceWorkerPath,
647
+ activationTimeoutMs: options.serviceWorkerActivationTimeoutMs,
648
+ });
583
649
  // Use the existing create method
584
650
  return ServiceWorkerWallet.create({
585
651
  ...options,
@@ -1,22 +1,53 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_DB_NAME = void 0;
3
+ exports.DEFAULT_SERVICE_WORKER_ACTIVATION_TIMEOUT_MS = exports.DEFAULT_DB_NAME = void 0;
4
4
  exports.setupServiceWorker = setupServiceWorker;
5
5
  exports.DEFAULT_DB_NAME = "arkade-service-worker";
6
+ exports.DEFAULT_SERVICE_WORKER_ACTIVATION_TIMEOUT_MS = 10000;
7
+ function normalizeOptions(pathOrOptions) {
8
+ if (typeof pathOrOptions === "string") {
9
+ return {
10
+ path: pathOrOptions,
11
+ activationTimeoutMs: exports.DEFAULT_SERVICE_WORKER_ACTIVATION_TIMEOUT_MS,
12
+ };
13
+ }
14
+ return {
15
+ path: pathOrOptions.path,
16
+ activationTimeoutMs: pathOrOptions.activationTimeoutMs ??
17
+ exports.DEFAULT_SERVICE_WORKER_ACTIVATION_TIMEOUT_MS,
18
+ };
19
+ }
20
+ function waitForServiceWorkerReady(timeoutMs) {
21
+ return new Promise((resolve, reject) => {
22
+ const timeoutId = setTimeout(() => {
23
+ reject(new Error(`Service worker activation timed out after ${timeoutMs}ms`));
24
+ }, timeoutMs);
25
+ navigator.serviceWorker.ready
26
+ .then((registration) => {
27
+ clearTimeout(timeoutId);
28
+ resolve(registration);
29
+ })
30
+ .catch((error) => {
31
+ clearTimeout(timeoutId);
32
+ reject(error);
33
+ });
34
+ });
35
+ }
6
36
  /**
7
37
  * setupServiceWorker sets up the service worker.
8
- * @param path - the path to the service worker script
38
+ * @param pathOrOptions - the path to the service worker script or setup options
9
39
  * @throws if service workers are not supported or activation fails
10
40
  * @example
11
41
  * ```typescript
12
42
  * const worker = await setupServiceWorker("/service-worker.js");
13
43
  * ```
14
44
  */
15
- async function setupServiceWorker(path) {
45
+ async function setupServiceWorker(pathOrOptions) {
16
46
  // check if service workers are supported
17
47
  if (!("serviceWorker" in navigator)) {
18
48
  throw new Error("Service workers are not supported in this browser");
19
49
  }
50
+ const { path, activationTimeoutMs } = normalizeOptions(pathOrOptions);
20
51
  // register service worker
21
52
  const registration = await navigator.serviceWorker.register(path);
22
53
  // force update to ensure the service worker is active
@@ -25,28 +56,12 @@ async function setupServiceWorker(path) {
25
56
  if (!serviceWorker) {
26
57
  throw new Error("Failed to get service worker instance");
27
58
  }
28
- // wait for the service worker to be ready
29
- return new Promise((resolve, reject) => {
30
- if (serviceWorker.state === "activated")
31
- return resolve(serviceWorker);
32
- const onActivate = () => {
33
- cleanup();
34
- resolve(serviceWorker);
35
- };
36
- const onError = () => {
37
- cleanup();
38
- reject(new Error("Service worker failed to activate"));
39
- };
40
- const timeout = setTimeout(() => {
41
- cleanup();
42
- reject(new Error("Service worker activation timed out"));
43
- }, 10000);
44
- const cleanup = () => {
45
- navigator.serviceWorker.removeEventListener("activate", onActivate);
46
- navigator.serviceWorker.removeEventListener("error", onError);
47
- clearTimeout(timeout);
48
- };
49
- navigator.serviceWorker.addEventListener("activate", onActivate);
50
- navigator.serviceWorker.addEventListener("error", onError);
51
- });
59
+ if (serviceWorker.state === "activated") {
60
+ return serviceWorker;
61
+ }
62
+ const readyRegistration = await waitForServiceWorkerReady(activationTimeoutMs);
63
+ if (!readyRegistration.active) {
64
+ throw new Error("Service worker registration is ready but has no active worker");
65
+ }
66
+ return readyRegistration.active;
52
67
  }
@@ -92,6 +92,9 @@ export class ServiceWorkerReadonlyWallet {
92
92
  constructor(serviceWorker, identity, walletRepository, contractRepository, messageTag) {
93
93
  this.serviceWorker = serviceWorker;
94
94
  this.messageTag = messageTag;
95
+ this.initConfig = null;
96
+ this.initWalletPayload = null;
97
+ this.reinitPromise = null;
95
98
  this.identity = identity;
96
99
  this.walletRepository = walletRepository;
97
100
  this.contractRepository = contractRepository;
@@ -132,6 +135,16 @@ export class ServiceWorkerReadonlyWallet {
132
135
  payload: initConfig,
133
136
  };
134
137
  await wallet.sendMessage(initMessage);
138
+ wallet.initConfig = {
139
+ wallet: initConfig.key,
140
+ arkServer: {
141
+ url: initConfig.arkServerUrl,
142
+ publicKey: initConfig.arkServerPublicKey,
143
+ },
144
+ delegatorUrl: initConfig.delegatorUrl,
145
+ };
146
+ wallet.initWalletPayload = initConfig;
147
+ wallet.messageBusTimeoutMs = options.messageBusTimeoutMs;
135
148
  return wallet;
136
149
  }
137
150
  /**
@@ -157,15 +170,17 @@ export class ServiceWorkerReadonlyWallet {
157
170
  */
158
171
  static async setup(options) {
159
172
  // Register and setup the service worker
160
- const serviceWorker = await setupServiceWorker(options.serviceWorkerPath);
173
+ const serviceWorker = await setupServiceWorker({
174
+ path: options.serviceWorkerPath,
175
+ activationTimeoutMs: options.serviceWorkerActivationTimeoutMs,
176
+ });
161
177
  // Use the existing create method
162
178
  return await ServiceWorkerReadonlyWallet.create({
163
179
  ...options,
164
180
  serviceWorker,
165
181
  });
166
182
  }
167
- // send a message and wait for a response
168
- async sendMessage(request) {
183
+ sendMessageDirect(request) {
169
184
  return new Promise((resolve, reject) => {
170
185
  const cleanup = () => {
171
186
  clearTimeout(timeoutId);
@@ -192,6 +207,44 @@ export class ServiceWorkerReadonlyWallet {
192
207
  this.serviceWorker.postMessage(request);
193
208
  });
194
209
  }
210
+ // send a message, retrying up to 2 times if the service worker was
211
+ // killed and restarted by the OS (mobile browsers do this aggressively)
212
+ async sendMessage(request) {
213
+ const maxRetries = 2;
214
+ for (let attempt = 0;; attempt++) {
215
+ try {
216
+ return await this.sendMessageDirect(request);
217
+ }
218
+ catch (error) {
219
+ const isNotInitialized = typeof error?.message === "string" &&
220
+ error.message.includes("MessageBus not initialized");
221
+ if (!isNotInitialized || attempt >= maxRetries) {
222
+ throw error;
223
+ }
224
+ await this.reinitialize();
225
+ }
226
+ }
227
+ }
228
+ async reinitialize() {
229
+ if (this.reinitPromise)
230
+ return this.reinitPromise;
231
+ this.reinitPromise = (async () => {
232
+ if (!this.initConfig || !this.initWalletPayload) {
233
+ throw new Error("Cannot re-initialize: missing configuration");
234
+ }
235
+ await initializeMessageBus(this.serviceWorker, this.initConfig, this.messageBusTimeoutMs);
236
+ const initMessage = {
237
+ tag: this.messageTag,
238
+ type: "INIT_WALLET",
239
+ id: getRandomId(),
240
+ payload: this.initWalletPayload,
241
+ };
242
+ await this.sendMessageDirect(initMessage);
243
+ })().finally(() => {
244
+ this.reinitPromise = null;
245
+ });
246
+ return this.reinitPromise;
247
+ }
195
248
  async clear() {
196
249
  const message = {
197
250
  id: getRandomId(),
@@ -550,6 +603,16 @@ export class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
550
603
  };
551
604
  // Initialize the service worker
552
605
  await wallet.sendMessage(initMessage);
606
+ wallet.initConfig = {
607
+ wallet: initConfig.key,
608
+ arkServer: {
609
+ url: initConfig.arkServerUrl,
610
+ publicKey: initConfig.arkServerPublicKey,
611
+ },
612
+ delegatorUrl: initConfig.delegatorUrl,
613
+ };
614
+ wallet.initWalletPayload = initConfig;
615
+ wallet.messageBusTimeoutMs = options.messageBusTimeoutMs;
553
616
  return wallet;
554
617
  }
555
618
  /**
@@ -575,7 +638,10 @@ export class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
575
638
  */
576
639
  static async setup(options) {
577
640
  // Register and setup the service worker
578
- const serviceWorker = await setupServiceWorker(options.serviceWorkerPath);
641
+ const serviceWorker = await setupServiceWorker({
642
+ path: options.serviceWorkerPath,
643
+ activationTimeoutMs: options.serviceWorkerActivationTimeoutMs,
644
+ });
579
645
  // Use the existing create method
580
646
  return ServiceWorkerWallet.create({
581
647
  ...options,
@@ -1,18 +1,49 @@
1
1
  export const DEFAULT_DB_NAME = "arkade-service-worker";
2
+ export const DEFAULT_SERVICE_WORKER_ACTIVATION_TIMEOUT_MS = 10000;
3
+ function normalizeOptions(pathOrOptions) {
4
+ if (typeof pathOrOptions === "string") {
5
+ return {
6
+ path: pathOrOptions,
7
+ activationTimeoutMs: DEFAULT_SERVICE_WORKER_ACTIVATION_TIMEOUT_MS,
8
+ };
9
+ }
10
+ return {
11
+ path: pathOrOptions.path,
12
+ activationTimeoutMs: pathOrOptions.activationTimeoutMs ??
13
+ DEFAULT_SERVICE_WORKER_ACTIVATION_TIMEOUT_MS,
14
+ };
15
+ }
16
+ function waitForServiceWorkerReady(timeoutMs) {
17
+ return new Promise((resolve, reject) => {
18
+ const timeoutId = setTimeout(() => {
19
+ reject(new Error(`Service worker activation timed out after ${timeoutMs}ms`));
20
+ }, timeoutMs);
21
+ navigator.serviceWorker.ready
22
+ .then((registration) => {
23
+ clearTimeout(timeoutId);
24
+ resolve(registration);
25
+ })
26
+ .catch((error) => {
27
+ clearTimeout(timeoutId);
28
+ reject(error);
29
+ });
30
+ });
31
+ }
2
32
  /**
3
33
  * setupServiceWorker sets up the service worker.
4
- * @param path - the path to the service worker script
34
+ * @param pathOrOptions - the path to the service worker script or setup options
5
35
  * @throws if service workers are not supported or activation fails
6
36
  * @example
7
37
  * ```typescript
8
38
  * const worker = await setupServiceWorker("/service-worker.js");
9
39
  * ```
10
40
  */
11
- export async function setupServiceWorker(path) {
41
+ export async function setupServiceWorker(pathOrOptions) {
12
42
  // check if service workers are supported
13
43
  if (!("serviceWorker" in navigator)) {
14
44
  throw new Error("Service workers are not supported in this browser");
15
45
  }
46
+ const { path, activationTimeoutMs } = normalizeOptions(pathOrOptions);
16
47
  // register service worker
17
48
  const registration = await navigator.serviceWorker.register(path);
18
49
  // force update to ensure the service worker is active
@@ -21,28 +52,12 @@ export async function setupServiceWorker(path) {
21
52
  if (!serviceWorker) {
22
53
  throw new Error("Failed to get service worker instance");
23
54
  }
24
- // wait for the service worker to be ready
25
- return new Promise((resolve, reject) => {
26
- if (serviceWorker.state === "activated")
27
- return resolve(serviceWorker);
28
- const onActivate = () => {
29
- cleanup();
30
- resolve(serviceWorker);
31
- };
32
- const onError = () => {
33
- cleanup();
34
- reject(new Error("Service worker failed to activate"));
35
- };
36
- const timeout = setTimeout(() => {
37
- cleanup();
38
- reject(new Error("Service worker activation timed out"));
39
- }, 10000);
40
- const cleanup = () => {
41
- navigator.serviceWorker.removeEventListener("activate", onActivate);
42
- navigator.serviceWorker.removeEventListener("error", onError);
43
- clearTimeout(timeout);
44
- };
45
- navigator.serviceWorker.addEventListener("activate", onActivate);
46
- navigator.serviceWorker.addEventListener("error", onError);
47
- });
55
+ if (serviceWorker.state === "activated") {
56
+ return serviceWorker;
57
+ }
58
+ const readyRegistration = await waitForServiceWorkerReady(activationTimeoutMs);
59
+ if (!readyRegistration.active) {
60
+ throw new Error("Service worker registration is ready but has no active worker");
61
+ }
62
+ return readyRegistration.active;
48
63
  }
@@ -3,7 +3,7 @@ import { SettlementEvent } from "../../providers/ark";
3
3
  import { Identity, ReadonlyIdentity } from "../../identity";
4
4
  import { WalletRepository } from "../../repositories/walletRepository";
5
5
  import { ContractRepository } from "../../repositories/contractRepository";
6
- import { ResponseGetStatus, WalletUpdaterRequest, WalletUpdaterResponse } from "./wallet-message-handler";
6
+ import { RequestInitWallet, ResponseGetStatus, WalletUpdaterRequest, WalletUpdaterResponse } from "./wallet-message-handler";
7
7
  import type { IContractManager } from "../../contracts/contractManager";
8
8
  import type { IDelegatorManager } from "../delegator";
9
9
  type PrivateKeyIdentity = Identity & {
@@ -56,6 +56,20 @@ export type ServiceWorkerWalletCreateOptions = ServiceWorkerWalletOptions & {
56
56
  };
57
57
  export type ServiceWorkerWalletSetupOptions = ServiceWorkerWalletOptions & {
58
58
  serviceWorkerPath: string;
59
+ serviceWorkerActivationTimeoutMs?: number;
60
+ };
61
+ type MessageBusInitConfig = {
62
+ wallet: {
63
+ privateKey: string;
64
+ } | {
65
+ publicKey: string;
66
+ };
67
+ arkServer: {
68
+ url: string;
69
+ publicKey?: string;
70
+ };
71
+ delegatorUrl?: string;
72
+ timeoutMs?: number;
59
73
  };
60
74
  export declare class ServiceWorkerReadonlyWallet implements IReadonlyWallet {
61
75
  readonly serviceWorker: ServiceWorker;
@@ -64,6 +78,10 @@ export declare class ServiceWorkerReadonlyWallet implements IReadonlyWallet {
64
78
  readonly contractRepository: ContractRepository;
65
79
  readonly identity: ReadonlyIdentity;
66
80
  private readonly _readonlyAssetManager;
81
+ protected initConfig: MessageBusInitConfig | null;
82
+ protected initWalletPayload: RequestInitWallet["payload"] | null;
83
+ protected messageBusTimeoutMs?: number;
84
+ private reinitPromise;
67
85
  get assetManager(): IReadonlyAssetManager;
68
86
  protected constructor(serviceWorker: ServiceWorker, identity: ReadonlyIdentity, walletRepository: WalletRepository, contractRepository: ContractRepository, messageTag: string);
69
87
  static create(options: ServiceWorkerWalletCreateOptions): Promise<ServiceWorkerReadonlyWallet>;
@@ -89,7 +107,9 @@ export declare class ServiceWorkerReadonlyWallet implements IReadonlyWallet {
89
107
  * ```
90
108
  */
91
109
  static setup(options: ServiceWorkerWalletSetupOptions): Promise<ServiceWorkerReadonlyWallet>;
110
+ private sendMessageDirect;
92
111
  protected sendMessage(request: WalletUpdaterRequest): Promise<WalletUpdaterResponse>;
112
+ private reinitialize;
93
113
  clear(): Promise<void>;
94
114
  getAddress(): Promise<string>;
95
115
  getBoardingAddress(): Promise<string>;
@@ -1,11 +1,17 @@
1
1
  export declare const DEFAULT_DB_NAME = "arkade-service-worker";
2
+ export declare const DEFAULT_SERVICE_WORKER_ACTIVATION_TIMEOUT_MS = 10000;
3
+ type SetupServiceWorkerOptions = {
4
+ path: string;
5
+ activationTimeoutMs?: number;
6
+ };
2
7
  /**
3
8
  * setupServiceWorker sets up the service worker.
4
- * @param path - the path to the service worker script
9
+ * @param pathOrOptions - the path to the service worker script or setup options
5
10
  * @throws if service workers are not supported or activation fails
6
11
  * @example
7
12
  * ```typescript
8
13
  * const worker = await setupServiceWorker("/service-worker.js");
9
14
  * ```
10
15
  */
11
- export declare function setupServiceWorker(path: string): Promise<ServiceWorker>;
16
+ export declare function setupServiceWorker(pathOrOptions: string | SetupServiceWorkerOptions): Promise<ServiceWorker>;
17
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arkade-os/sdk",
3
- "version": "0.4.0-next.7",
3
+ "version": "0.4.0-next.8",
4
4
  "description": "Bitcoin wallet SDK with Taproot and Ark integration",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",