@asaidimu/utils-persistence 5.0.2 → 6.1.0

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 CHANGED
@@ -10,20 +10,20 @@
10
10
 
11
11
  ## 📖 Table of Contents
12
12
 
13
- * [Overview & Features](#overview--features)
14
- * [Installation & Setup](#installation--setup)
15
- * [Usage Documentation](#usage-documentation)
16
- * [Core Interface: `SimplePersistence`](#core-interface-simplepersistence)
17
- * [The Power of Adapters](#the-power-of-adapters)
18
- * [Usage Guidelines](#usage-guidelines-for-those-consuming-the-simplepersistence-interface)
19
- * [When to Use and When to Avoid `SimplePersistence`](#when-to-use-and-when-to-avoid-simplepersistence)
20
- * [Web Storage Persistence (`WebStoragePersistence`)](#web-storage-persistence-webstoragepersistence)
21
- * [IndexedDB Persistence (`IndexedDBPersistence`)](#indexeddb-persistence-indexeddbpersistence)
22
- * [Ephemeral Persistence (`EphemeralPersistence`)](#ephemeral-persistence-ephemeralpersistence)
23
- * [Common Use Cases](#common-use-cases)
24
- * [Project Architecture](#project-architecture)
25
- * [Development & Contributing](#development--contributing)
26
- * [Additional Information](#additional-information)
13
+ - [Overview & Features](#overview--features)
14
+ - [Installation & Setup](#installation--setup)
15
+ - [Usage Documentation](#usage-documentation)
16
+ - [Core Interface: `SimplePersistence`](#core-interface-simplepersistence)
17
+ - [The Power of Adapters](#the-power-of-adapters)
18
+ - [Usage Guidelines](#usage-guidelines-for-those-consuming-the-simplepersistence-interface)
19
+ - [When to Use and When to Avoid `SimplePersistence`](#when-to-use-and-when-to-avoid-simplepersistence)
20
+ - [Web Storage Persistence (`WebStoragePersistence`)](#web-storage-persistence-webstoragepersistence)
21
+ - [IndexedDB Persistence (`IndexedDBPersistence`)](#indexeddb-persistence-indexeddbpersistence)
22
+ - [Ephemeral Persistence (`EphemeralPersistence`)](#ephemeral-persistence-ephemeralpersistence)
23
+ - [Common Use Cases](#common-use-cases)
24
+ - [Project Architecture](#project-architecture)
25
+ - [Development & Contributing](#development--contributing)
26
+ - [Additional Information](#additional-information)
27
27
 
28
28
  ---
29
29
 
@@ -35,18 +35,18 @@ This library is ideal for single-page applications (SPAs) that require robust st
35
35
 
36
36
  ### 🚀 Key Features
37
37
 
38
- * **Unified `SimplePersistence<T>` API**: A consistent interface for all storage adapters, simplifying integration and making switching storage types straightforward.
39
- * **Flexible Storage Options**:
40
- * `WebStoragePersistence`: Supports both `localStorage` (default) and `sessionStorage` for simple key-value storage. Ideal for user preferences or small, temporary data.
41
- * `IndexedDBPersistence`: Provides robust, high-capacity, and structured data storage for more complex needs like offline caching or large datasets. Leverages `@asaidimu/indexed` for simplified IndexedDB interactions.
42
- * `EphemeralPersistence`: Offers an in-memory store with cross-tab synchronization using a Last Write Wins (LWW) strategy. Ideal for transient, session-specific shared state that does not need to persist across page reloads.
43
- * **Automatic Cross-Instance Synchronization**: Real-time updates across multiple browser tabs, windows, or even components within the same tab. This is achieved by leveraging native `StorageEvent` (for `localStorage`) and `BroadcastChannel`-based event buses (from `@asaidimu/events`) for `WebStoragePersistence`, `IndexedDBPersistence`, and `EphemeralPersistence`.
44
- * **Data Versioning and Migration**: Each store can be configured with a `version` and an optional `onUpgrade` handler to manage data schema changes gracefully across application updates.
45
- * **Type-Safe**: Fully written in TypeScript, providing strong typing, compile-time checks, and autocompletion for a better developer experience.
46
- * **Lightweight & Minimal Dependencies**: Designed to be small and efficient, relying on a few focused internal utilities (`@asaidimu/events`, `@asaidimu/indexed`, `@asaidimu/query`).
47
- * **Robust Error Handling**: Includes internal error handling for common storage operations, providing clearer debugging messages when issues arise.
48
- * **Instance-Specific Subscriptions**: The `subscribe` method intelligently uses a unique `instanceId` to listen for changes from *other* instances of the application (e.g., other tabs or different components sharing the same store), deliberately preventing self-triggered updates and enabling efficient state reconciliation without infinite loops.
49
- * **Asynchronous Operations**: `IndexedDBPersistence` methods return Promises, allowing for non-blocking UI and efficient handling of large data operations. `WebStoragePersistence` and `EphemeralPersistence` are synchronous where possible, but their cross-tab synchronization aspects are asynchronous.
38
+ - **Unified `SimplePersistence<T>` API**: A consistent interface for all storage adapters, simplifying integration and making switching storage types straightforward.
39
+ - **Flexible Storage Options**:
40
+ - `WebStoragePersistence`: Supports both `localStorage` (default) and `sessionStorage` for simple key-value storage. Ideal for user preferences or small, temporary data.
41
+ - `IndexedDBPersistence`: Provides robust, high-capacity, and structured data storage for more complex needs like offline caching or large datasets. Leverages `@asaidimu/indexed` for simplified IndexedDB interactions.
42
+ - `EphemeralPersistence`: Offers an in-memory store with cross-tab synchronization using a Last Write Wins (LWW) strategy. Ideal for transient, session-specific shared state that does not need to persist across page reloads.
43
+ - **Automatic Cross-Instance Synchronization**: Real-time updates across multiple browser tabs, windows, or even components within the same tab. This is achieved by leveraging native `StorageEvent` (for `localStorage`) and `BroadcastChannel`-based event buses (from `@asaidimu/events`) for `WebStoragePersistence`, `IndexedDBPersistence`, and `EphemeralPersistence`.
44
+ - **Data Versioning and Migration**: Each store can be configured with a `version` and an optional `onUpgrade` handler to manage data schema changes gracefully across application updates.
45
+ - **Type-Safe**: Fully written in TypeScript, providing strong typing, compile-time checks, and autocompletion for a better developer experience.
46
+ - **Lightweight & Minimal Dependencies**: Designed to be small and efficient, relying on a few focused internal utilities (`@asaidimu/events`, `@asaidimu/indexed`, `@asaidimu/query`).
47
+ - **Robust Error Handling**: Includes internal error handling for common storage operations, providing clearer debugging messages when issues arise.
48
+ - **Instance-Specific Subscriptions**: The `subscribe` method intelligently uses a unique `instanceId` to listen for changes from _other_ instances of the application (e.g., other tabs or different components sharing the same store), deliberately preventing self-triggered updates and enabling efficient state reconciliation without infinite loops.
49
+ - **Asynchronous Operations**: `IndexedDBPersistence` methods return Promises, allowing for non-blocking UI and efficient handling of large data operations. `WebStoragePersistence` and `EphemeralPersistence` are synchronous where possible, but their cross-tab synchronization aspects are asynchronous.
50
50
 
51
51
  ---
52
52
 
@@ -56,9 +56,9 @@ This library is ideal for single-page applications (SPAs) that require robust st
56
56
 
57
57
  To use `@asaidimu/utils-persistence`, you need:
58
58
 
59
- * Node.js (LTS version recommended)
60
- * npm, Yarn, or Bun package manager
61
- * A modern web browser environment (e.g., Chrome, Firefox, Safari, Edge) that supports `localStorage`, `sessionStorage`, `IndexedDB`, and `BroadcastChannel`.
59
+ - Node.js (LTS version recommended)
60
+ - npm, Yarn, or Bun package manager
61
+ - A modern web browser environment (e.g., Chrome, Firefox, Safari, Edge) that supports `localStorage`, `sessionStorage`, `IndexedDB`, and `BroadcastChannel`.
62
62
 
63
63
  ### Installation Steps
64
64
 
@@ -92,27 +92,29 @@ This library does not require global configuration. All settings are passed dire
92
92
  ```typescript
93
93
  // The base configuration for any persistence store
94
94
  export interface StoreConfig<T> {
95
- /**
96
- * The semantic version string (e.g., "1.0.0") of the data schema or application.
97
- * Used for version control and data migrations.
98
- */
99
- version: string;
95
+ /**
96
+ * The semantic version string (e.g., "1.0.0") of the data schema or application.
97
+ * Used for version control and data migrations.
98
+ */
99
+ version: string;
100
100
 
101
- /**
102
- * A unique application identifier (e.g., "chat-app").
103
- * Prevents collisions between different apps sharing the same persistence backend.
104
- */
105
- app: string;
101
+ /**
102
+ * A unique application identifier (e.g., "chat-app").
103
+ * Prevents collisions between different apps sharing the same persistence backend.
104
+ */
105
+ app: string;
106
106
 
107
- /**
108
- * Optional handler for upgrading persisted state across versions.
109
- * Called when the persisted state’s `version` does not match the config's `version`.
110
- * @param state The existing state object { data: T | null; version: string; app: string }.
111
- * @returns A new state object { state: T | null; version: string } with the migrated data.
112
- */
113
- onUpgrade?: (
114
- state: { data: T | null; version: string; app: string }
115
- ) => Promise<{ state: T | null; version: string }>;
107
+ /**
108
+ * Optional handler for upgrading persisted state across versions.
109
+ * Called when the persisted state’s `version` does not match the config's `version`.
110
+ * @param state The existing state object { data: T | null; version: string; app: string }.
111
+ * @returns A new state object { state: T | null; version: string } with the migrated data.
112
+ */
113
+ onUpgrade?: (state: {
114
+ data: T | null;
115
+ version: string;
116
+ app: string;
117
+ }) => Promise<{ state: T | null; version: string }>;
116
118
  }
117
119
  ```
118
120
 
@@ -121,9 +123,13 @@ export interface StoreConfig<T> {
121
123
  You can quickly verify the installation by attempting to import one of the classes:
122
124
 
123
125
  ```typescript
124
- import { WebStoragePersistence, IndexedDBPersistence, EphemeralPersistence } from '@asaidimu/utils-persistence';
126
+ import {
127
+ WebStoragePersistence,
128
+ IndexedDBPersistence,
129
+ EphemeralPersistence,
130
+ } from "@asaidimu/utils-persistence";
125
131
 
126
- console.log('Persistence modules loaded successfully!');
132
+ console.log("Persistence modules loaded successfully!");
127
133
 
128
134
  // You can now create instances:
129
135
  // const myLocalStorageStore = new WebStoragePersistence<{ appState: string }>(/* config */);
@@ -161,7 +167,7 @@ export interface SimplePersistence<T> {
161
167
  * @returns The retrieved state of type `T`, or `null` if no data is found or if an error occurs during retrieval/parsing.
162
168
  * For asynchronous implementations, this returns a `Promise<T | null>`.
163
169
  */
164
- get(): (T | null) | (Promise<T | null>);
170
+ get(): (T | null) | Promise<T | null>;
165
171
 
166
172
  /**
167
173
  * Subscribes to changes in the global persisted data that originate from *other* instances of your application (e.g., other tabs or independent components using the same persistence layer).
@@ -179,27 +185,29 @@ export interface SimplePersistence<T> {
179
185
  */
180
186
  clear(): boolean | Promise<boolean>;
181
187
 
182
- /**
183
- * Returns metadata about the persistence layer.
184
- *
185
- * This is useful for distinguishing between multiple apps running on the same host
186
- * (e.g., several apps served at `localhost:3000` that share the same storage key).
187
- *
188
- * @returns An object containing:
189
- * - `version`: The semantic version string of the persistence schema or application.
190
- * - `id`: A unique identifier for the application using this persistence instance.
191
- */
192
- stats(): { version: string; id: string };
188
+ /**
189
+ * Returns metadata about the persistence layer.
190
+ *
191
+ * This is useful for distinguishing between multiple apps running on the same host
192
+ * (e.g., several apps served at `localhost:3000` that share the same storage key).
193
+ *
194
+ * @returns An object containing:
195
+ * - `version`: The semantic version string of the persistence schema or application.
196
+ * - `id`: A unique identifier for the application using this persistence instance.
197
+ */
198
+ stats(): { version: string; id: string };
193
199
  }
194
200
  ```
195
201
 
196
202
  ### The Power of Adapters
203
+
197
204
  The adapter pattern used by `SimplePersistence<T>` is a key strength, enabling seamless swapping of persistence backends without altering application logic. This decoupling offers several advantages:
198
- - **Interchangeability**: Switch between storage mechanisms (e.g., `localStorage`, `IndexedDB`, an in-memory store, or a remote API) by simply changing the adapter, keeping the interface consistent.
199
- - **Scalability**: Start with a lightweight adapter (e.g., `EphemeralPersistence` for prototyping) and transition to a robust solution (e.g., `IndexedDBPersistence` or a server-based database) as needs evolve, with minimal changes to the consuming code.
200
- - **Extensibility**: Easily create custom adapters for new storage technologies or environments (e.g., file systems, serverless functions) while adhering to the same interface.
201
- - **Environment-Agnostic**: Use the same interface in browser, server, or hybrid applications, supporting diverse use cases like offline-first apps or cross-tab synchronization.
202
- - **Testing Simplicity**: Implement mock adapters for testing, isolating persistence logic without touching real storage.
205
+
206
+ - **Interchangeability**: Switch between storage mechanisms (e.g., `localStorage`, `IndexedDB`, an in-memory store, or a remote API) by simply changing the adapter, keeping the interface consistent.
207
+ - **Scalability**: Start with a lightweight adapter (e.g., `EphemeralPersistence` for prototyping) and transition to a robust solution (e.g., `IndexedDBPersistence` or a server-based database) as needs evolve, with minimal changes to the consuming code.
208
+ - **Extensibility**: Easily create custom adapters for new storage technologies or environments (e.g., file systems, serverless functions) while adhering to the same interface.
209
+ - **Environment-Agnostic**: Use the same interface in browser, server, or hybrid applications, supporting diverse use cases like offline-first apps or cross-tab synchronization.
210
+ - **Testing Simplicity**: Implement mock adapters for testing, isolating persistence logic without touching real storage.
203
211
 
204
212
  This flexibility ensures that the persistence layer can adapt to varying requirements, from small-scale prototypes to production-grade systems, while maintaining a consistent and predictable API.
205
213
 
@@ -211,16 +219,17 @@ This section provides practical advice for consuming the `SimplePersistence<T>`
211
219
 
212
220
  This is the most crucial point to grasp:
213
221
 
214
- * **`id` does NOT refer to an ID *within* your data type `T`**. If your `T` represents a complex object (e.g., `{ users: User[]; settings: AppSettings; }`), any IDs for `User` objects or specific settings should be managed *inside* your `T` type.
215
- * **`id` refers to the unique identifier of the *consumer instance* that is interacting with the persistence layer.**
216
- * Think of a "consumer instance" as a specific browser tab, a running web worker, a distinct application component, or any other isolated context that uses `SimplePersistence`.
217
- * This `id` should be a **UUID (Universally Unique Identifier)**, generated **once** when that consumer instance initializes.
222
+ - **`id` does NOT refer to an ID _within_ your data type `T`**. If your `T` represents a complex object (e.g., `{ users: User[]; settings: AppSettings; }`), any IDs for `User` objects or specific settings should be managed _inside_ your `T` type.
223
+ - **`id` refers to the unique identifier of the _consumer instance_ that is interacting with the persistence layer.**
224
+ - Think of a "consumer instance" as a specific browser tab, a running web worker, a distinct application component, or any other isolated context that uses `SimplePersistence`.
225
+ - This `id` should be a **UUID (Universally Unique Identifier)**, generated **once** when that consumer instance initializes.
218
226
 
219
227
  **Why is this `id` essential?**
220
228
 
221
229
  It enables robust **multi-instance synchronization**. When multiple instances (e.g., different browser tabs) share the same underlying storage, the `id` allows the persistence layer to:
222
- * **Identify the source of a change:** When `set(id, state)` is called, the layer knows *which* instance initiated the save.
223
- * **Filter notifications:** The `subscribe(id, callback)` method uses this `id` to ensure that a subscribing instance is notified of changes *only* if they originated from *another* instance, preventing unnecessary self-triggered updates.
230
+
231
+ - **Identify the source of a change:** When `set(id, state)` is called, the layer knows _which_ instance initiated the save.
232
+ - **Filter notifications:** The `subscribe(id, callback)` method uses this `id` to ensure that a subscribing instance is notified of changes _only_ if they originated from _another_ instance, preventing unnecessary self-triggered updates.
224
233
 
225
234
  #### 2. Practical Examples for Consuming `SimplePersistence`
226
235
 
@@ -229,8 +238,12 @@ It enables robust **multi-instance synchronization**. When multiple instances (e
229
238
  Generate a unique UUID for your consumer instance at its very beginning (e.g., when your main application component mounts or your service initializes).
230
239
 
231
240
  ```typescript
232
- import { v4 as uuidv4 } from 'uuid'; // Requires 'uuid' library to be installed: `bun add uuid`
233
- import { SimplePersistence, WebStoragePersistence, StoreConfig } from '@asaidimu/utils-persistence';
241
+ import { v4 as uuidv4 } from "uuid"; // Requires 'uuid' library to be installed: `bun add uuid`
242
+ import {
243
+ SimplePersistence,
244
+ WebStoragePersistence,
245
+ StoreConfig,
246
+ } from "@asaidimu/utils-persistence";
234
247
 
235
248
  interface MyAppState {
236
249
  data: string;
@@ -242,7 +255,11 @@ class MyAppComponent {
242
255
  private instanceId: string;
243
256
  private persistence: SimplePersistence<MyAppState>;
244
257
  private unsubscribe: (() => void) | null = null;
245
- private appState: MyAppState = { data: 'initial', count: 0, lastUpdated: Date.now() };
258
+ private appState: MyAppState = {
259
+ data: "initial",
260
+ count: 0,
261
+ lastUpdated: Date.now(),
262
+ };
246
263
 
247
264
  constructor(persistenceAdapter: SimplePersistence<MyAppState>) {
248
265
  this.instanceId = uuidv4(); // Generate unique ID for this app/tab instance
@@ -255,31 +272,45 @@ class MyAppComponent {
255
272
  // Load initial state
256
273
  const storedState = await this.persistence.get();
257
274
  if (storedState) {
258
- console.log(`Instance ${this.instanceId}: Loaded initial state.`, storedState);
275
+ console.log(
276
+ `Instance ${this.instanceId}: Loaded initial state.`,
277
+ storedState,
278
+ );
259
279
  this.appState = storedState; // Update your app's internal state with loaded data
260
280
  } else {
261
- console.log(`Instance ${this.instanceId}: No initial state found. Using default.`);
281
+ console.log(
282
+ `Instance ${this.instanceId}: No initial state found. Using default.`,
283
+ );
262
284
  // Optionally, persist default state if none exists
263
285
  await this.persistence.set(this.instanceId, this.appState);
264
286
  }
265
287
 
266
288
  // Subscribe to changes from other instances
267
- this.unsubscribe = this.persistence.subscribe(this.instanceId, (newState) => {
268
- console.log(`Instance ${this.instanceId}: Received global state update from another instance.`, newState);
269
- // Crucial: Update your local application state based on this shared change
270
- this.appState = newState;
271
- this.render(); // Re-render your component/UI
272
- });
289
+ this.unsubscribe = this.persistence.subscribe(
290
+ this.instanceId,
291
+ (newState) => {
292
+ console.log(
293
+ `Instance ${this.instanceId}: Received global state update from another instance.`,
294
+ newState,
295
+ );
296
+ // Crucial: Update your local application state based on this shared change
297
+ this.appState = newState;
298
+ this.render(); // Re-render your component/UI
299
+ },
300
+ );
273
301
  console.log(`Instance ${this.instanceId}: Subscribed to updates.`);
274
302
  } catch (error) {
275
- console.error(`Instance ${this.instanceId}: Error during persistence initialization:`, error);
303
+ console.error(
304
+ `Instance ${this.instanceId}: Error during persistence initialization:`,
305
+ error,
306
+ );
276
307
  }
277
308
  this.render();
278
309
  }
279
310
 
280
311
  // Simulate a component render
281
312
  private render() {
282
- const outputElement = document.getElementById('app-output');
313
+ const outputElement = document.getElementById("app-output");
283
314
  if (outputElement) {
284
315
  outputElement.innerHTML = `
285
316
  <p><strong>Instance ID:</strong> ${this.instanceId}</p>
@@ -299,8 +330,16 @@ class MyAppComponent {
299
330
  }
300
331
 
301
332
  async updateAppState(newData: string) {
302
- this.appState = { ...this.appState, data: newData, count: this.appState.count + 1, lastUpdated: Date.now() };
303
- console.log(`Instance ${this.instanceId}: Attempting to save new state:`, this.appState);
333
+ this.appState = {
334
+ ...this.appState,
335
+ data: newData,
336
+ count: this.appState.count + 1,
337
+ lastUpdated: Date.now(),
338
+ };
339
+ console.log(
340
+ `Instance ${this.instanceId}: Attempting to save new state:`,
341
+ this.appState,
342
+ );
304
343
  const success = await this.persistence.set(this.instanceId, this.appState);
305
344
  if (!success) {
306
345
  console.error(`Instance ${this.instanceId}: Failed to save app state.`);
@@ -317,7 +356,7 @@ class MyAppComponent {
317
356
  console.error(`Instance ${this.instanceId}: Failed to clear app state.`);
318
357
  } else {
319
358
  console.log(`Instance ${this.instanceId}: App state cleared.`);
320
- this.appState = { data: 'cleared', count: 0, lastUpdated: Date.now() }; // Reset local state
359
+ this.appState = { data: "cleared", count: 0, lastUpdated: Date.now() }; // Reset local state
321
360
  }
322
361
  this.render();
323
362
  }
@@ -366,166 +405,206 @@ document.getElementById('close-btn')?.addEventListener('click', () => appInstanc
366
405
 
367
406
  ##### 2.2 Using `set(id, state)`
368
407
 
369
- * Always pass the unique `instanceId` of your consumer when calling `set`.
370
- * The `state` object you pass will generally overwrite the entire global persisted state. Ensure it contains all necessary data.
371
- ```typescript
372
- import { WebStoragePersistence, StoreConfig } from '@asaidimu/utils-persistence';
373
- import { v4 as uuidv4 } from 'uuid';
374
-
375
- interface Settings { theme: string; lastSaved: number; }
376
- const myInstanceId = uuidv4();
377
-
378
- async function saveSettings(newSettings: Settings) {
379
- const config: StoreConfig<Settings> & { storageKey: string } = {
380
- version: "1.0.0",
381
- app: "app-settings-manager",
382
- storageKey: 'app-settings'
383
- };
384
- const settingsStore = new WebStoragePersistence<Settings>(config);
385
-
386
- console.log(`Instance ${myInstanceId}: Attempting to save settings.`);
387
- const success = await settingsStore.set(myInstanceId, { ...newSettings, lastSaved: Date.now() });
388
- if (!success) {
389
- console.error(`Instance ${myInstanceId}: Failed to save settings.`);
390
- } else {
391
- console.log(`Instance ${myInstanceId}: Settings saved successfully.`);
392
- }
393
- }
408
+ - Always pass the unique `instanceId` of your consumer when calling `set`.
409
+ - The `state` object you pass will generally overwrite the entire global persisted state. Ensure it contains all necessary data.
394
410
 
395
- // Example:
396
- // saveSettings({ theme: 'dark', lastSaved: 0 });
397
- ```
411
+ ```typescript
412
+ import {
413
+ WebStoragePersistence,
414
+ StoreConfig,
415
+ } from "@asaidimu/utils-persistence";
416
+ import { v4 as uuidv4 } from "uuid";
398
417
 
399
- ##### 2.3 Using `get()`
400
-
401
- * `get()` retrieves the *entire global, shared* persisted state. It does *not* take an `id` because it's designed to fetch the single, overarching state accessible by all instances.
402
- * The returned `T | null` (or `Promise<T | null>`) should be used to initialize or update your application's local state.
403
- ```typescript
404
- import { WebStoragePersistence, StoreConfig } from '@asaidimu/utils-persistence';
418
+ interface Settings {
419
+ theme: string;
420
+ lastSaved: number;
421
+ }
422
+ const myInstanceId = uuidv4();
405
423
 
406
- interface Settings { theme: string; lastSaved: number; }
424
+ async function saveSettings(newSettings: Settings) {
407
425
  const config: StoreConfig<Settings> & { storageKey: string } = {
408
- version: "1.0.0",
409
- app: "app-settings-manager",
410
- storageKey: 'app-settings'
426
+ version: "1.0.0",
427
+ app: "app-settings-manager",
428
+ storageKey: "app-settings",
411
429
  };
412
430
  const settingsStore = new WebStoragePersistence<Settings>(config);
413
431
 
414
- async function retrieveGlobalState() {
415
- const storedState = await settingsStore.get(); // Await for async adapters if it returns a Promise
416
- if (storedState) {
417
- console.log("Retrieved global app settings:", storedState);
418
- // Integrate storedState into your application's current state
419
- } else {
420
- console.log("No global app settings found in storage.");
421
- }
432
+ console.log(`Instance ${myInstanceId}: Attempting to save settings.`);
433
+ const success = await settingsStore.set(myInstanceId, {
434
+ ...newSettings,
435
+ lastSaved: Date.now(),
436
+ });
437
+ if (!success) {
438
+ console.error(`Instance ${myInstanceId}: Failed to save settings.`);
439
+ } else {
440
+ console.log(`Instance ${myInstanceId}: Settings saved successfully.`);
422
441
  }
442
+ }
423
443
 
424
- // Example:
425
- // retrieveGlobalState();
426
- ```
444
+ // Example:
445
+ // saveSettings({ theme: 'dark', lastSaved: 0 });
446
+ ```
447
+
448
+ ##### 2.3 Using `get()`
449
+
450
+ - `get()` retrieves the _entire global, shared_ persisted state. It does _not_ take an `id` because it's designed to fetch the single, overarching state accessible by all instances.
451
+ - The returned `T | null` (or `Promise<T | null>`) should be used to initialize or update your application's local state.
452
+
453
+ ```typescript
454
+ import {
455
+ WebStoragePersistence,
456
+ StoreConfig,
457
+ } from "@asaidimu/utils-persistence";
458
+
459
+ interface Settings {
460
+ theme: string;
461
+ lastSaved: number;
462
+ }
463
+ const config: StoreConfig<Settings> & { storageKey: string } = {
464
+ version: "1.0.0",
465
+ app: "app-settings-manager",
466
+ storageKey: "app-settings",
467
+ };
468
+ const settingsStore = new WebStoragePersistence<Settings>(config);
469
+
470
+ async function retrieveGlobalState() {
471
+ const storedState = await settingsStore.get(); // Await for async adapters if it returns a Promise
472
+ if (storedState) {
473
+ console.log("Retrieved global app settings:", storedState);
474
+ // Integrate storedState into your application's current state
475
+ } else {
476
+ console.log("No global app settings found in storage.");
477
+ }
478
+ }
479
+
480
+ // Example:
481
+ // retrieveGlobalState();
482
+ ```
427
483
 
428
484
  ##### 2.4 Using `subscribe(id, callback)`
429
485
 
430
- * Pass your consumer `instanceId` as the first argument.
431
- * The `callback` will be invoked when the global persisted state changes due to a `set` operation initiated by *another* consumer instance.
432
- * **Always store the returned unsubscribe function** and call it when your consumer instance is no longer active (e.g., component unmounts, service shuts down) to prevent memory leaks and unnecessary processing.
486
+ - Pass your consumer `instanceId` as the first argument.
487
+ - The `callback` will be invoked when the global persisted state changes due to a `set` operation initiated by _another_ consumer instance.
488
+ - **Always store the returned unsubscribe function** and call it when your consumer instance is no longer active (e.g., component unmounts, service shuts down) to prevent memory leaks and unnecessary processing.
433
489
 
434
- ```typescript
435
- import { WebStoragePersistence, StoreConfig } from '@asaidimu/utils-persistence';
436
- import { v4 as uuidv4 } from 'uuid';
490
+ ```typescript
491
+ import {
492
+ WebStoragePersistence,
493
+ StoreConfig,
494
+ } from "@asaidimu/utils-persistence";
495
+ import { v4 as uuidv4 } from "uuid";
437
496
 
438
- interface NotificationState { count: number; lastMessage: string; }
439
- const myInstanceId = uuidv4();
497
+ interface NotificationState {
498
+ count: number;
499
+ lastMessage: string;
500
+ }
501
+ const myInstanceId = uuidv4();
440
502
 
441
- const config: StoreConfig<NotificationState> & { storageKey: string } = {
442
- version: "1.0.0",
443
- app: "notification-center",
444
- storageKey: 'app-notifications'
445
- };
446
- const notificationStore = new WebStoragePersistence<NotificationState>(config);
503
+ const config: StoreConfig<NotificationState> & { storageKey: string } = {
504
+ version: "1.0.0",
505
+ app: "notification-center",
506
+ storageKey: "app-notifications",
507
+ };
508
+ const notificationStore = new WebStoragePersistence<NotificationState>(
509
+ config,
510
+ );
511
+
512
+ const unsubscribe = notificationStore.subscribe(myInstanceId, (newState) => {
513
+ console.log(
514
+ `🔔 Instance ${myInstanceId}: Received update from another instance:`,
515
+ newState,
516
+ );
517
+ // Update UI or internal state based on `newState`
518
+ });
447
519
 
448
- const unsubscribe = notificationStore.subscribe(myInstanceId, (newState) => {
449
- console.log(`🔔 Instance ${myInstanceId}: Received update from another instance:`, newState);
450
- // Update UI or internal state based on `newState`
451
- });
520
+ console.log(`Instance ${myInstanceId}: Subscribed to notifications.`);
452
521
 
453
- console.log(`Instance ${myInstanceId}: Subscribed to notifications.`);
454
-
455
- // To simulate a change from another instance, open a new browser tab
456
- // and run something like this (ensure it's a different instanceId):
457
- /*
458
- setTimeout(async () => {
459
- const anotherInstanceId = uuidv4();
460
- const anotherNotificationStore = new WebStoragePersistence<NotificationState>(config);
461
- console.log(`Instance ${anotherInstanceId}: Sending new notification...`);
462
- await anotherNotificationStore.set(anotherInstanceId, { count: 5, lastMessage: 'New notification from another tab!' });
463
- console.log(`Instance ${anotherInstanceId}: Notification sent.`);
464
- }, 2000);
465
- */
466
-
467
- // When no longer needed (e.g., component unmounts):
468
- // setTimeout(() => {
469
- // unsubscribe();
470
- // console.log(`Instance ${myInstanceId}: Unsubscribed from notification updates.`);
471
- // }, 5000);
472
- ```
522
+ // To simulate a change from another instance, open a new browser tab
523
+ // and run something like this (ensure it's a different instanceId):
524
+ /*
525
+ setTimeout(async () => {
526
+ const anotherInstanceId = uuidv4();
527
+ const anotherNotificationStore = new WebStoragePersistence<NotificationState>(config);
528
+ console.log(`Instance ${anotherInstanceId}: Sending new notification...`);
529
+ await anotherNotificationStore.set(anotherInstanceId, { count: 5, lastMessage: 'New notification from another tab!' });
530
+ console.log(`Instance ${anotherInstanceId}: Notification sent.`);
531
+ }, 2000);
532
+ */
533
+
534
+ // When no longer needed (e.g., component unmounts):
535
+ // setTimeout(() => {
536
+ // unsubscribe();
537
+ // console.log(`Instance ${myInstanceId}: Unsubscribed from notification updates.`);
538
+ // }, 5000);
539
+ ```
473
540
 
474
541
  ##### 2.5 Using `clear()`
475
542
 
476
- * `clear()` performs a global reset, completely removing the shared persisted data for *all* instances. Use with caution.
477
- ```typescript
478
- import { WebStoragePersistence, StoreConfig } from '@asaidimu/utils-persistence';
543
+ - `clear()` performs a global reset, completely removing the shared persisted data for _all_ instances. Use with caution.
479
544
 
480
- interface UserData { username: string; email: string; }
481
- const config: StoreConfig<UserData> & { storageKey: string } = {
482
- version: "1.0.0",
483
- app: "user-profile",
484
- storageKey: 'user-data'
485
- };
486
- const userDataStore = new WebStoragePersistence<UserData>(config);
545
+ ```typescript
546
+ import {
547
+ WebStoragePersistence,
548
+ StoreConfig,
549
+ } from "@asaidimu/utils-persistence";
487
550
 
488
- async function resetUserData() {
489
- console.log("Attempting to clear all persisted user data...");
490
- const success = await userDataStore.clear(); // Await for async adapters if it returns a Promise
491
- if (success) {
492
- console.log("All persisted user data cleared successfully.");
493
- } else {
494
- console.error("Failed to clear persisted user data.");
495
- }
551
+ interface UserData {
552
+ username: string;
553
+ email: string;
554
+ }
555
+ const config: StoreConfig<UserData> & { storageKey: string } = {
556
+ version: "1.0.0",
557
+ app: "user-profile",
558
+ storageKey: "user-data",
559
+ };
560
+ const userDataStore = new WebStoragePersistence<UserData>(config);
561
+
562
+ async function resetUserData() {
563
+ console.log("Attempting to clear all persisted user data...");
564
+ const success = await userDataStore.clear(); // Await for async adapters if it returns a Promise
565
+ if (success) {
566
+ console.log("All persisted user data cleared successfully.");
567
+ } else {
568
+ console.error("Failed to clear persisted user data.");
496
569
  }
570
+ }
497
571
 
498
- // Example:
499
- // resetUserData();
500
- ```
572
+ // Example:
573
+ // resetUserData();
574
+ ```
501
575
 
502
576
  #### 3. When to Use and When to Avoid `SimplePersistence`
503
577
 
504
578
  To ensure proper use of the `SimplePersistence<T>` interface and prevent misuse, consider the following guidelines for when it is appropriate and when it should be avoided.
505
579
 
506
580
  **When to Use**
507
- * **Multi-Instance Synchronization**: Ideal for applications where multiple instances (e.g., browser tabs, web workers, or independent components) need to share and synchronize a single global state, such as collaborative web apps, note-taking apps, or shared dashboards.
508
- * **Interchangeable Persistence Needs**: Perfect for projects requiring flexibility to switch between storage backends (e.g., `localStorage` for prototyping, `IndexedDB` for production, `EphemeralPersistence` for transient state, or even server-based storage for scalability) without changing application logic.
509
- * **Simple Global State Management**: Suitable for managing a single, shared state object (e.g., user settings, app configurations, or a shared data model) that needs to be persisted and accessed across instances.
510
- * **Offline-First Applications**: Useful for apps that need to persist state locally and optionally sync with a server when online, leveraging the adapter pattern to handle different storage mechanisms.
511
- * **Prototyping and Testing**: Great for quickly implementing persistence with a lightweight adapter (e.g., in-memory or `localStorage`) and later scaling to more robust solutions.
581
+
582
+ - **Multi-Instance Synchronization**: Ideal for applications where multiple instances (e.g., browser tabs, web workers, or independent components) need to share and synchronize a single global state, such as collaborative web apps, note-taking apps, or shared dashboards.
583
+ - **Interchangeable Persistence Needs**: Perfect for projects requiring flexibility to switch between storage backends (e.g., `localStorage` for prototyping, `IndexedDB` for production, `EphemeralPersistence` for transient state, or even server-based storage for scalability) without changing application logic.
584
+ - **Simple Global State Management**: Suitable for managing a single, shared state object (e.g., user settings, app configurations, or a shared data model) that needs to be persisted and accessed across instances.
585
+ - **Offline-First Applications**: Useful for apps that need to persist state locally and optionally sync with a server when online, leveraging the adapter pattern to handle different storage mechanisms.
586
+ - **Prototyping and Testing**: Great for quickly implementing persistence with a lightweight adapter (e.g., in-memory or `localStorage`) and later scaling to more robust solutions.
512
587
 
513
588
  **When to Avoid**
514
- * **Complex Data Relationships**: Avoid using for applications requiring complex data models with relational queries or indexing (e.g., large-scale databases with multiple tables or complex joins). The interface is designed for a single global state, not for managing multiple entities with intricate relationships. For such cases, consider using a dedicated database library (like `@asaidimu/indexed` directly) or a backend service.
515
- * **High-Frequency Updates**: Not suitable for scenarios with rapid, high-frequency state changes (e.g., real-time gaming or live data streams that update hundreds of times per second), as the global state overwrite model and broadcasting mechanism may introduce performance bottlenecks.
516
- * **Fine-Grained Data Access**: Do not use if you need to persist or retrieve specific parts of the state independently, as `set` and `get` operate on the entire state object, which can be inefficient for very large datasets where only small portions change.
517
- * **Critical Data with Strict Consistency**: Not ideal for systems requiring strict consistency guarantees (e.g., financial transactions) across distributed clients, as the interface does not enforce ACID properties or advanced conflict resolution beyond basic Last Write Wins (LWW) semantics for ephemeral storage, or simple overwrite for others.
589
+
590
+ - **Complex Data Relationships**: Avoid using for applications requiring complex data models with relational queries or indexing (e.g., large-scale databases with multiple tables or complex joins). The interface is designed for a single global state, not for managing multiple entities with intricate relationships. For such cases, consider using a dedicated database library (like `@asaidimu/indexed` directly) or a backend service.
591
+ - **High-Frequency Updates**: Not suitable for scenarios with rapid, high-frequency state changes (e.g., real-time gaming or live data streams that update hundreds of times per second), as the global state overwrite model and broadcasting mechanism may introduce performance bottlenecks.
592
+ - **Fine-Grained Data Access**: Do not use if you need to persist or retrieve specific parts of the state independently, as `set` and `get` operate on the entire state object, which can be inefficient for very large datasets where only small portions change.
593
+ - **Critical Data with Strict Consistency**: Not ideal for systems requiring strict consistency guarantees (e.g., financial transactions) across distributed clients, as the interface does not enforce ACID properties or advanced conflict resolution beyond basic Last Write Wins (LWW) semantics for ephemeral storage, or simple overwrite for others.
518
594
 
519
595
  ### Web Storage Persistence (`WebStoragePersistence`)
520
596
 
521
597
  `WebStoragePersistence` uses the browser's `localStorage` (default) or `sessionStorage`. Its `set`, `get`, and `clear` operations are synchronous, meaning they return `boolean` values directly, not Promises. It supports cross-tab synchronization and data versioning with upgrade handlers.
522
598
 
523
599
  ```typescript
524
- import { WebStoragePersistence, StoreConfig } from '@asaidimu/utils-persistence';
525
- import { v4 as uuidv4 } from 'uuid'; // Recommended for generating unique instance IDs
600
+ import {
601
+ WebStoragePersistence,
602
+ StoreConfig,
603
+ } from "@asaidimu/utils-persistence";
604
+ import { v4 as uuidv4 } from "uuid"; // Recommended for generating unique instance IDs
526
605
 
527
606
  interface UserPreferences {
528
- theme: 'dark' | 'light';
607
+ theme: "dark" | "light";
529
608
  notificationsEnabled: boolean;
530
609
  language: string;
531
610
  }
@@ -538,46 +617,60 @@ const instanceId = uuidv4();
538
617
  const commonConfig: StoreConfig<UserPreferences> = {
539
618
  version: "1.0.0",
540
619
  app: "user-dashboard",
541
- async onUpgrade: ({ data, version, app }) => {
542
- console.log(`[${app}] Migrating user preferences from version ${version} to 1.0.0.`);
620
+ onUpgrade: ({ data, version, app }) => {
621
+ console.log(
622
+ `[${app}] Migrating user preferences from version ${version} to 1.0.0.`,
623
+ );
543
624
  // Example migration: ensure language is set to default if missing
544
625
  if (data && !data.language) {
545
- return { state: { ...data, language: 'en-US' }, version: "1.0.0" };
626
+ return { state: { ...data, language: "en-US" }, version: "1.0.0" };
546
627
  }
547
628
  // For WebStoragePersistence, if data is null during upgrade, it means no data existed.
548
629
  // We could return a default state here.
549
- return { state: data || { theme: 'light', notificationsEnabled: true, language: 'en-US' }, version: "1.0.0" };
550
- }
630
+ return {
631
+ state: data || {
632
+ theme: "light",
633
+ notificationsEnabled: true,
634
+ language: "en-US",
635
+ },
636
+ version: "1.0.0",
637
+ };
638
+ },
551
639
  };
552
640
 
553
641
  // 1. Using localStorage (default for persistent data)
554
642
  // Data stored here will persist across browser sessions.
555
- const localStorageConfig = { ...commonConfig, storageKey: 'user-preferences-local' };
556
- const userPrefsStore = new WebStoragePersistence<UserPreferences>(localStorageConfig);
643
+ const localStorageConfig = {
644
+ ...commonConfig,
645
+ storageKey: "user-preferences-local",
646
+ };
647
+ const userPrefsStore = new WebStoragePersistence<UserPreferences>(
648
+ localStorageConfig,
649
+ );
557
650
 
558
- console.log('--- localStorage Example ---');
651
+ console.log("--- localStorage Example ---");
559
652
 
560
653
  async function manageLocalStorage() {
561
654
  // Set initial preferences
562
655
  const initialPrefs: UserPreferences = {
563
- theme: 'dark',
656
+ theme: "dark",
564
657
  notificationsEnabled: true,
565
- language: 'en-US',
658
+ language: "en-US",
566
659
  };
567
660
  const setResult = userPrefsStore.set(instanceId, initialPrefs);
568
- console.log('Preferences set successfully:', setResult); // Expected: true
661
+ console.log("Preferences set successfully:", setResult); // Expected: true
569
662
 
570
663
  // Retrieve data
571
664
  let currentPrefs = userPrefsStore.get();
572
- console.log('Current preferences:', currentPrefs);
665
+ console.log("Current preferences:", currentPrefs);
573
666
 
574
667
  // Subscribe to changes from *other* tabs/instances.
575
668
  // Open another browser tab to the same application URL to test this.
576
669
  const unsubscribePrefs = userPrefsStore.subscribe(instanceId, (newState) => {
577
- console.log('🔔 Preferences updated from another instance:', newState);
670
+ console.log("🔔 Preferences updated from another instance:", newState);
578
671
  // In a real app, you would update your UI or state management system here.
579
672
  });
580
- console.log('Subscribed to localStorage updates.');
673
+ console.log("Subscribed to localStorage updates.");
581
674
 
582
675
  // Simulate an update from another tab:
583
676
  // (In a different tab, generate a new instanceId and call set on the same storageKey)
@@ -587,7 +680,7 @@ async function manageLocalStorage() {
587
680
  anotherPrefsStore_ls.set(anotherInstanceId_ls, { theme: 'light', notificationsEnabled: false, language: 'es-ES' });
588
681
  */
589
682
  // You would then see the "🔔 Preferences updated from another instance:" message in the first tab.
590
- await new Promise(resolve => setTimeout(resolve, 100)); // Allow event propagation
683
+ await new Promise((resolve) => setTimeout(resolve, 100)); // Allow event propagation
591
684
 
592
685
  // After a while, if no longer needed, unsubscribe
593
686
  // setTimeout(() => {
@@ -597,33 +690,46 @@ async function manageLocalStorage() {
597
690
 
598
691
  // Clear data when no longer needed (e.g., user logs out)
599
692
  const clearResult = userPrefsStore.clear();
600
- console.log('Preferences cleared successfully:', clearResult); // Expected: true
601
- console.log('Preferences after clear:', userPrefsStore.get()); // Expected: null
693
+ console.log("Preferences cleared successfully:", clearResult); // Expected: true
694
+ console.log("Preferences after clear:", userPrefsStore.get()); // Expected: null
602
695
  unsubscribePrefs(); // Unsubscribe immediately after clearing for the example
603
696
  }
604
697
  manageLocalStorage();
605
698
 
606
- console.log('\n--- sessionStorage Example ---');
699
+ console.log("\n--- sessionStorage Example ---");
607
700
 
608
701
  // 2. Using sessionStorage (for session-specific data)
609
702
  // Data stored here will only persist for the duration of the browser tab.
610
703
  // It is cleared when the tab is closed.
611
- const sessionStorageConfig = { ...commonConfig, storageKey: 'session-data', session: true };
612
- const sessionDataStore = new WebStoragePersistence<{ lastVisitedPage: string } & UserPreferences>(sessionStorageConfig);
704
+ const sessionStorageConfig = {
705
+ ...commonConfig,
706
+ storageKey: "session-data",
707
+ session: true,
708
+ };
709
+ const sessionDataStore = new WebStoragePersistence<
710
+ { lastVisitedPage: string } & UserPreferences
711
+ >(sessionStorageConfig);
613
712
 
614
713
  async function manageSessionStorage() {
615
714
  const initialSessionData: { lastVisitedPage: string } & UserPreferences = {
616
- ...commonConfig.onUpgrade!({ data: null, version: "0.0.0", app: "user-dashboard" }).state!, // Use migrated default
617
- lastVisitedPage: '/dashboard'
715
+ ...commonConfig.onUpgrade!({
716
+ data: null,
717
+ version: "0.0.0",
718
+ app: "user-dashboard",
719
+ }).state!, // Use migrated default
720
+ lastVisitedPage: "/dashboard",
618
721
  };
619
722
  sessionDataStore.set(instanceId, initialSessionData);
620
- console.log('Session data set:', sessionDataStore.get());
723
+ console.log("Session data set:", sessionDataStore.get());
621
724
 
622
725
  // sessionStorage also supports cross-tab synchronization via BroadcastChannel
623
- const unsubscribeSession = sessionDataStore.subscribe(instanceId, (newState) => {
624
- console.log('🔔 Session data updated from another instance:', newState);
625
- });
626
- console.log('Subscribed to sessionStorage updates.');
726
+ const unsubscribeSession = sessionDataStore.subscribe(
727
+ instanceId,
728
+ (newState) => {
729
+ console.log("🔔 Session data updated from another instance:", newState);
730
+ },
731
+ );
732
+ console.log("Subscribed to sessionStorage updates.");
627
733
 
628
734
  // To test session storage cross-tab, open two tabs to the same URL,
629
735
  // set a value in one tab, and the other tab's subscriber will be notified.
@@ -633,7 +739,7 @@ async function manageSessionStorage() {
633
739
  const anotherSessionDataStore_ss = new WebStoragePersistence<{ lastVisitedPage: string } & UserPreferences>(sessionStorageConfig);
634
740
  anotherSessionDataStore_ss.set(anotherInstanceId_ss, { ...initialSessionData, lastVisitedPage: '/settings', theme: 'dark' });
635
741
  */
636
- await new Promise(resolve => setTimeout(resolve, 100)); // Allow event propagation
742
+ await new Promise((resolve) => setTimeout(resolve, 100)); // Allow event propagation
637
743
 
638
744
  // unsubscribeSession();
639
745
  // console.log('Unsubscribed from sessionStorage updates.');
@@ -647,8 +753,12 @@ manageSessionStorage();
647
753
  `IndexedDBPersistence` is designed for storing larger or more complex data structures. All its methods (`set`, `get`, `clear`, `close`) return `Promise`s because IndexedDB operations are asynchronous and non-blocking. It uses `@asaidimu/indexed` internally, which provides a higher-level, promise-based API over native IndexedDB.
648
754
 
649
755
  ```typescript
650
- import { IndexedDBPersistence, StoreConfig, IndexedDBPersistenceConfig } from '@asaidimu/utils-persistence';
651
- import { v4 as uuidv4 } from 'uuid'; // Recommended for generating unique instance IDs
756
+ import {
757
+ IndexedDBPersistence,
758
+ StoreConfig,
759
+ IndexedDBPersistenceConfig,
760
+ } from "@asaidimu/utils-persistence";
761
+ import { v4 as uuidv4 } from "uuid"; // Recommended for generating unique instance IDs
652
762
 
653
763
  interface Product {
654
764
  id: string;
@@ -668,74 +778,109 @@ const instanceId = uuidv4();
668
778
  const indexedDBConfig: StoreConfig<Product[]> & IndexedDBPersistenceConfig = {
669
779
  version: "2.0.0", // Let's use a new version for migration example
670
780
  app: "product-catalog",
671
- store: 'all-products-inventory', // Unique identifier for this piece of data/document
672
- database: 'my-app-database', // Name of the IndexedDB database
673
- collection: 'app-stores', // Name of the object store (table-like structure)
674
- enableTelemetry: false, // Optional: enable telemetry for underlying IndexedDB lib
675
- async onUpgrade: ({ data, version, app }) => {
676
- console.log(`[${app}] Migrating product data from version ${version} to 2.0.0.`);
781
+ store: "all-products-inventory", // Unique identifier for this piece of data/document
782
+ database: "my-app-database", // Name of the IndexedDB database
783
+ collection: "app-stores", // Name of the object store (table-like structure)
784
+ enableTelemetry: false, // Optional: enable telemetry for underlying IndexedDB lib
785
+ onUpgrade: ({ data, version, app }) => {
786
+ console.log(
787
+ `[${app}] Migrating product data from version ${version} to 2.0.0.`,
788
+ );
677
789
  if (version === "1.0.0" && data) {
678
790
  // Example migration from v1.0.0: Add 'lastUpdated' field if it doesn't exist
679
- const migratedData = data.map(p => ({ ...p, lastUpdated: p.lastUpdated || Date.now() }));
791
+ const migratedData = data.map((p) => ({
792
+ ...p,
793
+ lastUpdated: p.lastUpdated || Date.now(),
794
+ }));
680
795
  return { state: migratedData, version: "2.0.0" };
681
796
  }
682
797
  // If no existing data or unknown version, return null or a sensible default
683
798
  return { state: data, version: "2.0.0" };
684
- }
799
+ },
685
800
  };
686
801
 
687
802
  const productCache = new IndexedDBPersistence<Product[]>(indexedDBConfig);
688
803
 
689
804
  async function manageProductCache() {
690
- console.log('--- IndexedDB Example ---');
805
+ console.log("--- IndexedDB Example ---");
691
806
 
692
807
  // Set initial product data - AWAIT the promise!
693
808
  const products: Product[] = [
694
- { id: 'p001', name: 'Laptop', price: 1200, stock: 50, lastUpdated: Date.now() },
695
- { id: 'p002', name: 'Mouse', price: 25, stock: 200, lastUpdated: Date.now() },
809
+ {
810
+ id: "p001",
811
+ name: "Laptop",
812
+ price: 1200,
813
+ stock: 50,
814
+ lastUpdated: Date.now(),
815
+ },
816
+ {
817
+ id: "p002",
818
+ name: "Mouse",
819
+ price: 25,
820
+ stock: 200,
821
+ lastUpdated: Date.now(),
822
+ },
696
823
  ];
697
824
  try {
698
825
  const setResult = await productCache.set(instanceId, products);
699
- console.log('Products cached successfully:', setResult); // Expected: true
826
+ console.log("Products cached successfully:", setResult); // Expected: true
700
827
  } catch (error) {
701
- console.error('Failed to set products:', error);
828
+ console.error("Failed to set products:", error);
702
829
  }
703
830
 
704
831
  // Get data - AWAIT the promise!
705
832
  const cachedProducts = await productCache.get();
706
833
  if (cachedProducts) {
707
- console.log('Retrieved products:', cachedProducts);
834
+ console.log("Retrieved products:", cachedProducts);
708
835
  } else {
709
- console.log('No products found in cache.');
836
+ console.log("No products found in cache.");
710
837
  }
711
838
 
712
839
  // Subscribe to changes from *other* tabs/instances
713
840
  const unsubscribeProducts = productCache.subscribe(instanceId, (newState) => {
714
- console.log('🔔 Product cache updated by another instance:', newState);
841
+ console.log("🔔 Product cache updated by another instance:", newState);
715
842
  // Refresh your product list in the UI, re-fetch data, etc.
716
843
  });
717
- console.log('Subscribed to IndexedDB product updates.');
844
+ console.log("Subscribed to IndexedDB product updates.");
718
845
 
719
846
  // Simulate an update from another instance (e.g., from a different tab)
720
847
  const updatedProducts: Product[] = [
721
- { id: 'p001', name: 'Laptop Pro', price: 1150, stock: 45, lastUpdated: Date.now() }, // Price and stock updated
722
- { id: 'p002', name: 'Wireless Mouse', price: 30, stock: 190, lastUpdated: Date.now() },
723
- { id: 'p003', name: 'Mechanical Keyboard', price: 75, stock: 150, lastUpdated: Date.now() }, // New product
848
+ {
849
+ id: "p001",
850
+ name: "Laptop Pro",
851
+ price: 1150,
852
+ stock: 45,
853
+ lastUpdated: Date.now(),
854
+ }, // Price and stock updated
855
+ {
856
+ id: "p002",
857
+ name: "Wireless Mouse",
858
+ price: 30,
859
+ stock: 190,
860
+ lastUpdated: Date.now(),
861
+ },
862
+ {
863
+ id: "p003",
864
+ name: "Mechanical Keyboard",
865
+ price: 75,
866
+ stock: 150,
867
+ lastUpdated: Date.now(),
868
+ }, // New product
724
869
  ];
725
870
  try {
726
871
  // Use a *new* instanceId for simulation to trigger the subscriber in the *first* tab
727
872
  const updateResult = await productCache.set(uuidv4(), updatedProducts);
728
- console.log('Simulated update from another instance:', updateResult);
873
+ console.log("Simulated update from another instance:", updateResult);
729
874
  } catch (error) {
730
- console.error('Failed to simulate product update:', error);
875
+ console.error("Failed to simulate product update:", error);
731
876
  }
732
877
 
733
878
  // Give time for the event to propagate and be processed by the subscriber
734
- await new Promise(resolve => setTimeout(resolve, 100)); // Short delay for async event bus
879
+ await new Promise((resolve) => setTimeout(resolve, 100)); // Short delay for async event bus
735
880
 
736
881
  // Verify updated data
737
882
  const updatedCachedProducts = await productCache.get();
738
- console.log('Products after update:', updatedCachedProducts);
883
+ console.log("Products after update:", updatedCachedProducts);
739
884
 
740
885
  // After a while, if no longer needed, unsubscribe
741
886
  // setTimeout(() => {
@@ -759,7 +904,7 @@ async function manageProductCache() {
759
904
  await productCache.close(); // Closes 'my-app-database'
760
905
  console.log('IndexedDB connection for "my-app-database" closed.');
761
906
  } catch (error) {
762
- console.error('Failed to close IndexedDB connection:', error);
907
+ console.error("Failed to close IndexedDB connection:", error);
763
908
  }
764
909
  }
765
910
 
@@ -774,8 +919,8 @@ async function manageProductCache() {
774
919
  This adapter's `set`, `get`, and `clear` operations are synchronous, returning `boolean` values or `T | null` directly.
775
920
 
776
921
  ```typescript
777
- import { EphemeralPersistence, StoreConfig } from '@asaidimu/utils-persistence';
778
- import { v4 as uuidv4 } from 'uuid';
922
+ import { EphemeralPersistence, StoreConfig } from "@asaidimu/utils-persistence";
923
+ import { v4 as uuidv4 } from "uuid";
779
924
 
780
925
  interface SessionData {
781
926
  activeUsers: number;
@@ -792,16 +937,27 @@ const ephemeralConfig: StoreConfig<SessionData> & { storageKey: string } = {
792
937
  version: "1.0.0",
793
938
  app: "multi-tab-session",
794
939
  storageKey: "global-session-state",
795
- async onUpgrade: ({ data, version, app }) => {
796
- console.log(`[${app}] Initializing/Migrating ephemeral state from version ${version}.`);
940
+ onUpgrade: ({ data, version, app }) => {
941
+ console.log(
942
+ `[${app}] Initializing/Migrating ephemeral state from version ${version}.`,
943
+ );
797
944
  // For EphemeralPersistence, onUpgrade is called with data: null initially
798
- return { state: data || { activeUsers: 0, lastActivity: new Date().toISOString(), isPolling: false }, version: "1.0.0" };
799
- }
945
+ return {
946
+ state: data || {
947
+ activeUsers: 0,
948
+ lastActivity: new Date().toISOString(),
949
+ isPolling: false,
950
+ },
951
+ version: "1.0.0",
952
+ };
953
+ },
800
954
  };
801
- const sessionStateStore = new EphemeralPersistence<SessionData>(ephemeralConfig);
955
+ const sessionStateStore = new EphemeralPersistence<SessionData>(
956
+ ephemeralConfig,
957
+ );
802
958
 
803
959
  async function manageSessionState() {
804
- console.log('--- Ephemeral Persistence Example ---');
960
+ console.log("--- Ephemeral Persistence Example ---");
805
961
 
806
962
  // Set initial session data
807
963
  const initialData: SessionData = {
@@ -810,19 +966,22 @@ async function manageSessionState() {
810
966
  isPolling: true,
811
967
  };
812
968
  const setResult = sessionStateStore.set(instanceId, initialData);
813
- console.log('Session state set successfully:', setResult); // Expected: true
969
+ console.log("Session state set successfully:", setResult); // Expected: true
814
970
 
815
971
  // Get data
816
972
  let currentSessionState = sessionStateStore.get();
817
- console.log('Current session state:', currentSessionState);
973
+ console.log("Current session state:", currentSessionState);
818
974
 
819
975
  // Subscribe to changes from *other* tabs/instances
820
976
  // Open another browser tab to the same application URL to test this.
821
- const unsubscribeSessionState = sessionStateStore.subscribe(instanceId, (newState) => {
822
- console.log('🔔 Session state updated from another instance:', newState);
823
- // You might update a UI counter, re-render a component, etc.
824
- });
825
- console.log('Subscribed to Ephemeral session state updates.');
977
+ const unsubscribeSessionState = sessionStateStore.subscribe(
978
+ instanceId,
979
+ (newState) => {
980
+ console.log("🔔 Session state updated from another instance:", newState);
981
+ // You might update a UI counter, re-render a component, etc.
982
+ },
983
+ );
984
+ console.log("Subscribed to Ephemeral session state updates.");
826
985
 
827
986
  // Simulate an update from another instance (e.g., from a different tab)
828
987
  const updatedData: SessionData = {
@@ -832,14 +991,14 @@ async function manageSessionState() {
832
991
  };
833
992
  // Use a *new* instanceId for simulation to trigger the subscriber in the *first* tab
834
993
  const updateResult = sessionStateStore.set(uuidv4(), updatedData);
835
- console.log('Simulated update from another instance:', updateResult);
994
+ console.log("Simulated update from another instance:", updateResult);
836
995
 
837
996
  // Give time for the event to propagate and be processed by the subscriber
838
- await new Promise(resolve => setTimeout(resolve, 50)); // Short delay for async event bus
997
+ await new Promise((resolve) => setTimeout(resolve, 50)); // Short delay for async event bus
839
998
 
840
999
  // Verify updated data locally
841
1000
  const updatedCurrentSessionState = sessionStateStore.get();
842
- console.log('Session state after update:', updatedCurrentSessionState);
1001
+ console.log("Session state after update:", updatedCurrentSessionState);
843
1002
 
844
1003
  // After a while, if no longer needed, unsubscribe
845
1004
  // setTimeout(() => {
@@ -850,8 +1009,8 @@ async function manageSessionState() {
850
1009
 
851
1010
  // Clear data: this will also propagate via LWW to other tabs
852
1011
  const clearResult = sessionStateStore.clear();
853
- console.log('Session state cleared:', clearResult); // Expected: true
854
- console.log('Session state after clear:', sessionStateStore.get()); // Expected: null
1012
+ console.log("Session state cleared:", clearResult); // Expected: true
1013
+ console.log("Session state after clear:", sessionStateStore.get()); // Expected: null
855
1014
  }
856
1015
 
857
1016
  // Call the async function to start the example
@@ -860,12 +1019,12 @@ async function manageSessionState() {
860
1019
 
861
1020
  ### Common Use Cases
862
1021
 
863
- * **User Preferences**: Store user settings like theme, language, or notification preferences using `WebStoragePersistence`. These are often small and need to persist across sessions.
864
- * **Offline Data Caching**: Cache large datasets (e.g., product catalogs, article content, user-generated content) using `IndexedDBPersistence` to enable offline access and improve perceived performance.
865
- * **Shopping Cart State**: Persist a user's shopping cart items using `WebStoragePersistence` (for simple carts with limited items) or `IndexedDBPersistence` (for more complex carts with detailed product information, images, or large quantities) to survive page refreshes or browser restarts.
866
- * **Form State Preservation**: Temporarily save complex multi-step form data using `sessionStorage` (via `WebStoragePersistence`) to prevent data loss on accidental navigation or refreshes within the same browser tab session.
867
- * **Cross-Tab/Instance Synchronization**: Use the `subscribe` method to build features that require real-time updates across multiple browser tabs, such as a shared todo list, live chat status indicators, synchronized media playback state, or collaborative document editing. This is particularly useful for `EphemeralPersistence` for transient, non-persistent shared state.
868
- * **Feature Flags/A/B Testing**: Store user-specific feature flag assignments or A/B test group allocations in `localStorage` for consistent experiences across visits.
1022
+ - **User Preferences**: Store user settings like theme, language, or notification preferences using `WebStoragePersistence`. These are often small and need to persist across sessions.
1023
+ - **Offline Data Caching**: Cache large datasets (e.g., product catalogs, article content, user-generated content) using `IndexedDBPersistence` to enable offline access and improve perceived performance.
1024
+ - **Shopping Cart State**: Persist a user's shopping cart items using `WebStoragePersistence` (for simple carts with limited items) or `IndexedDBPersistence` (for more complex carts with detailed product information, images, or large quantities) to survive page refreshes or browser restarts.
1025
+ - **Form State Preservation**: Temporarily save complex multi-step form data using `sessionStorage` (via `WebStoragePersistence`) to prevent data loss on accidental navigation or refreshes within the same browser tab session.
1026
+ - **Cross-Tab/Instance Synchronization**: Use the `subscribe` method to build features that require real-time updates across multiple browser tabs, such as a shared todo list, live chat status indicators, synchronized media playback state, or collaborative document editing. This is particularly useful for `EphemeralPersistence` for transient, non-persistent shared state.
1027
+ - **Feature Flags/A/B Testing**: Store user-specific feature flag assignments or A/B test group allocations in `localStorage` for consistent experiences across visits.
869
1028
 
870
1029
  ---
871
1030
 
@@ -875,38 +1034,38 @@ The `@asaidimu/utils-persistence` library is structured to be modular and extens
875
1034
 
876
1035
  ### Core Components
877
1036
 
878
- * **`SimplePersistence<T>`**: This is the fundamental TypeScript interface that defines the contract for any persistence adapter. It ensures a consistent API for `set`, `get`, `subscribe`, `clear`, and `stats` operations, regardless of the underlying storage mechanism. The `StoreConfig<T>` interface provides common configuration for versioning, application identification, and data migration.
879
- * **`WebStoragePersistence<T>`**:
880
- * **Purpose**: Provides simple key-value persistence leveraging the browser's `localStorage` or `sessionStorage` APIs.
881
- * **Mechanism**: Directly interacts with `window.localStorage` or `window.sessionStorage`. Data is serialized/deserialized using `JSON.stringify` and `JSON.parse`.
882
- * **Synchronization**: Utilizes `window.addEventListener('storage', ...)` for `localStorage` (which triggers when changes occur in *other* tabs on the same origin) and the `@asaidimu/events` event bus (which uses `BroadcastChannel` internally) to ensure real-time updates across multiple browser tabs for both `localStorage` and `sessionStorage`.
883
- * **`EphemeralPersistence<T>`**:
884
- * **Purpose**: Offers an in-memory store for transient data that needs cross-tab synchronization but *not* persistence across page reloads.
885
- * **Mechanism**: Stores data in a private class property. Data is cloned using `structuredClone` to prevent direct mutation issues.
886
- * **Synchronization**: Leverages the `@asaidimu/events` event bus (via `BroadcastChannel`) for cross-instance synchronization. It implements a Last Write Wins (LWW) strategy based on timestamps included in the broadcast events, ensuring all tabs converge to the most recently written state.
887
- * **`IndexedDBPersistence<T>`**:
888
- * **Purpose**: Provides robust, asynchronous persistence using the browser's `IndexedDB` API, suitable for larger datasets and structured data.
889
- * **Mechanism**: Builds upon `@asaidimu/indexed` for simplified IndexedDB interactions (handling databases, object stores, and transactions) and `@asaidimu/query` for declarative data querying. Data is stored in a specific `collection` (object store) within a `database`, identified by a `store` key.
890
- * **Shared Resources**: Employs a `SharedResources` singleton pattern to manage and cache `Database` connections, `Collection` instances, and `EventBus` instances efficiently across multiple `IndexedDBPersistence` instances. This avoids redundant connections, ensures a single source of truth for IndexedDB operations, and manages global event listeners effectively.
891
- * **Synchronization**: Leverages the `@asaidimu/events` event bus (via `BroadcastChannel`) for cross-instance synchronization of IndexedDB changes, notifying other instances about updates.
892
- * **`@asaidimu/events`**: An internal utility package that provides a powerful, cross-tab compatible event bus using `BroadcastChannel`. It's crucial for enabling the automatic synchronization features of all persistence adapters.
893
- * **`@asaidimu/indexed` & `@asaidimu/query`**: These are internal utility packages specifically used by `IndexedDBPersistence` to abstract and simplify complex interactions with the native IndexedDB API, offering a more declarative and promise-based interface. `@asaidimu/indexed` handles database schema, versions, and CRUD operations, while `@asaidimu/query` provides a query builder for data retrieval.
1037
+ - **`SimplePersistence<T>`**: This is the fundamental TypeScript interface that defines the contract for any persistence adapter. It ensures a consistent API for `set`, `get`, `subscribe`, `clear`, and `stats` operations, regardless of the underlying storage mechanism. The `StoreConfig<T>` interface provides common configuration for versioning, application identification, and data migration.
1038
+ - **`WebStoragePersistence<T>`**:
1039
+ - **Purpose**: Provides simple key-value persistence leveraging the browser's `localStorage` or `sessionStorage` APIs.
1040
+ - **Mechanism**: Directly interacts with `window.localStorage` or `window.sessionStorage`. Data is serialized/deserialized using `JSON.stringify` and `JSON.parse`.
1041
+ - **Synchronization**: Utilizes `window.addEventListener('storage', ...)` for `localStorage` (which triggers when changes occur in _other_ tabs on the same origin) and the `@asaidimu/events` event bus (which uses `BroadcastChannel` internally) to ensure real-time updates across multiple browser tabs for both `localStorage` and `sessionStorage`.
1042
+ - **`EphemeralPersistence<T>`**:
1043
+ - **Purpose**: Offers an in-memory store for transient data that needs cross-tab synchronization but _not_ persistence across page reloads.
1044
+ - **Mechanism**: Stores data in a private class property. Data is cloned using `structuredClone` to prevent direct mutation issues.
1045
+ - **Synchronization**: Leverages the `@asaidimu/events` event bus (via `BroadcastChannel`) for cross-instance synchronization. It implements a Last Write Wins (LWW) strategy based on timestamps included in the broadcast events, ensuring all tabs converge to the most recently written state.
1046
+ - **`IndexedDBPersistence<T>`**:
1047
+ - **Purpose**: Provides robust, asynchronous persistence using the browser's `IndexedDB` API, suitable for larger datasets and structured data.
1048
+ - **Mechanism**: Builds upon `@asaidimu/indexed` for simplified IndexedDB interactions (handling databases, object stores, and transactions) and `@asaidimu/query` for declarative data querying. Data is stored in a specific `collection` (object store) within a `database`, identified by a `store` key.
1049
+ - **Shared Resources**: Employs a `SharedResources` singleton pattern to manage and cache `Database` connections, `Collection` instances, and `EventBus` instances efficiently across multiple `IndexedDBPersistence` instances. This avoids redundant connections, ensures a single source of truth for IndexedDB operations, and manages global event listeners effectively.
1050
+ - **Synchronization**: Leverages the `@asaidimu/events` event bus (via `BroadcastChannel`) for cross-instance synchronization of IndexedDB changes, notifying other instances about updates.
1051
+ - **`@asaidimu/events`**: An internal utility package that provides a powerful, cross-tab compatible event bus using `BroadcastChannel`. It's crucial for enabling the automatic synchronization features of all persistence adapters.
1052
+ - **`@asaidimu/indexed` & `@asaidimu/query`**: These are internal utility packages specifically used by `IndexedDBPersistence` to abstract and simplify complex interactions with the native IndexedDB API, offering a more declarative and promise-based interface. `@asaidimu/indexed` handles database schema, versions, and CRUD operations, while `@asaidimu/query` provides a query builder for data retrieval.
894
1053
 
895
1054
  ### Data Flow for State Changes
896
1055
 
897
1056
  1. **Setting State (`set(instanceId, state)`):**
898
- * The provided `state` (of type `T`) is first serialized into a format suitable for the underlying storage (e.g., `JSON.stringify` for web storage, `structuredClone` for ephemeral, or directly as an object for IndexedDB).
899
- * It's then saved to the respective storage mechanism (`localStorage`, `sessionStorage`, in-memory, or an IndexedDB object store). For IndexedDB, existing data for the given `store` key is updated, or new data is created.
900
- * An event (of type `store:updated`) is immediately `emit`ted on an internal event bus (from `@asaidimu/events`). This event includes the `instanceId` of the updater, the `storageKey`/`store` identifying the data, and the new `state`. For `EphemeralPersistence`, a `timestamp` is also included for LWW resolution. This event is broadcast to other browser tabs via `BroadcastChannel` (managed by `@asaidimu/events`).
901
- * For `localStorage`, native `StorageEvent`s also trigger when the value is set from another tab. The `WebStoragePersistence` adapter listens for these native events and re-emits them on its internal event bus, ensuring consistent notification pathways.
1057
+ - The provided `state` (of type `T`) is first serialized into a format suitable for the underlying storage (e.g., `JSON.stringify` for web storage, `structuredClone` for ephemeral, or directly as an object for IndexedDB).
1058
+ - It's then saved to the respective storage mechanism (`localStorage`, `sessionStorage`, in-memory, or an IndexedDB object store). For IndexedDB, existing data for the given `store` key is updated, or new data is created.
1059
+ - An event (of type `store:updated`) is immediately `emit`ted on an internal event bus (from `@asaidimu/events`). This event includes the `instanceId` of the updater, the `storageKey`/`store` identifying the data, and the new `state`. For `EphemeralPersistence`, a `timestamp` is also included for LWW resolution. This event is broadcast to other browser tabs via `BroadcastChannel` (managed by `@asaidimu/events`).
1060
+ - For `localStorage`, native `StorageEvent`s also trigger when the value is set from another tab. The `WebStoragePersistence` adapter listens for these native events and re-emits them on its internal event bus, ensuring consistent notification pathways.
902
1061
  2. **Getting State (`get()`):**
903
- * The adapter retrieves the serialized data using the configured `storageKey` or `store` from the underlying storage.
904
- * It attempts to parse/deserialize the data back into the original `T` type (e.g., `JSON.parse` or direct access).
905
- * Returns the deserialized data, or `null` if the data is not found or cannot be parsed.
1062
+ - The adapter retrieves the serialized data using the configured `storageKey` or `store` from the underlying storage.
1063
+ - It attempts to parse/deserialize the data back into the original `T` type (e.g., `JSON.parse` or direct access).
1064
+ - Returns the deserialized data, or `null` if the data is not found or cannot be parsed.
906
1065
  3. **Subscribing to Changes (`subscribe(instanceId, callback)`):**
907
- * A consumer instance registers a `callback` function with its unique `instanceId` to listen for `store:updated` events on the internal event bus.
908
- * When an `store:updated` event is received, the adapter checks if the `instanceId` of the event's source matches the `instanceId` of the subscribing instance.
909
- * The `callback` is invoked *only if* the `instanceId` of the update source does *not* match the `instanceId` of the subscribing instance. This crucial filtering prevents self-triggered loops (where an instance updates its own state, receives its own update notification, and attempts to re-update, leading to an infinite cycle) and ensures the `callback` is exclusively for external changes.
1066
+ - A consumer instance registers a `callback` function with its unique `instanceId` to listen for `store:updated` events on the internal event bus.
1067
+ - When an `store:updated` event is received, the adapter checks if the `instanceId` of the event's source matches the `instanceId` of the subscribing instance.
1068
+ - The `callback` is invoked _only if_ the `instanceId` of the update source does _not_ match the `instanceId` of the subscribing instance. This crucial filtering prevents self-triggered loops (where an instance updates its own state, receives its own update notification, and attempts to re-update, leading to an infinite cycle) and ensures the `callback` is exclusively for external changes.
910
1069
 
911
1070
  ### Extension Points
912
1071
 
@@ -914,9 +1073,9 @@ The library is designed with extensibility in mind. You can implement your own c
914
1073
 
915
1074
  For example, you could create adapters for:
916
1075
 
917
- * **Remote Backend API**: An adapter that persists data to a remote server endpoint via `fetch` or `XMLHttpRequest`, enabling cross-device synchronization.
918
- * **Service Worker Cache API**: Leverage Service Workers for advanced caching strategies, providing highly performant offline capabilities.
919
- * **Custom Local Storage**: Implement a persistence layer over a custom browser extension storage or a file system in an Electron app.
1076
+ - **Remote Backend API**: An adapter that persists data to a remote server endpoint via `fetch` or `XMLHttpRequest`, enabling cross-device synchronization.
1077
+ - **Service Worker Cache API**: Leverage Service Workers for advanced caching strategies, providing highly performant offline capabilities.
1078
+ - **Custom Local Storage**: Implement a persistence layer over a custom browser extension storage or a file system in an Electron app.
920
1079
 
921
1080
  ---
922
1081
 
@@ -941,48 +1100,48 @@ We welcome contributions to `@asaidimu/utils-persistence`! Please follow these g
941
1100
 
942
1101
  The following `bun` scripts are available for development within the `src/persistence` directory (or can be run from the monorepo root if configured):
943
1102
 
944
- * `bun run build` (or `npm run build`): Compiles the TypeScript source files to JavaScript.
945
- * `bun run test` (or `npm run test`): Runs the test suite using `vitest`.
946
- * `bun run test:watch` (or `npm run test:watch`): Runs tests in watch mode, re-running on file changes.
947
- * `bun run lint` (or `npm run lint`): Lints the codebase using ESLint to identify potential issues and enforce coding standards.
948
- * `bun run format` (or `npm run format`): Formats the code using Prettier to ensure consistent code style.
1103
+ - `bun run build` (or `npm run build`): Compiles the TypeScript source files to JavaScript.
1104
+ - `bun run test` (or `npm run test`): Runs the test suite using `vitest`.
1105
+ - `bun run test:watch` (or `npm run test:watch`): Runs tests in watch mode, re-running on file changes.
1106
+ - `bun run lint` (or `npm run lint`): Lints the codebase using ESLint to identify potential issues and enforce coding standards.
1107
+ - `bun run format` (or `npm run format`): Formats the code using Prettier to ensure consistent code style.
949
1108
 
950
1109
  ### Testing
951
1110
 
952
1111
  Tests are crucial for maintaining the quality and stability of the library. The project uses `vitest` for testing.
953
1112
 
954
- * To run all tests:
955
- ```bash
956
- bun test
957
- ```
958
- * To run tests in watch mode during development:
959
- ```bash
960
- bun test:watch
961
- ```
962
- * Ensure that your changes are covered by new or existing tests, and that all tests pass before submitting a pull request. The `fixtures.ts` file provides a generic test suite (`testSimplePersistence`) for any `SimplePersistence` implementation, ensuring consistent behavior across adapters.
1113
+ - To run all tests:
1114
+ ```bash
1115
+ bun test
1116
+ ```
1117
+ - To run tests in watch mode during development:
1118
+ ```bash
1119
+ bun test:watch
1120
+ ```
1121
+ - Ensure that your changes are covered by new or existing tests, and that all tests pass before submitting a pull request. The `fixtures.ts` file provides a generic test suite (`testSimplePersistence`) for any `SimplePersistence` implementation, ensuring consistent behavior across adapters.
963
1122
 
964
1123
  ### Contributing Guidelines
965
1124
 
966
- * **Fork the repository** and create your branch from `main`.
967
- * **Follow existing coding standards**: Adhere to the TypeScript, ESLint, and Prettier configurations defined in the project.
968
- * **Commit messages**: Use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for clear and consistent commit history (e.g., `feat(persistence): add new adapter`, `fix(webstorage): resolve subscription issue`, `docs(readme): update usage examples`).
969
- * **Pull Requests**:
970
- * Open a pull request against the `main` branch.
971
- * Provide a clear and detailed description of your changes, including the problem solved and the approach taken.
972
- * Reference any related issues (e.g., `Closes #123`).
973
- * Ensure all tests pass and the code is lint-free before submitting.
974
- * **Code Review**: Be open to feedback and suggestions during the code review process.
1125
+ - **Fork the repository** and create your branch from `main`.
1126
+ - **Follow existing coding standards**: Adhere to the TypeScript, ESLint, and Prettier configurations defined in the project.
1127
+ - **Commit messages**: Use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for clear and consistent commit history (e.g., `feat(persistence): add new adapter`, `fix(webstorage): resolve subscription issue`, `docs(readme): update usage examples`).
1128
+ - **Pull Requests**:
1129
+ - Open a pull request against the `main` branch.
1130
+ - Provide a clear and detailed description of your changes, including the problem solved and the approach taken.
1131
+ - Reference any related issues (e.g., `Closes #123`).
1132
+ - Ensure all tests pass and the code is lint-free before submitting.
1133
+ - **Code Review**: Be open to feedback and suggestions during the code review process.
975
1134
 
976
1135
  ### Issue Reporting
977
1136
 
978
1137
  If you find a bug or have a feature request, please open an issue on our [GitHub Issues page](https://github.com/asaidimu/erp-utils/issues). When reporting a bug, please include:
979
1138
 
980
- * A clear, concise title.
981
- * Steps to reproduce the issue.
982
- * Expected behavior.
983
- * Actual behavior.
984
- * Your environment (browser version, Node.js version, `@asaidimu/utils-persistence` version).
985
- * Any relevant code snippets or error messages.
1139
+ - A clear, concise title.
1140
+ - Steps to reproduce the issue.
1141
+ - Expected behavior.
1142
+ - Actual behavior.
1143
+ - Your environment (browser version, Node.js version, `@asaidimu/utils-persistence` version).
1144
+ - Any relevant code snippets or error messages.
986
1145
 
987
1146
  ---
988
1147
 
@@ -990,36 +1149,37 @@ If you find a bug or have a feature request, please open an issue on our [GitHub
990
1149
 
991
1150
  ### Troubleshooting
992
1151
 
993
- * **Storage Limits**: Be aware that browser storage mechanisms have size limitations. `localStorage` and `sessionStorage` typically offer 5-10 MB, while `IndexedDB` can store much larger amounts (often gigabytes, depending on browser and available disk space). For large data sets, always prefer `IndexedDBPersistence`.
994
- * **JSON Parsing Errors**: `WebStoragePersistence` and `IndexedDBPersistence` (for the `data` field) serialize and deserialize your data using `JSON.stringify` and `JSON.parse`. `EphemeralPersistence` uses `structuredClone`. Ensure that the `state` object you are passing to `set` is a valid JSON-serializable object (i.e., it doesn't contain circular references, functions, or Symbols that JSON cannot handle).
995
- * **Cross-Origin Restrictions**: Browser storage is typically restricted to the same origin (protocol, host, port). You cannot access data stored by a different origin. Ensure your application is running on the same origin across all tabs for cross-tab synchronization to work.
996
- * **`IndexedDBPersistence` Asynchronous Nature**: Remember that `IndexedDBPersistence` methods (`set`, `get`, `clear`, `close`) return `Promise`s. Always use `await` or `.then()` to handle their results and ensure operations complete before proceeding. Forgetting to `await` can lead to unexpected behavior or race conditions.
997
- * **`EphemeralPersistence` Data Loss**: Data stored with `EphemeralPersistence` is *not* persisted across page reloads or browser restarts. It is strictly an in-memory solution synchronized across active tabs for the current browsing session. If you need data to survive refreshes, use `WebStoragePersistence` or `IndexedDBPersistence`.
998
- * **`instanceId` Usage**: The `instanceId` parameter in `set` and `subscribe` is crucial for distinguishing between local updates and updates from other instances. Ensure each tab/instance of your application generates a *unique* ID (e.g., a UUID, like `uuidv4()`) upon startup and consistently uses it for all `set` and `subscribe` calls from that instance. This ID is *not* persisted to storage; it's ephemeral for the current session of that tab.
999
- * **`subscribe` Callback Not Firing**: The most common reason for this is attempting to trigger the callback from the *same* `instanceId` that subscribed. The `subscribe` method deliberately filters out updates from the `instanceId` that subscribed to prevent infinite loops and ensure efficient state reconciliation. To test cross-instance synchronization, open two browser tabs to your application, or ensure two different logical instances are created (each with a unique `instanceId`). Perform a `set` operation in one instance using its unique `instanceId`; the `subscribe` callback in the *other* instance (with a different `instanceId`) should then fire.
1000
- * **Data Migration (`onUpgrade`):** When implementing `onUpgrade`, remember that the `data` parameter can be `null` if no prior state was found. Your handler should gracefully manage this, potentially returning a default state for the new version. The returned `version` in the `onUpgrade` handler *must* match the `config.version` provided to the persistence constructor.
1152
+ - **Storage Limits**: Be aware that browser storage mechanisms have size limitations. `localStorage` and `sessionStorage` typically offer 5-10 MB, while `IndexedDB` can store much larger amounts (often gigabytes, depending on browser and available disk space). For large data sets, always prefer `IndexedDBPersistence`.
1153
+ - **JSON Parsing Errors**: `WebStoragePersistence` and `IndexedDBPersistence` (for the `data` field) serialize and deserialize your data using `JSON.stringify` and `JSON.parse`. `EphemeralPersistence` uses `structuredClone`. Ensure that the `state` object you are passing to `set` is a valid JSON-serializable object (i.e., it doesn't contain circular references, functions, or Symbols that JSON cannot handle).
1154
+ - **Cross-Origin Restrictions**: Browser storage is typically restricted to the same origin (protocol, host, port). You cannot access data stored by a different origin. Ensure your application is running on the same origin across all tabs for cross-tab synchronization to work.
1155
+ - **`IndexedDBPersistence` Asynchronous Nature**: Remember that `IndexedDBPersistence` methods (`set`, `get`, `clear`, `close`) return `Promise`s. Always use `await` or `.then()` to handle their results and ensure operations complete before proceeding. Forgetting to `await` can lead to unexpected behavior or race conditions.
1156
+ - **`EphemeralPersistence` Data Loss**: Data stored with `EphemeralPersistence` is _not_ persisted across page reloads or browser restarts. It is strictly an in-memory solution synchronized across active tabs for the current browsing session. If you need data to survive refreshes, use `WebStoragePersistence` or `IndexedDBPersistence`.
1157
+ - **`instanceId` Usage**: The `instanceId` parameter in `set` and `subscribe` is crucial for distinguishing between local updates and updates from other instances. Ensure each tab/instance of your application generates a _unique_ ID (e.g., a UUID, like `uuidv4()`) upon startup and consistently uses it for all `set` and `subscribe` calls from that instance. This ID is _not_ persisted to storage; it's ephemeral for the current session of that tab.
1158
+ - **`subscribe` Callback Not Firing**: The most common reason for this is attempting to trigger the callback from the _same_ `instanceId` that subscribed. The `subscribe` method deliberately filters out updates from the `instanceId` that subscribed to prevent infinite loops and ensure efficient state reconciliation. To test cross-instance synchronization, open two browser tabs to your application, or ensure two different logical instances are created (each with a unique `instanceId`). Perform a `set` operation in one instance using its unique `instanceId`; the `subscribe` callback in the _other_ instance (with a different `instanceId`) should then fire.
1159
+ - **Data Migration (`onUpgrade`):** When implementing `onUpgrade`, remember that the `data` parameter can be `null` if no prior state was found. Your handler should gracefully manage this, potentially returning a default state for the new version. The returned `version` in the `onUpgrade` handler _must_ match the `config.version` provided to the persistence constructor.
1001
1160
 
1002
1161
  ### FAQ
1003
1162
 
1004
1163
  **Q: What's the difference between `store` (or `storageKey`) and `instanceId`?**
1005
- A: `store` (for `IndexedDBPersistence` and `EphemeralPersistence`) or `storageKey` (for `WebStoragePersistence`) is the name or identifier of the *data set* or "document" you are persisting (e.g., `'user-profile'`, `'shopping-cart'`, `'all-products-inventory'`). It's the key under which the data itself is stored.
1006
- `instanceId` is a unique identifier for the *browser tab, application window, or a specific component instance* currently interacting with the store. It's an ephemeral ID (e.g., a UUID generated at app startup) that helps the `subscribe` method differentiate between updates originating from the current instance versus those coming from *other* instances (tabs, windows, or distinct components) of your application, preventing self-triggered loops in your state management.
1164
+ A: `store` (for `IndexedDBPersistence` and `EphemeralPersistence`) or `storageKey` (for `WebStoragePersistence`) is the name or identifier of the _data set_ or "document" you are persisting (e.g., `'user-profile'`, `'shopping-cart'`, `'all-products-inventory'`). It's the key under which the data itself is stored.
1165
+ `instanceId` is a unique identifier for the _browser tab, application window, or a specific component instance_ currently interacting with the store. It's an ephemeral ID (e.g., a UUID generated at app startup) that helps the `subscribe` method differentiate between updates originating from the current instance versus those coming from _other_ instances (tabs, windows, or distinct components) of your application, preventing self-triggered loops in your state management.
1007
1166
 
1008
1167
  **Q: Why is `IndexedDBPersistence` asynchronous, but `WebStoragePersistence` and `EphemeralPersistence` are synchronous?**
1009
- A: `WebStoragePersistence` utilizes `localStorage` and `sessionStorage`, which are inherently synchronous APIs in web browsers. This means operations block the main thread until they complete. Similarly, `EphemeralPersistence` is an in-memory solution, and its direct data access is synchronous. In contrast, `IndexedDB` is an asynchronous API by design to avoid blocking the main thread, which is especially important when dealing with potentially large datasets or complex queries. Our `IndexedDBPersistence` wrapper naturally exposes this asynchronous behavior through Promises to align with best practices for non-blocking operations. All three adapters use asynchronous mechanisms for *cross-tab synchronization*.
1168
+ A: `WebStoragePersistence` utilizes `localStorage` and `sessionStorage`, which are inherently synchronous APIs in web browsers. This means operations block the main thread until they complete. Similarly, `EphemeralPersistence` is an in-memory solution, and its direct data access is synchronous. In contrast, `IndexedDB` is an asynchronous API by design to avoid blocking the main thread, which is especially important when dealing with potentially large datasets or complex queries. Our `IndexedDBPersistence` wrapper naturally exposes this asynchronous behavior through Promises to align with best practices for non-blocking operations. All three adapters use asynchronous mechanisms for _cross-tab synchronization_.
1010
1169
 
1011
1170
  **Q: Can I use this library in a Node.js environment?**
1012
1171
  A: No, this library is specifically designed for browser environments. It relies heavily on browser-specific global APIs such as `window.localStorage`, `window.sessionStorage`, `indexedDB`, and `BroadcastChannel` (for cross-tab synchronization), none of which are available in a standard Node.js runtime.
1013
1172
 
1014
1173
  **Q: My `subscribe` callback isn't firing, even when I change data.**
1015
- A: The most common reason for this is attempting to trigger the callback from the *same* `instanceId` that subscribed. The `subscribe` method deliberately filters out updates from the `instanceId` that subscribed to prevent infinite loops and ensure efficient state reconciliation. To test cross-instance synchronization, open two browser tabs to your application, or ensure two different logical instances are created (each with a unique `instanceId`). Perform a `set` operation in one instance using its unique `instanceId`; the `subscribe` callback in the *other* instance (with a different `instanceId`) should then fire.
1174
+ A: The most common reason for this is attempting to trigger the callback from the _same_ `instanceId` that subscribed. The `subscribe` method deliberately filters out updates from the `instanceId` that subscribed to prevent infinite loops and ensure efficient state reconciliation. To test cross-instance synchronization, open two browser tabs to your application, or ensure two different logical instances are created (each with a unique `instanceId`). Perform a `set` operation in one instance using its unique `instanceId`; the `subscribe` callback in the _other_ instance (with a different `instanceId`) should then fire.
1016
1175
 
1017
1176
  **Q: When should I choose `EphemeralPersistence` over `WebStoragePersistence` or `IndexedDBPersistence`?**
1018
1177
  A: Choose `EphemeralPersistence` when:
1019
- * You need cross-tab synchronized state that **does not need to persist across page reloads**.
1020
- * The data is transient and only relevant for the current browsing session.
1021
- * You want very fast in-memory operations.
1022
- Use `WebStoragePersistence` for small, persistent key-value data, and `IndexedDBPersistence` for larger, structured data that needs to persist reliably.
1178
+
1179
+ - You need cross-tab synchronized state that **does not need to persist across page reloads**.
1180
+ - The data is transient and only relevant for the current browsing session.
1181
+ - You want very fast in-memory operations.
1182
+ Use `WebStoragePersistence` for small, persistent key-value data, and `IndexedDBPersistence` for larger, structured data that needs to persist reliably.
1023
1183
 
1024
1184
  ### Changelog
1025
1185
 
@@ -1033,6 +1193,6 @@ This project is licensed under the MIT License. See the [LICENSE](https://github
1033
1193
 
1034
1194
  This library leverages and builds upon the excellent work from:
1035
1195
 
1036
- * [`@asaidimu/events`](https://github.com/asaidimu/events): For robust cross-tab event communication and asynchronous event bus capabilities.
1037
- * [`@asaidimu/indexed`](https://github.com/asaidimu/indexed): For providing a simplified and promise-based interface for IndexedDB interactions.
1038
- * [`@asaidimu/query`](https://github.com/asaidimu/query): For offering a declarative query builder used internally with IndexedDB operations.
1196
+ - [`@asaidimu/events`](https://github.com/asaidimu/events): For robust cross-tab event communication and asynchronous event bus capabilities.
1197
+ - [`@asaidimu/indexed`](https://github.com/asaidimu/indexed): For providing a simplified and promise-based interface for IndexedDB interactions.
1198
+ - [`@asaidimu/query`](https://github.com/asaidimu/query): For offering a declarative query builder used internally with IndexedDB operations.