@asaidimu/utils-persistence 2.0.4 → 2.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
@@ -3,8 +3,8 @@
3
3
  **Robust Data Persistence for Web Applications**
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@asaidimu/utils-persistence.svg?style=flat-square)](https://www.npmjs.com/package/@asaidimu/utils-persistence)
6
- [![License](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](LICENSE)
7
- [![Build Status](https://img.shields.io/badge/Build-Passing-brightgreen?style=flat-square)](https://github.com/asaidimu/erp-utils-actions)
6
+ [![License](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](https://github.com/asaidimu/erp-utils/blob/main/LICENSE)
7
+ [![Build Status](https://img.shields.io/badge/Build-Passing-brightgreen?style=flat-square)](https://github.com/asaidimu/erp-utils/actions)
8
8
 
9
9
  ---
10
10
 
@@ -29,7 +29,7 @@
29
29
 
30
30
  ## ✨ Overview & Features
31
31
 
32
- `@asaidimu/utils-persistence` is a lightweight, type-safe library designed to simplify robust data persistence in modern web applications. It provides a unified, asynchronous-friendly API for interacting with various browser storage mechanisms, including `localStorage`, `sessionStorage`, `IndexedDB`, and even an in-memory store. A core strength of this library is its built-in support for **cross-instance synchronization**, ensuring that state changes made in one browser tab, window, or even a logically separate component instance within the same tab, are automatically reflected and propagated to other active instances using the same persistence mechanism. This enables seamless, real-time data consistency across your application.
32
+ `@asaidimu/utils-persistence` is a lightweight, type-safe TypeScript library designed to simplify robust data persistence in modern web applications. It provides a unified, asynchronous-friendly API for interacting with various browser storage mechanisms, including `localStorage`, `sessionStorage`, `IndexedDB`, and even an in-memory store. A core strength of this library is its built-in support for **cross-instance synchronization**, ensuring that state changes made in one browser tab, window, or even a logically separate component instance within the same tab, are automatically reflected and propagated to other active instances using the same persistence mechanism. This enables seamless, real-time data consistency across your application.
33
33
 
34
34
  This library is ideal for single-page applications (SPAs) that require robust state management, offline capabilities, or seamless data synchronization across multiple browser instances. By abstracting away the complexities of different storage APIs and handling synchronization, it allows developers to focus on application logic rather than intricate persistence details. It integrates smoothly with popular state management libraries or can be used standalone for direct data access.
35
35
 
@@ -41,6 +41,7 @@ This library is ideal for single-page applications (SPAs) that require robust st
41
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
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
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.
44
45
  * **Type-Safe**: Fully written in TypeScript, providing strong typing, compile-time checks, and autocompletion for a better developer experience.
45
46
  * **Lightweight & Minimal Dependencies**: Designed to be small and efficient, relying on a few focused internal utilities (`@asaidimu/events`, `@asaidimu/indexed`, `@asaidimu/query`).
46
47
  * **Robust Error Handling**: Includes internal error handling for common storage operations, providing clearer debugging messages when issues arise.
@@ -86,26 +87,48 @@ npm install uuid
86
87
 
87
88
  ### Configuration
88
89
 
89
- This library does not require global configuration. All settings are passed directly to the constructor of the respective persistence adapter during instantiation. For instance, `IndexedDBPersistence` requires a configuration object for its database and collection details.
90
+ This library does not require global configuration. All settings are passed directly to the constructor of the respective persistence adapter during instantiation. Each adapter's constructor expects a `StoreConfig<T>` object, potentially extended with adapter-specific options (e.g., `storageKey` for `WebStoragePersistence` or `database`/`collection` for `IndexedDBPersistence`).
91
+
92
+ ```typescript
93
+ // The base configuration for any persistence store
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;
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;
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
+ ) => { state: T | null; version: string };
116
+ }
117
+ ```
90
118
 
91
119
  ### Verification
92
120
 
93
121
  You can quickly verify the installation by attempting to import one of the classes:
94
122
 
95
123
  ```typescript
96
- // Import in your application code (e.g., an entry point or component)
97
124
  import { WebStoragePersistence, IndexedDBPersistence, EphemeralPersistence } from '@asaidimu/utils-persistence';
98
125
 
99
126
  console.log('Persistence modules loaded successfully!');
100
127
 
101
128
  // You can now create instances:
102
- // const myLocalStorageStore = new WebStoragePersistence<{ appState: string }>('my-app-state');
103
- // const myIndexedDBStore = new IndexedDBPersistence<{ userId: string; data: any }>({
104
- // store: 'user-settings',
105
- // database: 'app-db',
106
- // collection: 'settings'
107
- // });
108
- // const myEphemeralStore = new EphemeralPersistence<{ sessionCount: number }>('session-counter');
129
+ // const myLocalStorageStore = new WebStoragePersistence<{ appState: string }>(/* config */);
130
+ // const myIndexedDBStore = new IndexedDBPersistence<{ userId: string; data: any }>(/* config */);
131
+ // const myEphemeralStore = new EphemeralPersistence<{ sessionCount: number }>(/* config */);
109
132
  ```
110
133
 
111
134
  ---
@@ -155,16 +178,28 @@ export interface SimplePersistence<T> {
155
178
  * @returns `true` if the operation was successful, `false` if an error occurred. For asynchronous implementations, this returns a `Promise<boolean>`.
156
179
  */
157
180
  clear(): boolean | Promise<boolean>;
181
+
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 };
158
193
  }
159
194
  ```
160
195
 
161
196
  ### The Power of Adapters
162
197
  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:
163
- - **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.
164
- - **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.
165
- - **Extensibility**: Easily create custom adapters for new storage technologies or environments (e.g., file systems, serverless functions) while adhering to the same interface.
166
- - **Environment-Agnostic**: Use the same interface in browser, server, or hybrid applications, supporting diverse use cases like offline-first apps or cross-tab synchronization.
167
- - **Testing Simplicity**: Implement mock adapters for testing, isolating persistence logic without touching real storage.
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.
168
203
 
169
204
  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.
170
205
 
@@ -187,97 +222,169 @@ It enables robust **multi-instance synchronization**. When multiple instances (e
187
222
  * **Identify the source of a change:** When `set(id, state)` is called, the layer knows *which* instance initiated the save.
188
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.
189
224
 
190
- #### 2. Usage Guidelines (For those consuming the `SimplePersistence` interface)
225
+ #### 2. Practical Examples for Consuming `SimplePersistence`
191
226
 
192
227
  ##### 2.1 Generating and Managing the Consumer `instanceId`
193
228
 
194
- * **Generate Once:** Create a unique UUID for your consumer instance at its very beginning (e.g., when your main application component mounts or your service initializes).
195
- ```typescript
196
- import { v4 as uuidv4 } from 'uuid'; // Requires 'uuid' library to be installed: `bun add uuid`
229
+ Generate a unique UUID for your consumer instance at its very beginning (e.g., when your main application component mounts or your service initializes).
197
230
 
198
- interface MyAppState {
199
- data: string;
200
- lastUpdated: number;
201
- }
231
+ ```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';
202
234
 
203
- class MyAppComponent {
204
- private instanceId: string;
205
- private persistence: SimplePersistence<MyAppState>;
206
- private unsubscribe: (() => void) | null = null; // To store the unsubscribe function
207
- private appState: MyAppState = { data: 'initial', lastUpdated: Date.now() };
235
+ interface MyAppState {
236
+ data: string;
237
+ count: number;
238
+ lastUpdated: number;
239
+ }
208
240
 
209
- constructor(persistenceAdapter: SimplePersistence<MyAppState>) {
210
- this.instanceId = uuidv4(); // Generate unique ID for this app/tab instance
211
- this.persistence = persistenceAdapter;
212
- this.initializePersistence();
213
- }
241
+ class MyAppComponent {
242
+ private instanceId: string;
243
+ private persistence: SimplePersistence<MyAppState>;
244
+ private unsubscribe: (() => void) | null = null;
245
+ private appState: MyAppState = { data: 'initial', count: 0, lastUpdated: Date.now() };
214
246
 
215
- private async initializePersistence() {
216
- // Load initial state
217
- const storedState = await this.persistence.get();
218
- if (storedState) {
219
- console.log(`Instance ${this.instanceId}: Loaded initial state.`, storedState);
220
- this.appState = storedState; // Update your app's internal state with loaded data
221
- }
222
-
223
- // Subscribe to changes from other instances
224
- this.unsubscribe = this.persistence.subscribe(this.instanceId, (newState) => {
225
- console.log(`Instance ${this.instanceId}: Received global state update from another instance.`, newState);
226
- // Crucial: Update your local application state based on this shared change
227
- this.appState = newState;
228
- });
229
- }
247
+ constructor(persistenceAdapter: SimplePersistence<MyAppState>) {
248
+ this.instanceId = uuidv4(); // Generate unique ID for this app/tab instance
249
+ this.persistence = persistenceAdapter;
250
+ this.initializePersistence();
251
+ }
230
252
 
231
- // Call this when the component/app instance is being destroyed or unmounted
232
- cleanup() {
233
- if (this.unsubscribe) {
234
- this.unsubscribe(); // Stop listening to updates
235
- console.log(`Instance ${this.instanceId}: Unsubscribed from updates.`);
236
- }
253
+ private async initializePersistence() {
254
+ try {
255
+ // Load initial state
256
+ const storedState = await this.persistence.get();
257
+ if (storedState) {
258
+ console.log(`Instance ${this.instanceId}: Loaded initial state.`, storedState);
259
+ this.appState = storedState; // Update your app's internal state with loaded data
260
+ } else {
261
+ console.log(`Instance ${this.instanceId}: No initial state found. Using default.`);
262
+ // Optionally, persist default state if none exists
263
+ await this.persistence.set(this.instanceId, this.appState);
237
264
  }
238
265
 
239
- async updateAppState(newData: string) {
240
- this.appState = { ...this.appState, data: newData, lastUpdated: Date.now() };
241
- console.log(`Instance ${this.instanceId}: Attempting to save new state:`, this.appState);
242
- const success = await this.persistence.set(this.instanceId, this.appState);
243
- if (!success) {
244
- console.error(`Instance ${this.instanceId}: Failed to save app state.`);
245
- } else {
246
- console.log(`Instance ${this.instanceId}: State saved successfully.`);
247
- }
248
- }
266
+ // 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
+ });
273
+ console.log(`Instance ${this.instanceId}: Subscribed to updates.`);
274
+ } catch (error) {
275
+ console.error(`Instance ${this.instanceId}: Error during persistence initialization:`, error);
276
+ }
277
+ this.render();
278
+ }
249
279
 
250
- getCurrentState(): MyAppState {
251
- return this.appState;
252
- }
280
+ // Simulate a component render
281
+ private render() {
282
+ const outputElement = document.getElementById('app-output');
283
+ if (outputElement) {
284
+ outputElement.innerHTML = `
285
+ <p><strong>Instance ID:</strong> ${this.instanceId}</p>
286
+ <p><strong>App State:</strong> ${JSON.stringify(this.appState, null, 2)}</p>
287
+ <p><strong>Persistence Stats:</strong> ${JSON.stringify(this.persistence.stats(), null, 2)}</p>
288
+ `;
253
289
  }
290
+ }
254
291
 
255
- // Example of how to use:
256
- // const webStore = new WebStoragePersistence<MyAppState>('my-shared-app-state');
257
- // const appInstance = new MyAppComponent(webStore);
292
+ // Call this when the component/app instance is being destroyed or unmounted
293
+ cleanup() {
294
+ if (this.unsubscribe) {
295
+ this.unsubscribe(); // Stop listening to updates
296
+ console.log(`Instance ${this.instanceId}: Unsubscribed from updates.`);
297
+ this.unsubscribe = null;
298
+ }
299
+ }
258
300
 
259
- // // Simulate an update from this instance
260
- // appInstance.updateAppState('New data from tab 1');
301
+ 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);
304
+ const success = await this.persistence.set(this.instanceId, this.appState);
305
+ if (!success) {
306
+ console.error(`Instance ${this.instanceId}: Failed to save app state.`);
307
+ } else {
308
+ console.log(`Instance ${this.instanceId}: State saved successfully.`);
309
+ }
310
+ this.render(); // Render local changes immediately
311
+ }
261
312
 
262
- // // Simulate a cleanup (e.g., when the component unmounts)
263
- // // appInstance.cleanup();
264
- ```
313
+ async clearAppState() {
314
+ console.log(`Instance ${this.instanceId}: Attempting to clear app state.`);
315
+ const success = await this.persistence.clear();
316
+ if (!success) {
317
+ console.error(`Instance ${this.instanceId}: Failed to clear app state.`);
318
+ } else {
319
+ console.log(`Instance ${this.instanceId}: App state cleared.`);
320
+ this.appState = { data: 'cleared', count: 0, lastUpdated: Date.now() }; // Reset local state
321
+ }
322
+ this.render();
323
+ }
324
+ }
325
+
326
+ // Example of how to use this in an HTML environment:
327
+ // Add a div with id="app-output" and some buttons to trigger actions.
328
+ /*
329
+ document.body.innerHTML = `
330
+ <div id="app-output">Loading...</div>
331
+ <button id="update-btn">Update State (Local)</button>
332
+ <button id="clear-btn">Clear State (Global)</button>
333
+ <button id="close-btn">Cleanup Instance</button>
334
+ `;
335
+
336
+ const config: StoreConfig<MyAppState> = {
337
+ version: "1.0.0",
338
+ app: "my-first-app",
339
+ onUpgrade: ({ data, version, app }) => {
340
+ console.log(`Migrating data for ${app} from version ${version} to 1.0.0. Current data:`, data);
341
+ // Simple example: if data was older, initialize count
342
+ return { state: data ? { ...data, count: data.count ?? 0 } : { data: 'migrated-default', count: 0, lastUpdated: Date.now() }, version: "1.0.0" };
343
+ }
344
+ };
345
+
346
+ const webStore = new WebStoragePersistence<MyAppState>({ ...config, storageKey: 'my-shared-app-state' });
347
+ const appInstance = new MyAppComponent(webStore);
348
+
349
+ document.getElementById('update-btn')?.addEventListener('click', () => {
350
+ const newData = prompt('Enter new data:', appInstance.getCurrentState().data);
351
+ if (newData !== null) {
352
+ appInstance.updateAppState(newData);
353
+ }
354
+ });
355
+ document.getElementById('clear-btn')?.addEventListener('click', () => appInstance.clearAppState());
356
+ document.getElementById('close-btn')?.addEventListener('click', () => appInstance.cleanup());
357
+
358
+ // To test cross-tab synchronization:
359
+ // 1. Open this page in two browser tabs.
360
+ // 2. Click "Update State (Local)" in one tab.
361
+ // 3. Observe the "Received global state update from another instance" message in the other tab.
362
+ // 4. Click "Clear State (Global)" in one tab.
363
+ // 5. Observe the state clearing in both tabs.
364
+ */
365
+ ```
265
366
 
266
367
  ##### 2.2 Using `set(id, state)`
267
368
 
268
369
  * Always pass the unique `instanceId` of your consumer when calling `set`.
269
- * The `state` object you pass will overwrite the entire global persisted state. Ensure it contains all necessary data.
370
+ * The `state` object you pass will generally overwrite the entire global persisted state. Ensure it contains all necessary data.
270
371
  ```typescript
271
- import { WebStoragePersistence } from '@asaidimu/utils-persistence';
372
+ import { WebStoragePersistence, StoreConfig } from '@asaidimu/utils-persistence';
272
373
  import { v4 as uuidv4 } from 'uuid';
273
374
 
274
- interface Settings { theme: string; }
275
- const settingsStore = new WebStoragePersistence<Settings>('app-settings');
375
+ interface Settings { theme: string; lastSaved: number; }
276
376
  const myInstanceId = uuidv4();
277
377
 
278
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
+
279
386
  console.log(`Instance ${myInstanceId}: Attempting to save settings.`);
280
- const success = await settingsStore.set(myInstanceId, newSettings);
387
+ const success = await settingsStore.set(myInstanceId, { ...newSettings, lastSaved: Date.now() });
281
388
  if (!success) {
282
389
  console.error(`Instance ${myInstanceId}: Failed to save settings.`);
283
390
  } else {
@@ -286,7 +393,7 @@ It enables robust **multi-instance synchronization**. When multiple instances (e
286
393
  }
287
394
 
288
395
  // Example:
289
- // saveSettings({ theme: 'dark' });
396
+ // saveSettings({ theme: 'dark', lastSaved: 0 });
290
397
  ```
291
398
 
292
399
  ##### 2.3 Using `get()`
@@ -294,10 +401,15 @@ It enables robust **multi-instance synchronization**. When multiple instances (e
294
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.
295
402
  * The returned `T | null` (or `Promise<T | null>`) should be used to initialize or update your application's local state.
296
403
  ```typescript
297
- import { WebStoragePersistence } from '@asaidimu/utils-persistence';
404
+ import { WebStoragePersistence, StoreConfig } from '@asaidimu/utils-persistence';
298
405
 
299
- interface Settings { theme: string; }
300
- const settingsStore = new WebStoragePersistence<Settings>('app-settings');
406
+ interface Settings { theme: string; lastSaved: number; }
407
+ const config: StoreConfig<Settings> & { storageKey: string } = {
408
+ version: "1.0.0",
409
+ app: "app-settings-manager",
410
+ storageKey: 'app-settings'
411
+ };
412
+ const settingsStore = new WebStoragePersistence<Settings>(config);
301
413
 
302
414
  async function retrieveGlobalState() {
303
415
  const storedState = await settingsStore.get(); // Await for async adapters if it returns a Promise
@@ -320,37 +432,58 @@ It enables robust **multi-instance synchronization**. When multiple instances (e
320
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.
321
433
 
322
434
  ```typescript
323
- import { WebStoragePersistence } from '@asaidimu/utils-persistence';
435
+ import { WebStoragePersistence, StoreConfig } from '@asaidimu/utils-persistence';
324
436
  import { v4 as uuidv4 } from 'uuid';
325
437
 
326
438
  interface NotificationState { count: number; lastMessage: string; }
327
- const notificationStore = new WebStoragePersistence<NotificationState>('app-notifications');
328
439
  const myInstanceId = uuidv4();
329
440
 
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);
447
+
330
448
  const unsubscribe = notificationStore.subscribe(myInstanceId, (newState) => {
331
- console.log(`🔔 Received update from another instance:`, newState);
449
+ console.log(`🔔 Instance ${myInstanceId}: Received update from another instance:`, newState);
332
450
  // Update UI or internal state based on `newState`
333
451
  });
334
452
 
453
+ console.log(`Instance ${myInstanceId}: Subscribed to notifications.`);
454
+
335
455
  // To simulate a change from another instance, open a new browser tab
336
- // and run something like:
337
- // const anotherInstanceId = uuidv4();
338
- // const anotherNotificationStore = new WebStoragePersistence<NotificationState>('app-notifications');
339
- // anotherNotificationStore.set(anotherInstanceId, { count: 5, lastMessage: 'New notification!' });
340
-
341
- // When no longer needed:
342
- // unsubscribe();
343
- // console.log("Unsubscribed from notification updates.");
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);
344
472
  ```
345
473
 
346
474
  ##### 2.5 Using `clear()`
347
475
 
348
476
  * `clear()` performs a global reset, completely removing the shared persisted data for *all* instances. Use with caution.
349
477
  ```typescript
350
- import { WebStoragePersistence } from '@asaidimu/utils-persistence';
478
+ import { WebStoragePersistence, StoreConfig } from '@asaidimu/utils-persistence';
351
479
 
352
- interface UserData { username: string; }
353
- const userDataStore = new WebStoragePersistence<UserData>('user-data');
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);
354
487
 
355
488
  async function resetUserData() {
356
489
  console.log("Attempting to clear all persisted user data...");
@@ -385,10 +518,10 @@ To ensure proper use of the `SimplePersistence<T>` interface and prevent misuse,
385
518
 
386
519
  ### Web Storage Persistence (`WebStoragePersistence`)
387
520
 
388
- `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.
521
+ `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.
389
522
 
390
523
  ```typescript
391
- import { WebStoragePersistence } from '@asaidimu/utils-persistence';
524
+ import { WebStoragePersistence, StoreConfig } from '@asaidimu/utils-persistence';
392
525
  import { v4 as uuidv4 } from 'uuid'; // Recommended for generating unique instance IDs
393
526
 
394
527
  interface UserPreferences {
@@ -401,72 +534,112 @@ interface UserPreferences {
401
534
  // This ID is crucial for differentiating self-updates from cross-instance updates.
402
535
  const instanceId = uuidv4();
403
536
 
537
+ // Common Store Configuration
538
+ const commonConfig: StoreConfig<UserPreferences> = {
539
+ version: "1.0.0",
540
+ app: "user-dashboard",
541
+ onUpgrade: ({ data, version, app }) => {
542
+ console.log(`[${app}] Migrating user preferences from version ${version} to 1.0.0.`);
543
+ // Example migration: ensure language is set to default if missing
544
+ if (data && !data.language) {
545
+ return { state: { ...data, language: 'en-US' }, version: "1.0.0" };
546
+ }
547
+ // For WebStoragePersistence, if data is null during upgrade, it means no data existed.
548
+ // We could return a default state here.
549
+ return { state: data || { theme: 'light', notificationsEnabled: true, language: 'en-US' }, version: "1.0.0" };
550
+ }
551
+ };
552
+
404
553
  // 1. Using localStorage (default for persistent data)
405
554
  // Data stored here will persist across browser sessions.
406
- const userPrefsStore = new WebStoragePersistence<UserPreferences>('user-preferences');
555
+ const localStorageConfig = { ...commonConfig, storageKey: 'user-preferences-local' };
556
+ const userPrefsStore = new WebStoragePersistence<UserPreferences>(localStorageConfig);
407
557
 
408
558
  console.log('--- localStorage Example ---');
409
559
 
410
- // Set initial preferences
411
- const initialPrefs: UserPreferences = {
412
- theme: 'dark',
413
- notificationsEnabled: true,
414
- language: 'en-US',
415
- };
416
- const setResult = userPrefsStore.set(instanceId, initialPrefs);
417
- console.log('Preferences set successfully:', setResult); // Expected: true
418
-
419
- // Retrieve data
420
- let currentPrefs = userPrefsStore.get();
421
- console.log('Current preferences:', currentPrefs);
422
- // Expected output: Current preferences: { theme: 'dark', notificationsEnabled: true, language: 'en-US' }
423
-
424
- // Subscribe to changes from *other* tabs/instances.
425
- // Open another browser tab to the same application URL to test this.
426
- const unsubscribePrefs = userPrefsStore.subscribe(instanceId, (newState) => {
427
- console.log('🔔 Preferences updated from another instance:', newState);
428
- // In a real app, you would update your UI or state management system here.
429
- // Example: update application theme based on newState.theme
430
- });
560
+ async function manageLocalStorage() {
561
+ // Set initial preferences
562
+ const initialPrefs: UserPreferences = {
563
+ theme: 'dark',
564
+ notificationsEnabled: true,
565
+ language: 'en-US',
566
+ };
567
+ const setResult = userPrefsStore.set(instanceId, initialPrefs);
568
+ console.log('Preferences set successfully:', setResult); // Expected: true
431
569
 
432
- // To simulate an update from another tab:
433
- // Open a new tab, run this code (generating a *different* instanceId), and call set:
434
- // const anotherInstanceId = uuidv4();
435
- // const anotherPrefsStore = new WebStoragePersistence<UserPreferences>('user-preferences');
436
- // anotherPrefsStore.set(anotherInstanceId, { theme: 'light', notificationsEnabled: false, language: 'es-ES' });
437
- // You would then see the "🔔 Preferences updated from another instance:" message in the first tab.
570
+ // Retrieve data
571
+ let currentPrefs = userPrefsStore.get();
572
+ console.log('Current preferences:', currentPrefs);
438
573
 
439
- // After a while, if no longer needed, unsubscribe
440
- // setTimeout(() => {
441
- // unsubscribePrefs();
442
- // console.log('Unsubscribed from preferences updates.');
443
- // }, 5000);
574
+ // Subscribe to changes from *other* tabs/instances.
575
+ // Open another browser tab to the same application URL to test this.
576
+ const unsubscribePrefs = userPrefsStore.subscribe(instanceId, (newState) => {
577
+ console.log('🔔 Preferences updated from another instance:', newState);
578
+ // In a real app, you would update your UI or state management system here.
579
+ });
580
+ console.log('Subscribed to localStorage updates.');
581
+
582
+ // Simulate an update from another tab:
583
+ // (In a different tab, generate a new instanceId and call set on the same storageKey)
584
+ /*
585
+ const anotherInstanceId_ls = uuidv4();
586
+ const anotherPrefsStore_ls = new WebStoragePersistence<UserPreferences>(localStorageConfig);
587
+ anotherPrefsStore_ls.set(anotherInstanceId_ls, { theme: 'light', notificationsEnabled: false, language: 'es-ES' });
588
+ */
589
+ // 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
444
591
 
445
- // Clear data when no longer needed (e.g., user logs out)
446
- // const clearResult = userPrefsStore.clear();
447
- // console.log('Preferences cleared successfully:', clearResult); // Expected: true
448
- // console.log('Preferences after clear:', userPrefsStore.get()); // Expected: null
592
+ // After a while, if no longer needed, unsubscribe
593
+ // setTimeout(() => {
594
+ // unsubscribePrefs();
595
+ // console.log('Unsubscribed from localStorage preferences updates.');
596
+ // }, 5000);
597
+
598
+ // Clear data when no longer needed (e.g., user logs out)
599
+ const clearResult = userPrefsStore.clear();
600
+ console.log('Preferences cleared successfully:', clearResult); // Expected: true
601
+ console.log('Preferences after clear:', userPrefsStore.get()); // Expected: null
602
+ unsubscribePrefs(); // Unsubscribe immediately after clearing for the example
603
+ }
604
+ manageLocalStorage();
449
605
 
450
606
  console.log('\n--- sessionStorage Example ---');
451
607
 
452
608
  // 2. Using sessionStorage (for session-specific data)
453
609
  // Data stored here will only persist for the duration of the browser tab.
454
610
  // It is cleared when the tab is closed.
455
- const sessionDataStore = new WebStoragePersistence<{ lastVisitedPage: string }>('session-data', true); // Pass `true` for sessionStorage
456
-
457
- sessionDataStore.set(instanceId, { lastVisitedPage: '/dashboard' });
458
- console.log('Session data set:', sessionDataStore.get());
459
- // Expected output: Session data set: { lastVisitedPage: '/dashboard' }
611
+ const sessionStorageConfig = { ...commonConfig, storageKey: 'session-data', session: true };
612
+ const sessionDataStore = new WebStoragePersistence<{ lastVisitedPage: string } & UserPreferences>(sessionStorageConfig);
460
613
 
461
- // sessionStorage also supports cross-tab synchronization via BroadcastChannel
462
- const unsubscribeSession = sessionDataStore.subscribe(instanceId, (newState) => {
463
- console.log('🔔 Session data updated from another instance:', newState);
464
- });
614
+ async function manageSessionStorage() {
615
+ const initialSessionData: { lastVisitedPage: string } & UserPreferences = {
616
+ ...commonConfig.onUpgrade!({ data: null, version: "0.0.0", app: "user-dashboard" }).state!, // Use migrated default
617
+ lastVisitedPage: '/dashboard'
618
+ };
619
+ sessionDataStore.set(instanceId, initialSessionData);
620
+ console.log('Session data set:', sessionDataStore.get());
465
621
 
466
- // To test session storage cross-tab, open two tabs to the same URL,
467
- // set a value in one tab, and the other tab's subscriber will be notified.
468
- // Note: If you close and reopen the tab, sessionStorage is cleared.
469
- // unsubscribeSession();
622
+ // 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.');
627
+
628
+ // To test session storage cross-tab, open two tabs to the same URL,
629
+ // set a value in one tab, and the other tab's subscriber will be notified.
630
+ // Note: If you close and reopen the tab, sessionStorage is cleared.
631
+ /*
632
+ const anotherInstanceId_ss = uuidv4();
633
+ const anotherSessionDataStore_ss = new WebStoragePersistence<{ lastVisitedPage: string } & UserPreferences>(sessionStorageConfig);
634
+ anotherSessionDataStore_ss.set(anotherInstanceId_ss, { ...initialSessionData, lastVisitedPage: '/settings', theme: 'dark' });
635
+ */
636
+ await new Promise(resolve => setTimeout(resolve, 100)); // Allow event propagation
637
+
638
+ // unsubscribeSession();
639
+ // console.log('Unsubscribed from sessionStorage updates.');
640
+ unsubscribeSession(); // Unsubscribe for example cleanup
641
+ }
642
+ manageSessionStorage();
470
643
  ```
471
644
 
472
645
  ### IndexedDB Persistence (`IndexedDBPersistence`)
@@ -474,7 +647,7 @@ const unsubscribeSession = sessionDataStore.subscribe(instanceId, (newState) =>
474
647
  `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.
475
648
 
476
649
  ```typescript
477
- import { IndexedDBPersistence } from '@asaidimu/utils-persistence';
650
+ import { IndexedDBPersistence, StoreConfig, IndexedDBPersistenceConfig } from '@asaidimu/utils-persistence';
478
651
  import { v4 as uuidv4 } from 'uuid'; // Recommended for generating unique instance IDs
479
652
 
480
653
  interface Product {
@@ -482,6 +655,7 @@ interface Product {
482
655
  name: string;
483
656
  price: number;
484
657
  stock: number;
658
+ lastUpdated: number;
485
659
  }
486
660
 
487
661
  // Generate a unique instance ID for this specific browser tab/session/component.
@@ -491,29 +665,48 @@ const instanceId = uuidv4();
491
665
  // A 'store' identifies the specific document/state within the database/collection.
492
666
  // The 'database' is the IndexedDB database name.
493
667
  // The 'collection' is the object store name within that database.
494
- const productCache = new IndexedDBPersistence<Product[]>({
668
+ const indexedDBConfig: StoreConfig<Product[]> & IndexedDBPersistenceConfig = {
669
+ version: "2.0.0", // Let's use a new version for migration example
670
+ app: "product-catalog",
495
671
  store: 'all-products-inventory', // Unique identifier for this piece of data/document
496
672
  database: 'my-app-database', // Name of the IndexedDB database
497
673
  collection: 'app-stores', // Name of the object store (table-like structure)
498
674
  enableTelemetry: false, // Optional: enable telemetry for underlying IndexedDB lib
499
- });
675
+ onUpgrade: ({ data, version, app }) => {
676
+ console.log(`[${app}] Migrating product data from version ${version} to 2.0.0.`);
677
+ if (version === "1.0.0" && data) {
678
+ // 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() }));
680
+ return { state: migratedData, version: "2.0.0" };
681
+ }
682
+ // If no existing data or unknown version, return null or a sensible default
683
+ return { state: data, version: "2.0.0" };
684
+ }
685
+ };
686
+
687
+ const productCache = new IndexedDBPersistence<Product[]>(indexedDBConfig);
500
688
 
501
689
  async function manageProductCache() {
502
690
  console.log('--- IndexedDB Example ---');
503
691
 
504
692
  // Set initial product data - AWAIT the promise!
505
693
  const products: Product[] = [
506
- { id: 'p001', name: 'Laptop', price: 1200, stock: 50 },
507
- { id: 'p002', name: 'Mouse', price: 25, stock: 200 },
694
+ { id: 'p001', name: 'Laptop', price: 1200, stock: 50, lastUpdated: Date.now() },
695
+ { id: 'p002', name: 'Mouse', price: 25, stock: 200, lastUpdated: Date.now() },
508
696
  ];
509
- const setResult = await productCache.set(instanceId, products);
510
- console.log('Products cached successfully:', setResult); // Expected: true
697
+ try {
698
+ const setResult = await productCache.set(instanceId, products);
699
+ console.log('Products cached successfully:', setResult); // Expected: true
700
+ } catch (error) {
701
+ console.error('Failed to set products:', error);
702
+ }
511
703
 
512
704
  // Get data - AWAIT the promise!
513
705
  const cachedProducts = await productCache.get();
514
706
  if (cachedProducts) {
515
707
  console.log('Retrieved products:', cachedProducts);
516
- // Expected output: Retrieved products: [{ id: 'p001', ... }, { id: 'p002', ... }]
708
+ } else {
709
+ console.log('No products found in cache.');
517
710
  }
518
711
 
519
712
  // Subscribe to changes from *other* tabs/instances
@@ -521,21 +714,24 @@ async function manageProductCache() {
521
714
  console.log('🔔 Product cache updated by another instance:', newState);
522
715
  // Refresh your product list in the UI, re-fetch data, etc.
523
716
  });
717
+ console.log('Subscribed to IndexedDB product updates.');
524
718
 
525
719
  // Simulate an update from another instance (e.g., from a different tab)
526
- // In a real scenario, another tab would call `productCache.set` with a different instanceId
527
720
  const updatedProducts: Product[] = [
528
- { id: 'p001', name: 'Laptop', price: 1150, stock: 45 }, // Price and stock updated
529
- { id: 'p002', name: 'Mouse', price: 25, stock: 190 },
530
- { id: 'p003', name: 'Keyboard', price: 75, stock: 150 }, // New 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
531
724
  ];
532
- // Use a *new* instanceId for simulation to trigger the subscriber in the *first* tab
533
- const updateResult = await productCache.set(uuidv4(), updatedProducts);
534
- console.log('Simulated update from another instance:', updateResult);
725
+ try {
726
+ // Use a *new* instanceId for simulation to trigger the subscriber in the *first* tab
727
+ const updateResult = await productCache.set(uuidv4(), updatedProducts);
728
+ console.log('Simulated update from another instance:', updateResult);
729
+ } catch (error) {
730
+ console.error('Failed to simulate product update:', error);
731
+ }
535
732
 
536
733
  // Give time for the event to propagate and be processed by the subscriber
537
734
  await new Promise(resolve => setTimeout(resolve, 100)); // Short delay for async event bus
538
- // You would see "🔔 Product cache updated by another instance:" followed by the updatedProducts
539
735
 
540
736
  // Verify updated data
541
737
  const updatedCachedProducts = await productCache.get();
@@ -546,6 +742,7 @@ async function manageProductCache() {
546
742
  // unsubscribeProducts();
547
743
  // console.log('Unsubscribed from product cache updates.');
548
744
  // }, 5000);
745
+ unsubscribeProducts(); // Unsubscribe for example cleanup
549
746
 
550
747
  // Clear data - AWAIT the promise!
551
748
  // const cleared = await productCache.clear();
@@ -556,24 +753,18 @@ async function manageProductCache() {
556
753
  // or when no more IndexedDB operations are expected across all instances of IndexedDBPersistence.
557
754
  // This is a static method that closes shared database connections managed by the library.
558
755
  // This is generally called once when the application (or a specific database usage) fully shuts down.
559
- // await IndexedDBPersistence.closeAll();
560
- // console.log('All IndexedDB connections closed.');
756
+ // await IndexedDBPersistence.closeAll(); // If SharedResources was exposed publicly.
757
+ // Instead, use the instance's close() method if you want to close that specific database.
758
+ try {
759
+ await productCache.close(); // Closes 'my-app-database'
760
+ console.log('IndexedDB connection for "my-app-database" closed.');
761
+ } catch (error) {
762
+ console.error('Failed to close IndexedDB connection:', error);
763
+ }
561
764
  }
562
765
 
563
766
  // Call the async function to start the example
564
767
  // manageProductCache();
565
-
566
- // You can also close a specific database connection if you have multiple:
567
- // async function closeSpecificDb() {
568
- // const specificDbPersistence = new IndexedDBPersistence<any>({
569
- // store: 'another-store',
570
- // database: 'another-database',
571
- // collection: 'data'
572
- // });
573
- // await specificDbPersistence.close(); // Closes 'another-database'
574
- // console.log('Specific IndexedDB connection closed.');
575
- // }
576
- // closeSpecificDb();
577
768
  ```
578
769
 
579
770
  ### Ephemeral Persistence (`EphemeralPersistence`)
@@ -583,7 +774,7 @@ async function manageProductCache() {
583
774
  This adapter's `set`, `get`, and `clear` operations are synchronous, returning `boolean` values or `T | null` directly.
584
775
 
585
776
  ```typescript
586
- import { EphemeralPersistence } from '@asaidimu/utils-persistence';
777
+ import { EphemeralPersistence, StoreConfig } from '@asaidimu/utils-persistence';
587
778
  import { v4 as uuidv4 } from 'uuid';
588
779
 
589
780
  interface SessionData {
@@ -597,7 +788,17 @@ const instanceId = uuidv4();
597
788
 
598
789
  // Instantiate the Ephemeral store.
599
790
  // The 'storageKey' is a logical key for this specific piece of in-memory data.
600
- const sessionStateStore = new EphemeralPersistence<SessionData>('global-session-state');
791
+ const ephemeralConfig: StoreConfig<SessionData> & { storageKey: string } = {
792
+ version: "1.0.0",
793
+ app: "multi-tab-session",
794
+ storageKey: "global-session-state",
795
+ onUpgrade: ({ data, version, app }) => {
796
+ console.log(`[${app}] Initializing/Migrating ephemeral state from version ${version}.`);
797
+ // 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
+ }
800
+ };
801
+ const sessionStateStore = new EphemeralPersistence<SessionData>(ephemeralConfig);
601
802
 
602
803
  async function manageSessionState() {
603
804
  console.log('--- Ephemeral Persistence Example ---');
@@ -621,9 +822,9 @@ async function manageSessionState() {
621
822
  console.log('🔔 Session state updated from another instance:', newState);
622
823
  // You might update a UI counter, re-render a component, etc.
623
824
  });
825
+ console.log('Subscribed to Ephemeral session state updates.');
624
826
 
625
827
  // Simulate an update from another instance (e.g., from a different tab)
626
- // In a real scenario, another tab would call `sessionStateStore.set` with a different instanceId
627
828
  const updatedData: SessionData = {
628
829
  activeUsers: 2, // User joined in another tab
629
830
  lastActivity: new Date().toISOString(),
@@ -635,7 +836,6 @@ async function manageSessionState() {
635
836
 
636
837
  // Give time for the event to propagate and be processed by the subscriber
637
838
  await new Promise(resolve => setTimeout(resolve, 50)); // Short delay for async event bus
638
- // You would see "🔔 Session state updated from another instance:" followed by the updatedData
639
839
 
640
840
  // Verify updated data locally
641
841
  const updatedCurrentSessionState = sessionStateStore.get();
@@ -646,11 +846,12 @@ async function manageSessionState() {
646
846
  // unsubscribeSessionState();
647
847
  // console.log('Unsubscribed from session state updates.');
648
848
  // }, 5000);
849
+ unsubscribeSessionState(); // Unsubscribe for example cleanup
649
850
 
650
851
  // Clear data: this will also propagate via LWW to other tabs
651
- // const clearResult = sessionStateStore.clear();
652
- // console.log('Session state cleared:', clearResult); // Expected: true
653
- // console.log('Session state after clear:', sessionStateStore.get()); // Expected: null
852
+ const clearResult = sessionStateStore.clear();
853
+ console.log('Session state cleared:', clearResult); // Expected: true
854
+ console.log('Session state after clear:', sessionStateStore.get()); // Expected: null
654
855
  }
655
856
 
656
857
  // Call the async function to start the example
@@ -674,16 +875,16 @@ The `@asaidimu/utils-persistence` library is structured to be modular and extens
674
875
 
675
876
  ### Core Components
676
877
 
677
- * **`SimplePersistence<T>` (in `types.ts`)**: This is the fundamental TypeScript interface that defines the contract for any persistence adapter. It ensures a consistent API for `set`, `get`, `subscribe`, and `clear` operations, regardless of the underlying storage mechanism.
678
- * **`WebStoragePersistence<T>` (in `webstorage.ts`)**:
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>`**:
679
880
  * **Purpose**: Provides simple key-value persistence leveraging the browser's `localStorage` or `sessionStorage` APIs.
680
881
  * **Mechanism**: Directly interacts with `window.localStorage` or `window.sessionStorage`. Data is serialized/deserialized using `JSON.stringify` and `JSON.parse`.
681
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`.
682
- * **`EphemeralPersistence<T>` (in `ephemeral.ts`)**:
883
+ * **`EphemeralPersistence<T>`**:
683
884
  * **Purpose**: Offers an in-memory store for transient data that needs cross-tab synchronization but *not* persistence across page reloads.
684
885
  * **Mechanism**: Stores data in a private class property. Data is cloned using `structuredClone` to prevent direct mutation issues.
685
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.
686
- * **`IndexedDBPersistence<T>` (in `indexedb.ts`)**:
887
+ * **`IndexedDBPersistence<T>`**:
687
888
  * **Purpose**: Provides robust, asynchronous persistence using the browser's `IndexedDB` API, suitable for larger datasets and structured data.
688
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.
689
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.
@@ -694,7 +895,7 @@ The `@asaidimu/utils-persistence` library is structured to be modular and extens
694
895
  ### Data Flow for State Changes
695
896
 
696
897
  1. **Setting State (`set(instanceId, state)`):**
697
- * The provided `state` (of type `T`) is first serialized into a format suitable for the underlying storage (e.g., `JSON.stringify` for web storage, or `structuredClone` for ephemeral, or directly as an object for IndexedDB).
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).
698
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.
699
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`).
700
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.
@@ -738,7 +939,7 @@ We welcome contributions to `@asaidimu/utils-persistence`! Please follow these g
738
939
 
739
940
  ### Scripts
740
941
 
741
- The following `npm` scripts are available for development within the `src/persistence` directory (or can be run from the monorepo root if configured):
942
+ The following `bun` scripts are available for development within the `src/persistence` directory (or can be run from the monorepo root if configured):
742
943
 
743
944
  * `bun run build` (or `npm run build`): Compiles the TypeScript source files to JavaScript.
744
945
  * `bun run test` (or `npm run test`): Runs the test suite using `vitest`.
@@ -796,6 +997,7 @@ If you find a bug or have a feature request, please open an issue on our [GitHub
796
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`.
797
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.
798
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.
799
1001
 
800
1002
  ### FAQ
801
1003
 
@@ -833,4 +1035,4 @@ This library leverages and builds upon the excellent work from:
833
1035
 
834
1036
  * [`@asaidimu/events`](https://github.com/asaidimu/events): For robust cross-tab event communication and asynchronous event bus capabilities.
835
1037
  * [`@asaidimu/indexed`](https://github.com/asaidimu/indexed): For providing a simplified and promise-based interface for IndexedDB interactions.
836
- * [`@asaidimu/query`](https://github.com/asaidimu/query): For offering a declarative query builder used internally with IndexedDB operations.
1038
+ * [`@asaidimu/query`](https://github.com/asaidimu/query): For offering a declarative query builder used internally with IndexedDB operations.