@asaidimu/utils-remote-store 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +1475 -0
- package/index.d.mts +574 -0
- package/index.d.ts +574 -0
- package/index.js +1 -0
- package/index.mjs +1 -0
- package/package.json +51 -0
package/index.d.ts
ADDED
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
import { QueryCache, CacheMetrics } from '../cache/index.js';
|
|
2
|
+
import '../types-DUZGkNEB.js';
|
|
3
|
+
|
|
4
|
+
interface StoreErrorDetails {
|
|
5
|
+
code: string;
|
|
6
|
+
message: string;
|
|
7
|
+
status?: number;
|
|
8
|
+
data?: unknown;
|
|
9
|
+
isRetryable: boolean;
|
|
10
|
+
}
|
|
11
|
+
declare class StoreError extends Error {
|
|
12
|
+
readonly code: string;
|
|
13
|
+
readonly status?: number;
|
|
14
|
+
readonly data?: unknown;
|
|
15
|
+
readonly isRetryable: boolean;
|
|
16
|
+
readonly originalError?: Error;
|
|
17
|
+
constructor(details: StoreErrorDetails, originalError?: Error);
|
|
18
|
+
static fromError(error: any, operation: string): StoreError;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Represents a paginated response containing a list of records and pagination details.
|
|
23
|
+
* @template T The type of the records in the page.
|
|
24
|
+
*/
|
|
25
|
+
interface Page<T> {
|
|
26
|
+
/** The array of records for the current page. */
|
|
27
|
+
data: T[];
|
|
28
|
+
/** Pagination metadata. */
|
|
29
|
+
page: PaginationInfo;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Represents an event emitted by the store, typically for notifications or state changes.
|
|
33
|
+
*/
|
|
34
|
+
interface StoreEvent {
|
|
35
|
+
/** The scope of the event, indicating its type and context. Custom scopes are allowed. */
|
|
36
|
+
scope: string;
|
|
37
|
+
/** Optional payload carrying data related to the event. */
|
|
38
|
+
payload?: any;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Defines the types of mutation operations that can occur.
|
|
42
|
+
*/
|
|
43
|
+
type MutationOperation = 'create' | 'update' | 'delete' | 'upload';
|
|
44
|
+
/**
|
|
45
|
+
* A correlator function that determines which active queries should be invalidated
|
|
46
|
+
* based on a mutation operation.
|
|
47
|
+
* @param mutation An object describing the mutation operation and its parameters.
|
|
48
|
+
* @param activeQueries An array of currently active queries.
|
|
49
|
+
* @returns An array of query keys to be invalidated.
|
|
50
|
+
*/
|
|
51
|
+
type Correlator = (mutation: {
|
|
52
|
+
operation: MutationOperation;
|
|
53
|
+
params: any;
|
|
54
|
+
}, activeQueries: ActiveQuery[]) => string[];
|
|
55
|
+
/**
|
|
56
|
+
* A correlator function that determines which active queries should be invalidated
|
|
57
|
+
* based on a store event.
|
|
58
|
+
* @param event The StoreEvent that occurred.
|
|
59
|
+
* @param activeQueries An array of currently active queries.
|
|
60
|
+
* @returns An array of query keys to be invalidated.
|
|
61
|
+
*/
|
|
62
|
+
type StoreEventCorrelator = (event: StoreEvent, activeQueries: ActiveQuery[]) => string[];
|
|
63
|
+
/**
|
|
64
|
+
* Defines the core interface for interacting with a remote data store.
|
|
65
|
+
* @template T The type of the records managed by the store, extending Record.
|
|
66
|
+
* @template TFindOptions Options for the find operation.
|
|
67
|
+
* @template TReadOptions Options for the read operation.
|
|
68
|
+
* @template TListOptions Options for the list operation.
|
|
69
|
+
* @template TDeleteOptions Options for the delete operation.
|
|
70
|
+
* @template TUpdateOptions Options for the update operation.
|
|
71
|
+
* @template TCreateOptions Options for the create operation.
|
|
72
|
+
* @template TUploadOptions Options for the upload operation.
|
|
73
|
+
* @template TStreamOptions Options for the stream operation.
|
|
74
|
+
*/
|
|
75
|
+
interface BaseStore<T extends StoreRecord, TFindOptions = Record<string, unknown>, TReadOptions = Record<string, unknown>, TListOptions = Record<string, unknown>, TDeleteOptions = Record<string, unknown>, TUpdateOptions = Record<string, unknown>, TCreateOptions = Record<string, unknown>, TUploadOptions = Record<string, unknown>, TStreamOptions = Record<string, unknown>> {
|
|
76
|
+
/**
|
|
77
|
+
* Finds records based on provided options, returning a paginated result.
|
|
78
|
+
* @param options The options for the find operation.
|
|
79
|
+
* @returns A promise that resolves to a Page of records.
|
|
80
|
+
*/
|
|
81
|
+
find: (options: TFindOptions) => Promise<Page<T>>;
|
|
82
|
+
/**
|
|
83
|
+
* Reads a single record by its identifier or other read options.
|
|
84
|
+
* @param options The options for the read operation, typically including an ID.
|
|
85
|
+
* @returns A promise that resolves to the record or undefined if not found.
|
|
86
|
+
*/
|
|
87
|
+
read: (options: TReadOptions) => Promise<T | undefined>;
|
|
88
|
+
/**
|
|
89
|
+
* Lists records based on provided options, returning a paginated result.
|
|
90
|
+
* @param options The options for the list operation.
|
|
91
|
+
* @returns A promise that resolves to a Page of records.
|
|
92
|
+
*/
|
|
93
|
+
list: (options: TListOptions) => Promise<Page<T>>;
|
|
94
|
+
/**
|
|
95
|
+
* Deletes a record based on provided options, typically including an ID.
|
|
96
|
+
* @param options The options for the delete operation.
|
|
97
|
+
* @returns A promise that resolves when the deletion is complete.
|
|
98
|
+
*/
|
|
99
|
+
delete: (options: TDeleteOptions) => Promise<void>;
|
|
100
|
+
/**
|
|
101
|
+
* Updates an existing record.
|
|
102
|
+
* @param props An object containing the ID of the record to update, the partial data, and optional update options.
|
|
103
|
+
* @returns A promise that resolves to the updated record or undefined if not found.
|
|
104
|
+
*/
|
|
105
|
+
update: (props: {
|
|
106
|
+
data: Partial<T>;
|
|
107
|
+
options?: TUpdateOptions;
|
|
108
|
+
}) => Promise<T | undefined>;
|
|
109
|
+
/**
|
|
110
|
+
* Creates a new record.
|
|
111
|
+
* @param props An object containing the data for the new record and optional create options.
|
|
112
|
+
* @returns A promise that resolves to the newly created record or undefined if creation failed.
|
|
113
|
+
*/
|
|
114
|
+
create: (props: {
|
|
115
|
+
data: Partial<T>;
|
|
116
|
+
options?: TCreateOptions;
|
|
117
|
+
}) => Promise<T | undefined>;
|
|
118
|
+
/**
|
|
119
|
+
* Uploads a file associated with a record. Optionally updates the record with upload details.
|
|
120
|
+
* @param props An object containing the file to upload and optional upload options.
|
|
121
|
+
* @returns A promise that resolves to the updated record after upload or undefined if upload failed.
|
|
122
|
+
*/
|
|
123
|
+
upload: (props: {
|
|
124
|
+
file: File;
|
|
125
|
+
options?: TUploadOptions;
|
|
126
|
+
}) => Promise<T | undefined>;
|
|
127
|
+
/**
|
|
128
|
+
* Subscribes to store events for a given scope.
|
|
129
|
+
* @param scope The event scope to subscribe to (e.g., 'todos:created:success', '*').
|
|
130
|
+
* @param callback The function to call when an event matching the scope is received.
|
|
131
|
+
* @returns A promise that resolves to an unsubscribe function. Call this function to stop receiving events.
|
|
132
|
+
*/
|
|
133
|
+
subscribe(scope: string, callback: (event: StoreEvent) => void): Promise<() => void>;
|
|
134
|
+
/**
|
|
135
|
+
* Notifies the store of an event, which can then be broadcast to subscribers.
|
|
136
|
+
* @param event The StoreEvent to notify.
|
|
137
|
+
* @returns A promise that resolves when the notification has been processed.
|
|
138
|
+
*/
|
|
139
|
+
notify: (event: StoreEvent) => Promise<void>;
|
|
140
|
+
/**
|
|
141
|
+
* Establishes a stream of records based on provided options.
|
|
142
|
+
* @param options Options for configuring the stream (e.g., batch size, delay, filters).
|
|
143
|
+
* @param onStreamChange A callback function that is called when the stream's status changes.
|
|
144
|
+
* @returns An object containing the async iterable stream, a cancel function, and a status getter.
|
|
145
|
+
*/
|
|
146
|
+
stream: (options: TStreamOptions, onStreamChange: () => void) => {
|
|
147
|
+
/** An async iterable that yields records as they become available in the stream. */
|
|
148
|
+
stream: () => AsyncIterable<T>;
|
|
149
|
+
/** A function to call to cancel the ongoing stream. */
|
|
150
|
+
cancel: () => void;
|
|
151
|
+
/** A getter function that returns the current status of the stream ('active', 'cancelled', or 'completed'). */
|
|
152
|
+
status: () => 'active' | 'cancelled' | 'completed';
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Represents an active query in the cache, used for invalidation correlation.
|
|
157
|
+
*/
|
|
158
|
+
interface ActiveQuery {
|
|
159
|
+
/** The unique key identifying the query in the cache. */
|
|
160
|
+
queryKey: string;
|
|
161
|
+
/** The type of operation this query represents (e.g., 'read', 'list', 'find'). */
|
|
162
|
+
operation: string;
|
|
163
|
+
/** The parameters used to make this query. */
|
|
164
|
+
params: any;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Represents the result of a single record query.
|
|
168
|
+
* @template T The type of the data being queried.
|
|
169
|
+
*/
|
|
170
|
+
interface QueryResult<T> {
|
|
171
|
+
/** The data returned by the query, or undefined if not found or still loading. */
|
|
172
|
+
data: T | undefined;
|
|
173
|
+
/** Indicates if the query is currently loading data. */
|
|
174
|
+
loading: boolean;
|
|
175
|
+
/** An error object if the query failed, otherwise undefined. */
|
|
176
|
+
error: Error | undefined;
|
|
177
|
+
/** Indicates if the cached data is stale and a refetch is needed. */
|
|
178
|
+
stale: boolean;
|
|
179
|
+
/** Timestamp of the last update to the data. */
|
|
180
|
+
updated: number;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Represents the result of a paginated query (list or find).
|
|
184
|
+
* @template T The type of the records in the page.
|
|
185
|
+
*/
|
|
186
|
+
interface PagedQueryResult<T> {
|
|
187
|
+
/** The paginated data, or undefined if not found or still loading. */
|
|
188
|
+
page: Page<T> | undefined;
|
|
189
|
+
/** Indicates if the query is currently loading data. */
|
|
190
|
+
loading: boolean;
|
|
191
|
+
/** An error object if the query failed, otherwise undefined. */
|
|
192
|
+
error: StoreError | undefined;
|
|
193
|
+
/** Indicates if the cached data is stale and a refetch is needed. */
|
|
194
|
+
stale: boolean;
|
|
195
|
+
/** Timestamp of the last update to the data. */
|
|
196
|
+
updated: number;
|
|
197
|
+
/** True if there is a next page, false otherwise. */
|
|
198
|
+
hasNext: boolean;
|
|
199
|
+
/** True if there is a previous page, false otherwise. */
|
|
200
|
+
hasPrevious: boolean;
|
|
201
|
+
/** Function to fetch the next page of data. */
|
|
202
|
+
next: () => Promise<void>;
|
|
203
|
+
/** Function to fetch the previous page of data. */
|
|
204
|
+
previous: () => Promise<void>;
|
|
205
|
+
/** Function to fetch a specific page of data.
|
|
206
|
+
* @param page The page number to fetch.
|
|
207
|
+
*/
|
|
208
|
+
fetch: (page: number) => Promise<void>;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Represents a basic record in the remote store.
|
|
212
|
+
* All records must have a unique `id` of type string.
|
|
213
|
+
* Other properties can be of any type.
|
|
214
|
+
*/
|
|
215
|
+
type StoreRecord<BaseProps extends Record<string, unknown> = Record<string, unknown>> = BaseProps;
|
|
216
|
+
/**
|
|
217
|
+
* Provides pagination information for a collection of records.
|
|
218
|
+
*/
|
|
219
|
+
type PaginationInfo = {
|
|
220
|
+
/** The current page number (1-based). */
|
|
221
|
+
number: number;
|
|
222
|
+
/** The number of items per page. */
|
|
223
|
+
size: number;
|
|
224
|
+
/** The total count of items across all pages. */
|
|
225
|
+
count: number;
|
|
226
|
+
/** The total number of available pages. */
|
|
227
|
+
pages: number;
|
|
228
|
+
};
|
|
229
|
+
/**
|
|
230
|
+
* Represents the name of a resource in the store, e.g., 'todos', 'users'.
|
|
231
|
+
*/
|
|
232
|
+
type ResourceName = string;
|
|
233
|
+
/**
|
|
234
|
+
* Represents the reactive result of a single record query.
|
|
235
|
+
* @template T The type of the data being queried.
|
|
236
|
+
*/
|
|
237
|
+
interface ReactiveQueryResult<T> {
|
|
238
|
+
/** The current query result, including data, loading state, and errors. */
|
|
239
|
+
value: () => QueryResult<T>;
|
|
240
|
+
/** A function to subscribe to changes in the query result. Returns an unsubscribe function. */
|
|
241
|
+
onValueChange: (callback: () => void) => () => void;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Represents the reactive result of a paginated query (list or find).
|
|
245
|
+
* @template T The type of the records in the page.
|
|
246
|
+
*/
|
|
247
|
+
interface ReactivePagedQueryResult<T> {
|
|
248
|
+
/** The current paginated query result, including page data, loading state, and errors. */
|
|
249
|
+
value: () => PagedQueryResult<T>;
|
|
250
|
+
/** A function to subscribe to changes in the query result. Returns an unsubscribe function. */
|
|
251
|
+
onValueChange: (callback: () => void) => () => void;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Internal interface representing an active subscription to a query.
|
|
255
|
+
* @template T The type of the data being queried.
|
|
256
|
+
*/
|
|
257
|
+
interface QuerySubscription<T> {
|
|
258
|
+
/** Callbacks to unsubscribe from cache events. */
|
|
259
|
+
unsubscribeCallbacks: (() => void)[];
|
|
260
|
+
/** Set of functions to notify when the query data changes. */
|
|
261
|
+
subscribers: Set<() => void>;
|
|
262
|
+
/** A cached function to subscribe to changes for this query. */
|
|
263
|
+
cachedSubscribe: (callback: () => void) => () => void;
|
|
264
|
+
/** A cached selector function to get the current query result. */
|
|
265
|
+
cachedSelector: () => QueryResult<T> | PagedQueryResult<T>;
|
|
266
|
+
/** The operation type of the query (e.g., 'read', 'list'). */
|
|
267
|
+
operation: string;
|
|
268
|
+
/** The parameters for the query. */
|
|
269
|
+
params: any;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* A reactive remote store that provides cached and observable access to data
|
|
274
|
+
* from a `BaseStore`. It handles caching, invalidation, and real-time updates
|
|
275
|
+
* via server-sent events (SSE).
|
|
276
|
+
*
|
|
277
|
+
* @template T The type of the records managed by the store, extending Record.
|
|
278
|
+
* @template TFindOptions Options for the find operation.
|
|
279
|
+
* @template TReadOptions Options for the read operation.
|
|
280
|
+
* @template TListOptions Options for the list operation.
|
|
281
|
+
* @template TDeleteOptions Options for the delete operation.
|
|
282
|
+
* @template TUpdateOptions Options for the update operation.
|
|
283
|
+
* @template TCreateOptions Options for the create operation.
|
|
284
|
+
* @template TUploadOptions Options for the upload operation.
|
|
285
|
+
* @template TStreamOptions Options for the stream operation.
|
|
286
|
+
*/
|
|
287
|
+
declare class ReactiveRemoteStore<T extends StoreRecord, TFindOptions = Record<string, unknown>, TReadOptions = Record<string, unknown>, TListOptions = Record<string, unknown>, TDeleteOptions = Record<string, unknown>, TUpdateOptions = Record<string, unknown>, TCreateOptions = Record<string, unknown>, TUploadOptions = Record<string, unknown>, TStreamOptions = Record<string, unknown>> {
|
|
288
|
+
private cache;
|
|
289
|
+
private baseStore;
|
|
290
|
+
private correlator?;
|
|
291
|
+
private storeEventCorrelator?;
|
|
292
|
+
private querySubscriptions;
|
|
293
|
+
private keyCache;
|
|
294
|
+
private unsubscribeFromBaseStore;
|
|
295
|
+
/**
|
|
296
|
+
* Creates an instance of ReactiveRemoteStore.
|
|
297
|
+
* @param cache The QueryCache instance to use for caching data.
|
|
298
|
+
* @param baseStore The underlying BaseStore for actual data fetching and mutations.
|
|
299
|
+
* @param correlator Optional function to determine which queries to invalidate after a mutation.
|
|
300
|
+
* @param storeEventCorrelator Optional function to determine which queries to invalidate after a store event.
|
|
301
|
+
*/
|
|
302
|
+
constructor(cache: QueryCache, baseStore: BaseStore<T, TFindOptions, TReadOptions, TListOptions, TDeleteOptions, TUpdateOptions, TCreateOptions, TUploadOptions, TStreamOptions>, correlator?: Correlator | undefined, storeEventCorrelator?: StoreEventCorrelator | undefined);
|
|
303
|
+
/**
|
|
304
|
+
* Creates a new query subscription and registers it with the cache.
|
|
305
|
+
* @template T The type of the data for the subscription.
|
|
306
|
+
* @param queryKey The unique key for the query.
|
|
307
|
+
* @param operation The operation type (e.g., 'read', 'list').
|
|
308
|
+
* @param params The parameters for the query.
|
|
309
|
+
* @param selector A function that returns the current query result.
|
|
310
|
+
* @param notificationCondition A function to determine if a cache event should trigger a notification.
|
|
311
|
+
* @returns The created QuerySubscription.
|
|
312
|
+
*/
|
|
313
|
+
private createSubscription;
|
|
314
|
+
/**
|
|
315
|
+
* Retrieves an existing subscription or creates a new one if it doesn't exist.
|
|
316
|
+
* @template T The type of the data for the subscription.
|
|
317
|
+
* @param queryKey The unique key for the query.
|
|
318
|
+
* @param operation The operation type (e.g., 'read', 'list').
|
|
319
|
+
* @param params The parameters for the query.
|
|
320
|
+
* @param selector A function that returns the current query result.
|
|
321
|
+
* @param notificationCondition A function to determine if a cache event should trigger a notification.
|
|
322
|
+
* @returns The existing or newly created QuerySubscription.
|
|
323
|
+
*/
|
|
324
|
+
private getOrCreateSubscription;
|
|
325
|
+
/**
|
|
326
|
+
* Builds a unique cache key for a given operation and its parameters.
|
|
327
|
+
* Uses a WeakMap for caching keys of object parameters to avoid re-hashing.
|
|
328
|
+
* @param operation The name of the operation (e.g., 'read', 'list').
|
|
329
|
+
* @param params The parameters for the operation.
|
|
330
|
+
* @returns A unique string key for the operation and parameters.
|
|
331
|
+
*/
|
|
332
|
+
private buildKey;
|
|
333
|
+
/**
|
|
334
|
+
* Creates a selector function for a single record query.
|
|
335
|
+
* @template T The type of the data being queried.
|
|
336
|
+
* @param queryKey The unique key for the query.
|
|
337
|
+
* @returns A function that returns the current QueryResult for the given key.
|
|
338
|
+
*/
|
|
339
|
+
private createSelector;
|
|
340
|
+
/**
|
|
341
|
+
* Reads a single record from the store.
|
|
342
|
+
* @param params The read options.
|
|
343
|
+
* @returns An object containing the current QueryResult and a function to subscribe to changes.
|
|
344
|
+
*/
|
|
345
|
+
/**
|
|
346
|
+
* Retrieves a single record reactively.
|
|
347
|
+
*
|
|
348
|
+
* This method provides a reactive query result for a single record. The data is fetched from the cache if available,
|
|
349
|
+
* otherwise it's fetched from the underlying `baseStore`. The method returns an object containing the current
|
|
350
|
+
* query result and a function to subscribe to future updates.
|
|
351
|
+
*
|
|
352
|
+
* @param params The options for the read operation, used to identify the record.
|
|
353
|
+
* @returns A `ReactiveQueryResult` object containing the reactive value and an `onValueChange` subscription function.
|
|
354
|
+
*/
|
|
355
|
+
read(params: TReadOptions): ReactiveQueryResult<T>;
|
|
356
|
+
/**
|
|
357
|
+
* Creates a selector function for a paginated query (list or find).
|
|
358
|
+
* @template TParams The type of the parameters for the query.
|
|
359
|
+
* @param baseQueryKey The base unique key for the query.
|
|
360
|
+
* @param baseParams The base parameters for the query.
|
|
361
|
+
* @param fetchFn The function to call to fetch a page of data.
|
|
362
|
+
* @returns A function that returns the current PagedQueryResult.
|
|
363
|
+
*/
|
|
364
|
+
private createPagedSelector;
|
|
365
|
+
/**
|
|
366
|
+
* Sets up a paginated query (list or find) with caching and reactivity.
|
|
367
|
+
* @template TParams The type of the parameters for the query.
|
|
368
|
+
* @param type The type of the paginated query ('list' or 'find').
|
|
369
|
+
* @param params The parameters for the query.
|
|
370
|
+
* @param fetchFn The function to call to fetch a page of data.
|
|
371
|
+
* @returns An object containing the current PagedQueryResult and a function to subscribe to changes.
|
|
372
|
+
*/
|
|
373
|
+
private setupPagedQuery;
|
|
374
|
+
/**
|
|
375
|
+
* Lists records from the store with pagination and reactivity.
|
|
376
|
+
* @param params The list options.
|
|
377
|
+
* @returns An object containing the current PagedQueryResult and a function to subscribe to changes.
|
|
378
|
+
*/
|
|
379
|
+
/**
|
|
380
|
+
* Retrieves a paginated list of records reactively.
|
|
381
|
+
*
|
|
382
|
+
* This method provides a reactive query result for a list of records. It supports pagination and automatically
|
|
383
|
+
* fetches data from the cache or the `baseStore`. The result includes methods for navigating to the next,
|
|
384
|
+
* previous, or a specific page.
|
|
385
|
+
*
|
|
386
|
+
* @param params The options for the list operation, including pagination details.
|
|
387
|
+
* @returns A `ReactivePagedQueryResult` object containing the reactive paged value and an `onValueChange` subscription function.
|
|
388
|
+
*/
|
|
389
|
+
list(params: TListOptions): ReactivePagedQueryResult<T>;
|
|
390
|
+
/**
|
|
391
|
+
* Finds records from the store with pagination and reactivity.
|
|
392
|
+
* @param params The find options.
|
|
393
|
+
* @returns An object containing the current PagedQueryResult and a function to subscribe to changes.
|
|
394
|
+
*/
|
|
395
|
+
/**
|
|
396
|
+
* Finds and retrieves a paginated list of records reactively based on a query.
|
|
397
|
+
*
|
|
398
|
+
* Similar to `list`, this method provides a reactive query result for a set of records that match the given
|
|
399
|
+
* find options. It supports pagination and provides methods for navigating through the pages.
|
|
400
|
+
*
|
|
401
|
+
* @param params The options for the find operation, used to query for specific records.
|
|
402
|
+
* @returns A `ReactivePagedQueryResult` object containing the reactive paged value and an `onValueChange` subscription function.
|
|
403
|
+
*/
|
|
404
|
+
find(params: TFindOptions): ReactivePagedQueryResult<T>;
|
|
405
|
+
/**
|
|
406
|
+
* Invalidates relevant queries in the cache based on a mutation operation.
|
|
407
|
+
* If a correlator is provided, it will be used to determine which queries to invalidate.
|
|
408
|
+
* Otherwise, it invalidates all 'list' and 'find' queries.
|
|
409
|
+
* @param mutation An object describing the mutation operation and its parameters.
|
|
410
|
+
*/
|
|
411
|
+
private invalidateQueries;
|
|
412
|
+
/**
|
|
413
|
+
* Handles incoming store events, invalidating relevant queries if a `storeEventCorrelator` is provided.
|
|
414
|
+
* @param event The StoreEvent received.
|
|
415
|
+
*/
|
|
416
|
+
private handleStoreEvent;
|
|
417
|
+
/**
|
|
418
|
+
* Creates a new record.
|
|
419
|
+
*
|
|
420
|
+
* This method sends a request to the `baseStore` to create a new record. Upon successful creation,
|
|
421
|
+
* it updates the cache with the new record and invalidates relevant queries to ensure data consistency.
|
|
422
|
+
*
|
|
423
|
+
* @param params An object containing the data for the new record and optional create options.
|
|
424
|
+
* @returns A promise that resolves to the newly created record, or `undefined` if the creation fails.
|
|
425
|
+
* @throws An error if the create operation fails.
|
|
426
|
+
*/
|
|
427
|
+
create(params: {
|
|
428
|
+
data: Partial<T>;
|
|
429
|
+
options?: TCreateOptions;
|
|
430
|
+
}): Promise<T | undefined>;
|
|
431
|
+
/**
|
|
432
|
+
* Updates an existing record.
|
|
433
|
+
*
|
|
434
|
+
* This method sends a request to the `baseStore` to update a record. If the update is successful,
|
|
435
|
+
* it updates the cache with the modified record and invalidates relevant queries.
|
|
436
|
+
*
|
|
437
|
+
* @param params An object containing the ID of the record to update, the partial data, and optional update options.
|
|
438
|
+
* @returns A promise that resolves to the updated record, or `undefined` if the update fails.
|
|
439
|
+
* @throws An error if the update operation fails.
|
|
440
|
+
*/
|
|
441
|
+
update(params: {
|
|
442
|
+
data: Partial<T>;
|
|
443
|
+
options?: TUpdateOptions;
|
|
444
|
+
}): Promise<T | undefined>;
|
|
445
|
+
/**
|
|
446
|
+
* Deletes a record.
|
|
447
|
+
*
|
|
448
|
+
* This method sends a request to the `baseStore` to delete a record. Upon successful deletion,
|
|
449
|
+
* it removes the record from the cache and invalidates relevant queries.
|
|
450
|
+
*
|
|
451
|
+
* @param params The options for the delete operation, used to identify the record to be deleted.
|
|
452
|
+
* @returns A promise that resolves when the deletion is complete.
|
|
453
|
+
* @throws An error if the delete operation fails.
|
|
454
|
+
*/
|
|
455
|
+
delete(params: TDeleteOptions): Promise<void>;
|
|
456
|
+
/**
|
|
457
|
+
* Sends a notification event to the base store.
|
|
458
|
+
*
|
|
459
|
+
* This can be used to trigger server-side events or other custom actions in the `baseStore`.
|
|
460
|
+
*
|
|
461
|
+
* @param event The `StoreEvent` to be sent.
|
|
462
|
+
* @returns A promise that resolves when the notification has been processed.
|
|
463
|
+
*/
|
|
464
|
+
notify(event: StoreEvent): Promise<void>;
|
|
465
|
+
/**
|
|
466
|
+
* Establishes a real-time data stream.
|
|
467
|
+
*
|
|
468
|
+
* This method delegates to the `baseStore`'s `stream` method to create a persistent connection for
|
|
469
|
+
* receiving real-time updates. The provided callback will be invoked when new data is available.
|
|
470
|
+
*
|
|
471
|
+
* @param options The options for the stream operation.
|
|
472
|
+
* @param onStreamChange A callback function that is executed when the stream's data changes.
|
|
473
|
+
* @returns A promise that resolves with a function to close the stream.
|
|
474
|
+
*/
|
|
475
|
+
stream(options: TStreamOptions, onStreamChange: () => void): Promise<{
|
|
476
|
+
stream: () => AsyncIterable<T>;
|
|
477
|
+
cancel: () => void;
|
|
478
|
+
status: () => "active" | "cancelled" | "completed";
|
|
479
|
+
}>;
|
|
480
|
+
/**
|
|
481
|
+
* Uploads a file and creates a new record associated with it.
|
|
482
|
+
*
|
|
483
|
+
* This method handles file uploads through the `baseStore`. After a successful upload, it updates the cache
|
|
484
|
+
* with the new record and invalidates relevant queries.
|
|
485
|
+
*
|
|
486
|
+
* @param params An object containing the file to upload and optional upload options.
|
|
487
|
+
* @returns A promise that resolves to the newly created record, or `undefined` if the upload fails.
|
|
488
|
+
* @throws An error if the upload operation fails.
|
|
489
|
+
*/
|
|
490
|
+
upload(params: {
|
|
491
|
+
file: File;
|
|
492
|
+
options?: TUploadOptions;
|
|
493
|
+
}): Promise<T | undefined>;
|
|
494
|
+
/**
|
|
495
|
+
* Forces a refresh of a specific query.
|
|
496
|
+
*
|
|
497
|
+
* This method bypasses the cache's staleness checks and forces a refetch of the data from the `baseStore`.
|
|
498
|
+
* The method is overloaded to support `read`, `list`, and `find` operations.
|
|
499
|
+
*
|
|
500
|
+
* @param operation The type of operation to refresh ('read', 'list', or 'find').
|
|
501
|
+
* @param params The parameters for the operation.
|
|
502
|
+
* @returns A promise that resolves to the refreshed data.
|
|
503
|
+
*/
|
|
504
|
+
refresh(operation: 'read', params: TReadOptions): Promise<T | undefined>;
|
|
505
|
+
refresh(operation: 'list', params: TListOptions): Promise<Page<T> | undefined>;
|
|
506
|
+
refresh(operation: 'find', query: TFindOptions): Promise<Page<T> | undefined>;
|
|
507
|
+
/**
|
|
508
|
+
* Pre-fetches data for a query and caches it.
|
|
509
|
+
*
|
|
510
|
+
* This method is used to proactively fetch data that is likely to be needed soon. It fetches the data
|
|
511
|
+
* and stores it in the cache, so that subsequent requests for the same data can be served instantly.
|
|
512
|
+
* The method is overloaded for `read`, `list`, and `find` operations.
|
|
513
|
+
*
|
|
514
|
+
* @param operation The type of operation to prefetch ('read', 'list', or 'find').
|
|
515
|
+
* @param params The parameters for the operation.
|
|
516
|
+
*/
|
|
517
|
+
prefetch(operation: 'read', params: TReadOptions): void;
|
|
518
|
+
prefetch(operation: 'list', params: TListOptions): void;
|
|
519
|
+
prefetch(operation: 'find', params: TFindOptions): void;
|
|
520
|
+
/**
|
|
521
|
+
* Invalidates the cached data for a specific query.
|
|
522
|
+
*
|
|
523
|
+
* This marks the query's data as stale, forcing a refetch the next time it's accessed. This is useful
|
|
524
|
+
* when you know the data has changed on the server, but the change was not triggered by a mutation
|
|
525
|
+
* through this store.
|
|
526
|
+
*
|
|
527
|
+
* @param operation The type of operation to invalidate ('read', 'list', or 'find').
|
|
528
|
+
* @param params The parameters for the operation.
|
|
529
|
+
* @returns A promise that resolves when the invalidation is complete.
|
|
530
|
+
*/
|
|
531
|
+
invalidate(operation: string, params: any): Promise<void>;
|
|
532
|
+
/**
|
|
533
|
+
* Invalidates all active queries in the store.
|
|
534
|
+
*
|
|
535
|
+
* This method marks all currently active queries as stale, forcing them to be refetched the next time
|
|
536
|
+
* they are accessed. This is a more aggressive approach to cache invalidation.
|
|
537
|
+
*
|
|
538
|
+
* @returns A promise that resolves when all invalidations are complete.
|
|
539
|
+
*/
|
|
540
|
+
invalidateAll(): Promise<void>;
|
|
541
|
+
/**
|
|
542
|
+
* Retrieves statistics about the store and its cache.
|
|
543
|
+
*
|
|
544
|
+
* This method returns an object containing statistics from the underlying cache, plus the number of
|
|
545
|
+
* active subscriptions in the reactive store.
|
|
546
|
+
*
|
|
547
|
+
* @returns An object with cache statistics and the number of active subscriptions.
|
|
548
|
+
*/
|
|
549
|
+
getStats(): {
|
|
550
|
+
activeSubscriptions: number;
|
|
551
|
+
size: number;
|
|
552
|
+
metrics: CacheMetrics;
|
|
553
|
+
hitRate: number;
|
|
554
|
+
staleHitRate: number;
|
|
555
|
+
entries: Array<{
|
|
556
|
+
key: string;
|
|
557
|
+
lastAccessed: number;
|
|
558
|
+
lastUpdated: number;
|
|
559
|
+
accessCount: number;
|
|
560
|
+
isStale: boolean;
|
|
561
|
+
isLoading?: boolean;
|
|
562
|
+
error?: boolean;
|
|
563
|
+
}>;
|
|
564
|
+
};
|
|
565
|
+
/**
|
|
566
|
+
* Cleans up all resources used by the store.
|
|
567
|
+
*
|
|
568
|
+
* This method should be called when the store is no longer needed. It unsubscribes from all cache events,
|
|
569
|
+
* clears all query subscriptions, and unsubscribes from the base store's events.
|
|
570
|
+
*/
|
|
571
|
+
destroy(): void;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export { type ActiveQuery, type BaseStore, type Correlator, type MutationOperation, type Page, type PagedQueryResult, type PaginationInfo, type QueryResult, type QuerySubscription, type ReactivePagedQueryResult, type ReactiveQueryResult, ReactiveRemoteStore, type ResourceName, StoreError, type StoreErrorDetails, type StoreEvent, type StoreEventCorrelator, type StoreRecord };
|
package/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var e=class e extends Error{code;status;data;isRetryable;originalError;constructor(e,t){super(e.message),this.name="StoreError",this.code=e.code,this.status=e.status,this.data=e.data,this.isRetryable=e.isRetryable,this.originalError=t}static fromError(t,r){if(t.isAbort||"AbortError"===t.name)return new e({code:"ABORTED",message:"Request was cancelled",isRetryable:!1},t);if("NetworkError"===t.name||!navigator.onLine)return new e({code:"NETWORK_ERROR",message:"Network connection failed. Please check your internet connection.",isRetryable:!0},t);if(t.status)switch(t.status){case 400:return new e({code:"VALIDATION_ERROR",message:t.message||"Invalid request data",status:400,data:t.data,isRetryable:!1},t);case 401:return new e({code:"UNAUTHORIZED",message:"Authentication required or invalid credentials",status:401,isRetryable:!1},t);case 403:return new e({code:"FORBIDDEN",message:"Access denied for this resource",status:403,isRetryable:!1},t);case 404:return new e({code:"NOT_FOUND",message:"Resource not found",status:404,isRetryable:!1},t);case 409:return new e({code:"CONFLICT",message:t.message||"Resource conflict occurred",status:409,isRetryable:!1},t);case 429:return new e({code:"RATE_LIMITED",message:"Too many requests. Please try again later.",status:429,isRetryable:!0},t);case 500:case 502:case 503:case 504:return new e({code:"SERVER_ERROR",message:"Server error occurred. Please try again later.",status:t.status,isRetryable:!0},t);default:return new e({code:"HTTP_ERROR",message:t.message||`HTTP ${t.status} error occurred`,status:t.status,isRetryable:t.status>=500},t)}return"TimeoutError"===t.name||"TIMEOUT"===t.code?new e({code:"TIMEOUT",message:"Request timed out. Please try again.",isRetryable:!0},t):new e({code:"UNKNOWN_ERROR",message:t.message||`Unknown error occurred during ${r}`,isRetryable:!0},t)}};function t(e,r=new WeakMap){return void 0===e?"undefined":"function"==typeof e||"symbol"==typeof e?"":"string"==typeof e?`"${e}"`:"number"==typeof e||"boolean"==typeof e||null===e?String(e):Array.isArray(e)?`[${e.map((e=>t(e,r))).join(",")}]`:"object"==typeof e?r.has(e)?'"__circular__"':(r.set(e,!0),`{${Object.keys(e).sort().map((a=>`"${a}":${t(e[a],r)}`)).join(",")}}`):""}function r(e){let r=3421674724,a=2216829733;const s=t(e);for(let e=0;e<s.length;e++){a^=s.charCodeAt(e);const t=Math.imul(a,435),i=Math.imul(a,435)+Math.imul(r,435);a=t>>>0,r=i>>>0}return(r>>>0).toString(16)+(a>>>0).toString(16)}exports.ReactiveRemoteStore=class{constructor(e,t,r,a){if(this.cache=e,this.baseStore=t,this.correlator=r,this.storeEventCorrelator=a,this.storeEventCorrelator){queueMicrotask((async()=>{this.unsubscribeFromBaseStore=await this.baseStore.subscribe("*",this.handleStoreEvent.bind(this))}))}}querySubscriptions=new Map;keyCache=new WeakMap;unsubscribeFromBaseStore;createSubscription(e,t,r,a,s){const i=new Set,o=()=>{i.forEach((e=>{try{e()}catch(e){console.error("ReactiveRemoteStore: Subscriber callback error:",e)}}))},c=[this.cache.on("cache:fetch:success",(e=>{s(e.key)&&o()})),this.cache.on("cache:fetch:error",(e=>{s(e.key)&&o()})),this.cache.on("cache:data:set",(e=>{s(e.key)&&o()})),this.cache.on("cache:data:invalidate",(e=>{s(e.key)&&o()})),this.cache.on("cache:read:hit",(e=>{s(e.key)&&e.isStale&&o()}))],n={unsubscribeCallbacks:c,subscribers:i,cachedSubscribe:t=>(i.add(t),()=>{i.delete(t),0===i.size&&(c.forEach((e=>e())),this.querySubscriptions.delete(e))}),cachedSelector:a,operation:t,params:r};return this.querySubscriptions.set(e,n),n}getOrCreateSubscription(e,t,r,a,s){let i=this.querySubscriptions.get(e);return i||(i=this.createSubscription(e,t,r,a,s)),i}buildKey(e,t){if("object"==typeof t&&null!==t&&this.keyCache.has(t))return this.keyCache.get(t);const a=`${e}:${r(t)}`;return"object"==typeof t&&null!==t&&this.keyCache.set(t,a),a}createSelector(e){return()=>{const t=this.cache.getStats().entries.find((t=>t.key===e)),r=this.cache.peek(e);return t?.isStale&&this.cache.get(e,{waitForFresh:!1}).catch((t=>{console.warn(`ReactiveRemoteStore: Background refetch failed for ${e}:`,t)})),void 0!==r&&this.cache.has(e)||this.cache.get(e,{waitForFresh:!1}).catch((t=>{console.warn(`ReactiveRemoteStore: Background fetch failed for ${e}:`,t)})),{data:r,loading:t?.isLoading??void 0===r,error:t?.error?new Error("Query failed"):void 0,stale:t?.isStale??!0,updated:t?.lastUpdated??0}}}read(e){const t=this.buildKey("read",e);this.querySubscriptions.has(t)||this.cache.registerQuery(t,(async()=>await this.baseStore.read(e)));const r=this.createSelector(t),a=this.getOrCreateSubscription(t,"read",e,r,(e=>e===t));return{value:a.cachedSelector,onValueChange:a.cachedSubscribe}}createPagedSelector(t,r,a){return()=>{const s=this.cache.getStats().entries.find((e=>e.key===t)),i=this.cache.peek(t);s?.isStale&&this.cache.get(t,{waitForFresh:!1}).catch((e=>{console.warn(`ReactiveRemoteStore: Background refetch failed for ${t}:`,e)})),void 0!==i&&this.cache.has(t)||this.cache.get(t,{waitForFresh:!1}).catch((e=>{console.warn(`ReactiveRemoteStore: Background fetch failed for ${t}:`,e)}));const o=r.page||1;return{page:i,loading:s?.isLoading??void 0===i,error:s?.error?e.fromError(new Error("Query failed"),"query"):void 0,stale:s?.isStale??!0,updated:s?.lastUpdated??0,hasNext:!!i&&o<i.page.pages,hasPrevious:o>1,next:async()=>{if(i&&o<i.page.pages){const e={...r,page:o+1},s=this.buildKey(t.split(":")[0],e);this.querySubscriptions.has(s)||this.cache.registerQuery(s,(()=>a(e))),await this.cache.get(s,{waitForFresh:!0})}},previous:async()=>{if(o>1){const e={...r,page:o-1},s=this.buildKey(t.split(":")[0],e);this.querySubscriptions.has(s)||this.cache.registerQuery(s,(()=>a(e))),await this.cache.get(s,{waitForFresh:!0})}},fetch:async e=>{const s={...r,page:e},i=this.buildKey(t.split(":")[0],s);this.querySubscriptions.has(i)||this.cache.registerQuery(i,(()=>a(s))),await this.cache.get(i,{waitForFresh:!0})}}}}setupPagedQuery(e,t,r){const a=this.buildKey(e,t);this.querySubscriptions.has(a)||this.cache.registerQuery(a,(async()=>r(t)));const s=this.createPagedSelector(a,t,r),i=this.getOrCreateSubscription(a,e,t,s,(t=>t.startsWith(`${e}:`)));return{value:i.cachedSelector,onValueChange:i.cachedSubscribe}}list(e){return this.setupPagedQuery("list",e,this.baseStore.list.bind(this.baseStore))}find(e){return this.setupPagedQuery("find",e,this.baseStore.find.bind(this.baseStore))}async invalidateQueries(e){if(this.correlator){const t=Array.from(this.querySubscriptions.entries()).map((([e,t])=>({queryKey:e,operation:t.operation,params:t.params}))),r=this.correlator(e,t).map((e=>this.cache.invalidate(e)));await Promise.all(r)}else await this.cache.invalidatePattern(/^list:/),await this.cache.invalidatePattern(/^find:/)}handleStoreEvent(e){if(console.log("ReactiveRemoteStore: Received store event:",e),this.storeEventCorrelator){const t=Array.from(this.querySubscriptions.entries()).map((([e,t])=>({queryKey:e,operation:t.operation,params:t.params})));console.log("ReactiveRemoteStore: Active queries:",t);const r=this.storeEventCorrelator(e,t);console.log("ReactiveRemoteStore: Queries to invalidate:",r),r.forEach((e=>this.cache.invalidate(e)))}else console.warn("ReactiveRemoteStore: handleStoreEvent called without _storeEventCorrelator.")}async create(e){try{const t=await this.baseStore.create(e);return t&&(this.cache.setData(this.buildKey("read",{id:t.id}),t),await this.invalidateQueries({operation:"create",params:e})),t}catch(e){throw console.error("ReactiveRemoteStore: Create operation failed:",e),e}}async update(e){try{const t=await this.baseStore.update(e);return t&&await this.invalidateQueries({operation:"update",params:e}),t}catch(e){throw console.error("ReactiveRemoteStore: Update operation failed:",e),e}}async delete(e){try{await this.baseStore.delete(e),this.cache.remove(this.buildKey("read",{id:e.id})),await this.invalidateQueries({operation:"delete",params:e})}catch(e){throw console.error("ReactiveRemoteStore: Delete operation failed:",e),e}}async notify(e){return this.baseStore.notify(e)}async stream(e,t){return this.baseStore.stream(e,t)}async upload(e){try{const t=await this.baseStore.upload(e);return t&&(this.cache.setData(this.buildKey("read",{id:t.id}),t),await this.invalidateQueries({operation:"upload",params:e})),t}catch(e){throw console.error("ReactiveRemoteStore: Upload operation failed:",e),e}}async refresh(e,t){const r=this.buildKey(e,t);return this.cache.refresh(r)}prefetch(e,t){const r=this.buildKey(e,t);this.cache.prefetch(r).catch((e=>{console.warn(`ReactiveRemoteStore: Prefetch failed for ${r}:`,e)}))}async invalidate(e,t){const r=this.buildKey(e,t);await this.cache.invalidate(r)}async invalidateAll(){const e=Array.from(this.querySubscriptions.keys()).map((e=>this.cache.invalidate(e)));await Promise.all(e)}getStats(){return{...this.cache.getStats(),activeSubscriptions:this.querySubscriptions.size}}destroy(){this.querySubscriptions.forEach((e=>{e.unsubscribeCallbacks.forEach((e=>e()))})),this.querySubscriptions.clear(),this.keyCache=new WeakMap,this.unsubscribeFromBaseStore&&this.unsubscribeFromBaseStore()}},exports.StoreError=e;
|
package/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e=class e extends Error{code;status;data;isRetryable;originalError;constructor(e,t){super(e.message),this.name="StoreError",this.code=e.code,this.status=e.status,this.data=e.data,this.isRetryable=e.isRetryable,this.originalError=t}static fromError(t,r){if(t.isAbort||"AbortError"===t.name)return new e({code:"ABORTED",message:"Request was cancelled",isRetryable:!1},t);if("NetworkError"===t.name||!navigator.onLine)return new e({code:"NETWORK_ERROR",message:"Network connection failed. Please check your internet connection.",isRetryable:!0},t);if(t.status)switch(t.status){case 400:return new e({code:"VALIDATION_ERROR",message:t.message||"Invalid request data",status:400,data:t.data,isRetryable:!1},t);case 401:return new e({code:"UNAUTHORIZED",message:"Authentication required or invalid credentials",status:401,isRetryable:!1},t);case 403:return new e({code:"FORBIDDEN",message:"Access denied for this resource",status:403,isRetryable:!1},t);case 404:return new e({code:"NOT_FOUND",message:"Resource not found",status:404,isRetryable:!1},t);case 409:return new e({code:"CONFLICT",message:t.message||"Resource conflict occurred",status:409,isRetryable:!1},t);case 429:return new e({code:"RATE_LIMITED",message:"Too many requests. Please try again later.",status:429,isRetryable:!0},t);case 500:case 502:case 503:case 504:return new e({code:"SERVER_ERROR",message:"Server error occurred. Please try again later.",status:t.status,isRetryable:!0},t);default:return new e({code:"HTTP_ERROR",message:t.message||`HTTP ${t.status} error occurred`,status:t.status,isRetryable:t.status>=500},t)}return"TimeoutError"===t.name||"TIMEOUT"===t.code?new e({code:"TIMEOUT",message:"Request timed out. Please try again.",isRetryable:!0},t):new e({code:"UNKNOWN_ERROR",message:t.message||`Unknown error occurred during ${r}`,isRetryable:!0},t)}};function t(e,r=new WeakMap){return void 0===e?"undefined":"function"==typeof e||"symbol"==typeof e?"":"string"==typeof e?`"${e}"`:"number"==typeof e||"boolean"==typeof e||null===e?String(e):Array.isArray(e)?`[${e.map((e=>t(e,r))).join(",")}]`:"object"==typeof e?r.has(e)?'"__circular__"':(r.set(e,!0),`{${Object.keys(e).sort().map((a=>`"${a}":${t(e[a],r)}`)).join(",")}}`):""}function r(e){let r=3421674724,a=2216829733;const s=t(e);for(let e=0;e<s.length;e++){a^=s.charCodeAt(e);const t=Math.imul(a,435),i=Math.imul(a,435)+Math.imul(r,435);a=t>>>0,r=i>>>0}return(r>>>0).toString(16)+(a>>>0).toString(16)}var a=class{constructor(e,t,r,a){if(this.cache=e,this.baseStore=t,this.correlator=r,this.storeEventCorrelator=a,this.storeEventCorrelator){queueMicrotask((async()=>{this.unsubscribeFromBaseStore=await this.baseStore.subscribe("*",this.handleStoreEvent.bind(this))}))}}querySubscriptions=new Map;keyCache=new WeakMap;unsubscribeFromBaseStore;createSubscription(e,t,r,a,s){const i=new Set,o=()=>{i.forEach((e=>{try{e()}catch(e){console.error("ReactiveRemoteStore: Subscriber callback error:",e)}}))},c=[this.cache.on("cache:fetch:success",(e=>{s(e.key)&&o()})),this.cache.on("cache:fetch:error",(e=>{s(e.key)&&o()})),this.cache.on("cache:data:set",(e=>{s(e.key)&&o()})),this.cache.on("cache:data:invalidate",(e=>{s(e.key)&&o()})),this.cache.on("cache:read:hit",(e=>{s(e.key)&&e.isStale&&o()}))],n={unsubscribeCallbacks:c,subscribers:i,cachedSubscribe:t=>(i.add(t),()=>{i.delete(t),0===i.size&&(c.forEach((e=>e())),this.querySubscriptions.delete(e))}),cachedSelector:a,operation:t,params:r};return this.querySubscriptions.set(e,n),n}getOrCreateSubscription(e,t,r,a,s){let i=this.querySubscriptions.get(e);return i||(i=this.createSubscription(e,t,r,a,s)),i}buildKey(e,t){if("object"==typeof t&&null!==t&&this.keyCache.has(t))return this.keyCache.get(t);const a=`${e}:${r(t)}`;return"object"==typeof t&&null!==t&&this.keyCache.set(t,a),a}createSelector(e){return()=>{const t=this.cache.getStats().entries.find((t=>t.key===e)),r=this.cache.peek(e);return t?.isStale&&this.cache.get(e,{waitForFresh:!1}).catch((t=>{console.warn(`ReactiveRemoteStore: Background refetch failed for ${e}:`,t)})),void 0!==r&&this.cache.has(e)||this.cache.get(e,{waitForFresh:!1}).catch((t=>{console.warn(`ReactiveRemoteStore: Background fetch failed for ${e}:`,t)})),{data:r,loading:t?.isLoading??void 0===r,error:t?.error?new Error("Query failed"):void 0,stale:t?.isStale??!0,updated:t?.lastUpdated??0}}}read(e){const t=this.buildKey("read",e);this.querySubscriptions.has(t)||this.cache.registerQuery(t,(async()=>await this.baseStore.read(e)));const r=this.createSelector(t),a=this.getOrCreateSubscription(t,"read",e,r,(e=>e===t));return{value:a.cachedSelector,onValueChange:a.cachedSubscribe}}createPagedSelector(t,r,a){return()=>{const s=this.cache.getStats().entries.find((e=>e.key===t)),i=this.cache.peek(t);s?.isStale&&this.cache.get(t,{waitForFresh:!1}).catch((e=>{console.warn(`ReactiveRemoteStore: Background refetch failed for ${t}:`,e)})),void 0!==i&&this.cache.has(t)||this.cache.get(t,{waitForFresh:!1}).catch((e=>{console.warn(`ReactiveRemoteStore: Background fetch failed for ${t}:`,e)}));const o=r.page||1;return{page:i,loading:s?.isLoading??void 0===i,error:s?.error?e.fromError(new Error("Query failed"),"query"):void 0,stale:s?.isStale??!0,updated:s?.lastUpdated??0,hasNext:!!i&&o<i.page.pages,hasPrevious:o>1,next:async()=>{if(i&&o<i.page.pages){const e={...r,page:o+1},s=this.buildKey(t.split(":")[0],e);this.querySubscriptions.has(s)||this.cache.registerQuery(s,(()=>a(e))),await this.cache.get(s,{waitForFresh:!0})}},previous:async()=>{if(o>1){const e={...r,page:o-1},s=this.buildKey(t.split(":")[0],e);this.querySubscriptions.has(s)||this.cache.registerQuery(s,(()=>a(e))),await this.cache.get(s,{waitForFresh:!0})}},fetch:async e=>{const s={...r,page:e},i=this.buildKey(t.split(":")[0],s);this.querySubscriptions.has(i)||this.cache.registerQuery(i,(()=>a(s))),await this.cache.get(i,{waitForFresh:!0})}}}}setupPagedQuery(e,t,r){const a=this.buildKey(e,t);this.querySubscriptions.has(a)||this.cache.registerQuery(a,(async()=>r(t)));const s=this.createPagedSelector(a,t,r),i=this.getOrCreateSubscription(a,e,t,s,(t=>t.startsWith(`${e}:`)));return{value:i.cachedSelector,onValueChange:i.cachedSubscribe}}list(e){return this.setupPagedQuery("list",e,this.baseStore.list.bind(this.baseStore))}find(e){return this.setupPagedQuery("find",e,this.baseStore.find.bind(this.baseStore))}async invalidateQueries(e){if(this.correlator){const t=Array.from(this.querySubscriptions.entries()).map((([e,t])=>({queryKey:e,operation:t.operation,params:t.params}))),r=this.correlator(e,t).map((e=>this.cache.invalidate(e)));await Promise.all(r)}else await this.cache.invalidatePattern(/^list:/),await this.cache.invalidatePattern(/^find:/)}handleStoreEvent(e){if(console.log("ReactiveRemoteStore: Received store event:",e),this.storeEventCorrelator){const t=Array.from(this.querySubscriptions.entries()).map((([e,t])=>({queryKey:e,operation:t.operation,params:t.params})));console.log("ReactiveRemoteStore: Active queries:",t);const r=this.storeEventCorrelator(e,t);console.log("ReactiveRemoteStore: Queries to invalidate:",r),r.forEach((e=>this.cache.invalidate(e)))}else console.warn("ReactiveRemoteStore: handleStoreEvent called without _storeEventCorrelator.")}async create(e){try{const t=await this.baseStore.create(e);return t&&(this.cache.setData(this.buildKey("read",{id:t.id}),t),await this.invalidateQueries({operation:"create",params:e})),t}catch(e){throw console.error("ReactiveRemoteStore: Create operation failed:",e),e}}async update(e){try{const t=await this.baseStore.update(e);return t&&await this.invalidateQueries({operation:"update",params:e}),t}catch(e){throw console.error("ReactiveRemoteStore: Update operation failed:",e),e}}async delete(e){try{await this.baseStore.delete(e),this.cache.remove(this.buildKey("read",{id:e.id})),await this.invalidateQueries({operation:"delete",params:e})}catch(e){throw console.error("ReactiveRemoteStore: Delete operation failed:",e),e}}async notify(e){return this.baseStore.notify(e)}async stream(e,t){return this.baseStore.stream(e,t)}async upload(e){try{const t=await this.baseStore.upload(e);return t&&(this.cache.setData(this.buildKey("read",{id:t.id}),t),await this.invalidateQueries({operation:"upload",params:e})),t}catch(e){throw console.error("ReactiveRemoteStore: Upload operation failed:",e),e}}async refresh(e,t){const r=this.buildKey(e,t);return this.cache.refresh(r)}prefetch(e,t){const r=this.buildKey(e,t);this.cache.prefetch(r).catch((e=>{console.warn(`ReactiveRemoteStore: Prefetch failed for ${r}:`,e)}))}async invalidate(e,t){const r=this.buildKey(e,t);await this.cache.invalidate(r)}async invalidateAll(){const e=Array.from(this.querySubscriptions.keys()).map((e=>this.cache.invalidate(e)));await Promise.all(e)}getStats(){return{...this.cache.getStats(),activeSubscriptions:this.querySubscriptions.size}}destroy(){this.querySubscriptions.forEach((e=>{e.unsubscribeCallbacks.forEach((e=>e()))})),this.querySubscriptions.clear(),this.keyCache=new WeakMap,this.unsubscribeFromBaseStore&&this.unsubscribeFromBaseStore()}};export{a as ReactiveRemoteStore,e as StoreError};
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@asaidimu/utils-remote-store",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A reactive store for remote data, built on top of @asaidimu/utils-cache",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"module": "index.mjs",
|
|
7
|
+
"types": "index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"./*"
|
|
10
|
+
],
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@asaidimu/utils-cache": "2.0.5",
|
|
13
|
+
"eventsource": "^4.0.0"
|
|
14
|
+
},
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"import": {
|
|
18
|
+
"types": "./index.d.ts",
|
|
19
|
+
"default": "./index.mjs"
|
|
20
|
+
},
|
|
21
|
+
"require": {
|
|
22
|
+
"types": "./index.d.ts",
|
|
23
|
+
"default": "./index.js"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"registry": "https://registry.npmjs.org/",
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"release": {
|
|
32
|
+
"plugins": [
|
|
33
|
+
[
|
|
34
|
+
"@semantic-release/npm",
|
|
35
|
+
{
|
|
36
|
+
"pkgRoot": "./dist"
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
[
|
|
40
|
+
"@semantic-release/git",
|
|
41
|
+
{
|
|
42
|
+
"assets": [
|
|
43
|
+
"CHANGELOG.md",
|
|
44
|
+
"package.json"
|
|
45
|
+
],
|
|
46
|
+
"message": "chore(release): Release @asaidimu/utils-remote-store v\n{nextRelease.version} [skip ci]\n\n{nextRelease.notes}"
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
}
|