@chromahq/store 1.0.20 → 1.0.21

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
+ // ~1 frame at 60fps
113
114
  constructor(bridge, initialState, storeName = "default", readyCallbacks = /* @__PURE__ */ new Set()) {
114
115
  this.listeners = /* @__PURE__ */ new Set();
115
116
  this.currentState = null;
@@ -121,6 +122,12 @@ class BridgeStore {
121
122
  this.maxInitializationAttempts = 10;
122
123
  this.initializationTimer = null;
123
124
  this.isInitializing = false;
125
+ // Store handler references for cleanup (prevents memory leaks)
126
+ this.reconnectHandler = null;
127
+ this.stateChangedHandler = null;
128
+ // Debounce timer for state sync (optimization for rapid updates)
129
+ this.stateSyncDebounceTimer = null;
130
+ this.stateSyncDebounceMs = 16;
124
131
  this.initialize = async () => {
125
132
  if (this.isInitializing) {
126
133
  return;
@@ -228,6 +235,20 @@ class BridgeStore {
228
235
  clearTimeout(this.initializationTimer);
229
236
  this.initializationTimer = null;
230
237
  }
238
+ if (this.stateSyncDebounceTimer) {
239
+ clearTimeout(this.stateSyncDebounceTimer);
240
+ this.stateSyncDebounceTimer = null;
241
+ }
242
+ if (this.bridge.off) {
243
+ if (this.reconnectHandler) {
244
+ this.bridge.off("bridge:connected", this.reconnectHandler);
245
+ this.reconnectHandler = null;
246
+ }
247
+ if (this.stateChangedHandler) {
248
+ this.bridge.off(`store:${this.storeName}:stateChanged`, this.stateChangedHandler);
249
+ this.stateChangedHandler = null;
250
+ }
251
+ }
231
252
  if (this.listeners) {
232
253
  this.listeners.clear();
233
254
  }
@@ -333,36 +354,47 @@ class BridgeStore {
333
354
  }
334
355
  setupReconnectListener() {
335
356
  if (this.bridge.on) {
336
- this.bridge.on("bridge:connected", () => {
357
+ this.reconnectHandler = () => {
337
358
  if (STORE_ENABLE_LOGS) {
338
359
  console.log(`BridgeStore[${this.storeName}]: Bridge reconnected, re-initializing...`);
339
360
  }
340
361
  this.forceInitialize();
341
- });
362
+ };
363
+ this.bridge.on("bridge:connected", this.reconnectHandler);
364
+ }
365
+ }
366
+ fetchAndApplyState() {
367
+ if (this.pendingStateSync) {
368
+ return;
342
369
  }
370
+ this.pendingStateSync = true;
371
+ const currentSequence = ++this.stateSyncSequence;
372
+ this.bridge.send(`store:${this.storeName}:getState`).then((newState) => {
373
+ if (currentSequence === this.stateSyncSequence) {
374
+ this.previousState = this.currentState;
375
+ this.currentState = newState;
376
+ this.notifyListeners();
377
+ }
378
+ }).catch((error) => {
379
+ if (STORE_ENABLE_LOGS) {
380
+ console.error(`BridgeStore[${this.storeName}]: Failed to sync state:`, error);
381
+ }
382
+ }).finally(() => {
383
+ this.pendingStateSync = false;
384
+ });
343
385
  }
344
386
  setupStateSync() {
345
387
  if (this.bridge.on) {
346
- this.bridge.on(`store:${this.storeName}:stateChanged`, () => {
347
- if (this.pendingStateSync) {
348
- return;
388
+ this.stateChangedHandler = () => {
389
+ if (this.stateSyncDebounceTimer) {
390
+ clearTimeout(this.stateSyncDebounceTimer);
349
391
  }
350
- this.pendingStateSync = true;
351
- const currentSequence = ++this.stateSyncSequence;
352
- this.bridge.send(`store:${this.storeName}:getState`).then((newState) => {
353
- if (currentSequence === this.stateSyncSequence) {
354
- this.previousState = this.currentState;
355
- this.currentState = newState;
356
- this.notifyListeners();
357
- }
358
- }).catch((error) => {
359
- if (STORE_ENABLE_LOGS) {
360
- console.error(`BridgeStore[${this.storeName}]: Failed to sync state:`, error);
361
- }
362
- }).finally(() => {
363
- this.pendingStateSync = false;
364
- });
365
- });
392
+ this.stateSyncDebounceTimer = setTimeout(() => {
393
+ this.stateSyncDebounceTimer = null;
394
+ this.fetchAndApplyState();
395
+ }, this.stateSyncDebounceMs);
396
+ };
397
+ this.bridge.on(`store:${this.storeName}:stateChanged`, this.stateChangedHandler);
366
398
  } else {
367
399
  if (STORE_ENABLE_LOGS) {
368
400
  console.warn(`BridgeStore[${this.storeName}]: Bridge does not support event listening`);
@@ -378,9 +410,7 @@ class BridgeStore {
378
410
  }
379
411
  return;
380
412
  }
381
- if (globalThis.__CHROMA_ENABLE_LOGS__ !== false) {
382
- console.warn("BridgeStore: Cannot execute function update, state not initialized");
383
- }
413
+ actualUpdate = partial(this.currentState);
384
414
  } else {
385
415
  actualUpdate = partial;
386
416
  }
@@ -390,11 +420,6 @@ class BridgeStore {
390
420
  `BridgeStore[${this.storeName}]: Bridge disconnected, state update queued locally only`
391
421
  );
392
422
  }
393
- if (globalThis.__CHROMA_ENABLE_LOGS__ !== false) {
394
- console.warn(
395
- `BridgeStore[${this.storeName}]: Bridge disconnected, state update queued locally only`
396
- );
397
- }
398
423
  }
399
424
  const stateBeforeUpdate = this.currentState ? { ...this.currentState } : null;
400
425
  this.applyOptimisticUpdate(actualUpdate, replace);
package/dist/index.d.ts CHANGED
@@ -72,11 +72,16 @@ declare class BridgeStore<T> implements CentralStore<T> {
72
72
  private readonly maxInitializationAttempts;
73
73
  private initializationTimer;
74
74
  private isInitializing;
75
+ private reconnectHandler;
76
+ private stateChangedHandler;
77
+ private stateSyncDebounceTimer;
78
+ private readonly stateSyncDebounceMs;
75
79
  constructor(bridge: BridgeWithEvents, initialState?: T, storeName?: string, readyCallbacks?: Set<() => void>);
76
80
  private setupReconnectListener;
77
81
  initialize: () => Promise<void>;
78
82
  private stateSyncSequence;
79
83
  private pendingStateSync;
84
+ private fetchAndApplyState;
80
85
  private setupStateSync;
81
86
  private notifyListeners;
82
87
  getState: () => T;
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
+ // ~1 frame at 60fps
93
94
  constructor(bridge, initialState, storeName = "default", readyCallbacks = /* @__PURE__ */ new Set()) {
94
95
  this.listeners = /* @__PURE__ */ new Set();
95
96
  this.currentState = null;
@@ -101,6 +102,12 @@ class BridgeStore {
101
102
  this.maxInitializationAttempts = 10;
102
103
  this.initializationTimer = null;
103
104
  this.isInitializing = false;
105
+ // Store handler references for cleanup (prevents memory leaks)
106
+ this.reconnectHandler = null;
107
+ this.stateChangedHandler = null;
108
+ // Debounce timer for state sync (optimization for rapid updates)
109
+ this.stateSyncDebounceTimer = null;
110
+ this.stateSyncDebounceMs = 16;
104
111
  this.initialize = async () => {
105
112
  if (this.isInitializing) {
106
113
  return;
@@ -208,6 +215,20 @@ class BridgeStore {
208
215
  clearTimeout(this.initializationTimer);
209
216
  this.initializationTimer = null;
210
217
  }
218
+ if (this.stateSyncDebounceTimer) {
219
+ clearTimeout(this.stateSyncDebounceTimer);
220
+ this.stateSyncDebounceTimer = null;
221
+ }
222
+ if (this.bridge.off) {
223
+ if (this.reconnectHandler) {
224
+ this.bridge.off("bridge:connected", this.reconnectHandler);
225
+ this.reconnectHandler = null;
226
+ }
227
+ if (this.stateChangedHandler) {
228
+ this.bridge.off(`store:${this.storeName}:stateChanged`, this.stateChangedHandler);
229
+ this.stateChangedHandler = null;
230
+ }
231
+ }
211
232
  if (this.listeners) {
212
233
  this.listeners.clear();
213
234
  }
@@ -313,36 +334,47 @@ class BridgeStore {
313
334
  }
314
335
  setupReconnectListener() {
315
336
  if (this.bridge.on) {
316
- this.bridge.on("bridge:connected", () => {
337
+ this.reconnectHandler = () => {
317
338
  if (STORE_ENABLE_LOGS) {
318
339
  console.log(`BridgeStore[${this.storeName}]: Bridge reconnected, re-initializing...`);
319
340
  }
320
341
  this.forceInitialize();
321
- });
342
+ };
343
+ this.bridge.on("bridge:connected", this.reconnectHandler);
344
+ }
345
+ }
346
+ fetchAndApplyState() {
347
+ if (this.pendingStateSync) {
348
+ return;
322
349
  }
350
+ this.pendingStateSync = true;
351
+ const currentSequence = ++this.stateSyncSequence;
352
+ this.bridge.send(`store:${this.storeName}:getState`).then((newState) => {
353
+ if (currentSequence === this.stateSyncSequence) {
354
+ this.previousState = this.currentState;
355
+ this.currentState = newState;
356
+ this.notifyListeners();
357
+ }
358
+ }).catch((error) => {
359
+ if (STORE_ENABLE_LOGS) {
360
+ console.error(`BridgeStore[${this.storeName}]: Failed to sync state:`, error);
361
+ }
362
+ }).finally(() => {
363
+ this.pendingStateSync = false;
364
+ });
323
365
  }
324
366
  setupStateSync() {
325
367
  if (this.bridge.on) {
326
- this.bridge.on(`store:${this.storeName}:stateChanged`, () => {
327
- if (this.pendingStateSync) {
328
- return;
368
+ this.stateChangedHandler = () => {
369
+ if (this.stateSyncDebounceTimer) {
370
+ clearTimeout(this.stateSyncDebounceTimer);
329
371
  }
330
- this.pendingStateSync = true;
331
- const currentSequence = ++this.stateSyncSequence;
332
- this.bridge.send(`store:${this.storeName}:getState`).then((newState) => {
333
- if (currentSequence === this.stateSyncSequence) {
334
- this.previousState = this.currentState;
335
- this.currentState = newState;
336
- this.notifyListeners();
337
- }
338
- }).catch((error) => {
339
- if (STORE_ENABLE_LOGS) {
340
- console.error(`BridgeStore[${this.storeName}]: Failed to sync state:`, error);
341
- }
342
- }).finally(() => {
343
- this.pendingStateSync = false;
344
- });
345
- });
372
+ this.stateSyncDebounceTimer = setTimeout(() => {
373
+ this.stateSyncDebounceTimer = null;
374
+ this.fetchAndApplyState();
375
+ }, this.stateSyncDebounceMs);
376
+ };
377
+ this.bridge.on(`store:${this.storeName}:stateChanged`, this.stateChangedHandler);
346
378
  } else {
347
379
  if (STORE_ENABLE_LOGS) {
348
380
  console.warn(`BridgeStore[${this.storeName}]: Bridge does not support event listening`);
@@ -358,9 +390,7 @@ class BridgeStore {
358
390
  }
359
391
  return;
360
392
  }
361
- if (globalThis.__CHROMA_ENABLE_LOGS__ !== false) {
362
- console.warn("BridgeStore: Cannot execute function update, state not initialized");
363
- }
393
+ actualUpdate = partial(this.currentState);
364
394
  } else {
365
395
  actualUpdate = partial;
366
396
  }
@@ -370,11 +400,6 @@ class BridgeStore {
370
400
  `BridgeStore[${this.storeName}]: Bridge disconnected, state update queued locally only`
371
401
  );
372
402
  }
373
- if (globalThis.__CHROMA_ENABLE_LOGS__ !== false) {
374
- console.warn(
375
- `BridgeStore[${this.storeName}]: Bridge disconnected, state update queued locally only`
376
- );
377
- }
378
403
  }
379
404
  const stateBeforeUpdate = this.currentState ? { ...this.currentState } : null;
380
405
  this.applyOptimisticUpdate(actualUpdate, replace);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chromahq/store",
3
- "version": "1.0.20",
3
+ "version": "1.0.21",
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",