@asaidimu/utils-remote-store 1.0.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 ADDED
@@ -0,0 +1,1475 @@
1
+ # Reactive Remote Store
2
+
3
+ An intelligent, reactive data management library for JavaScript/TypeScript applications, designed for optimal performance, real-time data synchronization, and seamless integration with any remote data source.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@asaidimu/erp-utils-remote-store.svg)](https://www.npmjs.com/package/@asaidimu/erp-utils-remote-store)
6
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
+ [![TypeScript](https://img.shields.io/badge/Built%20with-TypeScript-blue.svg)](https://www.typescriptlang.org/)
8
+
9
+ ---
10
+
11
+ ## 🚀 Quick Links
12
+
13
+ - [Overview & Features](#-overview--features)
14
+ - [Installation & Setup](#-installation--setup)
15
+ - [Prerequisites](#prerequisites)
16
+ - [Installation Steps](#installation-steps)
17
+ - [Configuration](#configuration)
18
+ - [Verification](#verification)
19
+ - [Usage Documentation](#-usage-documentation)
20
+ - [Basic Usage](#basic-usage)
21
+ - [API Usage](#api-usage)
22
+ - [Configuration Examples](#configuration-examples)
23
+ - [Common Use Cases](#common-use-cases)
24
+ - [Project Architecture](#-project-architecture)
25
+ - [Directory Structure](#directory-structure)
26
+ - [Core Components](#core-components)
27
+ - [Data Flow](#data-flow)
28
+ - [Extension Points](#extension-points)
29
+ - [Development & Contributing](#-development--contributing)
30
+ - [Development Setup](#development-setup)
31
+ - [Scripts](#scripts)
32
+ - [Testing](#testing)
33
+ - [Contributing Guidelines](#contributing-guidelines)
34
+ - [Issue Reporting](#issue-reporting)
35
+ - [Additional Information](#-additional-information)
36
+ - [Troubleshooting](#troubleshooting)
37
+ - [FAQ](#faq)
38
+ - [Changelog/Roadmap](#changelogroadmap)
39
+ - [License](#license)
40
+ - [Acknowledgments](#acknowledgments)
41
+
42
+ ---
43
+
44
+ ## 📦 Overview & Features
45
+
46
+ `ReactiveRemoteStore` provides a robust, reactive data management solution for applications that interact with remote APIs. It intelligently handles data fetching, caching, and synchronization, ensuring your application always has access to up-to-date information while minimizing network overhead and improving user experience. By integrating with a `QueryCache` and a `BaseStore` (your API client), it abstracts away the complexities of data lifecycle management, allowing developers to focus on application logic.
47
+
48
+ Key aspects of `ReactiveRemoteStore` include:
49
+
50
+ * **Reactive Queries**: Data is exposed through observable query results that automatically update when the underlying data changes, whether from local mutations or external server notifications.
51
+ * **Intelligent Caching**: It leverages a `QueryCache` to store data, reducing redundant API calls and providing instant access to previously fetched information.
52
+ * **Automatic Invalidation**: Mutations (create, update, delete, upload) automatically trigger intelligent cache invalidation, ensuring data consistency across your application.
53
+ * **Real-time Synchronization**: Through the `BaseStore`'s implementation of `subscribe` and `notify` (which can utilize technologies like Server-Sent Events, WebSockets, or other real-time protocols) and custom event correlators, `ReactiveRemoteStore` can react to external changes pushed from your backend, keeping your client-side data in sync with the server in real-time.
54
+ * **Pluggable `BaseStore`**: The library is agnostic to your specific API client. You provide an implementation of the `BaseStore` interface, allowing `ReactiveRemoteStore` to work with any REST, GraphQL, or custom API.
55
+ * **Data Streaming**: Supports consuming continuous data streams from your backend, ideal for dashboards, live feeds, or large datasets.
56
+
57
+ ### Key Features
58
+
59
+ * **Reactive Data Access**: Consume data through observable queries (`read`, `list`, `find`) that automatically update when data changes.
60
+ * **Query Caching**: Efficiently caches data from `read`, `list`, and `find` operations.
61
+ * **Automatic Invalidation**: Cache entries are intelligently invalidated upon `create`, `update`, `delete`, and `upload` mutations.
62
+ * **Real-time Event Integration**: Reacts to real-time data changes pushed from the `BaseStore` (via `subscribe` and `notify` methods, supporting various protocols like SSE or WebSockets).
63
+ * **Custom Invalidation Logic (Correlators)**: Define custom functions (`Correlator` and `StoreEventCorrelator`) to precisely control which cached queries are invalidated based on mutations or incoming store events.
64
+ * **Data Streaming**: Provides a robust mechanism for consuming real-time data streams from the `BaseStore` via an `AsyncIterable` interface.
65
+ * **Prefetching & Refreshing**: Imperative methods (`prefetch`, `refresh`) to proactively load data or force re-fetches, optimizing user experience.
66
+ * **Type-Safe**: Fully written in TypeScript, providing strong typing for enhanced developer experience, compile-time safety, and autocompletion.
67
+ * **Error Handling**: Centralized error handling for data operations.
68
+
69
+ ---
70
+
71
+ ## 🛠️ Installation & Setup
72
+
73
+ ### Prerequisites
74
+
75
+ * Node.js (v18.x or higher recommended)
76
+ * `bun`, `npm`, or `yarn` package manager
77
+ * A `QueryCache` implementation (e.g., `@core/cache` if available in your project, or a custom one).
78
+ * An implementation of the `BaseStore` interface that connects to your remote data source.
79
+
80
+ ### Installation Steps
81
+
82
+ To install `ReactiveRemoteStore` and its peer dependencies, use your preferred package manager:
83
+
84
+ ```bash
85
+ bun add @asaidimu/erp-utils-remote-store @core/cache
86
+ # or
87
+ npm install @asaidimu/erp-utils-remote-store @core/cache
88
+ # or
89
+ yarn add @asaidimu/erp-utils-remote-store @core/cache
90
+ ```
91
+
92
+ ### Configuration
93
+
94
+ `ReactiveRemoteStore` is initialized with a `QueryCache` instance and an implementation of the `BaseStore` interface. Optionally, you can provide `correlator` and `storeEventCorrelator` functions for advanced invalidation logic.
95
+
96
+ ```typescript
97
+ import { ReactiveRemoteStore } from '@asaidimu/erp-utils-remote-store';
98
+ import { QueryCache } from '@core/cache'; // Your QueryCache implementation
99
+ import { BaseStore, Record, Page, StoreEvent, ActiveQuery, MutationOperation } from '@asaidimu/erp-utils-remote-store/types';
100
+
101
+ // --- Example: Define your data types ---
102
+ interface Product extends Record {
103
+ name: string;
104
+ price: number;
105
+ inStock: boolean;
106
+ }
107
+
108
+ // --- Example: Implement your BaseStore (API client) ---
109
+ // This would typically interact with your backend via fetch, axios, etc.
110
+ class MyApiBaseStore implements BaseStore<Product> {
111
+ private baseUrl: string;
112
+ constructor(baseUrl: string) { this.baseUrl = baseUrl; }
113
+
114
+ async find(options: any): Promise<Page<Product>> { /* ... API call ... */ return { data: [], page: { number: 1, size: 0, count: 0, pages: 0 } }; }
115
+ async read(options: { id: string }): Promise<Product | undefined> { /* ... API call ... */ return undefined; }
116
+ async list(options: any): Promise<Page<Product>> { /* ... API call ... */ return { data: [], page: { number: 1, size: 0, count: 0, pages: 0 } }; }
117
+ async create(props: { data: Omit<Product, "id">; options?: any; }): Promise<Product | undefined> { /* ... API call ... */ return { id: 'new-id', ...props.data }; }
118
+ async update(props: { id: string; data: Partial<Omit<Product, "id">>; options?: any; }): Promise<Product | undefined> { /* ... API call ... */ return { id: props.id, ...props.data }; }
119
+ async delete(options: { id: string; }): Promise<void> { /* ... API call ... */ }
120
+ async upload(props: { file: File; options?: any; }): Promise<Product | undefined> { /* ... API call ... */ return { id: 'uploaded-id', name: 'uploaded', price: 0, inStock: true }; }
121
+
122
+ // For real-time updates, your `BaseStore` implementation would establish and manage
123
+ // a connection using technologies like Server-Sent Events (SSE), WebSockets, or polling.
124
+ // The `ReactiveRemoteStore` simply calls these methods on your `BaseStore`.
125
+ async subscribe(scope: string, callback: (event: StoreEvent) => void): Promise<() => void> {
126
+ console.log(`Subscribing to ${scope}`);
127
+ // Example: Connect to an SSE endpoint (as in test-server.ts)
128
+ // const eventSource = new EventSource(`${this.baseUrl}/events?scope=${scope}`);
129
+ // eventSource.onmessage = (e) => callback(JSON.parse(e.data));
130
+ // return () => eventSource.close();
131
+
132
+ // Example: Using WebSockets
133
+ // const ws = new WebSocket(`${this.baseUrl}/ws?scope=${scope}`);
134
+ // ws.onmessage = (e) => callback(JSON.parse(e.data));
135
+ // return () => ws.close();
136
+
137
+ return async () => { console.log(`Unsubscribed from ${scope}`); };
138
+ }
139
+ async notify(event: StoreEvent): Promise<void> {
140
+ console.log('Notifying base store:', event);
141
+ // Your implementation here to send the event to the backend
142
+ // e.g., via a POST request, WebSocket message, etc.
143
+ // await fetch(`${this.baseUrl}/notify`, { method: 'POST', body: JSON.stringify(event) });
144
+ }
145
+ stream(options: any): { stream: () => AsyncIterable<Product>; cancel: () => void; status: () => "active" | "cancelled" | "completed"; } {
146
+ console.log('Streaming with options:', options);
147
+ const mockStream = (async function* () {
148
+ yield { id: 'p1', name: 'Streamed Product 1', price: 10, inStock: true };
149
+ await new Promise(r => setTimeout(r, 100));
150
+ yield { id: 'p2', name: 'Streamed Product 2', price: 20, inStock: false };
151
+ })();
152
+ return { stream: () => mockStream, cancel: () => console.log('Stream cancelled'), status: () => 'completed' };
153
+ }
154
+ }
155
+
156
+ // --- Optional: Define custom correlators for invalidation ---
157
+ const myCorrelator = (
158
+ mutation: { operation: MutationOperation; params: any },
159
+ activeQueries: ActiveQuery[]
160
+ ): string[] => {
161
+ // Example: Invalidate 'list' queries on any create/delete operation
162
+ if (mutation.operation === 'create' || mutation.operation === 'delete') {
163
+ return activeQueries.filter(q => q.operation === 'list').map(q => q.queryKey);
164
+ }
165
+ // Example: Invalidate specific 'read' query if its ID matches the updated item
166
+ if (mutation.operation === 'update' && mutation.params.id) {
167
+ return activeQueries
168
+ .filter(q => q.operation === 'read' && q.params.id === mutation.params.id)
169
+ .map(q => q.queryKey);
170
+ }
171
+ return [];
172
+ };
173
+
174
+ const myStoreEventCorrelator = (
175
+ event: StoreEvent, // { scope: string, payload?: any }
176
+ activeQueries: ActiveQuery[]
177
+ ): string[] => {
178
+ // Example: Invalidate 'read' query if an external event updates its ID
179
+ if (event.scope === 'product:updated:external' && event.payload?.id) {
180
+ return activeQueries
181
+ .filter(q => q.operation === 'read' && q.params.id === event.payload.id)
182
+ .map(q => q.queryKey);
183
+ }
184
+ return [];
185
+ };
186
+
187
+ // --- Initialize ReactiveRemoteStore ---
188
+ const cache = new QueryCache();
189
+ const baseStore = new MyApiBaseStore('https://api.example.com');
190
+
191
+ const productStore = new ReactiveRemoteStore<Product>(
192
+ cache,
193
+ baseStore,
194
+ myCorrelator, // Optional: for mutation-based invalidation
195
+ myStoreEventCorrelator // Optional: for store event-based invalidation
196
+ );
197
+
198
+ console.log('ReactiveRemoteStore initialized successfully!');
199
+ ```
200
+
201
+ ### Verification
202
+
203
+ To verify that `ReactiveRemoteStore` is installed and initialized correctly, you can run a simple test:
204
+
205
+ ```typescript
206
+ import { ReactiveRemoteStore } from '@asaidimu/erp-utils-remote-store';
207
+ import { QueryCache } from '@core/cache';
208
+ import { BaseStore, Record, Page, StoreEvent } from '@asaidimu/erp-utils-remote-store/types';
209
+
210
+ // Minimal BaseStore for verification
211
+ class MinimalBaseStore implements BaseStore<Record> {
212
+ async find(): Promise<Page<Record>> { return { data: [], page: { number: 1, size: 0, count: 0, pages: 0 } }; }
213
+ async read(): Promise<Record | undefined> { return undefined; }
214
+ async list(): Promise<Page<Record>> { return { data: [], page: { number: 1, size: 0, count: 0, pages: 0 } }; }
215
+ async create(props: any): Promise<Record | undefined> { return { id: '1', ...props.data }; }
216
+ async update(props: any): Promise<Record | undefined> { return { id: props.id, ...props.data }; }
217
+ async delete(): Promise<void> { }
218
+ async upload(): Promise<Record | undefined> { return undefined; }
219
+ async subscribe(): Promise<() => void> { return () => {}; }
220
+ async notify(): Promise<void> { }
221
+ stream(): { stream: () => AsyncIterable<Record>; cancel: () => void; status: () => "active" | "cancelled" | "completed"; } {
222
+ return { stream: async function*() {}, cancel: () => {}, status: () => 'completed' };
223
+ }
224
+ }
225
+
226
+ const cache = new QueryCache();
227
+ const baseStore = new MinimalBaseStore();
228
+ const store = new ReactiveRemoteStore(cache, baseStore);
229
+
230
+ console.log('ReactiveRemoteStore instance:', store);
231
+ console.log('Store initialized successfully!');
232
+
233
+ // You can also run the project's tests to verify full functionality:
234
+ // bun run vitest --run
235
+ ```
236
+
237
+ ---
238
+
239
+ ## A Developer's Guide to `BaseStore`
240
+
241
+ The `BaseStore` interface is the heart of the `ReactiveRemoteStore`'s extensibility. It acts as a bridge between the reactive store and your backend, allowing you to use any API, protocol, or data source. This guide provides a comprehensive overview of how to implement it.
242
+
243
+ ### The Role of `BaseStore`
244
+
245
+ The `BaseStore`'s responsibility is to handle the direct communication with your remote data source. `ReactiveRemoteStore` delegates all data operations (fetching, creating, updating, etc.) to your `BaseStore` implementation. This separation of concerns means `ReactiveRemoteStore` doesn't need to know anything about your API's specifics, such as URLs, headers, or authentication.
246
+
247
+ ### Getting Started
248
+
249
+ First, define the data structure for the records your store will manage. This type must extend `StoreRecord`, which requires an `id: string` property.
250
+
251
+ ```typescript
252
+ import type { BaseStore, StoreRecord, Page, StoreEvent } from '@asaidimu/erp-utils-remote-store/types';
253
+ import { StoreError } from '@asaidimu/erp-utils-remote-store/error';
254
+
255
+ // Your record type
256
+ interface Product extends StoreRecord {
257
+ name: string;
258
+ price: number;
259
+ // ... any other fields
260
+ }
261
+
262
+ // Define the option types for your API
263
+ // These are used for type-safety when calling the store's methods
264
+
265
+ // Options for reading a single product (e.g., by ID)
266
+ interface ProductReadOptions { id: string; }
267
+
268
+ // Options for listing products (e.g., with pagination and sorting)
269
+ interface ProductListOptions { page?: number; pageSize?: number; sortBy?: keyof Product; order?: 'asc' | 'desc'; }
270
+
271
+ // Options for finding products (e.g., with a search query)
272
+ interface ProductFindOptions extends ProductListOptions { query: string; }
273
+
274
+ // Now, create your class that implements the BaseStore interface
275
+ class ProductApiStore implements BaseStore<Product, ProductFindOptions, ProductReadOptions, ProductListOptions> {
276
+ private readonly baseUrl = 'https://api.example.com';
277
+
278
+ // Implementation of each method will go here...
279
+ }
280
+ ```
281
+
282
+ ### Implementing the Methods
283
+
284
+ Here is a detailed look at each method in the `BaseStore` interface.
285
+
286
+ #### `read(options: TReadOptions): Promise<T | undefined>`
287
+
288
+ **Purpose**: Fetch a single record by its identifier.
289
+
290
+ - **`options`**: The parameters for the read operation, typically containing the record's `id`.
291
+ - **Returns**: A `Promise` that resolves to the fetched record or `undefined` if it's not found.
292
+
293
+ ```typescript
294
+ // Example implementation for `read` with error handling
295
+ async read(options: ProductReadOptions): Promise<Product | undefined> {
296
+ try {
297
+ const response = await fetch(`${this.baseUrl}/products/${options.id}`);
298
+ if (response.status === 404) {
299
+ return undefined; // Handle not found gracefully
300
+ }
301
+ if (!response.ok) {
302
+ // Throw a generic error for non-404 issues
303
+ throw new Error(`HTTP error! status: ${response.status}`);
304
+ }
305
+ return await response.json();
306
+ } catch (error: any) {
307
+ // Convert any caught error into a standardized StoreError
308
+ throw StoreError.fromError(error, 'read');
309
+ }
310
+ }
311
+ ```
312
+
313
+ #### `list(options: TListOptions): Promise<Page<T>>`
314
+
315
+ **Purpose**: Fetch a paginated list of records.
316
+
317
+ - **`options`**: Parameters for listing, such as `page`, `pageSize`, `sortBy`, etc.
318
+ - **Returns**: A `Promise` that resolves to a `Page<T>` object, which contains the `data` array and pagination details.
319
+
320
+ ```typescript
321
+ // Example implementation for `list`
322
+ async list(options: ProductListOptions): Promise<Page<Product>> {
323
+ const params = new URLSearchParams();
324
+ if (options.page) params.set('page', String(options.page));
325
+ if (options.pageSize) params.set('pageSize', String(options.pageSize));
326
+ if (options.sortBy) params.set('sortBy', options.sortBy);
327
+ if (options.order) params.set('order', options.order);
328
+
329
+ try {
330
+ const response = await fetch(`${this.baseUrl}/products?${params.toString()}`);
331
+ if (!response.ok) {
332
+ throw new Error(`HTTP error! status: ${response.status}`);
333
+ }
334
+ // Assume the API returns a body like: { data: [...], page: { number, size, count, pages } }
335
+ return await response.json();
336
+ } catch (error: any) {
337
+ throw StoreError.fromError(error, 'list');
338
+ }
339
+ }
340
+ ```
341
+
342
+ #### `find(options: TFindOptions): Promise<Page<T>>`
343
+
344
+ **Purpose**: Fetch a paginated list of records based on search criteria.
345
+
346
+ - **`options`**: Parameters for finding, which might include a search `query` along with pagination and sorting.
347
+ - **Returns**: A `Promise` that resolves to a `Page<T>` object.
348
+
349
+ ```typescript
350
+ // Example implementation for `find`
351
+ async find(options: ProductFindOptions): Promise<Page<Product>> {
352
+ const params = new URLSearchParams({ query: options.query });
353
+ if (options.page) params.set('page', String(options.page));
354
+ // ... other params
355
+
356
+ try {
357
+ const response = await fetch(`${this.baseUrl}/products/search?${params.toString()}`);
358
+ if (!response.ok) {
359
+ throw new Error(`HTTP error! status: ${response.status}`);
360
+ }
361
+ return await response.json();
362
+ } catch (error: any) {
363
+ throw StoreError.fromError(error, 'find');
364
+ }
365
+ }
366
+ ```
367
+
368
+ #### `create(props: { data: Partial<T> }): Promise<T | undefined>`
369
+
370
+ **Purpose**: Create a new record.
371
+
372
+ - **`props.data`**: The record to create.
373
+ - **Returns**: A `Promise` that resolves to the newly created record as returned by the server (including the server-generated `id`).
374
+
375
+ ```typescript
376
+ // Example implementation for `create`
377
+ async create(props: { data: Partial<Product> }): Promise<Product | undefined> {
378
+ try {
379
+ const response = await fetch(`${this.baseUrl}/products`, {
380
+ method: 'POST',
381
+ headers: { 'Content-Type': 'application/json' },
382
+ body: JSON.stringify(props.data),
383
+ });
384
+ if (!response.ok) {
385
+ throw new Error(`HTTP error! status: ${response.status}`);
386
+ }
387
+ return await response.json(); // Important: return the server's response
388
+ } catch (error: any) {
389
+ throw StoreError.fromError(error, 'create');
390
+ }
391
+ }
392
+ ```
393
+
394
+ #### `update(props: { data: Partial<T> }): Promise<T | undefined>`
395
+
396
+ **Purpose**: Update an existing record.
397
+
398
+ - **`props.data`**: A partial object of the fields to update, including the `id`.
399
+ - **Returns**: A `Promise` that resolves to the updated record.
400
+
401
+ ```typescript
402
+ // Example implementation for `update`
403
+ async update(props: { data: Partial<Product> }): Promise<Product | undefined> {
404
+ try {
405
+ const response = await fetch(`${this.baseUrl}/products/${props.data.id}`,
406
+ {
407
+ method: 'PATCH', // or 'PUT'
408
+ headers: { 'Content-Type': 'application/json' },
409
+ body: JSON.stringify(props.data),
410
+ });
411
+ if (!response.ok) {
412
+ throw new Error(`HTTP error! status: ${response.status}`);
413
+ }
414
+ return await response.json();
415
+ } catch (error: any) {
416
+ throw StoreError.fromError(error, 'update');
417
+ }
418
+ }
419
+ ```
420
+
421
+ #### `delete(options: TDeleteOptions): Promise<void>`
422
+
423
+ **Purpose**: Delete a record.
424
+
425
+ - **`options`**: Parameters for deletion, typically `{ id: string }`.
426
+ - **Returns**: A `Promise` that resolves when the operation is complete.
427
+
428
+ ```typescript
429
+ // Example implementation for `delete`
430
+ async delete(options: { id: string }): Promise<void> {
431
+ try {
432
+ const response = await fetch(`${this.baseUrl}/products/${options.id}`, {
433
+ method: 'DELETE',
434
+ });
435
+ if (!response.ok) {
436
+ throw new Error(`HTTP error! status: ${response.status}`);
437
+ }
438
+ } catch (error: any) {
439
+ throw StoreError.fromError(error, 'delete');
440
+ }
441
+ }
442
+ ```
443
+
444
+ ### Implementing Real-Time Features
445
+
446
+ `ReactiveRemoteStore` supports real-time updates through the `subscribe` and `notify` methods.
447
+
448
+ #### `subscribe(scope: string, callback: (event: StoreEvent) => void): Promise<() => void>`
449
+
450
+ **Purpose**: Establish a connection to your backend to receive real-time events.
451
+
452
+ - **`scope`**: A string indicating the event scope to subscribe to. `ReactiveRemoteStore` will call this with `'*'` to listen for all events.
453
+ - **`callback`**: A function that `ReactiveRemoteStore` provides. Your implementation must call this function whenever a `StoreEvent` is received from the backend.
454
+ - **Returns**: A `Promise` that resolves to an `unsubscribe` function. `ReactiveRemoteStore` will call this function when it's destroyed to clean up the connection.
455
+
456
+ ```typescript
457
+ // Example implementation for `subscribe` using Server-Sent Events (SSE)
458
+ async subscribe(scope: string, callback: (event: StoreEvent) => void): Promise<() => void> {
459
+ console.log(`Subscribing to event scope: ${scope}`);
460
+ const eventSource = new EventSource(`${this.baseUrl}/events?scope=${scope}`);
461
+
462
+ eventSource.onmessage = (event) => {
463
+ const storeEvent: StoreEvent = JSON.parse(event.data);
464
+ callback(storeEvent); // Pass the event to the ReactiveRemoteStore
465
+ };
466
+
467
+ eventSource.onerror = (error) => {
468
+ console.error('SSE Error:', error);
469
+ // You might want to add reconnection logic here
470
+ };
471
+
472
+ // Return a function that closes the connection
473
+ return () => {
474
+ console.log('Unsubscribing from events.');
475
+ eventSource.close();
476
+ };
477
+ }
478
+ ```
479
+
480
+ #### `notify(event: StoreEvent): Promise<void>`
481
+
482
+ **Purpose**: Send an event from the client to the server. This is less common but can be used for client-initiated notifications.
483
+
484
+ - **`event`**: The `StoreEvent` to send.
485
+ - **Returns**: A `Promise` that resolves when the event has been sent.
486
+
487
+ ```typescript
488
+ // Example implementation for `notify`
489
+ async notify(event: StoreEvent): Promise<void> {
490
+ try {
491
+ const response = await fetch(`${this.baseUrl}/notify`, {
492
+ method: 'POST',
493
+ headers: { 'Content-Type': 'application/json' },
494
+ body: JSON.stringify(event),
495
+ });
496
+ if (!response.ok) {
497
+ throw new Error(`HTTP error! status: ${response.status}`);
498
+ }
499
+ } catch (error: any) {
500
+ throw StoreError.fromError(error, 'notify');
501
+ }
502
+ }
503
+ ```
504
+
505
+ ### Error Handling
506
+
507
+ For robust error handling, your `BaseStore` methods should wrap their logic in a `try...catch` block and convert any caught errors into a `StoreError`. This provides standardized, structured error information to the `ReactiveRemoteStore` and your application.
508
+
509
+ The library provides a `StoreError.fromError(error, operation)` utility that intelligently converts various error types (network errors, HTTP errors, timeouts) into a consistent `StoreError` object.
510
+
511
+ **Benefits of using `StoreError`**:
512
+ - **Standardization**: All errors from the data layer have a consistent shape.
513
+ - **Rich Information**: Includes `code`, `status`, `isRetryable`, and the `originalError`.
514
+ - **Automatic Handling**: Correctly identifies common network and HTTP issues from `fetch` responses.
515
+
516
+ Here is how you should structure your methods:
517
+
518
+ ```typescript
519
+ import { StoreError } from '@asaidimu/erp-utils-remote-store/error';
520
+
521
+ // Inside your BaseStore implementation...
522
+
523
+ async read(options: ProductReadOptions): Promise<Product | undefined> {
524
+ try {
525
+ const response = await fetch(`${this.baseUrl}/products/${options.id}`);
526
+
527
+ if (response.status === 404) {
528
+ return undefined; // Not an error, just not found
529
+ }
530
+
531
+ // The `fromError` utility will automatically handle non-ok responses
532
+ // if you pass the response object to it, but throwing manually is also fine.
533
+ if (!response.ok) {
534
+ throw new Error(`HTTP Error: ${response.status}`);
535
+ }
536
+
537
+ return await response.json();
538
+
539
+ } catch (error: any) {
540
+ // The `fromError` factory will create the appropriate StoreError
541
+ // based on the type of error caught (e.g., network, timeout, HTTP).
542
+ console.error('Error in read operation:', error);
543
+ throw StoreError.fromError(error, 'read');
544
+ }
545
+ }
546
+ ```
547
+
548
+
549
+
550
+ ## 📖 Usage Documentation
551
+
552
+ ### Basic Usage
553
+
554
+ The `ReactiveRemoteStore` simplifies data interactions by providing reactive hooks into your data. Here's how to perform basic CRUD operations and observe data changes.
555
+
556
+ ```typescript
557
+ import { ReactiveRemoteStore } from '@asaidimu/erp-utils-remote-store';
558
+ import { QueryCache } from '@core/cache';
559
+ import { BaseStore, Record, Page, StoreEvent } from '@asaidimu/erp-utils-remote-store/types';
560
+
561
+ // Assume you have a Todo type and a TodoBaseStore implementation
562
+ interface Todo extends Record {
563
+ title: string;
564
+ completed: boolean;
565
+ }
566
+
567
+ class TodoBaseStore implements BaseStore<Todo> {
568
+ // ... (implementation similar to MyApiBaseStore above)
569
+ // For simplicity, let's use a mock in-memory store for this example
570
+ private todos: Todo[] = [];
571
+ private nextId = 1;
572
+
573
+ async create(props: { data: Omit<Todo, "id">; options?: any; }): Promise<Todo | undefined> {
574
+ const newTodo = { id: String(this.nextId++), ...props.data };
575
+ this.todos.push(newTodo);
576
+ return newTodo;
577
+ }
578
+ async read(options: { id: string }): Promise<Todo | undefined> {
579
+ return this.todos.find(t => t.id === options.id);
580
+ }
581
+ async list(options: any): Promise<Page<Todo>> {
582
+ return { data: this.todos, page: { number: 1, size: this.todos.length, count: this.todos.length, pages: 1 } };
583
+ }
584
+ async update(props: { id: string; data: Partial<Omit<Todo, "id">>; options?: any; }): Promise<Todo | undefined> {
585
+ const todo = this.todos.find(t => t.id === props.id);
586
+ if (todo) {
587
+ Object.assign(todo, props.data);
588
+ return todo;
589
+ }
590
+ return undefined;
591
+ }
592
+ async delete(options: { id: string; }): Promise<void> {
593
+ this.todos = this.todos.filter(t => t.id !== options.id);
594
+ }
595
+ async upload(): Promise<Todo | undefined> { return undefined; }
596
+ async subscribe(): Promise<() => void> { return () => {}; }
597
+ async notify(): Promise<void> { }
598
+ stream(): { stream: () => AsyncIterable<Todo>; cancel: () => void; status: () => "active" | "cancelled" | "completed"; } {
599
+ return { stream: async function*() {}, cancel: () => {}, status: () => 'completed' };
600
+ }
601
+ }
602
+
603
+ const cache = new QueryCache();
604
+ const todoBaseStore = new TodoBaseStore();
605
+ const todoStore = new ReactiveRemoteStore<Todo>(cache, todoBaseStore);
606
+
607
+ async function runBasicUsage() {
608
+ console.log('\n--- Basic Usage Examples ---');
609
+
610
+ // 1. Create a Todo
611
+ const createdTodo = await todoStore.create({ data: { title: 'Buy groceries', completed: false } });
612
+ console.log('Created Todo:', createdTodo);
613
+
614
+ // 2. Read a Todo and subscribe to changes
615
+ if (createdTodo) {
616
+ const { value: selectRead, onValueChange: subscribeRead } = todoStore.read({ id: createdTodo.id });
617
+ const unsubscribeRead = subscribeRead(() => {
618
+ const result = selectRead();
619
+ console.log('Read Todo (reactive update):', result.data, 'loading:', result.loading);
620
+ });
621
+
622
+ console.log('Initial Read:', selectRead().data);
623
+
624
+ // 3. List all Todos and subscribe to changes
625
+ const { value , onValueChange: subscribeToList } = todoStore.list({});
626
+ let result = value()
627
+ const unsubscribeList = subscribeToList(() => {
628
+ result = value();
629
+ console.log('List Todos (reactive update):', result.page?.data, 'loading:', result.loading);
630
+ });
631
+
632
+ console.log('Initial List:', result.page?.data);
633
+
634
+ // 4. Update the Todo (this will trigger reactive updates in read and list queries)
635
+ await todoStore.update({ id: createdTodo.id, data: { completed: true } });
636
+ console.log('Updated Todo to completed.');
637
+
638
+ // 5. Delete the Todo (this will trigger reactive updates)
639
+ await todoStore.delete({ id: createdTodo.id });
640
+ console.log('Deleted Todo.');
641
+
642
+ // Clean up subscriptions
643
+ unsubscribeRead();
644
+ unsubscribeList();
645
+ }
646
+
647
+ // Don't forget to destroy the store when your application shuts down
648
+ todoStore.destroy();
649
+ }
650
+
651
+ runBasicUsage();
652
+ ```
653
+
654
+ ### API Usage
655
+
656
+ `ReactiveRemoteStore` exposes a comprehensive API for managing your data. Below are detailed descriptions of its public methods.
657
+
658
+ #### `constructor(cache: QueryCache, baseStore: BaseStore<T, ...>, correlator?: Correlator, storeEventCorrelator?: StoreEventCorrelator)`
659
+
660
+ Creates a new instance of `ReactiveRemoteStore`.
661
+
662
+ - `cache`: An instance of `QueryCache` (or compatible interface) used for caching data.
663
+ - `baseStore`: Your implementation of the `BaseStore` interface, responsible for direct interaction with the remote API.
664
+ - `correlator` (optional): A function (`Correlator` type) that defines how mutations (create, update, delete, upload) should invalidate active queries in the `QueryCache`. If not provided, a default invalidation strategy (invalidating all `list` and `find` queries) is used.
665
+ - `storeEventCorrelator` (optional): A function (`StoreEventCorrelator` type) that defines how incoming `StoreEvent`s (e.g., from SSE) should invalidate active queries. If not provided, external events will be logged but won't trigger invalidation.
666
+
667
+ #### `read(params: TReadOptions): ReactiveQueryResult<T>`
668
+
669
+ Retrieves a single record from the store. This method returns an object containing a `selector` function and an `onValueChange` function, enabling reactive data access.
670
+
671
+ - `params`: Options specific to your `BaseStore`'s `read` operation (e.g., `{ id: string }`).
672
+
673
+ **Returns:**
674
+
675
+ - `{ value, onValueChange }`: An object.
676
+ - `value`: A function `() => QueryResult<T>` that returns the current state of the query, including `data`, `loading` status, `error`, `stale` status, and `updated` timestamp.
677
+ - `onValueChange`: A function `(callback: () => void) => () => void` that allows you to register a callback to be notified when the query result changes. It returns an `unsubscribe` function.
678
+
679
+ ```typescript
680
+ // Example: Reading a user profile
681
+ interface UserProfile extends Record { name: string; email: string; }
682
+ // Assume userStore is ReactiveRemoteStore<UserProfile>
683
+
684
+ const { value: selectUser, onValueChange: subscribeUser } = userStore.read({ id: 'user-123' });
685
+
686
+ // Initial access
687
+ console.log('Initial User Data:', selectUser().data);
688
+
689
+ // Subscribe to changes
690
+ const unsubscribe = subscribeUser(() => {
691
+ const result = selectUser();
692
+ console.log('User Data Updated:', result.data, 'Loading:', result.loading, 'Stale:', result.stale);
693
+ if (result.error) console.error('Error fetching user:', result.error);
694
+ });
695
+
696
+ // Later, when no longer needed
697
+ // unsubscribe();
698
+ ```
699
+
700
+ #### `list(params: TListOptions): ReactivePagedQueryResult<T>`
701
+
702
+ Retrieves a paginated list of records from the store. This method returns an object containing a `selector` function and an `onValueChange` function, enabling reactive data access and pagination controls.
703
+
704
+ - `params`: Options specific to your `BaseStore`'s `list` operation (e.g., `{ page: number, pageSize: number, filter?: string }`).
705
+
706
+ **Returns:**
707
+
708
+ - `{ value, onValueChange }`: An object.
709
+ - `value`: A function `() => PagedQueryResult<T>` that returns the current state of the paginated query, including `page` data, `loading` status, `error`, `stale` status, `updated` timestamp, and pagination helpers (`hasNext`, `hasPrevious`, `next()`, `previous()`, `fetch(page)`).
710
+ - `onValueChange`: A function `(callback: () => void) => () => void` that allows you to register a callback to be notified when the query result changes. It returns an `unsubscribe` function.
711
+
712
+ ```typescript
713
+ // Example: Listing products with pagination
714
+ interface Product extends Record { name: string; price: number; }
715
+ // Assume productStore is ReactiveRemoteStore<Product>
716
+
717
+ const { value: selectProducts, onValueChange: subscribeProducts } = productStore.list({ page: 1, pageSize: 10 });
718
+
719
+ const unsubscribeProducts = subscribeProducts(() => {
720
+ const result = selectProducts();
721
+ console.log('Products List Updated:', result.page?.data, 'Loading:', result.loading);
722
+ if (result.page) {
723
+ console.log(`Page ${result.page.page.number} of ${result.page.page.pages}`);
724
+ }
725
+ });
726
+
727
+ // Navigate to the next page
728
+ const currentProducts = selectProducts();
729
+ if (currentProducts.hasNext) {
730
+ await currentProducts.next();
731
+ }
732
+
733
+ // Fetch a specific page
734
+ await selectProducts().fetch(3);
735
+
736
+ // unsubscribeProducts();
737
+ ```
738
+
739
+ #### `find(params: TFindOptions): ReactivePagedQueryResult<T>`
740
+
741
+ Retrieves a paginated list of records based on search criteria. Similar to `list`, but typically used for more complex search queries.
742
+
743
+ - `params`: Options specific to your `BaseStore`'s `find` operation (e.g., `{ query: string, category: string }`).
744
+
745
+ **Returns:**
746
+
747
+ - `{ value, onValueChange }`: An object identical in structure and behavior to the `list` method's return value.
748
+
749
+ ```typescript
750
+ // Example: Finding orders by customer ID
751
+ interface Order extends Record { customerId: string; amount: number; }
752
+ // Assume orderStore is ReactiveRemoteStore<Order>
753
+
754
+ const { value: selectOrders, onValueChange: subscribeOrders } = orderStore.find({ customerId: 'cust-456', status: 'pending' });
755
+
756
+ const unsubscribeOrders = subscribeOrders(() => {
757
+ const result = selectOrders();
758
+ console.log('Orders Found Updated:', result.page?.data, 'Loading:', result.loading);
759
+ });
760
+
761
+ // unsubscribeOrders();
762
+ ```
763
+
764
+ #### `create(params: { data: Omit<T, 'id'>, options?: TCreateOptions }): Promise<T | undefined>`
765
+
766
+ Creates a new record in the remote store. This operation automatically invalidates relevant cached queries (e.g., `list` or `find` queries) to ensure data consistency.
767
+
768
+ - `params.data`: The data for the new record, excluding the `id` (which is typically generated by the backend).
769
+ - `params.options` (optional): Options specific to your `BaseStore`'s `create` operation.
770
+
771
+ **Returns:** A promise that resolves to the newly created record (including its `id`) or `undefined` if creation failed.
772
+
773
+ ```typescript
774
+ // Example: Creating a new task
775
+ interface Task extends Record { description: string; dueDate: string; }
776
+ // Assume taskStore is ReactiveRemoteStore<Task>
777
+
778
+ const newTask = await taskStore.create({ data: { description: 'Write documentation', dueDate: '2025-12-31' } });
779
+ console.log('New Task Created:', newTask);
780
+ ```
781
+
782
+ #### `update(params: { id: string; data: Partial<Omit<T, 'id'>>; options?: TUpdateOptions }): Promise<T | undefined>`
783
+
784
+ Updates an existing record in the remote store. This operation automatically invalidates relevant cached queries (e.g., the specific `read` query for the updated ID, or `list`/`find` queries).
785
+
786
+ - `params.id`: The `id` of the record to update.
787
+ - `params.data`: A partial object containing the fields to update.
788
+ - `params.options` (optional): Options specific to your `BaseStore`'s `update` operation.
789
+
790
+ **Returns:** A promise that resolves to the updated record or `undefined` if the record was not found or the update failed.
791
+
792
+ ```typescript
793
+ // Example: Marking a task as completed
794
+ // Assume taskStore is ReactiveRemoteStore<Task> and taskId is known
795
+
796
+ const updatedTask = await taskStore.update({ id: taskId, data: { completed: true } });
797
+ console.log('Task Updated:', updatedTask);
798
+ ```
799
+
800
+ #### `delete(params: TDeleteOptions): Promise<void>`
801
+
802
+ Deletes a record from the remote store. This operation automatically invalidates relevant cached queries.
803
+
804
+ - `params`: Options specific to your `BaseStore`'s `delete` operation (e.g., `{ id: string }`).
805
+
806
+ **Returns:** A promise that resolves when the deletion is complete.
807
+
808
+ ```typescript
809
+ // Example: Deleting a task
810
+ // Assume taskStore is ReactiveRemoteStore<Task> and taskId is known
811
+
812
+ await taskStore.delete({ id: taskId });
813
+ console.log('Task Deleted.');
814
+ ```
815
+
816
+ #### `upload(params: { file: File; options?: TUploadOptions }): Promise<T | undefined>`
817
+
818
+ Uploads a file associated with a record. This operation automatically invalidates relevant cached queries.
819
+
820
+ - `params.file`: The `File` object to upload.
821
+ - `params.options` (optional): Options specific to your `BaseStore`'s `upload` operation (e.g., `{ id: string, fieldName: string }`).
822
+
823
+ **Returns:** A promise that resolves to the updated record (if the upload modifies the record) or `undefined` if upload failed.
824
+
825
+ ```typescript
826
+ // Example: Uploading an avatar for a user
827
+ interface User extends Record { name: string; avatarUrl?: string; }
828
+ // Assume userStore is ReactiveRemoteStore<User> and userId is known
829
+
830
+ const avatarFile = new File(['...'], 'avatar.png', { type: 'image/png' });
831
+ const updatedUser = await userStore.upload({ file: avatarFile, options: { id: userId, fieldName: 'avatar' } });
832
+ console.log('User Avatar Uploaded:', updatedUser?.avatarUrl);
833
+ ```
834
+
835
+ #### `notify(event: StoreEvent): Promise<void>`
836
+
837
+ Manually notifies the `ReactiveRemoteStore` of a `StoreEvent`. This can be used to simulate external events or trigger custom invalidation logic defined by `storeEventCorrelator`.
838
+
839
+ - `event`: The `StoreEvent` to process. It should have a `scope` and an optional `payload`.
840
+
841
+ **Returns:** A promise that resolves when the notification has been processed.
842
+
843
+ ```typescript
844
+ // Example: Notifying the store of an external product price change
845
+ // Assume productStore is ReactiveRemoteStore<Product>
846
+
847
+ await productStore.notify({
848
+ scope: 'product:price:changed',
849
+ payload: { id: 'prod-789', newPrice: 99.99 }
850
+ });
851
+ console.log('Notified store about product price change.');
852
+ ```
853
+
854
+ #### `stream(options: TStreamOptions): Promise<{ stream: () => AsyncIterable<T>; cancel: () => void; status: () => 'active' | 'cancelled' | 'completed'; }>`
855
+
856
+ Establishes a real-time data stream from the remote store. This method returns an object containing an `AsyncIterable` for consuming data, a `cancel` function to stop the stream, and a `status` getter to check the stream's current state.
857
+
858
+ - `options`: Options specific to your `BaseStore`'s `stream` operation (e.g., `{ filter: string, batchSize: number }`).
859
+
860
+ **Returns:** An object with:
861
+
862
+ - `stream()`: A function that returns an `AsyncIterable<T>` which yields records as they arrive.
863
+ - `cancel()`: A function to call to terminate the stream.
864
+ - `status()`: A getter function that returns the current state of the stream (`'active'`, `'cancelled'`, or `'completed'`).
865
+
866
+ ```typescript
867
+ // Example: Streaming live stock prices
868
+ interface StockPrice extends Record { symbol: string; price: number; timestamp: number; }
869
+ // Assume stockStore is ReactiveRemoteStore<StockPrice>
870
+
871
+ async function consumeStockStream() {
872
+ console.log('Starting stock price stream...');
873
+ const { stream, cancel, status } = await stockStore.stream({ symbols: ['AAPL', 'GOOG'], interval: 1000 });
874
+
875
+ const streamInterval = setInterval(() => {
876
+ console.log('Stream Status:', status());
877
+ }, 500);
878
+
879
+ try {
880
+ for await (const priceUpdate of stream()) {
881
+ console.log(`Received: ${priceUpdate.symbol} - $${priceUpdate.price.toFixed(2)}`);
882
+ }
883
+ console.log('Stock stream completed.');
884
+ } catch (error) {
885
+ console.error('Stock stream error:', error);
886
+ } finally {
887
+ clearInterval(streamInterval);
888
+ cancel(); // Ensure stream is cancelled
889
+ console.log('Stock stream cleanup.');
890
+ }
891
+ }
892
+
893
+ // consumeStockStream();
894
+ ```
895
+
896
+ #### `refresh(operation: 'read' | 'list' | 'find', params: TReadOptions | TListOptions | TFindOptions): Promise<T | Page<T> | undefined>`
897
+
898
+ Forces a re-fetch of data for a specific query, bypassing staleness checks. This ensures you get the absolute latest data from the `BaseStore`.
899
+
900
+ - `operation`: The type of operation to refresh (`'read'`, `'list'`, or `'find'`).
901
+ - `params`: The parameters for the query to refresh.
902
+
903
+ **Returns:** A promise that resolves to the refreshed data (single record or page) or `undefined` if the fetch fails.
904
+
905
+ ```typescript
906
+ // Example: Refreshing a user's session data after an action
907
+ // Assume userStore is ReactiveRemoteStore<UserProfile> and userId is known
908
+
909
+ const freshUserData = await userStore.refresh('read', { id: userId });
910
+ console.log('Refreshed User Data:', freshUserData);
911
+
912
+ // Example: Refreshing a list of notifications
913
+ // Assume notificationStore is ReactiveRemoteStore<Notification>
914
+
915
+ const freshNotifications = await notificationStore.refresh('list', { status: 'unread' });
916
+ console.log('Refreshed Notifications:', freshNotifications?.data);
917
+ ```
918
+
919
+ #### `prefetch(operation: 'read' | 'list' | 'find', params: TReadOptions | TListOptions | TFindOptions): void`
920
+
921
+ Triggers a background fetch for a specific query if it's not already in cache or is stale. Useful for loading data proactively before it's explicitly requested by the UI.
922
+
923
+ - `operation`: The type of operation to prefetch (`'read'`, `'list'`, or `'find'`).
924
+ - `params`: The parameters for the query to prefetch.
925
+
926
+ **Returns:** `void` (the operation runs in the background).
927
+
928
+ ```typescript
929
+ // Example: Prefetching related product details when a user hovers over a product card
930
+ // Assume productStore is ReactiveRemoteStore<Product>
931
+
932
+ productStore.prefetch('read', { id: 'prod-hovered-id' });
933
+ console.log('Prefetched product details.');
934
+
935
+ // Example: Prefetching the next page of a list
936
+ // Assume currentListParams has page: 1, pageSize: 10
937
+
938
+ const nextListPageParams = { ...currentListParams, page: currentListParams.page + 1 };
939
+ productStore.prefetch('list', nextListPageParams);
940
+ console.log('Prefetched next page of products.');
941
+ ```
942
+
943
+ #### `invalidate(operation: string, params: any): Promise<void>`
944
+
945
+ Manually invalidates a specific query in the cache, marking it as stale. The next time this query is accessed, it will trigger a re-fetch from the `BaseStore`.
946
+
947
+ - `operation`: The operation type of the query to invalidate (e.g., `'read'`, `'list'`, `'find'`).
948
+ - `params`: The parameters of the query to invalidate.
949
+
950
+ **Returns:** A promise that resolves when the invalidation is complete.
951
+
952
+ ```typescript
953
+ // Example: Invalidate a specific user's cached data after a direct API call outside the store
954
+ // Assume userStore is ReactiveRemoteStore<UserProfile>
955
+
956
+ await userStore.invalidate('read', { id: 'user-123' });
957
+ console.log('User-123 data invalidated.');
958
+ ```
959
+
960
+ #### `invalidateAll(): Promise<void>`
961
+
962
+ Invalidates all active queries currently managed by the `ReactiveRemoteStore`.
963
+
964
+ **Returns:** A promise that resolves when all active queries have been invalidated.
965
+
966
+ ```typescript
967
+ // Example: Invalidate all cached data after a global data reset or logout
968
+ // Assume anyStore is ReactiveRemoteStore<any>
969
+
970
+ await anyStore.invalidateAll();
971
+ console.log('All cached data invalidated.');
972
+ ```
973
+
974
+ #### `getStats(): { size: number; metrics: CacheMetrics; hitRate: number; staleHitRate: number; entries: Array<{ key: string; lastAccessed: number; lastUpdated: number; accessCount: number; isStale: boolean; isLoading?: boolean; error?: boolean }> }`
975
+
976
+ Retrieves current statistics about the underlying `QueryCache` and the number of active subscriptions within `ReactiveRemoteStore`.
977
+
978
+ **Returns:** An object containing:
979
+
980
+ - `size`: Number of active entries in the cache.
981
+ - `metrics`: An object containing raw counts (`hits`, `misses`, `fetches`, `errors`, `evictions`, `staleHits`).
982
+ - `hitRate`: Ratio of hits to total requests (hits + misses).
983
+ - `staleHitRate`: Ratio of stale hits to total hits.
984
+ - `entries`: An array of objects providing details for each cached item (key, lastAccessed, lastUpdated, accessCount, isStale, isLoading, error status).
985
+ - `activeSubscriptions`: Number of currently active query subscriptions in `ReactiveRemoteStore`.
986
+
987
+ ```typescript
988
+ // Example: Logging store statistics
989
+ const stats = productStore.getStats();
990
+ console.log('Store Stats:', stats);
991
+ console.log('Active Subscriptions:', stats.activeSubscriptions);
992
+ ```
993
+
994
+ #### `destroy(): void`
995
+
996
+ Cleans up all active subscriptions, internal timers, and resources held by the `ReactiveRemoteStore` instance. This method should be called when the store instance is no longer needed (e.g., on application shutdown or component unmount) to prevent memory leaks.
997
+
998
+ **Returns:** `void`.
999
+
1000
+ ```typescript
1001
+ // Example: Cleaning up the store on application exit
1002
+ // Assume productStore is ReactiveRemoteStore<Product>
1003
+
1004
+ productStore.destroy();
1005
+ console.log('ReactiveRemoteStore instance destroyed.');
1006
+ ```
1007
+
1008
+ ### Configuration Examples
1009
+
1010
+ #### Custom Correlators for Invalidation
1011
+
1012
+ Correlators allow you to define precise rules for cache invalidation based on mutations or external events. This is crucial for maintaining data consistency in complex applications.
1013
+
1014
+ ```typescript
1015
+ import { ReactiveRemoteStore } from '@asaidimu/erp-utils-remote-store';
1016
+ import { QueryCache } from '@core/cache';
1017
+ import { BaseStore, Record, Page, StoreEvent, ActiveQuery, MutationOperation, Correlator, StoreEventCorrelator } from '@asaidimu/erp-utils-remote-store/types';
1018
+
1019
+ interface Post extends Record { title: string; authorId: string; tags: string[]; }
1020
+
1021
+ class PostBaseStore implements BaseStore<Post> { /* ... implementation ... */
1022
+ async find(): Promise<Page<Post>> { return { data: [], page: { number: 1, size: 0, count: 0, pages: 0 } }; }
1023
+ async read(): Promise<Post | undefined> { return undefined; }
1024
+ async list(): Promise<Page<Post>> { return { data: [], page: { number: 1, size: 0, count: 0, pages: 0 } }; }
1025
+ async create(props: any): Promise<Post | undefined> { return { id: 'new-post', ...props.data }; }
1026
+ async update(props: any): Promise<Post | undefined> { return { id: props.id, ...props.data }; }
1027
+ async delete(): Promise<void> { }
1028
+ async upload(): Promise<Post | undefined> { return undefined; }
1029
+ async subscribe(): Promise<() => void> { return () => {}; }
1030
+ async notify(): Promise<void> { }
1031
+ stream(): { stream: () => AsyncIterable<Post>; cancel: () => void; status: () => "active" | "cancelled" | "completed"; } {
1032
+ return { stream: async function*() {}, cancel: () => {}, status: () => 'completed' };
1033
+ }
1034
+ }
1035
+
1036
+ // Correlator for mutations (create, update, delete, upload)
1037
+ const postMutationCorrelator: Correlator = (
1038
+ mutation, // { operation: 'create' | 'update' | 'delete' | 'upload', params: any }
1039
+ activeQueries // Array of { queryKey: string, operation: string, params: any }
1040
+ ) => {
1041
+ const invalidatedKeys: string[] = [];
1042
+
1043
+ // Invalidate all 'list' queries for posts on any post mutation
1044
+ if (mutation.operation === 'create' || mutation.operation === 'delete') {
1045
+ activeQueries.filter(q => q.operation === 'list').forEach(q => invalidatedKeys.push(q.queryKey));
1046
+ }
1047
+
1048
+ // If a post is updated, invalidate its specific 'read' query
1049
+ if (mutation.operation === 'update' && mutation.params.id) {
1050
+ activeQueries
1051
+ .filter(q => q.operation === 'read' && q.params.id === mutation.params.id)
1052
+ .forEach(q => invalidatedKeys.push(q.queryKey));
1053
+ }
1054
+
1055
+ // Example: Invalidate 'find' queries based on tags if a post's tags change
1056
+ if (mutation.operation === 'update' && mutation.params.id && mutation.params.data?.tags) {
1057
+ activeQueries
1058
+ .filter(q => q.operation === 'find' && q.params.tags &&
1059
+ (mutation.params.data.tags as string[]).some(tag => q.params.tags.includes(tag)))
1060
+ .forEach(q => invalidatedKeys.push(q.queryKey));
1061
+ }
1062
+
1063
+ return invalidatedKeys;
1064
+ };
1065
+
1066
+ // Correlator for external StoreEvents (e.g., from SSE)
1067
+ const postEventCorrelator: StoreEventCorrelator = (
1068
+ event, // { scope: string, payload?: any }
1069
+ activeQueries // Array of { queryKey: string, operation: string, params: any }
1070
+ ) => {
1071
+ const invalidatedKeys: string[] = [];
1072
+
1073
+ // If an external event indicates a specific post was updated, invalidate its 'read' query
1074
+ if (event.scope === 'post:external:updated' && event.payload?.id) {
1075
+ activeQueries
1076
+ .filter(q => q.operation === 'read' && q.params.id === event.payload.id)
1077
+ .forEach(q => invalidatedKeys.push(q.queryKey));
1078
+ }
1079
+
1080
+ // If an external event indicates a new post was created, invalidate all 'list' queries
1081
+ if (event.scope === 'post:external:created') {
1082
+ activeQueries.filter(q => q.operation === 'list').forEach(q => invalidatedKeys.push(q.queryKey));
1083
+ }
1084
+
1085
+ return invalidatedKeys;
1086
+ };
1087
+
1088
+ const cache = new QueryCache();
1089
+ const postBaseStore = new PostBaseStore();
1090
+
1091
+ const postStore = new ReactiveRemoteStore<Post>(
1092
+ cache,
1093
+ postBaseStore,
1094
+ postMutationCorrelator, // Pass your custom mutation correlator
1095
+ postEventCorrelator // Pass your custom store event correlator
1096
+ );
1097
+
1098
+ console.log('ReactiveRemoteStore with custom correlators initialized.');
1099
+ ```
1100
+
1101
+ ### Common Use Cases
1102
+
1103
+ #### Reactive UI Updates with `read` and `list`
1104
+
1105
+ Automatically update your UI components when data changes, without manual re-fetching.
1106
+
1107
+ ```typescript
1108
+ import { ReactiveRemoteStore } from '@asaidimu/erp-utils-remote-store';
1109
+ import { QueryCache } from '@core/cache';
1110
+ import { BaseStore, Record, Page, StoreEvent } from '@asaidimu/erp-utils-remote-store/types';
1111
+
1112
+ interface Item extends Record { name: string; status: 'active' | 'inactive'; }
1113
+
1114
+ class ItemBaseStore implements BaseStore<Item> { /* ... mock implementation ... */
1115
+ private items: Item[] = [];
1116
+ private nextId = 1;
1117
+
1118
+ async create(props: { data: Omit<Item, "id">; options?: any; }): Promise<Item | undefined> {
1119
+ const newItem = { id: String(this.nextId++), ...props.data };
1120
+ this.items.push(newItem);
1121
+ return newItem;
1122
+ }
1123
+ async read(options: { id: string }): Promise<Item | undefined> {
1124
+ return this.items.find(i => i.id === options.id);
1125
+ }
1126
+ async list(options: any): Promise<Page<Item>> {
1127
+ return { data: this.items, page: { number: 1, size: this.items.length, count: this.items.length, pages: 1 } };
1128
+ }
1129
+ async update(props: { id: string; data: Partial<Omit<Item, "id">>; options?: any; }): Promise<Item | undefined> {
1130
+ const item = this.items.find(i => i.id === props.id);
1131
+ if (item) {
1132
+ Object.assign(item, props.data);
1133
+ return item;
1134
+ }
1135
+ return undefined;
1136
+ }
1137
+ async delete(options: { id: string; }): Promise<void> {
1138
+ this.items = this.items.filter(i => i.id !== options.id);
1139
+ }
1140
+ async upload(): Promise<Item | undefined> { return undefined; }
1141
+ async subscribe(): Promise<() => void> { return () => {}; }
1142
+ async notify(): Promise<void> { }
1143
+ stream(): { stream: () => AsyncIterable<Item>; cancel: () => void; status: () => "active" | "cancelled" | "completed"; } {
1144
+ return { stream: async function*() {}, cancel: () => {}, status: () => 'completed' };
1145
+ }
1146
+ }
1147
+
1148
+ const cache = new QueryCache();
1149
+ const itemBaseStore = new ItemBaseStore();
1150
+ const itemStore = new ReactiveRemoteStore<Item>(cache, itemBaseStore);
1151
+
1152
+ async function runReactiveUIExample() {
1153
+ console.log('\n--- Reactive UI Updates Example ---');
1154
+
1155
+ // Create some initial items
1156
+ const item1 = await itemStore.create({ data: { name: 'Item A', status: 'active' } });
1157
+ const item2 = await itemStore.create({ data: { name: 'Item B', status: 'inactive' } });
1158
+
1159
+ // Subscribe to a list of all items
1160
+ const [subscribeAllItems, selectAllItems] = itemStore.list({});
1161
+ const unsubscribeAllItems = subscribeAllItems(() => {
1162
+ console.log('All Items (UI Update):', selectAllItems().page?.data.map(i => `${i.name} (${i.status})`));
1163
+ });
1164
+
1165
+ // Subscribe to a single item
1166
+ if (item1) {
1167
+ const [subscribeItem1, selectItem1] = itemStore.read({ id: item1.id });
1168
+ const unsubscribeItem1 = subscribeItem1(() => {
1169
+ console.log('Item A (UI Update):', selectItem1().data ? `${selectItem1().data?.name} (${selectItem1().data?.status})` : 'Deleted');
1170
+ });
1171
+
1172
+ // Simulate an update to Item A
1173
+ await new Promise(r => setTimeout(r, 1000));
1174
+ await itemStore.update({ id: item1.id, data: { status: 'inactive' } });
1175
+ console.log('Simulated update to Item A.');
1176
+
1177
+ // Simulate deleting Item B
1178
+ await new Promise(r => setTimeout(r, 1000));
1179
+ if (item2) {
1180
+ await itemStore.delete({ id: item2.id });
1181
+ console.log('Simulated deletion of Item B.');
1182
+ }
1183
+
1184
+ unsubscribeItem1();
1185
+ }
1186
+
1187
+ unsubscribeAllItems();
1188
+ itemStore.destroy();
1189
+ }
1190
+
1191
+ // runReactiveUIExample();
1192
+ ```
1193
+
1194
+ #### Handling Real-time Data Streams
1195
+
1196
+ Consume continuous data flows from your backend for live dashboards, chat applications, or sensor data.
1197
+
1198
+ ```typescript
1199
+ import { ReactiveRemoteStore } from '@asaidimu/erp-utils-remote-store';
1200
+ import { QueryCache } from '@core/cache';
1201
+ import { BaseStore, Record, Page, StoreEvent } from '@asaidimu/erp-utils-remote-store/types';
1202
+
1203
+ interface SensorReading extends Record { temperature: number; humidity: number; timestamp: number; }
1204
+
1205
+ class SensorBaseStore implements BaseStore<SensorReading> { /* ... mock implementation ... */
1206
+ async find(): Promise<Page<SensorReading>> { return { data: [], page: { number: 1, size: 0, count: 0, pages: 0 } }; }
1207
+ async read(): Promise<SensorReading | undefined> { return undefined; }
1208
+ async list(): Promise<Page<SensorReading>> { return { data: [], page: { number: 1, size: 0, count: 0, pages: 0 } }; }
1209
+ async create(props: any): Promise<SensorReading | undefined> { return undefined; }
1210
+ async update(props: any): Promise<SensorReading | undefined> { return undefined; }
1211
+ async delete(): Promise<void> { }
1212
+ async upload(): Promise<SensorReading | undefined> { return undefined; }
1213
+ async subscribe(): Promise<() => void> { return () => {}; }
1214
+ async notify(): Promise<void> { }
1215
+ stream(options: any): { stream: () => AsyncIterable<SensorReading>; cancel: () => void; status: () => "active" | "cancelled" | "completed"; } {
1216
+ let counter = 0;
1217
+ const intervalId = setInterval(() => {
1218
+ // Simulate new readings every second
1219
+ const newReading = { id: `s${counter++}`, temperature: Math.random() * 20 + 20, humidity: Math.random() * 30 + 50, timestamp: Date.now() };
1220
+ // In a real scenario, this would push to an internal buffer consumed by the async generator
1221
+ // For this example, we'll just yield directly.
1222
+ // This mock doesn't perfectly simulate a real async generator being pushed to.
1223
+ // A real implementation would involve a queue and a way to push items into it.
1224
+ // For now, assume the BaseStore's stream method correctly provides the AsyncIterable.
1225
+ }, 1000);
1226
+
1227
+ const mockAsyncIterable = (async function* () {
1228
+ for (let i = 0; i < 5; i++) { // Yield 5 readings for example
1229
+ await new Promise(r => setTimeout(r, 1000));
1230
+ yield { id: `mock-${i}`, temperature: Math.random() * 10 + 20, humidity: Math.random() * 10 + 50, timestamp: Date.now() };
1231
+ }
1232
+ })();
1233
+
1234
+ return {
1235
+ stream: () => mockAsyncIterable,
1236
+ cancel: () => clearInterval(intervalId),
1237
+ status: () => 'completed' // Simplified status for mock
1238
+ };
1239
+ }
1240
+ }
1241
+
1242
+ const cache = new QueryCache();
1243
+ const sensorBaseStore = new SensorBaseStore();
1244
+ const sensorStore = new ReactiveRemoteStore<SensorReading>(cache, sensorBaseStore);
1245
+
1246
+ async function runStreamExample() {
1247
+ console.log('\n--- Real-time Data Stream Example ---');
1248
+
1249
+ const { stream, cancel, status } = sensorStore.stream({});
1250
+
1251
+ const statusInterval = setInterval(() => {
1252
+ console.log('Stream Status:', status());
1253
+ }, 500);
1254
+
1255
+ try {
1256
+ for await (const reading of stream()) {
1257
+ console.log(`Sensor Reading: Temp=${reading.temperature.toFixed(2)}°C, Humidity=${reading.humidity.toFixed(2)}%`);
1258
+ }
1259
+ console.log('Sensor stream completed.');
1260
+ } catch (error) {
1261
+ console.error('Sensor stream error:', error);
1262
+ } finally {
1263
+ clearInterval(statusInterval);
1264
+ cancel(); // Ensure stream is cancelled
1265
+ console.log('Stream cleanup.');
1266
+ }
1267
+ }
1268
+
1269
+ // runStreamExample();
1270
+ ```
1271
+
1272
+ ---
1273
+
1274
+ ## 🏗️ Project Architecture
1275
+
1276
+ `ReactiveRemoteStore` is designed with modularity and extensibility in mind, separating concerns to allow for flexible integration with various caching strategies and backend communication methods.
1277
+
1278
+ ### Directory Structure
1279
+
1280
+ ```
1281
+ src/remote-store/
1282
+ ├── error.ts # Custom error classes for store operations.
1283
+ ├── hash.ts # Utility for hashing query parameters to create unique cache keys.
1284
+ ├── store.ts # The core ReactiveRemoteStore class implementation.
1285
+ ├── types.ts # TypeScript interfaces and types for the store, events, and options.
1286
+ ├── test-server.ts # A mock backend implementation used for e2e testing and examples.
1287
+ ├── package.json # Package metadata and dependencies for this specific module.
1288
+ ├── README.md # This documentation file.
1289
+ └── ... # Other related files (e.g., test files, build configs)
1290
+ ```
1291
+
1292
+ ### Core Components
1293
+
1294
+ * **`ReactiveRemoteStore` (`store.ts`)**: The main class providing the reactive data access layer. It orchestrates data flow between the UI, the `QueryCache`, and the `BaseStore`. It manages query subscriptions, triggers fetches, handles invalidation, and processes real-time events.
1295
+ * **`BaseStore<T>` (`types.ts`)**: An interface that defines the fundamental CRUD (Create, Read, Update, Delete), Upload, Subscribe, Notify, and Stream operations that your remote data source must implement. This makes `ReactiveRemoteStore` backend-agnostic.
1296
+ * **`QueryCache` (from `@core/cache`)**: An external dependency responsible for the actual in-memory caching logic. `ReactiveRemoteStore` interacts with it to store, retrieve, and invalidate data. It handles cache policies like staleness, eviction, and background re-fetching.
1297
+ * **`StoreEvent` (`types.ts`)**: Represents a data change event, typically originating from the `BaseStore` (e.g., via SSE) or triggered by local mutations. It has a `scope` (e.g., `product:created:success`) and an optional `payload`.
1298
+ * **`Correlator` (`types.ts`)**: A function type that allows you to define custom logic for how local mutations (create, update, delete, upload) should affect the invalidation of active queries in the `QueryCache`.
1299
+ * **`StoreEventCorrelator` (`types.ts`)**: A function type that allows you to define custom logic for how incoming `StoreEvent`s (e.g., from SSE) should affect the invalidation of active queries in the `QueryCache`.
1300
+
1301
+ ### Data Flow
1302
+
1303
+ 1. **Query Initiation (e.g., `store.read()`, `store.list()`)**:
1304
+ * A component requests data via `ReactiveRemoteStore`'s reactive methods (`read`, `list`, `find`).
1305
+ * `ReactiveRemoteStore` generates a unique `queryKey` for the request.
1306
+ * It checks the `QueryCache` for existing data. If data is fresh, it's returned immediately.
1307
+ * If data is stale or not present, `ReactiveRemoteStore` registers a `fetchFunction` with the `QueryCache` (which then calls the appropriate `BaseStore` method) to fetch the data.
1308
+ * The `QueryCache` manages the actual fetching, retries, and updates its internal state.
1309
+ * `ReactiveRemoteStore` provides a `selector` function that components use to get the current state of the data (including `loading`, `stale`, `error`).
1310
+ * A `subscribe` function is also provided, allowing components to register callbacks that are notified whenever the query result changes in the cache.
1311
+
1312
+ 2. **Mutations (e.g., `store.create()`, `store.update()`)**:
1313
+ * When a mutation operation is performed, `ReactiveRemoteStore` first calls the corresponding method on the `BaseStore` to update the remote data.
1314
+ * Upon successful completion, it uses the configured `Correlator` (or a default strategy) to identify which active queries in the `QueryCache` should be invalidated.
1315
+ * Invalidated queries are marked stale, ensuring that subsequent accesses will trigger a re-fetch (or background re-fetch if SWR is enabled by the `QueryCache`).
1316
+
1317
+ 3. **Real-time Events (BaseStore-managed Connection)**:
1318
+ * `ReactiveRemoteStore` subscribes to a wildcard scope (`*`) on the `BaseStore`'s `subscribe` method, listening for all incoming `StoreEvent`s.
1319
+ * **Your `BaseStore` implementation is responsible for establishing and managing the actual real-time connection** (e.g., via Server-Sent Events, WebSockets, or other protocols) and pushing `StoreEvent`s to the `ReactiveRemoteStore` via the `callback` provided to `subscribe`.
1320
+ * When a `StoreEvent` is received, `ReactiveRemoteStore` uses the configured `StoreEventCorrelator` to determine which active queries should be invalidated based on the event's `scope` and `payload`.
1321
+ * This ensures that client-side data is automatically synchronized with server-side changes.
1322
+
1323
+ 4. **Data Streaming (`store.stream()`)**:
1324
+ * When `store.stream()` is called, `ReactiveRemoteStore` delegates the request to the `BaseStore`'s `stream` method.
1325
+ * The `BaseStore` is responsible for establishing and managing the actual stream (e.g., HTTP streaming, WebSockets) and returning an `AsyncIterable`.
1326
+ * `ReactiveRemoteStore` provides this `AsyncIterable` directly to the consumer, along with `cancel` and `status` controls.
1327
+
1328
+ ### Extension Points
1329
+
1330
+ `ReactiveRemoteStore` is designed to be highly extensible, allowing you to tailor its behavior to your specific application and backend needs:
1331
+
1332
+ * **Custom `BaseStore` Implementation**: The most significant extension point. By implementing the `BaseStore` interface, you can integrate `ReactiveRemoteStore` with any type of backend API (REST, GraphQL, custom RPC) and any communication library (Fetch API, Axios, gRPC clients).
1333
+ * Crucially, your `BaseStore` implementation also dictates the real-time communication method for `subscribe` and `notify` (e.g., Server-Sent Events, WebSockets, long polling, etc.), allowing you to choose the best fit for your backend and application needs.
1334
+ * **Custom `QueryCache`**: While `@core/cache` is assumed, you can swap it out for any caching library that adheres to the expected `QueryCache` interface, allowing you to control caching policies, persistence, and memory management.
1335
+ * **Custom `Correlator` and `StoreEventCorrelator`**: These functions provide powerful control over cache invalidation. You can implement complex logic to precisely invalidate only the necessary queries based on the specifics of your data model and backend events, optimizing performance and consistency.
1336
+ * **Custom `TFindOptions`, `TReadOptions`, etc.**: The generic type parameters for options (`TFindOptions`, `TReadOptions`, etc.) allow you to define strongly-typed options objects that are specific to your API's query capabilities, enhancing type safety throughout your data layer.
1337
+
1338
+ ---
1339
+
1340
+ ## 🤝 Development & Contributing
1341
+
1342
+ We welcome contributions to `ReactiveRemoteStore`! Whether it's a bug fix, a new feature, or an improvement to the documentation, your help is appreciated.
1343
+
1344
+ ### Development Setup
1345
+
1346
+ To set up the development environment for `ReactiveRemoteStore`:
1347
+
1348
+ 1. **Clone the monorepo** (assuming this is part of a larger `erp-utils` monorepo):
1349
+ ```bash
1350
+ git clone https://github.com/your-org/erp-utils.git # Replace with actual repo URL
1351
+ cd erp-utils
1352
+ ```
1353
+ 2. **Navigate to the `remote-store` package**:
1354
+ ```bash
1355
+ cd src/remote-store
1356
+ ```
1357
+ 3. **Install dependencies**:
1358
+ ```bash
1359
+ bun install
1360
+ # or
1361
+ npm install
1362
+ # or
1363
+ yarn install
1364
+ ```
1365
+ 4. **Build the project**:
1366
+ ```bash
1367
+ bun run build
1368
+ # or
1369
+ npm run build
1370
+ # or
1371
+ yarn build
1372
+ ```
1373
+
1374
+ ### Scripts
1375
+
1376
+ The following `bun`/`npm` scripts are available in this project:
1377
+
1378
+ * `bun run build`: Compiles TypeScript source files from `src/` to JavaScript output.
1379
+ * `bun run test`: Runs the test suite using `Vitest`.
1380
+ * `bun run test:watch`: Runs tests in watch mode for continuous feedback during development.
1381
+ * `bun run test-server`: Starts a mock backend server used for e2e tests and examples.
1382
+ * `bun run lint`: Runs ESLint to check for code style and potential errors.
1383
+ * `bun run format`: Formats code using Prettier according to the project's style guidelines.
1384
+
1385
+ ### Testing
1386
+
1387
+ This project includes comprehensive unit, integration, and end-to-end tests to ensure reliability and correctness.
1388
+
1389
+ * **Unit Tests**: Test individual functions and classes in isolation (e.g., `store.test.ts`).
1390
+ * **Integration Tests**: Verify the interaction between `ReactiveRemoteStore` and a mock `BaseStore` (e.g., `store.integration.test.ts`).
1391
+ * **End-to-End (e2e) Tests**: Test the entire system, including `ReactiveRemoteStore`, `QueryCache`, and the `test-server` mock backend, simulating real-world scenarios (e.g., `store.e2e.test.ts`).
1392
+
1393
+ To run all tests:
1394
+
1395
+ ```bash
1396
+ bun run test
1397
+ ```
1398
+
1399
+ To run the mock test server (required for e2e tests):
1400
+
1401
+ ```bash
1402
+ bun run test-server
1403
+ ```
1404
+
1405
+ We aim for high test coverage. Please ensure that new features or bug fixes come with appropriate tests to maintain code quality and prevent regressions.
1406
+
1407
+ ### Contributing Guidelines
1408
+
1409
+ Please follow these steps to contribute:
1410
+
1411
+ 1. **Fork the repository** on GitHub.
1412
+ 2. **Create a new branch** for your feature or bug fix: `git checkout -b feature/my-new-feature` or `bugfix/fix-issue-123`.
1413
+ 3. **Make your changes**, ensuring they adhere to the existing code style and architecture.
1414
+ 4. **Write or update tests** to cover your changes and ensure existing functionality is not broken.
1415
+ 5. **Ensure all tests pass** locally by running `bun run test`.
1416
+ 6. **Run lint and format checks** (`bun run lint` and `bun run format`) and fix any reported issues.
1417
+ 7. **Write clear, concise commit messages** following the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification (e.g., `feat: add new streaming capability`, `fix: correct invalidation logic`).
1418
+ 8. **Push your branch** to your fork.
1419
+ 9. **Open a Pull Request** to the `main` branch of the original repository. Provide a detailed description of your changes and why they are necessary.
1420
+
1421
+ ### Issue Reporting
1422
+
1423
+ Found a bug, have a feature request, or need clarification? Please open an issue on our [GitHub Issues page](https://github.com/your-org/erp-utils/issues). <!-- Placeholder: Replace with actual issues URL -->
1424
+
1425
+ When reporting a bug, please include:
1426
+
1427
+ * A clear and concise description of the issue.
1428
+ * Detailed steps to reproduce the behavior.
1429
+ * The expected behavior.
1430
+ * Any relevant screenshots or code snippets.
1431
+ * Your environment details (Node.js version, OS, package manager, package version).
1432
+
1433
+ ---
1434
+
1435
+ ## 📚 Additional Information
1436
+
1437
+ ### Troubleshooting
1438
+
1439
+ * **Tests failing after changes in `test-server.ts`**: Ensure the `test-server` is restarted after any modifications to its code, as it runs as a separate process.
1440
+ ```bash
1441
+ bun run test-server # Stop and restart
1442
+ ```
1443
+ * **Data not updating reactively**:
1444
+ * Verify that your `BaseStore` implementation correctly calls `notify` or that your backend is sending `StoreEvent`s via SSE.
1445
+ * Check your `correlator` and `storeEventCorrelator` functions to ensure they are correctly identifying and invalidating relevant queries.
1446
+ * Ensure your UI components are correctly subscribing to the `ReactiveRemoteStore`'s query results.
1447
+ * **`TypeError: result.stream(...) is not a function`**: This typically occurs in unit tests if the mock for `baseStore.stream` does not return an object with a `stream` property that is a function returning an `AsyncIterable`. Ensure your mock matches the `BaseStore` interface precisely.
1448
+
1449
+ ### FAQ
1450
+
1451
+ **Q: What is the difference between `read`, `list`, and `find`?**
1452
+ A: All three methods retrieve data, but they serve different purposes:
1453
+ * `read`: Designed for fetching a single, specific record (e.g., by ID).
1454
+ * `list`: Designed for fetching a paginated collection of records, typically without complex search criteria (e.g., all products, all users).
1455
+ * `find`: Designed for fetching a paginated collection of records based on specific search criteria or filters.
1456
+
1457
+ **Q: How does `ReactiveRemoteStore` handle offline scenarios?**
1458
+ A: `ReactiveRemoteStore` itself does not directly handle offline persistence. Its caching capabilities (via `QueryCache`) provide in-memory caching. For true offline support, your `QueryCache` implementation would need to integrate with a persistent storage solution (e.g., IndexedDB, LocalStorage) and handle network connectivity changes.
1459
+
1460
+ **Q: Can I use `ReactiveRemoteStore` with GraphQL?**
1461
+ A: Yes! `ReactiveRemoteStore` is backend-agnostic. You would implement the `BaseStore` interface using a GraphQL client (e.g., Apollo Client, Relay, or a simple `fetch`-based client) to make your GraphQL queries and mutations.
1462
+
1463
+ ### Changelog/Roadmap
1464
+
1465
+ For a detailed history of changes, new features, and bug fixes, please refer to the [CHANGELOG.md](CHANGELOG.md) file in the repository root. Our future plans are outlined in the [ROADMAP.md](ROADMAP.md) (if available).
1466
+
1467
+ ### License
1468
+
1469
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
1470
+
1471
+ ### Acknowledgments
1472
+
1473
+ * Inspired by modern reactive data management patterns and libraries.
1474
+ * Leverages `QueryCache` for robust caching capabilities.
1475
+ * Uses `Vitest` for testing and `TypeScript` for type safety.