@asaidimu/utils-persistence 2.0.3 → 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 +398 -196
- package/index.d.mts +37 -40
- package/index.d.ts +37 -40
- package/index.js +1 -1
- package/index.mjs +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
**Robust Data Persistence for Web Applications**
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@asaidimu/utils-persistence)
|
|
6
|
-
[](LICENSE)
|
|
7
|
-
[](https://github.com/asaidimu/erp-utils
|
|
6
|
+
[](https://github.com/asaidimu/erp-utils/blob/main/LICENSE)
|
|
7
|
+
[](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.
|
|
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 }>(
|
|
103
|
-
// const myIndexedDBStore = new IndexedDBPersistence<{ userId: string; data: any }>(
|
|
104
|
-
//
|
|
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
|
-
-
|
|
164
|
-
-
|
|
165
|
-
-
|
|
166
|
-
-
|
|
167
|
-
-
|
|
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.
|
|
225
|
+
#### 2. Practical Examples for Consuming `SimplePersistence`
|
|
191
226
|
|
|
192
227
|
##### 2.1 Generating and Managing the Consumer `instanceId`
|
|
193
228
|
|
|
194
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
235
|
+
interface MyAppState {
|
|
236
|
+
data: string;
|
|
237
|
+
count: number;
|
|
238
|
+
lastUpdated: number;
|
|
239
|
+
}
|
|
208
240
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
console.log(`Instance ${this.instanceId}:
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
251
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
260
|
-
|
|
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
|
-
|
|
263
|
-
|
|
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
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
//
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
//
|
|
440
|
-
//
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
//
|
|
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
|
-
//
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
//
|
|
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
|
|
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
|
-
|
|
462
|
-
const
|
|
463
|
-
|
|
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
|
-
//
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
|
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
|
-
|
|
510
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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
|
|
678
|
-
* **`WebStoragePersistence<T
|
|
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
|
|
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
|
|
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,
|
|
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 `
|
|
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.
|