@asaidimu/utils-remote-store 1.2.9 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +921 -567
- package/index.d.mts +25 -72
- package/index.d.ts +25 -72
- 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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
98
|
-
import { QueryCache } from
|
|
99
|
-
import {
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
159
|
-
|
|
215
|
+
mutation: { operation: MutationOperation; params: any },
|
|
216
|
+
activeQueries: ActiveQuery[],
|
|
160
217
|
): string[] => {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
176
|
-
|
|
236
|
+
event: StoreEvent, // { scope: string, payload?: any }
|
|
237
|
+
activeQueries: ActiveQuery[],
|
|
177
238
|
): string[] => {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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(
|
|
250
|
+
const baseStore = new MyApiBaseStore("https://api.example.com");
|
|
190
251
|
|
|
191
252
|
const productStore = new ReactiveRemoteStore<Product>(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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(
|
|
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
|
|
207
|
-
import { QueryCache } from
|
|
208
|
-
import {
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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(
|
|
231
|
-
console.log(
|
|
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 {
|
|
253
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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 {
|
|
359
|
+
interface ProductReadOptions {
|
|
360
|
+
id: string;
|
|
361
|
+
}
|
|
267
362
|
|
|
268
363
|
// Options for listing products (e.g., with pagination and sorting)
|
|
269
|
-
interface ProductListOptions {
|
|
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 {
|
|
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<
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
-
|
|
291
|
-
-
|
|
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
|
-
-
|
|
318
|
-
-
|
|
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
|
-
-
|
|
347
|
-
-
|
|
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
|
-
-
|
|
373
|
-
-
|
|
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
|
-
-
|
|
399
|
-
-
|
|
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
|
-
-
|
|
426
|
-
-
|
|
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
|
-
-
|
|
453
|
-
-
|
|
454
|
-
-
|
|
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
|
-
-
|
|
485
|
-
-
|
|
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
|
|
558
|
-
import { QueryCache } from
|
|
559
|
-
import {
|
|
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
|
-
|
|
564
|
-
|
|
674
|
+
title: string;
|
|
675
|
+
completed: boolean;
|
|
565
676
|
}
|
|
566
677
|
|
|
567
678
|
class TodoBaseStore implements BaseStore<Todo> {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
|
|
784
|
+
console.log("Initial List:", result.page?.data);
|
|
633
785
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
794
|
+
// Clean up subscriptions
|
|
795
|
+
unsubscribeRead();
|
|
796
|
+
unsubscribeList();
|
|
797
|
+
}
|
|
646
798
|
|
|
647
|
-
|
|
648
|
-
|
|
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
|
-
-
|
|
663
|
-
-
|
|
664
|
-
-
|
|
665
|
-
-
|
|
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
|
-
-
|
|
823
|
+
- `params`: Options specific to your `BaseStore`'s `read` operation (e.g., `{ id: string }`).
|
|
672
824
|
|
|
673
825
|
**Returns:**
|
|
674
826
|
|
|
675
|
-
-
|
|
676
|
-
|
|
677
|
-
|
|
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 {
|
|
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({
|
|
839
|
+
const { value: selectUser, onValueChange: subscribeUser } = userStore.read({
|
|
840
|
+
id: "user-123",
|
|
841
|
+
});
|
|
685
842
|
|
|
686
843
|
// Initial access
|
|
687
|
-
console.log(
|
|
844
|
+
console.log("Initial User Data:", selectUser().data);
|
|
688
845
|
|
|
689
846
|
// Subscribe to changes
|
|
690
847
|
const unsubscribe = subscribeUser(() => {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
709
|
-
|
|
710
|
-
|
|
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 {
|
|
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 } =
|
|
884
|
+
const { value: selectProducts, onValueChange: subscribeProducts } =
|
|
885
|
+
productStore.list({ page: 1, pageSize: 10 });
|
|
718
886
|
|
|
719
887
|
const unsubscribeProducts = subscribeProducts(() => {
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
916
|
+
- `params`: Options specific to your `BaseStore`'s `find` operation (e.g., `{ query: string, category: string }`).
|
|
744
917
|
|
|
745
918
|
**Returns:**
|
|
746
919
|
|
|
747
|
-
-
|
|
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 {
|
|
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(
|
|
930
|
+
const { value: selectOrders, onValueChange: subscribeOrders } = orderStore.find(
|
|
931
|
+
{ customerId: "cust-456", status: "pending" },
|
|
932
|
+
);
|
|
755
933
|
|
|
756
934
|
const unsubscribeOrders = subscribeOrders(() => {
|
|
757
|
-
|
|
758
|
-
|
|
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
|
-
-
|
|
769
|
-
-
|
|
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 {
|
|
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({
|
|
779
|
-
|
|
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
|
-
-
|
|
787
|
-
-
|
|
788
|
-
-
|
|
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({
|
|
797
|
-
|
|
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
|
-
-
|
|
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(
|
|
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
|
-
-
|
|
821
|
-
-
|
|
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 {
|
|
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([
|
|
831
|
-
const updatedUser = await userStore.upload({
|
|
832
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
849
|
-
|
|
1045
|
+
scope: "product:price:changed",
|
|
1046
|
+
payload: { id: "prod-789", newPrice: 99.99 },
|
|
850
1047
|
});
|
|
851
|
-
console.log(
|
|
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
|
-
-
|
|
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
|
-
-
|
|
863
|
-
-
|
|
864
|
-
-
|
|
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 {
|
|
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
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
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
|
-
-
|
|
901
|
-
-
|
|
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(
|
|
910
|
-
console.log(
|
|
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(
|
|
916
|
-
|
|
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
|
-
-
|
|
924
|
-
-
|
|
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(
|
|
933
|
-
console.log(
|
|
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 = {
|
|
939
|
-
|
|
940
|
-
|
|
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
|
-
-
|
|
948
|
-
-
|
|
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(
|
|
957
|
-
console.log(
|
|
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(
|
|
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
|
-
-
|
|
981
|
-
-
|
|
982
|
-
-
|
|
983
|
-
-
|
|
984
|
-
-
|
|
985
|
-
-
|
|
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(
|
|
991
|
-
console.log(
|
|
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(
|
|
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
|
|
1016
|
-
import { QueryCache } from
|
|
1017
|
-
import {
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
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
|
-
|
|
1039
|
-
|
|
1285
|
+
mutation, // { operation: 'create' | 'update' | 'delete' | 'upload', params: any }
|
|
1286
|
+
activeQueries, // Array of { queryKey: string, operation: string, params: any }
|
|
1040
1287
|
) => {
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
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
|
-
|
|
1069
|
-
|
|
1329
|
+
event, // { scope: string, payload?: any }
|
|
1330
|
+
activeQueries, // Array of { queryKey: string, operation: string, params: any }
|
|
1070
1331
|
) => {
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
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
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
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(
|
|
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
|
|
1109
|
-
import { QueryCache } from
|
|
1110
|
-
import {
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
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
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
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
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
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
|
-
//
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1188
|
-
|
|
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
|
|
1200
|
-
import { QueryCache } from
|
|
1201
|
-
import {
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
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>(
|
|
1594
|
+
const sensorStore = new ReactiveRemoteStore<SensorReading>(
|
|
1595
|
+
cache,
|
|
1596
|
+
sensorBaseStore,
|
|
1597
|
+
);
|
|
1245
1598
|
|
|
1246
1599
|
async function runStreamExample() {
|
|
1247
|
-
|
|
1600
|
+
console.log("\n--- Real-time Data Stream Example ---");
|
|
1248
1601
|
|
|
1249
|
-
|
|
1602
|
+
const { stream, cancel, status } = sensorStore.stream({});
|
|
1250
1603
|
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1604
|
+
const statusInterval = setInterval(() => {
|
|
1605
|
+
console.log("Stream Status:", status());
|
|
1606
|
+
}, 500);
|
|
1254
1607
|
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
}
|
|
1259
|
-
|
|
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
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
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
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
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
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
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
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
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
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
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
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
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
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
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
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
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
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
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
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
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
|
-
|
|
1454
|
-
|
|
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
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
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.
|