@chromahq/store 1.0.45 → 1.0.47

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/dist/index.cjs.js CHANGED
@@ -110,6 +110,7 @@ function useStoreReset(store) {
110
110
 
111
111
  const STORE_ENABLE_LOGS = typeof globalThis !== "undefined" && globalThis.__CHROMA_ENABLE_LOGS__ === false ? false : true;
112
112
  class BridgeStore {
113
+ // Consider state stale after 30s hidden
113
114
  constructor(bridge, initialState, storeName = "default", readyCallbacks = /* @__PURE__ */ new Set()) {
114
115
  this.listeners = /* @__PURE__ */ new Set();
115
116
  this.currentState = null;
@@ -127,10 +128,14 @@ class BridgeStore {
127
128
  this.stateChangedHandler = null;
128
129
  // Debounce timer for state sync (optimization for rapid updates)
129
130
  this.stateSyncDebounceTimer = null;
130
- this.stateSyncDebounceMs = 100;
131
- // Increased to 100ms to reduce burst traffic on Windows
131
+ this.stateSyncDebounceMs = 50;
132
+ // Reduced to 50ms for faster reactivity
132
133
  // Reconnect delay timer (to allow SW to bootstrap before re-initializing)
133
134
  this.reconnectDelayTimer = null;
135
+ // Visibility change handling - refresh state when tab becomes visible
136
+ this.visibilityHandler = null;
137
+ this.lastVisibleAt = Date.now();
138
+ this.staleThresholdMs = 3e4;
134
139
  this.initialize = async () => {
135
140
  if (this.isInitializing) {
136
141
  return;
@@ -260,6 +265,10 @@ class BridgeStore {
260
265
  this.stateChangedHandler = null;
261
266
  }
262
267
  }
268
+ if (this.visibilityHandler && typeof document !== "undefined") {
269
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
270
+ this.visibilityHandler = null;
271
+ }
263
272
  if (this.listeners) {
264
273
  this.listeners.clear();
265
274
  }
@@ -353,6 +362,23 @@ class BridgeStore {
353
362
  maxInitializationAttempts: this.maxInitializationAttempts
354
363
  };
355
364
  };
365
+ /**
366
+ * Update the bridge reference and re-register all event listeners.
367
+ * Called when createBridgeStore receives a new bridge object (e.g., after React remount).
368
+ * This is critical for React StrictMode which causes double-mounting.
369
+ */
370
+ this.updateBridge = (newBridge) => {
371
+ if (this.bridge === newBridge) {
372
+ return;
373
+ }
374
+ if (STORE_ENABLE_LOGS) {
375
+ console.log(
376
+ `BridgeStore[${this.storeName}]: Updating bridge reference and re-registering listeners`
377
+ );
378
+ }
379
+ this.bridge = newBridge;
380
+ this.reregisterEventListeners();
381
+ };
356
382
  this.bridge = bridge;
357
383
  this.currentState = initialState || null;
358
384
  this.previousState = initialState || null;
@@ -361,6 +387,7 @@ class BridgeStore {
361
387
  this.readyCallbacks = readyCallbacks;
362
388
  this.setupStateSync();
363
389
  this.setupReconnectListener();
390
+ this.setupVisibilityListener();
364
391
  this.initialize();
365
392
  }
366
393
  setupReconnectListener() {
@@ -372,30 +399,71 @@ class BridgeStore {
372
399
  );
373
400
  }
374
401
  this.ready = false;
402
+ if (this.stateSyncDebounceTimer) {
403
+ clearTimeout(this.stateSyncDebounceTimer);
404
+ this.stateSyncDebounceTimer = null;
405
+ }
406
+ this.pendingStateSync = false;
407
+ this.isInitializing = false;
408
+ if (this.initializationTimer) {
409
+ clearTimeout(this.initializationTimer);
410
+ this.initializationTimer = null;
411
+ }
375
412
  };
376
413
  this.bridge.on("bridge:disconnected", this.disconnectHandler);
377
414
  this.reconnectHandler = () => {
378
415
  if (STORE_ENABLE_LOGS) {
379
416
  console.log(
380
- `BridgeStore[${this.storeName}]: Bridge reconnected, waiting for SW to initialize...`
417
+ `BridgeStore[${this.storeName}]: Bridge reconnected, re-registering listeners and re-initializing...`
381
418
  );
382
419
  }
383
420
  if (this.reconnectDelayTimer) {
384
421
  clearTimeout(this.reconnectDelayTimer);
385
422
  this.reconnectDelayTimer = null;
386
423
  }
387
- this.reconnectDelayTimer = setTimeout(() => {
388
- this.reconnectDelayTimer = null;
424
+ this.reregisterEventListeners();
425
+ this.forceInitialize();
426
+ };
427
+ this.bridge.on("bridge:connected", this.reconnectHandler);
428
+ }
429
+ }
430
+ /**
431
+ * Re-register all event listeners on the bridge
432
+ * Called after reconnection because React StrictMode may have created a new eventListenersRef
433
+ */
434
+ reregisterEventListeners() {
435
+ if (!this.bridge.on) return;
436
+ const eventKey = `store:${this.storeName}:stateChanged`;
437
+ if (this.stateChangedHandler) {
438
+ if (STORE_ENABLE_LOGS) {
439
+ console.log(`BridgeStore[${this.storeName}]: Re-registering listener for '${eventKey}'`);
440
+ }
441
+ this.bridge.on(eventKey, this.stateChangedHandler);
442
+ }
443
+ if (this.disconnectHandler) {
444
+ this.bridge.on("bridge:disconnected", this.disconnectHandler);
445
+ }
446
+ if (this.reconnectHandler) {
447
+ this.bridge.on("bridge:connected", this.reconnectHandler);
448
+ }
449
+ }
450
+ setupVisibilityListener() {
451
+ if (typeof document === "undefined") return;
452
+ this.visibilityHandler = () => {
453
+ if (document.visibilityState === "visible") {
454
+ const hiddenDuration = Date.now() - this.lastVisibleAt;
455
+ if (hiddenDuration > this.staleThresholdMs && this.ready && this.bridge.isConnected) {
389
456
  if (STORE_ENABLE_LOGS) {
390
457
  console.log(
391
- `BridgeStore[${this.storeName}]: Re-initializing after SW startup delay...`
458
+ `BridgeStore[${this.storeName}]: Tab visible after ${Math.round(hiddenDuration / 1e3)}s, refreshing state`
392
459
  );
393
460
  }
394
- this.forceInitialize();
395
- }, 1500);
396
- };
397
- this.bridge.on("bridge:connected", this.reconnectHandler);
398
- }
461
+ this.fetchAndApplyState();
462
+ }
463
+ this.lastVisibleAt = Date.now();
464
+ }
465
+ };
466
+ document.addEventListener("visibilitychange", this.visibilityHandler);
399
467
  }
400
468
  /**
401
469
  * Apply state directly from broadcast payload (no round-trip)
@@ -407,6 +475,11 @@ class BridgeStore {
407
475
  }
408
476
  return;
409
477
  }
478
+ if (STORE_ENABLE_LOGS) {
479
+ console.log(
480
+ `BridgeStore[${this.storeName}]: \u{1F4E6} Applying broadcast state, notifying ${this.listeners?.size ?? 0} listeners`
481
+ );
482
+ }
410
483
  this.previousState = this.currentState;
411
484
  this.currentState = newState;
412
485
  this.notifyListeners();
@@ -437,6 +510,12 @@ class BridgeStore {
437
510
  setupStateSync() {
438
511
  if (this.bridge.on) {
439
512
  this.stateChangedHandler = (payload) => {
513
+ if (STORE_ENABLE_LOGS) {
514
+ console.log(`BridgeStore[${this.storeName}]: \u{1F4E1} Received stateChanged broadcast`, {
515
+ hasPayload: !!payload,
516
+ payloadType: typeof payload
517
+ });
518
+ }
440
519
  if (this.stateSyncDebounceTimer) {
441
520
  clearTimeout(this.stateSyncDebounceTimer);
442
521
  }
@@ -449,7 +528,11 @@ class BridgeStore {
449
528
  }
450
529
  }, this.stateSyncDebounceMs);
451
530
  };
452
- this.bridge.on(`store:${this.storeName}:stateChanged`, this.stateChangedHandler);
531
+ const eventKey = `store:${this.storeName}:stateChanged`;
532
+ if (STORE_ENABLE_LOGS) {
533
+ console.log(`BridgeStore[${this.storeName}]: Registering listener for '${eventKey}'`);
534
+ }
535
+ this.bridge.on(eventKey, this.stateChangedHandler);
453
536
  } else {
454
537
  if (STORE_ENABLE_LOGS) {
455
538
  console.warn(`BridgeStore[${this.storeName}]: Bridge does not support event listening`);
@@ -514,6 +597,7 @@ function createBridgeStore(bridge, initialState, storeName = "default", readyCal
514
597
  if (STORE_ENABLE_LOGS) {
515
598
  console.log(`BridgeStore[${storeName}]: Returning cached instance (singleton)`);
516
599
  }
600
+ cached.updateBridge(bridge);
517
601
  readyCallbacks.forEach((cb) => cached.onReady(cb));
518
602
  return cached;
519
603
  }
package/dist/index.d.ts CHANGED
@@ -78,8 +78,17 @@ declare class BridgeStore<T> implements CentralStore<T> {
78
78
  private stateSyncDebounceTimer;
79
79
  private readonly stateSyncDebounceMs;
80
80
  private reconnectDelayTimer;
81
+ private visibilityHandler;
82
+ private lastVisibleAt;
83
+ private readonly staleThresholdMs;
81
84
  constructor(bridge: BridgeWithEvents, initialState?: T, storeName?: string, readyCallbacks?: Set<() => void>);
82
85
  private setupReconnectListener;
86
+ /**
87
+ * Re-register all event listeners on the bridge
88
+ * Called after reconnection because React StrictMode may have created a new eventListenersRef
89
+ */
90
+ private reregisterEventListeners;
91
+ private setupVisibilityListener;
83
92
  initialize: () => Promise<void>;
84
93
  private stateSyncSequence;
85
94
  private pendingStateSync;
@@ -122,6 +131,12 @@ declare class BridgeStore<T> implements CentralStore<T> {
122
131
  initializationAttempts: number;
123
132
  maxInitializationAttempts: number;
124
133
  };
134
+ /**
135
+ * Update the bridge reference and re-register all event listeners.
136
+ * Called when createBridgeStore receives a new bridge object (e.g., after React remount).
137
+ * This is critical for React StrictMode which causes double-mounting.
138
+ */
139
+ updateBridge: (newBridge: BridgeWithEvents) => void;
125
140
  }
126
141
  declare function createBridgeStore<T>(bridge: BridgeWithEvents, initialState?: T, storeName?: string, readyCallbacks?: Set<() => void>): CentralStore<T>;
127
142
  declare function clearStoreCache(): void;
package/dist/index.es.js CHANGED
@@ -90,6 +90,7 @@ function useStoreReset(store) {
90
90
 
91
91
  const STORE_ENABLE_LOGS = typeof globalThis !== "undefined" && globalThis.__CHROMA_ENABLE_LOGS__ === false ? false : true;
92
92
  class BridgeStore {
93
+ // Consider state stale after 30s hidden
93
94
  constructor(bridge, initialState, storeName = "default", readyCallbacks = /* @__PURE__ */ new Set()) {
94
95
  this.listeners = /* @__PURE__ */ new Set();
95
96
  this.currentState = null;
@@ -107,10 +108,14 @@ class BridgeStore {
107
108
  this.stateChangedHandler = null;
108
109
  // Debounce timer for state sync (optimization for rapid updates)
109
110
  this.stateSyncDebounceTimer = null;
110
- this.stateSyncDebounceMs = 100;
111
- // Increased to 100ms to reduce burst traffic on Windows
111
+ this.stateSyncDebounceMs = 50;
112
+ // Reduced to 50ms for faster reactivity
112
113
  // Reconnect delay timer (to allow SW to bootstrap before re-initializing)
113
114
  this.reconnectDelayTimer = null;
115
+ // Visibility change handling - refresh state when tab becomes visible
116
+ this.visibilityHandler = null;
117
+ this.lastVisibleAt = Date.now();
118
+ this.staleThresholdMs = 3e4;
114
119
  this.initialize = async () => {
115
120
  if (this.isInitializing) {
116
121
  return;
@@ -240,6 +245,10 @@ class BridgeStore {
240
245
  this.stateChangedHandler = null;
241
246
  }
242
247
  }
248
+ if (this.visibilityHandler && typeof document !== "undefined") {
249
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
250
+ this.visibilityHandler = null;
251
+ }
243
252
  if (this.listeners) {
244
253
  this.listeners.clear();
245
254
  }
@@ -333,6 +342,23 @@ class BridgeStore {
333
342
  maxInitializationAttempts: this.maxInitializationAttempts
334
343
  };
335
344
  };
345
+ /**
346
+ * Update the bridge reference and re-register all event listeners.
347
+ * Called when createBridgeStore receives a new bridge object (e.g., after React remount).
348
+ * This is critical for React StrictMode which causes double-mounting.
349
+ */
350
+ this.updateBridge = (newBridge) => {
351
+ if (this.bridge === newBridge) {
352
+ return;
353
+ }
354
+ if (STORE_ENABLE_LOGS) {
355
+ console.log(
356
+ `BridgeStore[${this.storeName}]: Updating bridge reference and re-registering listeners`
357
+ );
358
+ }
359
+ this.bridge = newBridge;
360
+ this.reregisterEventListeners();
361
+ };
336
362
  this.bridge = bridge;
337
363
  this.currentState = initialState || null;
338
364
  this.previousState = initialState || null;
@@ -341,6 +367,7 @@ class BridgeStore {
341
367
  this.readyCallbacks = readyCallbacks;
342
368
  this.setupStateSync();
343
369
  this.setupReconnectListener();
370
+ this.setupVisibilityListener();
344
371
  this.initialize();
345
372
  }
346
373
  setupReconnectListener() {
@@ -352,30 +379,71 @@ class BridgeStore {
352
379
  );
353
380
  }
354
381
  this.ready = false;
382
+ if (this.stateSyncDebounceTimer) {
383
+ clearTimeout(this.stateSyncDebounceTimer);
384
+ this.stateSyncDebounceTimer = null;
385
+ }
386
+ this.pendingStateSync = false;
387
+ this.isInitializing = false;
388
+ if (this.initializationTimer) {
389
+ clearTimeout(this.initializationTimer);
390
+ this.initializationTimer = null;
391
+ }
355
392
  };
356
393
  this.bridge.on("bridge:disconnected", this.disconnectHandler);
357
394
  this.reconnectHandler = () => {
358
395
  if (STORE_ENABLE_LOGS) {
359
396
  console.log(
360
- `BridgeStore[${this.storeName}]: Bridge reconnected, waiting for SW to initialize...`
397
+ `BridgeStore[${this.storeName}]: Bridge reconnected, re-registering listeners and re-initializing...`
361
398
  );
362
399
  }
363
400
  if (this.reconnectDelayTimer) {
364
401
  clearTimeout(this.reconnectDelayTimer);
365
402
  this.reconnectDelayTimer = null;
366
403
  }
367
- this.reconnectDelayTimer = setTimeout(() => {
368
- this.reconnectDelayTimer = null;
404
+ this.reregisterEventListeners();
405
+ this.forceInitialize();
406
+ };
407
+ this.bridge.on("bridge:connected", this.reconnectHandler);
408
+ }
409
+ }
410
+ /**
411
+ * Re-register all event listeners on the bridge
412
+ * Called after reconnection because React StrictMode may have created a new eventListenersRef
413
+ */
414
+ reregisterEventListeners() {
415
+ if (!this.bridge.on) return;
416
+ const eventKey = `store:${this.storeName}:stateChanged`;
417
+ if (this.stateChangedHandler) {
418
+ if (STORE_ENABLE_LOGS) {
419
+ console.log(`BridgeStore[${this.storeName}]: Re-registering listener for '${eventKey}'`);
420
+ }
421
+ this.bridge.on(eventKey, this.stateChangedHandler);
422
+ }
423
+ if (this.disconnectHandler) {
424
+ this.bridge.on("bridge:disconnected", this.disconnectHandler);
425
+ }
426
+ if (this.reconnectHandler) {
427
+ this.bridge.on("bridge:connected", this.reconnectHandler);
428
+ }
429
+ }
430
+ setupVisibilityListener() {
431
+ if (typeof document === "undefined") return;
432
+ this.visibilityHandler = () => {
433
+ if (document.visibilityState === "visible") {
434
+ const hiddenDuration = Date.now() - this.lastVisibleAt;
435
+ if (hiddenDuration > this.staleThresholdMs && this.ready && this.bridge.isConnected) {
369
436
  if (STORE_ENABLE_LOGS) {
370
437
  console.log(
371
- `BridgeStore[${this.storeName}]: Re-initializing after SW startup delay...`
438
+ `BridgeStore[${this.storeName}]: Tab visible after ${Math.round(hiddenDuration / 1e3)}s, refreshing state`
372
439
  );
373
440
  }
374
- this.forceInitialize();
375
- }, 1500);
376
- };
377
- this.bridge.on("bridge:connected", this.reconnectHandler);
378
- }
441
+ this.fetchAndApplyState();
442
+ }
443
+ this.lastVisibleAt = Date.now();
444
+ }
445
+ };
446
+ document.addEventListener("visibilitychange", this.visibilityHandler);
379
447
  }
380
448
  /**
381
449
  * Apply state directly from broadcast payload (no round-trip)
@@ -387,6 +455,11 @@ class BridgeStore {
387
455
  }
388
456
  return;
389
457
  }
458
+ if (STORE_ENABLE_LOGS) {
459
+ console.log(
460
+ `BridgeStore[${this.storeName}]: \u{1F4E6} Applying broadcast state, notifying ${this.listeners?.size ?? 0} listeners`
461
+ );
462
+ }
390
463
  this.previousState = this.currentState;
391
464
  this.currentState = newState;
392
465
  this.notifyListeners();
@@ -417,6 +490,12 @@ class BridgeStore {
417
490
  setupStateSync() {
418
491
  if (this.bridge.on) {
419
492
  this.stateChangedHandler = (payload) => {
493
+ if (STORE_ENABLE_LOGS) {
494
+ console.log(`BridgeStore[${this.storeName}]: \u{1F4E1} Received stateChanged broadcast`, {
495
+ hasPayload: !!payload,
496
+ payloadType: typeof payload
497
+ });
498
+ }
420
499
  if (this.stateSyncDebounceTimer) {
421
500
  clearTimeout(this.stateSyncDebounceTimer);
422
501
  }
@@ -429,7 +508,11 @@ class BridgeStore {
429
508
  }
430
509
  }, this.stateSyncDebounceMs);
431
510
  };
432
- this.bridge.on(`store:${this.storeName}:stateChanged`, this.stateChangedHandler);
511
+ const eventKey = `store:${this.storeName}:stateChanged`;
512
+ if (STORE_ENABLE_LOGS) {
513
+ console.log(`BridgeStore[${this.storeName}]: Registering listener for '${eventKey}'`);
514
+ }
515
+ this.bridge.on(eventKey, this.stateChangedHandler);
433
516
  } else {
434
517
  if (STORE_ENABLE_LOGS) {
435
518
  console.warn(`BridgeStore[${this.storeName}]: Bridge does not support event listening`);
@@ -494,6 +577,7 @@ function createBridgeStore(bridge, initialState, storeName = "default", readyCal
494
577
  if (STORE_ENABLE_LOGS) {
495
578
  console.log(`BridgeStore[${storeName}]: Returning cached instance (singleton)`);
496
579
  }
580
+ cached.updateBridge(bridge);
497
581
  readyCallbacks.forEach((cb) => cached.onReady(cb));
498
582
  return cached;
499
583
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chromahq/store",
3
- "version": "1.0.45",
3
+ "version": "1.0.47",
4
4
  "description": "Centralized, persistent store for Chrome extensions using zustand, accessible from service workers and React, with chrome.storage.local persistence.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs.js",