@asaidimu/utils-remote-store 1.2.9 → 1.3.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.
Files changed (4) hide show
  1. package/README.md +921 -567
  2. package/index.d.mts +24 -24
  3. package/index.d.ts +24 -24
  4. package/package.json +2 -2
package/README.md CHANGED
@@ -47,24 +47,24 @@ An intelligent, reactive data management library for JavaScript/TypeScript appli
47
47
 
48
48
  Key aspects of `ReactiveRemoteStore` include:
49
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.
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
56
 
57
57
  ### Key Features
58
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.
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
68
 
69
69
  ---
70
70
 
@@ -72,10 +72,10 @@ Key aspects of `ReactiveRemoteStore` include:
72
72
 
73
73
  ### Prerequisites
74
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.
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
79
 
80
80
  ### Installation Steps
81
81
 
@@ -94,108 +94,169 @@ yarn add @asaidimu/erp-utils-remote-store @core/cache
94
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
95
 
96
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';
97
+ import { ReactiveRemoteStore } from "@asaidimu/erp-utils-remote-store";
98
+ import { QueryCache } from "@core/cache"; // Your QueryCache implementation
99
+ import {
100
+ BaseStore,
101
+ Record,
102
+ Page,
103
+ StoreEvent,
104
+ ActiveQuery,
105
+ MutationOperation,
106
+ } from "@asaidimu/erp-utils-remote-store/types";
100
107
 
101
108
  // --- Example: Define your data types ---
102
109
  interface Product extends Record {
103
- name: string;
104
- price: number;
105
- inStock: boolean;
110
+ name: string;
111
+ price: number;
112
+ inStock: boolean;
106
113
  }
107
114
 
108
115
  // --- Example: Implement your BaseStore (API client) ---
109
116
  // This would typically interact with your backend via fetch, axios, etc.
110
117
  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
- }
118
+ private baseUrl: string;
119
+ constructor(baseUrl: string) {
120
+ this.baseUrl = baseUrl;
121
+ }
122
+
123
+ async find(options: any): Promise<Page<Product>> {
124
+ /* ... API call ... */ return {
125
+ data: [],
126
+ page: { number: 1, size: 0, count: 0, pages: 0 },
127
+ };
128
+ }
129
+ async read(options: { id: string }): Promise<Product | undefined> {
130
+ /* ... API call ... */ return undefined;
131
+ }
132
+ async list(options: any): Promise<Page<Product>> {
133
+ /* ... API call ... */ return {
134
+ data: [],
135
+ page: { number: 1, size: 0, count: 0, pages: 0 },
136
+ };
137
+ }
138
+ async create(props: {
139
+ data: Omit<Product, "id">;
140
+ options?: any;
141
+ }): Promise<Product | undefined> {
142
+ /* ... API call ... */ return { id: "new-id", ...props.data };
143
+ }
144
+ async update(props: {
145
+ id: string;
146
+ data: Partial<Omit<Product, "id">>;
147
+ options?: any;
148
+ }): Promise<Product | undefined> {
149
+ /* ... API call ... */ return { id: props.id, ...props.data };
150
+ }
151
+ async delete(options: { id: string }): Promise<void> {
152
+ /* ... API call ... */
153
+ }
154
+ async upload(props: {
155
+ file: File;
156
+ options?: any;
157
+ }): Promise<Product | undefined> {
158
+ /* ... API call ... */ return {
159
+ id: "uploaded-id",
160
+ name: "uploaded",
161
+ price: 0,
162
+ inStock: true,
163
+ };
164
+ }
165
+
166
+ // For real-time updates, your `BaseStore` implementation would establish and manage
167
+ // a connection using technologies like Server-Sent Events (SSE), WebSockets, or polling.
168
+ // The `ReactiveRemoteStore` simply calls these methods on your `BaseStore`.
169
+ async subscribe(
170
+ scope: string,
171
+ callback: (event: StoreEvent) => void,
172
+ ): Promise<() => void> {
173
+ console.log(`Subscribing to ${scope}`);
174
+ // Example: Connect to an SSE endpoint (as in test-server.ts)
175
+ // const eventSource = new EventSource(`${this.baseUrl}/events?scope=${scope}`);
176
+ // eventSource.onmessage = (e) => callback(JSON.parse(e.data));
177
+ // return () => eventSource.close();
178
+
179
+ // Example: Using WebSockets
180
+ // const ws = new WebSocket(`${this.baseUrl}/ws?scope=${scope}`);
181
+ // ws.onmessage = (e) => callback(JSON.parse(e.data));
182
+ // return () => ws.close();
183
+
184
+ return async () => {
185
+ console.log(`Unsubscribed from ${scope}`);
186
+ };
187
+ }
188
+ async notify(event: StoreEvent): Promise<void> {
189
+ console.log("Notifying base store:", event);
190
+ // Your implementation here to send the event to the backend
191
+ // e.g., via a POST request, WebSocket message, etc.
192
+ // await fetch(`${this.baseUrl}/notify`, { method: 'POST', body: JSON.stringify(event) });
193
+ }
194
+ stream(options: any): {
195
+ stream: () => AsyncIterable<Product>;
196
+ cancel: () => void;
197
+ status: () => "active" | "cancelled" | "completed";
198
+ } {
199
+ console.log("Streaming with options:", options);
200
+ const mockStream = (async function* () {
201
+ yield { id: "p1", name: "Streamed Product 1", price: 10, inStock: true };
202
+ await new Promise((r) => setTimeout(r, 100));
203
+ yield { id: "p2", name: "Streamed Product 2", price: 20, inStock: false };
204
+ })();
205
+ return {
206
+ stream: () => mockStream,
207
+ cancel: () => console.log("Stream cancelled"),
208
+ status: () => "completed",
209
+ };
210
+ }
154
211
  }
155
212
 
156
213
  // --- Optional: Define custom correlators for invalidation ---
157
214
  const myCorrelator = (
158
- mutation: { operation: MutationOperation; params: any },
159
- activeQueries: ActiveQuery[]
215
+ mutation: { operation: MutationOperation; params: any },
216
+ activeQueries: ActiveQuery[],
160
217
  ): 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 [];
218
+ // Example: Invalidate 'list' queries on any create/delete operation
219
+ if (mutation.operation === "create" || mutation.operation === "delete") {
220
+ return activeQueries
221
+ .filter((q) => q.operation === "list")
222
+ .map((q) => q.queryKey);
223
+ }
224
+ // Example: Invalidate specific 'read' query if its ID matches the updated item
225
+ if (mutation.operation === "update" && mutation.params.id) {
226
+ return activeQueries
227
+ .filter(
228
+ (q) => q.operation === "read" && q.params.id === mutation.params.id,
229
+ )
230
+ .map((q) => q.queryKey);
231
+ }
232
+ return [];
172
233
  };
173
234
 
174
235
  const myStoreEventCorrelator = (
175
- event: StoreEvent, // { scope: string, payload?: any }
176
- activeQueries: ActiveQuery[]
236
+ event: StoreEvent, // { scope: string, payload?: any }
237
+ activeQueries: ActiveQuery[],
177
238
  ): 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 [];
239
+ // Example: Invalidate 'read' query if an external event updates its ID
240
+ if (event.scope === "product:updated:external" && event.payload?.id) {
241
+ return activeQueries
242
+ .filter((q) => q.operation === "read" && q.params.id === event.payload.id)
243
+ .map((q) => q.queryKey);
244
+ }
245
+ return [];
185
246
  };
186
247
 
187
248
  // --- Initialize ReactiveRemoteStore ---
188
249
  const cache = new QueryCache();
189
- const baseStore = new MyApiBaseStore('https://api.example.com');
250
+ const baseStore = new MyApiBaseStore("https://api.example.com");
190
251
 
191
252
  const productStore = new ReactiveRemoteStore<Product>(
192
- cache,
193
- baseStore,
194
- myCorrelator, // Optional: for mutation-based invalidation
195
- myStoreEventCorrelator // Optional: for store event-based invalidation
253
+ cache,
254
+ baseStore,
255
+ myCorrelator, // Optional: for mutation-based invalidation
256
+ myStoreEventCorrelator, // Optional: for store event-based invalidation
196
257
  );
197
258
 
198
- console.log('ReactiveRemoteStore initialized successfully!');
259
+ console.log("ReactiveRemoteStore initialized successfully!");
199
260
  ```
200
261
 
201
262
  ### Verification
@@ -203,32 +264,59 @@ console.log('ReactiveRemoteStore initialized successfully!');
203
264
  To verify that `ReactiveRemoteStore` is installed and initialized correctly, you can run a simple test:
204
265
 
205
266
  ```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';
267
+ import { ReactiveRemoteStore } from "@asaidimu/erp-utils-remote-store";
268
+ import { QueryCache } from "@core/cache";
269
+ import {
270
+ BaseStore,
271
+ Record,
272
+ Page,
273
+ StoreEvent,
274
+ } from "@asaidimu/erp-utils-remote-store/types";
209
275
 
210
276
  // Minimal BaseStore for verification
211
277
  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
- }
278
+ async find(): Promise<Page<Record>> {
279
+ return { data: [], page: { number: 1, size: 0, count: 0, pages: 0 } };
280
+ }
281
+ async read(): Promise<Record | undefined> {
282
+ return undefined;
283
+ }
284
+ async list(): Promise<Page<Record>> {
285
+ return { data: [], page: { number: 1, size: 0, count: 0, pages: 0 } };
286
+ }
287
+ async create(props: any): Promise<Record | undefined> {
288
+ return { id: "1", ...props.data };
289
+ }
290
+ async update(props: any): Promise<Record | undefined> {
291
+ return { id: props.id, ...props.data };
292
+ }
293
+ async delete(): Promise<void> {}
294
+ async upload(): Promise<Record | undefined> {
295
+ return undefined;
296
+ }
297
+ async subscribe(): Promise<() => void> {
298
+ return () => {};
299
+ }
300
+ async notify(): Promise<void> {}
301
+ stream(): {
302
+ stream: () => AsyncIterable<Record>;
303
+ cancel: () => void;
304
+ status: () => "active" | "cancelled" | "completed";
305
+ } {
306
+ return {
307
+ stream: async function* () {},
308
+ cancel: () => {},
309
+ status: () => "completed",
310
+ };
311
+ }
224
312
  }
225
313
 
226
314
  const cache = new QueryCache();
227
315
  const baseStore = new MinimalBaseStore();
228
316
  const store = new ReactiveRemoteStore(cache, baseStore);
229
317
 
230
- console.log('ReactiveRemoteStore instance:', store);
231
- console.log('Store initialized successfully!');
318
+ console.log("ReactiveRemoteStore instance:", store);
319
+ console.log("Store initialized successfully!");
232
320
 
233
321
  // You can also run the project's tests to verify full functionality:
234
322
  // bun run vitest --run
@@ -249,33 +337,52 @@ The `BaseStore`'s responsibility is to handle the direct communication with your
249
337
  First, define the data structure for the records your store will manage. This type must extend `StoreRecord`, which requires an `id: string` property.
250
338
 
251
339
  ```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';
340
+ import type {
341
+ BaseStore,
342
+ StoreRecord,
343
+ Page,
344
+ StoreEvent,
345
+ } from "@asaidimu/erp-utils-remote-store/types";
346
+ import { StoreError } from "@asaidimu/erp-utils-remote-store/error";
254
347
 
255
348
  // Your record type
256
349
  interface Product extends StoreRecord {
257
- name: string;
258
- price: number;
259
- // ... any other fields
350
+ name: string;
351
+ price: number;
352
+ // ... any other fields
260
353
  }
261
354
 
262
355
  // Define the option types for your API
263
356
  // These are used for type-safety when calling the store's methods
264
357
 
265
358
  // Options for reading a single product (e.g., by ID)
266
- interface ProductReadOptions { id: string; }
359
+ interface ProductReadOptions {
360
+ id: string;
361
+ }
267
362
 
268
363
  // Options for listing products (e.g., with pagination and sorting)
269
- interface ProductListOptions { page?: number; pageSize?: number; sortBy?: keyof Product; order?: 'asc' | 'desc'; }
364
+ interface ProductListOptions {
365
+ page?: number;
366
+ pageSize?: number;
367
+ sortBy?: keyof Product;
368
+ order?: "asc" | "desc";
369
+ }
270
370
 
271
371
  // Options for finding products (e.g., with a search query)
272
- interface ProductFindOptions extends ProductListOptions { query: string; }
372
+ interface ProductFindOptions extends ProductListOptions {
373
+ query: string;
374
+ }
273
375
 
274
376
  // 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...
377
+ class ProductApiStore implements BaseStore<
378
+ Product,
379
+ ProductFindOptions,
380
+ ProductReadOptions,
381
+ ProductListOptions
382
+ > {
383
+ private readonly baseUrl = "https://api.example.com";
384
+
385
+ // Implementation of each method will go here...
279
386
  }
280
387
  ```
281
388
 
@@ -287,8 +394,8 @@ Here is a detailed look at each method in the `BaseStore` interface.
287
394
 
288
395
  **Purpose**: Fetch a single record by its identifier.
289
396
 
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.
397
+ - **`options`**: The parameters for the read operation, typically containing the record's `id`.
398
+ - **Returns**: A `Promise` that resolves to the fetched record or `undefined` if it's not found.
292
399
 
293
400
  ```typescript
294
401
  // Example implementation for `read` with error handling
@@ -314,8 +421,8 @@ async read(options: ProductReadOptions): Promise<Product | undefined> {
314
421
 
315
422
  **Purpose**: Fetch a paginated list of records.
316
423
 
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.
424
+ - **`options`**: Parameters for listing, such as `page`, `pageSize`, `sortBy`, etc.
425
+ - **Returns**: A `Promise` that resolves to a `Page<T>` object, which contains the `data` array and pagination details.
319
426
 
320
427
  ```typescript
321
428
  // Example implementation for `list`
@@ -343,8 +450,8 @@ async list(options: ProductListOptions): Promise<Page<Product>> {
343
450
 
344
451
  **Purpose**: Fetch a paginated list of records based on search criteria.
345
452
 
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.
453
+ - **`options`**: Parameters for finding, which might include a search `query` along with pagination and sorting.
454
+ - **Returns**: A `Promise` that resolves to a `Page<T>` object.
348
455
 
349
456
  ```typescript
350
457
  // Example implementation for `find`
@@ -369,8 +476,8 @@ async find(options: ProductFindOptions): Promise<Page<Product>> {
369
476
 
370
477
  **Purpose**: Create a new record.
371
478
 
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`).
479
+ - **`props.data`**: The record to create.
480
+ - **Returns**: A `Promise` that resolves to the newly created record as returned by the server (including the server-generated `id`).
374
481
 
375
482
  ```typescript
376
483
  // Example implementation for `create`
@@ -395,8 +502,8 @@ async create(props: { data: Partial<Product> }): Promise<Product | undefined> {
395
502
 
396
503
  **Purpose**: Update an existing record.
397
504
 
398
- - **`props.data`**: A partial object of the fields to update, including the `id`.
399
- - **Returns**: A `Promise` that resolves to the updated record.
505
+ - **`props.data`**: A partial object of the fields to update, including the `id`.
506
+ - **Returns**: A `Promise` that resolves to the updated record.
400
507
 
401
508
  ```typescript
402
509
  // Example implementation for `update`
@@ -422,8 +529,8 @@ async update(props: { data: Partial<Product> }): Promise<Product | undefined> {
422
529
 
423
530
  **Purpose**: Delete a record.
424
531
 
425
- - **`options`**: Parameters for deletion, typically `{ id: string }`.
426
- - **Returns**: A `Promise` that resolves when the operation is complete.
532
+ - **`options`**: Parameters for deletion, typically `{ id: string }`.
533
+ - **Returns**: A `Promise` that resolves when the operation is complete.
427
534
 
428
535
  ```typescript
429
536
  // Example implementation for `delete`
@@ -449,9 +556,9 @@ async delete(options: { id: string }): Promise<void> {
449
556
 
450
557
  **Purpose**: Establish a connection to your backend to receive real-time events.
451
558
 
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.
559
+ - **`scope`**: A string indicating the event scope to subscribe to. `ReactiveRemoteStore` will call this with `'*'` to listen for all events.
560
+ - **`callback`**: A function that `ReactiveRemoteStore` provides. Your implementation must call this function whenever a `StoreEvent` is received from the backend.
561
+ - **Returns**: A `Promise` that resolves to an `unsubscribe` function. `ReactiveRemoteStore` will call this function when it's destroyed to clean up the connection.
455
562
 
456
563
  ```typescript
457
564
  // Example implementation for `subscribe` using Server-Sent Events (SSE)
@@ -481,8 +588,8 @@ async subscribe(scope: string, callback: (event: StoreEvent) => void): Promise<(
481
588
 
482
589
  **Purpose**: Send an event from the client to the server. This is less common but can be used for client-initiated notifications.
483
590
 
484
- - **`event`**: The `StoreEvent` to send.
485
- - **Returns**: A `Promise` that resolves when the event has been sent.
591
+ - **`event`**: The `StoreEvent` to send.
592
+ - **Returns**: A `Promise` that resolves when the event has been sent.
486
593
 
487
594
  ```typescript
488
595
  // Example implementation for `notify`
@@ -509,6 +616,7 @@ For robust error handling, your `BaseStore` methods should wrap their logic in a
509
616
  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
617
 
511
618
  **Benefits of using `StoreError`**:
619
+
512
620
  - **Standardization**: All errors from the data layer have a consistent shape.
513
621
  - **Rich Information**: Includes `code`, `status`, `isRetryable`, and the `originalError`.
514
622
  - **Automatic Handling**: Correctly identifies common network and HTTP issues from `fetch` responses.
@@ -545,8 +653,6 @@ async read(options: ProductReadOptions): Promise<Product | undefined> {
545
653
  }
546
654
  ```
547
655
 
548
-
549
-
550
656
  ## 📖 Usage Documentation
551
657
 
552
658
  ### Basic Usage
@@ -554,50 +660,82 @@ async read(options: ProductReadOptions): Promise<Product | undefined> {
554
660
  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
661
 
556
662
  ```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';
663
+ import { ReactiveRemoteStore } from "@asaidimu/erp-utils-remote-store";
664
+ import { QueryCache } from "@core/cache";
665
+ import {
666
+ BaseStore,
667
+ Record,
668
+ Page,
669
+ StoreEvent,
670
+ } from "@asaidimu/erp-utils-remote-store/types";
560
671
 
561
672
  // Assume you have a Todo type and a TodoBaseStore implementation
562
673
  interface Todo extends Record {
563
- title: string;
564
- completed: boolean;
674
+ title: string;
675
+ completed: boolean;
565
676
  }
566
677
 
567
678
  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' };
679
+ // ... (implementation similar to MyApiBaseStore above)
680
+ // For simplicity, let's use a mock in-memory store for this example
681
+ private todos: Todo[] = [];
682
+ private nextId = 1;
683
+
684
+ async create(props: {
685
+ data: Omit<Todo, "id">;
686
+ options?: any;
687
+ }): Promise<Todo | undefined> {
688
+ const newTodo = { id: String(this.nextId++), ...props.data };
689
+ this.todos.push(newTodo);
690
+ return newTodo;
691
+ }
692
+ async read(options: { id: string }): Promise<Todo | undefined> {
693
+ return this.todos.find((t) => t.id === options.id);
694
+ }
695
+ async list(options: any): Promise<Page<Todo>> {
696
+ return {
697
+ data: this.todos,
698
+ page: {
699
+ number: 1,
700
+ size: this.todos.length,
701
+ count: this.todos.length,
702
+ pages: 1,
703
+ },
704
+ };
705
+ }
706
+ async update(props: {
707
+ id: string;
708
+ data: Partial<Omit<Todo, "id">>;
709
+ options?: any;
710
+ }): Promise<Todo | undefined> {
711
+ const todo = this.todos.find((t) => t.id === props.id);
712
+ if (todo) {
713
+ Object.assign(todo, props.data);
714
+ return todo;
600
715
  }
716
+ return undefined;
717
+ }
718
+ async delete(options: { id: string }): Promise<void> {
719
+ this.todos = this.todos.filter((t) => t.id !== options.id);
720
+ }
721
+ async upload(): Promise<Todo | undefined> {
722
+ return undefined;
723
+ }
724
+ async subscribe(): Promise<() => void> {
725
+ return () => {};
726
+ }
727
+ async notify(): Promise<void> {}
728
+ stream(): {
729
+ stream: () => AsyncIterable<Todo>;
730
+ cancel: () => void;
731
+ status: () => "active" | "cancelled" | "completed";
732
+ } {
733
+ return {
734
+ stream: async function* () {},
735
+ cancel: () => {},
736
+ status: () => "completed",
737
+ };
738
+ }
601
739
  }
602
740
 
603
741
  const cache = new QueryCache();
@@ -605,47 +743,61 @@ const todoBaseStore = new TodoBaseStore();
605
743
  const todoStore = new ReactiveRemoteStore<Todo>(cache, todoBaseStore);
606
744
 
607
745
  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);
746
+ console.log("\n--- Basic Usage Examples ---");
747
+
748
+ // 1. Create a Todo
749
+ const createdTodo = await todoStore.create({
750
+ data: { title: "Buy groceries", completed: false },
751
+ });
752
+ console.log("Created Todo:", createdTodo);
753
+
754
+ // 2. Read a Todo and subscribe to changes
755
+ if (createdTodo) {
756
+ const { value: selectRead, onValueChange: subscribeRead } = todoStore.read({
757
+ id: createdTodo.id,
758
+ });
759
+ const unsubscribeRead = subscribeRead(() => {
760
+ const result = selectRead();
761
+ console.log(
762
+ "Read Todo (reactive update):",
763
+ result.data,
764
+ "loading:",
765
+ result.loading,
766
+ );
767
+ });
623
768
 
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
- });
769
+ console.log("Initial Read:", selectRead().data);
770
+
771
+ // 3. List all Todos and subscribe to changes
772
+ const { value, onValueChange: subscribeToList } = todoStore.list({});
773
+ let result = value();
774
+ const unsubscribeList = subscribeToList(() => {
775
+ result = value();
776
+ console.log(
777
+ "List Todos (reactive update):",
778
+ result.page?.data,
779
+ "loading:",
780
+ result.loading,
781
+ );
782
+ });
631
783
 
632
- console.log('Initial List:', result.page?.data);
784
+ console.log("Initial List:", result.page?.data);
633
785
 
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.');
786
+ // 4. Update the Todo (this will trigger reactive updates in read and list queries)
787
+ await todoStore.update({ id: createdTodo.id, data: { completed: true } });
788
+ console.log("Updated Todo to completed.");
637
789
 
638
- // 5. Delete the Todo (this will trigger reactive updates)
639
- await todoStore.delete({ id: createdTodo.id });
640
- console.log('Deleted Todo.');
790
+ // 5. Delete the Todo (this will trigger reactive updates)
791
+ await todoStore.delete({ id: createdTodo.id });
792
+ console.log("Deleted Todo.");
641
793
 
642
- // Clean up subscriptions
643
- unsubscribeRead();
644
- unsubscribeList();
645
- }
794
+ // Clean up subscriptions
795
+ unsubscribeRead();
796
+ unsubscribeList();
797
+ }
646
798
 
647
- // Don't forget to destroy the store when your application shuts down
648
- todoStore.destroy();
799
+ // Don't forget to destroy the store when your application shuts down
800
+ todoStore.destroy();
649
801
  }
650
802
 
651
803
  runBasicUsage();
@@ -659,38 +811,50 @@ runBasicUsage();
659
811
 
660
812
  Creates a new instance of `ReactiveRemoteStore`.
661
813
 
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.
814
+ - `cache`: An instance of `QueryCache` (or compatible interface) used for caching data.
815
+ - `baseStore`: Your implementation of the `BaseStore` interface, responsible for direct interaction with the remote API.
816
+ - `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.
817
+ - `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
818
 
667
819
  #### `read(params: TReadOptions): ReactiveQueryResult<T>`
668
820
 
669
821
  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
822
 
671
- - `params`: Options specific to your `BaseStore`'s `read` operation (e.g., `{ id: string }`).
823
+ - `params`: Options specific to your `BaseStore`'s `read` operation (e.g., `{ id: string }`).
672
824
 
673
825
  **Returns:**
674
826
 
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.
827
+ - `{ value, onValueChange }`: An object.
828
+ - `value`: A function `() => QueryResult<T>` that returns the current state of the query, including `data`, `loading` status, `error`, `stale` status, and `updated` timestamp.
829
+ - `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
830
 
679
831
  ```typescript
680
832
  // Example: Reading a user profile
681
- interface UserProfile extends Record { name: string; email: string; }
833
+ interface UserProfile extends Record {
834
+ name: string;
835
+ email: string;
836
+ }
682
837
  // Assume userStore is ReactiveRemoteStore<UserProfile>
683
838
 
684
- const { value: selectUser, onValueChange: subscribeUser } = userStore.read({ id: 'user-123' });
839
+ const { value: selectUser, onValueChange: subscribeUser } = userStore.read({
840
+ id: "user-123",
841
+ });
685
842
 
686
843
  // Initial access
687
- console.log('Initial User Data:', selectUser().data);
844
+ console.log("Initial User Data:", selectUser().data);
688
845
 
689
846
  // Subscribe to changes
690
847
  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);
848
+ const result = selectUser();
849
+ console.log(
850
+ "User Data Updated:",
851
+ result.data,
852
+ "Loading:",
853
+ result.loading,
854
+ "Stale:",
855
+ result.stale,
856
+ );
857
+ if (result.error) console.error("Error fetching user:", result.error);
694
858
  });
695
859
 
696
860
  // Later, when no longer needed
@@ -701,33 +865,42 @@ const unsubscribe = subscribeUser(() => {
701
865
 
702
866
  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
867
 
704
- - `params`: Options specific to your `BaseStore`'s `list` operation (e.g., `{ page: number, pageSize: number, filter?: string }`).
868
+ - `params`: Options specific to your `BaseStore`'s `list` operation (e.g., `{ page: number, pageSize: number, filter?: string }`).
705
869
 
706
870
  **Returns:**
707
871
 
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.
872
+ - `{ value, onValueChange }`: An object.
873
+ - `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)`).
874
+ - `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
875
 
712
876
  ```typescript
713
877
  // Example: Listing products with pagination
714
- interface Product extends Record { name: string; price: number; }
878
+ interface Product extends Record {
879
+ name: string;
880
+ price: number;
881
+ }
715
882
  // Assume productStore is ReactiveRemoteStore<Product>
716
883
 
717
- const { value: selectProducts, onValueChange: subscribeProducts } = productStore.list({ page: 1, pageSize: 10 });
884
+ const { value: selectProducts, onValueChange: subscribeProducts } =
885
+ productStore.list({ page: 1, pageSize: 10 });
718
886
 
719
887
  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
- }
888
+ const result = selectProducts();
889
+ console.log(
890
+ "Products List Updated:",
891
+ result.page?.data,
892
+ "Loading:",
893
+ result.loading,
894
+ );
895
+ if (result.page) {
896
+ console.log(`Page ${result.page.page.number} of ${result.page.page.pages}`);
897
+ }
725
898
  });
726
899
 
727
900
  // Navigate to the next page
728
901
  const currentProducts = selectProducts();
729
902
  if (currentProducts.hasNext) {
730
- await currentProducts.next();
903
+ await currentProducts.next();
731
904
  }
732
905
 
733
906
  // Fetch a specific page
@@ -740,22 +913,32 @@ await selectProducts().fetch(3);
740
913
 
741
914
  Retrieves a paginated list of records based on search criteria. Similar to `list`, but typically used for more complex search queries.
742
915
 
743
- - `params`: Options specific to your `BaseStore`'s `find` operation (e.g., `{ query: string, category: string }`).
916
+ - `params`: Options specific to your `BaseStore`'s `find` operation (e.g., `{ query: string, category: string }`).
744
917
 
745
918
  **Returns:**
746
919
 
747
- - `{ value, onValueChange }`: An object identical in structure and behavior to the `list` method's return value.
920
+ - `{ value, onValueChange }`: An object identical in structure and behavior to the `list` method's return value.
748
921
 
749
922
  ```typescript
750
923
  // Example: Finding orders by customer ID
751
- interface Order extends Record { customerId: string; amount: number; }
924
+ interface Order extends Record {
925
+ customerId: string;
926
+ amount: number;
927
+ }
752
928
  // Assume orderStore is ReactiveRemoteStore<Order>
753
929
 
754
- const { value: selectOrders, onValueChange: subscribeOrders } = orderStore.find({ customerId: 'cust-456', status: 'pending' });
930
+ const { value: selectOrders, onValueChange: subscribeOrders } = orderStore.find(
931
+ { customerId: "cust-456", status: "pending" },
932
+ );
755
933
 
756
934
  const unsubscribeOrders = subscribeOrders(() => {
757
- const result = selectOrders();
758
- console.log('Orders Found Updated:', result.page?.data, 'Loading:', result.loading);
935
+ const result = selectOrders();
936
+ console.log(
937
+ "Orders Found Updated:",
938
+ result.page?.data,
939
+ "Loading:",
940
+ result.loading,
941
+ );
759
942
  });
760
943
 
761
944
  // unsubscribeOrders();
@@ -765,27 +948,32 @@ const unsubscribeOrders = subscribeOrders(() => {
765
948
 
766
949
  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
950
 
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.
951
+ - `params.data`: The data for the new record, excluding the `id` (which is typically generated by the backend).
952
+ - `params.options` (optional): Options specific to your `BaseStore`'s `create` operation.
770
953
 
771
954
  **Returns:** A promise that resolves to the newly created record (including its `id`) or `undefined` if creation failed.
772
955
 
773
956
  ```typescript
774
957
  // Example: Creating a new task
775
- interface Task extends Record { description: string; dueDate: string; }
958
+ interface Task extends Record {
959
+ description: string;
960
+ dueDate: string;
961
+ }
776
962
  // Assume taskStore is ReactiveRemoteStore<Task>
777
963
 
778
- const newTask = await taskStore.create({ data: { description: 'Write documentation', dueDate: '2025-12-31' } });
779
- console.log('New Task Created:', newTask);
964
+ const newTask = await taskStore.create({
965
+ data: { description: "Write documentation", dueDate: "2025-12-31" },
966
+ });
967
+ console.log("New Task Created:", newTask);
780
968
  ```
781
969
 
782
970
  #### `update(params: { id: string; data: Partial<Omit<T, 'id'>>; options?: TUpdateOptions }): Promise<T | undefined>`
783
971
 
784
972
  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
973
 
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.
974
+ - `params.id`: The `id` of the record to update.
975
+ - `params.data`: A partial object containing the fields to update.
976
+ - `params.options` (optional): Options specific to your `BaseStore`'s `update` operation.
789
977
 
790
978
  **Returns:** A promise that resolves to the updated record or `undefined` if the record was not found or the update failed.
791
979
 
@@ -793,15 +981,18 @@ Updates an existing record in the remote store. This operation automatically inv
793
981
  // Example: Marking a task as completed
794
982
  // Assume taskStore is ReactiveRemoteStore<Task> and taskId is known
795
983
 
796
- const updatedTask = await taskStore.update({ id: taskId, data: { completed: true } });
797
- console.log('Task Updated:', updatedTask);
984
+ const updatedTask = await taskStore.update({
985
+ id: taskId,
986
+ data: { completed: true },
987
+ });
988
+ console.log("Task Updated:", updatedTask);
798
989
  ```
799
990
 
800
991
  #### `delete(params: TDeleteOptions): Promise<void>`
801
992
 
802
993
  Deletes a record from the remote store. This operation automatically invalidates relevant cached queries.
803
994
 
804
- - `params`: Options specific to your `BaseStore`'s `delete` operation (e.g., `{ id: string }`).
995
+ - `params`: Options specific to your `BaseStore`'s `delete` operation (e.g., `{ id: string }`).
805
996
 
806
997
  **Returns:** A promise that resolves when the deletion is complete.
807
998
 
@@ -810,33 +1001,39 @@ Deletes a record from the remote store. This operation automatically invalidates
810
1001
  // Assume taskStore is ReactiveRemoteStore<Task> and taskId is known
811
1002
 
812
1003
  await taskStore.delete({ id: taskId });
813
- console.log('Task Deleted.');
1004
+ console.log("Task Deleted.");
814
1005
  ```
815
1006
 
816
1007
  #### `upload(params: { file: File; options?: TUploadOptions }): Promise<T | undefined>`
817
1008
 
818
1009
  Uploads a file associated with a record. This operation automatically invalidates relevant cached queries.
819
1010
 
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 }`).
1011
+ - `params.file`: The `File` object to upload.
1012
+ - `params.options` (optional): Options specific to your `BaseStore`'s `upload` operation (e.g., `{ id: string, fieldName: string }`).
822
1013
 
823
1014
  **Returns:** A promise that resolves to the updated record (if the upload modifies the record) or `undefined` if upload failed.
824
1015
 
825
1016
  ```typescript
826
1017
  // Example: Uploading an avatar for a user
827
- interface User extends Record { name: string; avatarUrl?: string; }
1018
+ interface User extends Record {
1019
+ name: string;
1020
+ avatarUrl?: string;
1021
+ }
828
1022
  // Assume userStore is ReactiveRemoteStore<User> and userId is known
829
1023
 
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);
1024
+ const avatarFile = new File(["..."], "avatar.png", { type: "image/png" });
1025
+ const updatedUser = await userStore.upload({
1026
+ file: avatarFile,
1027
+ options: { id: userId, fieldName: "avatar" },
1028
+ });
1029
+ console.log("User Avatar Uploaded:", updatedUser?.avatarUrl);
833
1030
  ```
834
1031
 
835
1032
  #### `notify(event: StoreEvent): Promise<void>`
836
1033
 
837
1034
  Manually notifies the `ReactiveRemoteStore` of a `StoreEvent`. This can be used to simulate external events or trigger custom invalidation logic defined by `storeEventCorrelator`.
838
1035
 
839
- - `event`: The `StoreEvent` to process. It should have a `scope` and an optional `payload`.
1036
+ - `event`: The `StoreEvent` to process. It should have a `scope` and an optional `payload`.
840
1037
 
841
1038
  **Returns:** A promise that resolves when the notification has been processed.
842
1039
 
@@ -845,49 +1042,58 @@ Manually notifies the `ReactiveRemoteStore` of a `StoreEvent`. This can be used
845
1042
  // Assume productStore is ReactiveRemoteStore<Product>
846
1043
 
847
1044
  await productStore.notify({
848
- scope: 'product:price:changed',
849
- payload: { id: 'prod-789', newPrice: 99.99 }
1045
+ scope: "product:price:changed",
1046
+ payload: { id: "prod-789", newPrice: 99.99 },
850
1047
  });
851
- console.log('Notified store about product price change.');
1048
+ console.log("Notified store about product price change.");
852
1049
  ```
853
1050
 
854
1051
  #### `stream(options: TStreamOptions): Promise<{ stream: () => AsyncIterable<T>; cancel: () => void; status: () => 'active' | 'cancelled' | 'completed'; }>`
855
1052
 
856
1053
  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
1054
 
858
- - `options`: Options specific to your `BaseStore`'s `stream` operation (e.g., `{ filter: string, batchSize: number }`).
1055
+ - `options`: Options specific to your `BaseStore`'s `stream` operation (e.g., `{ filter: string, batchSize: number }`).
859
1056
 
860
1057
  **Returns:** An object with:
861
1058
 
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'`).
1059
+ - `stream()`: A function that returns an `AsyncIterable<T>` which yields records as they arrive.
1060
+ - `cancel()`: A function to call to terminate the stream.
1061
+ - `status()`: A getter function that returns the current state of the stream (`'active'`, `'cancelled'`, or `'completed'`).
865
1062
 
866
1063
  ```typescript
867
1064
  // Example: Streaming live stock prices
868
- interface StockPrice extends Record { symbol: string; price: number; timestamp: number; }
1065
+ interface StockPrice extends Record {
1066
+ symbol: string;
1067
+ price: number;
1068
+ timestamp: number;
1069
+ }
869
1070
  // Assume stockStore is ReactiveRemoteStore<StockPrice>
870
1071
 
871
1072
  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.');
1073
+ console.log("Starting stock price stream...");
1074
+ const { stream, cancel, status } = await stockStore.stream({
1075
+ symbols: ["AAPL", "GOOG"],
1076
+ interval: 1000,
1077
+ });
1078
+
1079
+ const streamInterval = setInterval(() => {
1080
+ console.log("Stream Status:", status());
1081
+ }, 500);
1082
+
1083
+ try {
1084
+ for await (const priceUpdate of stream()) {
1085
+ console.log(
1086
+ `Received: ${priceUpdate.symbol} - $${priceUpdate.price.toFixed(2)}`,
1087
+ );
890
1088
  }
1089
+ console.log("Stock stream completed.");
1090
+ } catch (error) {
1091
+ console.error("Stock stream error:", error);
1092
+ } finally {
1093
+ clearInterval(streamInterval);
1094
+ cancel(); // Ensure stream is cancelled
1095
+ console.log("Stock stream cleanup.");
1096
+ }
891
1097
  }
892
1098
 
893
1099
  // consumeStockStream();
@@ -897,8 +1103,8 @@ async function consumeStockStream() {
897
1103
 
898
1104
  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
1105
 
900
- - `operation`: The type of operation to refresh (`'read'`, `'list'`, or `'find'`).
901
- - `params`: The parameters for the query to refresh.
1106
+ - `operation`: The type of operation to refresh (`'read'`, `'list'`, or `'find'`).
1107
+ - `params`: The parameters for the query to refresh.
902
1108
 
903
1109
  **Returns:** A promise that resolves to the refreshed data (single record or page) or `undefined` if the fetch fails.
904
1110
 
@@ -906,22 +1112,24 @@ Forces a re-fetch of data for a specific query, bypassing staleness checks. This
906
1112
  // Example: Refreshing a user's session data after an action
907
1113
  // Assume userStore is ReactiveRemoteStore<UserProfile> and userId is known
908
1114
 
909
- const freshUserData = await userStore.refresh('read', { id: userId });
910
- console.log('Refreshed User Data:', freshUserData);
1115
+ const freshUserData = await userStore.refresh("read", { id: userId });
1116
+ console.log("Refreshed User Data:", freshUserData);
911
1117
 
912
1118
  // Example: Refreshing a list of notifications
913
1119
  // Assume notificationStore is ReactiveRemoteStore<Notification>
914
1120
 
915
- const freshNotifications = await notificationStore.refresh('list', { status: 'unread' });
916
- console.log('Refreshed Notifications:', freshNotifications?.data);
1121
+ const freshNotifications = await notificationStore.refresh("list", {
1122
+ status: "unread",
1123
+ });
1124
+ console.log("Refreshed Notifications:", freshNotifications?.data);
917
1125
  ```
918
1126
 
919
1127
  #### `prefetch(operation: 'read' | 'list' | 'find', params: TReadOptions | TListOptions | TFindOptions): void`
920
1128
 
921
1129
  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
1130
 
923
- - `operation`: The type of operation to prefetch (`'read'`, `'list'`, or `'find'`).
924
- - `params`: The parameters for the query to prefetch.
1131
+ - `operation`: The type of operation to prefetch (`'read'`, `'list'`, or `'find'`).
1132
+ - `params`: The parameters for the query to prefetch.
925
1133
 
926
1134
  **Returns:** `void` (the operation runs in the background).
927
1135
 
@@ -929,23 +1137,26 @@ Triggers a background fetch for a specific query if it's not already in cache or
929
1137
  // Example: Prefetching related product details when a user hovers over a product card
930
1138
  // Assume productStore is ReactiveRemoteStore<Product>
931
1139
 
932
- productStore.prefetch('read', { id: 'prod-hovered-id' });
933
- console.log('Prefetched product details.');
1140
+ productStore.prefetch("read", { id: "prod-hovered-id" });
1141
+ console.log("Prefetched product details.");
934
1142
 
935
1143
  // Example: Prefetching the next page of a list
936
1144
  // Assume currentListParams has page: 1, pageSize: 10
937
1145
 
938
- const nextListPageParams = { ...currentListParams, page: currentListParams.page + 1 };
939
- productStore.prefetch('list', nextListPageParams);
940
- console.log('Prefetched next page of products.');
1146
+ const nextListPageParams = {
1147
+ ...currentListParams,
1148
+ page: currentListParams.page + 1,
1149
+ };
1150
+ productStore.prefetch("list", nextListPageParams);
1151
+ console.log("Prefetched next page of products.");
941
1152
  ```
942
1153
 
943
1154
  #### `invalidate(operation: string, params: any): Promise<void>`
944
1155
 
945
1156
  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
1157
 
947
- - `operation`: The operation type of the query to invalidate (e.g., `'read'`, `'list'`, `'find'`).
948
- - `params`: The parameters of the query to invalidate.
1158
+ - `operation`: The operation type of the query to invalidate (e.g., `'read'`, `'list'`, `'find'`).
1159
+ - `params`: The parameters of the query to invalidate.
949
1160
 
950
1161
  **Returns:** A promise that resolves when the invalidation is complete.
951
1162
 
@@ -953,8 +1164,8 @@ Manually invalidates a specific query in the cache, marking it as stale. The nex
953
1164
  // Example: Invalidate a specific user's cached data after a direct API call outside the store
954
1165
  // Assume userStore is ReactiveRemoteStore<UserProfile>
955
1166
 
956
- await userStore.invalidate('read', { id: 'user-123' });
957
- console.log('User-123 data invalidated.');
1167
+ await userStore.invalidate("read", { id: "user-123" });
1168
+ console.log("User-123 data invalidated.");
958
1169
  ```
959
1170
 
960
1171
  #### `invalidateAll(): Promise<void>`
@@ -968,7 +1179,7 @@ Invalidates all active queries currently managed by the `ReactiveRemoteStore`.
968
1179
  // Assume anyStore is ReactiveRemoteStore<any>
969
1180
 
970
1181
  await anyStore.invalidateAll();
971
- console.log('All cached data invalidated.');
1182
+ console.log("All cached data invalidated.");
972
1183
  ```
973
1184
 
974
1185
  #### `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 }> }`
@@ -977,18 +1188,18 @@ Retrieves current statistics about the underlying `QueryCache` and the number of
977
1188
 
978
1189
  **Returns:** An object containing:
979
1190
 
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`.
1191
+ - `size`: Number of active entries in the cache.
1192
+ - `metrics`: An object containing raw counts (`hits`, `misses`, `fetches`, `errors`, `evictions`, `staleHits`).
1193
+ - `hitRate`: Ratio of hits to total requests (hits + misses).
1194
+ - `staleHitRate`: Ratio of stale hits to total hits.
1195
+ - `entries`: An array of objects providing details for each cached item (key, lastAccessed, lastUpdated, accessCount, isStale, isLoading, error status).
1196
+ - `activeSubscriptions`: Number of currently active query subscriptions in `ReactiveRemoteStore`.
986
1197
 
987
1198
  ```typescript
988
1199
  // Example: Logging store statistics
989
1200
  const stats = productStore.getStats();
990
- console.log('Store Stats:', stats);
991
- console.log('Active Subscriptions:', stats.activeSubscriptions);
1201
+ console.log("Store Stats:", stats);
1202
+ console.log("Active Subscriptions:", stats.activeSubscriptions);
992
1203
  ```
993
1204
 
994
1205
  #### `destroy(): void`
@@ -1002,7 +1213,7 @@ Cleans up all active subscriptions, internal timers, and resources held by the `
1002
1213
  // Assume productStore is ReactiveRemoteStore<Product>
1003
1214
 
1004
1215
  productStore.destroy();
1005
- console.log('ReactiveRemoteStore instance destroyed.');
1216
+ console.log("ReactiveRemoteStore instance destroyed.");
1006
1217
  ```
1007
1218
 
1008
1219
  ### Configuration Examples
@@ -1012,90 +1223,142 @@ console.log('ReactiveRemoteStore instance destroyed.');
1012
1223
  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
1224
 
1014
1225
  ```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
- }
1226
+ import { ReactiveRemoteStore } from "@asaidimu/erp-utils-remote-store";
1227
+ import { QueryCache } from "@core/cache";
1228
+ import {
1229
+ BaseStore,
1230
+ Record,
1231
+ Page,
1232
+ StoreEvent,
1233
+ ActiveQuery,
1234
+ MutationOperation,
1235
+ Correlator,
1236
+ StoreEventCorrelator,
1237
+ } from "@asaidimu/erp-utils-remote-store/types";
1238
+
1239
+ interface Post extends Record {
1240
+ title: string;
1241
+ authorId: string;
1242
+ tags: string[];
1243
+ }
1244
+
1245
+ class PostBaseStore implements BaseStore<Post> {
1246
+ /* ... implementation ... */
1247
+ async find(): Promise<Page<Post>> {
1248
+ return { data: [], page: { number: 1, size: 0, count: 0, pages: 0 } };
1249
+ }
1250
+ async read(): Promise<Post | undefined> {
1251
+ return undefined;
1252
+ }
1253
+ async list(): Promise<Page<Post>> {
1254
+ return { data: [], page: { number: 1, size: 0, count: 0, pages: 0 } };
1255
+ }
1256
+ async create(props: any): Promise<Post | undefined> {
1257
+ return { id: "new-post", ...props.data };
1258
+ }
1259
+ async update(props: any): Promise<Post | undefined> {
1260
+ return { id: props.id, ...props.data };
1261
+ }
1262
+ async delete(): Promise<void> {}
1263
+ async upload(): Promise<Post | undefined> {
1264
+ return undefined;
1265
+ }
1266
+ async subscribe(): Promise<() => void> {
1267
+ return () => {};
1268
+ }
1269
+ async notify(): Promise<void> {}
1270
+ stream(): {
1271
+ stream: () => AsyncIterable<Post>;
1272
+ cancel: () => void;
1273
+ status: () => "active" | "cancelled" | "completed";
1274
+ } {
1275
+ return {
1276
+ stream: async function* () {},
1277
+ cancel: () => {},
1278
+ status: () => "completed",
1279
+ };
1280
+ }
1034
1281
  }
1035
1282
 
1036
1283
  // Correlator for mutations (create, update, delete, upload)
1037
1284
  const postMutationCorrelator: Correlator = (
1038
- mutation, // { operation: 'create' | 'update' | 'delete' | 'upload', params: any }
1039
- activeQueries // Array of { queryKey: string, operation: string, params: any }
1285
+ mutation, // { operation: 'create' | 'update' | 'delete' | 'upload', params: any }
1286
+ activeQueries, // Array of { queryKey: string, operation: string, params: any }
1040
1287
  ) => {
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;
1288
+ const invalidatedKeys: string[] = [];
1289
+
1290
+ // Invalidate all 'list' queries for posts on any post mutation
1291
+ if (mutation.operation === "create" || mutation.operation === "delete") {
1292
+ activeQueries
1293
+ .filter((q) => q.operation === "list")
1294
+ .forEach((q) => invalidatedKeys.push(q.queryKey));
1295
+ }
1296
+
1297
+ // If a post is updated, invalidate its specific 'read' query
1298
+ if (mutation.operation === "update" && mutation.params.id) {
1299
+ activeQueries
1300
+ .filter(
1301
+ (q) => q.operation === "read" && q.params.id === mutation.params.id,
1302
+ )
1303
+ .forEach((q) => invalidatedKeys.push(q.queryKey));
1304
+ }
1305
+
1306
+ // Example: Invalidate 'find' queries based on tags if a post's tags change
1307
+ if (
1308
+ mutation.operation === "update" &&
1309
+ mutation.params.id &&
1310
+ mutation.params.data?.tags
1311
+ ) {
1312
+ activeQueries
1313
+ .filter(
1314
+ (q) =>
1315
+ q.operation === "find" &&
1316
+ q.params.tags &&
1317
+ (mutation.params.data.tags as string[]).some((tag) =>
1318
+ q.params.tags.includes(tag),
1319
+ ),
1320
+ )
1321
+ .forEach((q) => invalidatedKeys.push(q.queryKey));
1322
+ }
1323
+
1324
+ return invalidatedKeys;
1064
1325
  };
1065
1326
 
1066
1327
  // Correlator for external StoreEvents (e.g., from SSE)
1067
1328
  const postEventCorrelator: StoreEventCorrelator = (
1068
- event, // { scope: string, payload?: any }
1069
- activeQueries // Array of { queryKey: string, operation: string, params: any }
1329
+ event, // { scope: string, payload?: any }
1330
+ activeQueries, // Array of { queryKey: string, operation: string, params: any }
1070
1331
  ) => {
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;
1332
+ const invalidatedKeys: string[] = [];
1333
+
1334
+ // If an external event indicates a specific post was updated, invalidate its 'read' query
1335
+ if (event.scope === "post:external:updated" && event.payload?.id) {
1336
+ activeQueries
1337
+ .filter((q) => q.operation === "read" && q.params.id === event.payload.id)
1338
+ .forEach((q) => invalidatedKeys.push(q.queryKey));
1339
+ }
1340
+
1341
+ // If an external event indicates a new post was created, invalidate all 'list' queries
1342
+ if (event.scope === "post:external:created") {
1343
+ activeQueries
1344
+ .filter((q) => q.operation === "list")
1345
+ .forEach((q) => invalidatedKeys.push(q.queryKey));
1346
+ }
1347
+
1348
+ return invalidatedKeys;
1086
1349
  };
1087
1350
 
1088
1351
  const cache = new QueryCache();
1089
1352
  const postBaseStore = new PostBaseStore();
1090
1353
 
1091
1354
  const postStore = new ReactiveRemoteStore<Post>(
1092
- cache,
1093
- postBaseStore,
1094
- postMutationCorrelator, // Pass your custom mutation correlator
1095
- postEventCorrelator // Pass your custom store event correlator
1355
+ cache,
1356
+ postBaseStore,
1357
+ postMutationCorrelator, // Pass your custom mutation correlator
1358
+ postEventCorrelator, // Pass your custom store event correlator
1096
1359
  );
1097
1360
 
1098
- console.log('ReactiveRemoteStore with custom correlators initialized.');
1361
+ console.log("ReactiveRemoteStore with custom correlators initialized.");
1099
1362
  ```
1100
1363
 
1101
1364
  ### Common Use Cases
@@ -1105,44 +1368,80 @@ console.log('ReactiveRemoteStore with custom correlators initialized.');
1105
1368
  Automatically update your UI components when data changes, without manual re-fetching.
1106
1369
 
1107
1370
  ```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;
1371
+ import { ReactiveRemoteStore } from "@asaidimu/erp-utils-remote-store";
1372
+ import { QueryCache } from "@core/cache";
1373
+ import {
1374
+ BaseStore,
1375
+ Record,
1376
+ Page,
1377
+ StoreEvent,
1378
+ } from "@asaidimu/erp-utils-remote-store/types";
1379
+
1380
+ interface Item extends Record {
1381
+ name: string;
1382
+ status: "active" | "inactive";
1383
+ }
1117
1384
 
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' };
1385
+ class ItemBaseStore implements BaseStore<Item> {
1386
+ /* ... mock implementation ... */
1387
+ private items: Item[] = [];
1388
+ private nextId = 1;
1389
+
1390
+ async create(props: {
1391
+ data: Omit<Item, "id">;
1392
+ options?: any;
1393
+ }): Promise<Item | undefined> {
1394
+ const newItem = { id: String(this.nextId++), ...props.data };
1395
+ this.items.push(newItem);
1396
+ return newItem;
1397
+ }
1398
+ async read(options: { id: string }): Promise<Item | undefined> {
1399
+ return this.items.find((i) => i.id === options.id);
1400
+ }
1401
+ async list(options: any): Promise<Page<Item>> {
1402
+ return {
1403
+ data: this.items,
1404
+ page: {
1405
+ number: 1,
1406
+ size: this.items.length,
1407
+ count: this.items.length,
1408
+ pages: 1,
1409
+ },
1410
+ };
1411
+ }
1412
+ async update(props: {
1413
+ id: string;
1414
+ data: Partial<Omit<Item, "id">>;
1415
+ options?: any;
1416
+ }): Promise<Item | undefined> {
1417
+ const item = this.items.find((i) => i.id === props.id);
1418
+ if (item) {
1419
+ Object.assign(item, props.data);
1420
+ return item;
1145
1421
  }
1422
+ return undefined;
1423
+ }
1424
+ async delete(options: { id: string }): Promise<void> {
1425
+ this.items = this.items.filter((i) => i.id !== options.id);
1426
+ }
1427
+ async upload(): Promise<Item | undefined> {
1428
+ return undefined;
1429
+ }
1430
+ async subscribe(): Promise<() => void> {
1431
+ return () => {};
1432
+ }
1433
+ async notify(): Promise<void> {}
1434
+ stream(): {
1435
+ stream: () => AsyncIterable<Item>;
1436
+ cancel: () => void;
1437
+ status: () => "active" | "cancelled" | "completed";
1438
+ } {
1439
+ return {
1440
+ stream: async function* () {},
1441
+ cancel: () => {},
1442
+ status: () => "completed",
1443
+ };
1444
+ }
1146
1445
  }
1147
1446
 
1148
1447
  const cache = new QueryCache();
@@ -1150,42 +1449,54 @@ const itemBaseStore = new ItemBaseStore();
1150
1449
  const itemStore = new ReactiveRemoteStore<Item>(cache, itemBaseStore);
1151
1450
 
1152
1451
  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})`));
1452
+ console.log("\n--- Reactive UI Updates Example ---");
1453
+
1454
+ // Create some initial items
1455
+ const item1 = await itemStore.create({
1456
+ data: { name: "Item A", status: "active" },
1457
+ });
1458
+ const item2 = await itemStore.create({
1459
+ data: { name: "Item B", status: "inactive" },
1460
+ });
1461
+
1462
+ // Subscribe to a list of all items
1463
+ const [subscribeAllItems, selectAllItems] = itemStore.list({});
1464
+ const unsubscribeAllItems = subscribeAllItems(() => {
1465
+ console.log(
1466
+ "All Items (UI Update):",
1467
+ selectAllItems().page?.data.map((i) => `${i.name} (${i.status})`),
1468
+ );
1469
+ });
1470
+
1471
+ // Subscribe to a single item
1472
+ if (item1) {
1473
+ const [subscribeItem1, selectItem1] = itemStore.read({ id: item1.id });
1474
+ const unsubscribeItem1 = subscribeItem1(() => {
1475
+ console.log(
1476
+ "Item A (UI Update):",
1477
+ selectItem1().data
1478
+ ? `${selectItem1().data?.name} (${selectItem1().data?.status})`
1479
+ : "Deleted",
1480
+ );
1163
1481
  });
1164
1482
 
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
- }
1483
+ // Simulate an update to Item A
1484
+ await new Promise((r) => setTimeout(r, 1000));
1485
+ await itemStore.update({ id: item1.id, data: { status: "inactive" } });
1486
+ console.log("Simulated update to Item A.");
1183
1487
 
1184
- unsubscribeItem1();
1488
+ // Simulate deleting Item B
1489
+ await new Promise((r) => setTimeout(r, 1000));
1490
+ if (item2) {
1491
+ await itemStore.delete({ id: item2.id });
1492
+ console.log("Simulated deletion of Item B.");
1185
1493
  }
1186
1494
 
1187
- unsubscribeAllItems();
1188
- itemStore.destroy();
1495
+ unsubscribeItem1();
1496
+ }
1497
+
1498
+ unsubscribeAllItems();
1499
+ itemStore.destroy();
1189
1500
  }
1190
1501
 
1191
1502
  // runReactiveUIExample();
@@ -1196,74 +1507,118 @@ async function runReactiveUIExample() {
1196
1507
  Consume continuous data flows from your backend for live dashboards, chat applications, or sensor data.
1197
1508
 
1198
1509
  ```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
1510
+ import { ReactiveRemoteStore } from "@asaidimu/erp-utils-remote-store";
1511
+ import { QueryCache } from "@core/cache";
1512
+ import {
1513
+ BaseStore,
1514
+ Record,
1515
+ Page,
1516
+ StoreEvent,
1517
+ } from "@asaidimu/erp-utils-remote-store/types";
1518
+
1519
+ interface SensorReading extends Record {
1520
+ temperature: number;
1521
+ humidity: number;
1522
+ timestamp: number;
1523
+ }
1524
+
1525
+ class SensorBaseStore implements BaseStore<SensorReading> {
1526
+ /* ... mock implementation ... */
1527
+ async find(): Promise<Page<SensorReading>> {
1528
+ return { data: [], page: { number: 1, size: 0, count: 0, pages: 0 } };
1529
+ }
1530
+ async read(): Promise<SensorReading | undefined> {
1531
+ return undefined;
1532
+ }
1533
+ async list(): Promise<Page<SensorReading>> {
1534
+ return { data: [], page: { number: 1, size: 0, count: 0, pages: 0 } };
1535
+ }
1536
+ async create(props: any): Promise<SensorReading | undefined> {
1537
+ return undefined;
1538
+ }
1539
+ async update(props: any): Promise<SensorReading | undefined> {
1540
+ return undefined;
1541
+ }
1542
+ async delete(): Promise<void> {}
1543
+ async upload(): Promise<SensorReading | undefined> {
1544
+ return undefined;
1545
+ }
1546
+ async subscribe(): Promise<() => void> {
1547
+ return () => {};
1548
+ }
1549
+ async notify(): Promise<void> {}
1550
+ stream(options: any): {
1551
+ stream: () => AsyncIterable<SensorReading>;
1552
+ cancel: () => void;
1553
+ status: () => "active" | "cancelled" | "completed";
1554
+ } {
1555
+ let counter = 0;
1556
+ const intervalId = setInterval(() => {
1557
+ // Simulate new readings every second
1558
+ const newReading = {
1559
+ id: `s${counter++}`,
1560
+ temperature: Math.random() * 20 + 20,
1561
+ humidity: Math.random() * 30 + 50,
1562
+ timestamp: Date.now(),
1563
+ };
1564
+ // In a real scenario, this would push to an internal buffer consumed by the async generator
1565
+ // For this example, we'll just yield directly.
1566
+ // This mock doesn't perfectly simulate a real async generator being pushed to.
1567
+ // A real implementation would involve a queue and a way to push items into it.
1568
+ // For now, assume the BaseStore's stream method correctly provides the AsyncIterable.
1569
+ }, 1000);
1570
+
1571
+ const mockAsyncIterable = (async function* () {
1572
+ for (let i = 0; i < 5; i++) {
1573
+ // Yield 5 readings for example
1574
+ await new Promise((r) => setTimeout(r, 1000));
1575
+ yield {
1576
+ id: `mock-${i}`,
1577
+ temperature: Math.random() * 10 + 20,
1578
+ humidity: Math.random() * 10 + 50,
1579
+ timestamp: Date.now(),
1238
1580
  };
1239
- }
1581
+ }
1582
+ })();
1583
+
1584
+ return {
1585
+ stream: () => mockAsyncIterable,
1586
+ cancel: () => clearInterval(intervalId),
1587
+ status: () => "completed", // Simplified status for mock
1588
+ };
1589
+ }
1240
1590
  }
1241
1591
 
1242
1592
  const cache = new QueryCache();
1243
1593
  const sensorBaseStore = new SensorBaseStore();
1244
- const sensorStore = new ReactiveRemoteStore<SensorReading>(cache, sensorBaseStore);
1594
+ const sensorStore = new ReactiveRemoteStore<SensorReading>(
1595
+ cache,
1596
+ sensorBaseStore,
1597
+ );
1245
1598
 
1246
1599
  async function runStreamExample() {
1247
- console.log('\n--- Real-time Data Stream Example ---');
1600
+ console.log("\n--- Real-time Data Stream Example ---");
1248
1601
 
1249
- const { stream, cancel, status } = sensorStore.stream({});
1602
+ const { stream, cancel, status } = sensorStore.stream({});
1250
1603
 
1251
- const statusInterval = setInterval(() => {
1252
- console.log('Stream Status:', status());
1253
- }, 500);
1604
+ const statusInterval = setInterval(() => {
1605
+ console.log("Stream Status:", status());
1606
+ }, 500);
1254
1607
 
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.');
1608
+ try {
1609
+ for await (const reading of stream()) {
1610
+ console.log(
1611
+ `Sensor Reading: Temp=${reading.temperature.toFixed(2)}°C, Humidity=${reading.humidity.toFixed(2)}%`,
1612
+ );
1266
1613
  }
1614
+ console.log("Sensor stream completed.");
1615
+ } catch (error) {
1616
+ console.error("Sensor stream error:", error);
1617
+ } finally {
1618
+ clearInterval(statusInterval);
1619
+ cancel(); // Ensure stream is cancelled
1620
+ console.log("Stream cleanup.");
1621
+ }
1267
1622
  }
1268
1623
 
1269
1624
  // runStreamExample();
@@ -1291,49 +1646,49 @@ src/remote-store/
1291
1646
 
1292
1647
  ### Core Components
1293
1648
 
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`.
1649
+ - **`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.
1650
+ - **`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.
1651
+ - **`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.
1652
+ - **`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`.
1653
+ - **`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`.
1654
+ - **`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
1655
 
1301
1656
  ### Data Flow
1302
1657
 
1303
1658
  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.
1659
+ - A component requests data via `ReactiveRemoteStore`'s reactive methods (`read`, `list`, `find`).
1660
+ - `ReactiveRemoteStore` generates a unique `queryKey` for the request.
1661
+ - It checks the `QueryCache` for existing data. If data is fresh, it's returned immediately.
1662
+ - 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.
1663
+ - The `QueryCache` manages the actual fetching, retries, and updates its internal state.
1664
+ - `ReactiveRemoteStore` provides a `selector` function that components use to get the current state of the data (including `loading`, `stale`, `error`).
1665
+ - A `subscribe` function is also provided, allowing components to register callbacks that are notified whenever the query result changes in the cache.
1311
1666
 
1312
1667
  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`).
1668
+ - When a mutation operation is performed, `ReactiveRemoteStore` first calls the corresponding method on the `BaseStore` to update the remote data.
1669
+ - Upon successful completion, it uses the configured `Correlator` (or a default strategy) to identify which active queries in the `QueryCache` should be invalidated.
1670
+ - 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
1671
 
1317
1672
  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.
1673
+ - `ReactiveRemoteStore` subscribes to a wildcard scope (`*`) on the `BaseStore`'s `subscribe` method, listening for all incoming `StoreEvent`s.
1674
+ - **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`.
1675
+ - 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`.
1676
+ - This ensures that client-side data is automatically synchronized with server-side changes.
1322
1677
 
1323
1678
  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.
1679
+ - When `store.stream()` is called, `ReactiveRemoteStore` delegates the request to the `BaseStore`'s `stream` method.
1680
+ - The `BaseStore` is responsible for establishing and managing the actual stream (e.g., HTTP streaming, WebSockets) and returning an `AsyncIterable`.
1681
+ - `ReactiveRemoteStore` provides this `AsyncIterable` directly to the consumer, along with `cancel` and `status` controls.
1327
1682
 
1328
1683
  ### Extension Points
1329
1684
 
1330
1685
  `ReactiveRemoteStore` is designed to be highly extensible, allowing you to tailor its behavior to your specific application and backend needs:
1331
1686
 
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.
1687
+ - **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).
1688
+ - 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.
1689
+ - **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.
1690
+ - **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.
1691
+ - **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
1692
 
1338
1693
  ---
1339
1694
 
@@ -1375,20 +1730,20 @@ To set up the development environment for `ReactiveRemoteStore`:
1375
1730
 
1376
1731
  The following `bun`/`npm` scripts are available in this project:
1377
1732
 
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.
1733
+ - `bun run build`: Compiles TypeScript source files from `src/` to JavaScript output.
1734
+ - `bun run test`: Runs the test suite using `Vitest`.
1735
+ - `bun run test:watch`: Runs tests in watch mode for continuous feedback during development.
1736
+ - `bun run test-server`: Starts a mock backend server used for e2e tests and examples.
1737
+ - `bun run lint`: Runs ESLint to check for code style and potential errors.
1738
+ - `bun run format`: Formats code using Prettier according to the project's style guidelines.
1384
1739
 
1385
1740
  ### Testing
1386
1741
 
1387
1742
  This project includes comprehensive unit, integration, and end-to-end tests to ensure reliability and correctness.
1388
1743
 
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`).
1744
+ - **Unit Tests**: Test individual functions and classes in isolation (e.g., `store.test.ts`).
1745
+ - **Integration Tests**: Verify the interaction between `ReactiveRemoteStore` and a mock `BaseStore` (e.g., `store.integration.test.ts`).
1746
+ - **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
1747
 
1393
1748
  To run all tests:
1394
1749
 
@@ -1424,11 +1779,11 @@ Found a bug, have a feature request, or need clarification? Please open an issue
1424
1779
 
1425
1780
  When reporting a bug, please include:
1426
1781
 
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).
1782
+ - A clear and concise description of the issue.
1783
+ - Detailed steps to reproduce the behavior.
1784
+ - The expected behavior.
1785
+ - Any relevant screenshots or code snippets.
1786
+ - Your environment details (Node.js version, OS, package manager, package version).
1432
1787
 
1433
1788
  ---
1434
1789
 
@@ -1436,23 +1791,22 @@ When reporting a bug, please include:
1436
1791
 
1437
1792
  ### Troubleshooting
1438
1793
 
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.
1794
+ - **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.
1795
+ ```bash
1796
+ bun run test-server # Stop and restart
1797
+ ```
1798
+ - **Data not updating reactively**:
1799
+ - Verify that your `BaseStore` implementation correctly calls `notify` or that your backend is sending `StoreEvent`s via SSE.
1800
+ - Check your `correlator` and `storeEventCorrelator` functions to ensure they are correctly identifying and invalidating relevant queries.
1801
+ - Ensure your UI components are correctly subscribing to the `ReactiveRemoteStore`'s query results.
1802
+ - **`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
1803
 
1449
1804
  ### FAQ
1450
1805
 
1451
1806
  **Q: What is the difference between `read`, `list`, and `find`?**
1452
1807
  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.
1808
+ _ `read`: Designed for fetching a single, specific record (e.g., by ID).
1809
+ _ `list`: Designed for fetching a paginated collection of records, typically without complex search criteria (e.g., all products, all users). \* `find`: Designed for fetching a paginated collection of records based on specific search criteria or filters.
1456
1810
 
1457
1811
  **Q: How does `ReactiveRemoteStore` handle offline scenarios?**
1458
1812
  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.
@@ -1470,6 +1824,6 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
1470
1824
 
1471
1825
  ### Acknowledgments
1472
1826
 
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.
1827
+ - Inspired by modern reactive data management patterns and libraries.
1828
+ - Leverages `QueryCache` for robust caching capabilities.
1829
+ - Uses `Vitest` for testing and `TypeScript` for type safety.