@asaidimu/utils-remote-store 1.1.0 → 1.2.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/index.d.mts CHANGED
@@ -1,5 +1,198 @@
1
- import { QueryCache, CacheMetrics } from '../cache/index.mjs';
2
- import '../types-DUZGkNEB.js';
1
+ interface SimplePersistence<T> {
2
+ /**
3
+ * Persists data to storage.
4
+ *
5
+ * @param id The **unique identifier of the *consumer instance*** making the change. This is NOT the ID of the data (`T`) itself.
6
+ * Think of it as the ID of the specific browser tab, component, or module that's currently interacting with the persistence layer.
7
+ * It should typically be a **UUID** generated once at the consumer instance's instantiation.
8
+ * This `id` is crucial for the `subscribe` method, helping to differentiate updates originating from the current instance versus other instances/tabs, thereby preventing self-triggered notification loops.
9
+ * @param state The state (of type T) to persist. This state is generally considered the **global or shared state** that all instances interact with.
10
+ * @returns `true` if the operation was successful, `false` if an error occurred. For asynchronous implementations (like `IndexedDBPersistence`), this returns a `Promise<boolean>`.
11
+ */
12
+ set(id: string, state: T): boolean | Promise<boolean>;
13
+ /**
14
+ * Retrieves the global persisted data from storage.
15
+ *
16
+ * @returns The retrieved state of type `T`, or `null` if no data is found or if an error occurs during retrieval/parsing.
17
+ * For asynchronous implementations, this returns a `Promise<T | null>`.
18
+ */
19
+ get(): (T | null) | (Promise<T | null>);
20
+ /**
21
+ * Subscribes to changes in the global persisted data that originate from *other* instances of your application (e.g., other tabs or independent components using the same persistence layer).
22
+ *
23
+ * @param id The **unique identifier of the *consumer instance* subscribing**. This allows the persistence implementation to filter out notifications that were initiated by the subscribing instance itself.
24
+ * @param callback The function to call when the global persisted data changes from *another* source. The new state (`T`) is passed as an argument to this callback.
25
+ * @returns A function that, when called, will unsubscribe the provided callback from future updates. Call this when your component or instance is no longer active to prevent memory leaks.
26
+ */
27
+ subscribe(id: string, callback: (state: T) => void): () => void;
28
+ /**
29
+ * Clears (removes) the entire global persisted data from storage.
30
+ *
31
+ * @returns `true` if the operation was successful, `false` if an error occurred. For asynchronous implementations, this returns a `Promise<boolean>`.
32
+ */
33
+ clear(): boolean | Promise<boolean>;
34
+ /**
35
+ * Returns metadata about the persistence layer.
36
+ *
37
+ * This is useful for distinguishing between multiple apps running on the same host
38
+ * (e.g., several apps served at `localhost:3000` that share the same storage key).
39
+ *
40
+ * @returns An object containing:
41
+ * - `version`: The semantic version string of the persistence schema or application.
42
+ * - `id`: A unique identifier for the application using this persistence instance.
43
+ */
44
+ stats(): {
45
+ version: string;
46
+ id: string;
47
+ };
48
+ }
49
+
50
+ interface CacheOptions {
51
+ staleTime?: number;
52
+ cacheTime?: number;
53
+ retryAttempts?: number;
54
+ retryDelay?: number;
55
+ maxSize?: number;
56
+ enableMetrics?: boolean;
57
+ persistence?: SimplePersistence<SerializableCacheState>;
58
+ persistenceId?: string;
59
+ serializeValue?: (value: any) => any;
60
+ deserializeValue?: (value: any) => any;
61
+ persistenceDebounceTime?: number;
62
+ }
63
+ interface CacheMetrics {
64
+ hits: number;
65
+ misses: number;
66
+ fetches: number;
67
+ errors: number;
68
+ evictions: number;
69
+ staleHits: number;
70
+ }
71
+ interface SerializableCacheEntry {
72
+ data: any;
73
+ lastUpdated: number;
74
+ lastAccessed: number;
75
+ accessCount: number;
76
+ error?: {
77
+ name: string;
78
+ message: string;
79
+ stack?: string;
80
+ };
81
+ }
82
+ type SerializableCacheState = Array<[string, SerializableCacheEntry]>;
83
+ type CacheEventBase<Type extends string, Payload = {}> = {
84
+ type: Type;
85
+ key: string;
86
+ timestamp: number;
87
+ } & Payload;
88
+ type CacheReadHitEvent<T = any> = CacheEventBase<'cache:read:hit', {
89
+ data: T;
90
+ isStale: boolean;
91
+ }>;
92
+ type CacheReadMissEvent = CacheEventBase<'cache:read:miss'>;
93
+ type CacheFetchStartEvent = CacheEventBase<'cache:fetch:start', {
94
+ attempt: number;
95
+ }>;
96
+ type CacheFetchSuccessEvent<T = any> = CacheEventBase<'cache:fetch:success', {
97
+ data: T;
98
+ }>;
99
+ type CacheFetchErrorEvent = CacheEventBase<'cache:fetch:error', {
100
+ error: Error;
101
+ attempt: number;
102
+ }>;
103
+ type CacheDataEvictEvent = CacheEventBase<'cache:data:evict', {
104
+ reason?: string;
105
+ }>;
106
+ type CacheDataInvalidateEvent = CacheEventBase<'cache:data:invalidate'>;
107
+ type CacheDataSetEvent<T = any> = CacheEventBase<'cache:data:set', {
108
+ newData: T;
109
+ oldData?: T;
110
+ }>;
111
+ type CachePersistenceLoadSuccessEvent = CacheEventBase<'cache:persistence:load:success', {
112
+ message?: string;
113
+ }>;
114
+ type CachePersistenceLoadErrorEvent = CacheEventBase<'cache:persistence:load:error', {
115
+ message?: string;
116
+ error?: any;
117
+ }>;
118
+ type CachePersistenceSaveSuccessEvent = CacheEventBase<'cache:persistence:save:success'>;
119
+ type CachePersistenceSaveErrorEvent = CacheEventBase<'cache:persistence:save:error', {
120
+ message?: string;
121
+ error?: any;
122
+ }>;
123
+ type CachePersistenceClearSuccessEvent = CacheEventBase<'cache:persistence:clear:success'>;
124
+ type CachePersistenceClearErrorEvent = CacheEventBase<'cache:persistence:clear:error', {
125
+ message?: string;
126
+ error?: any;
127
+ }>;
128
+ type CachePersistenceSyncEvent = CacheEventBase<'cache:persistence:sync', {
129
+ message?: string;
130
+ }>;
131
+ type CacheEvent = CacheReadHitEvent | CacheReadMissEvent | CacheFetchStartEvent | CacheFetchSuccessEvent | CacheFetchErrorEvent | CacheDataEvictEvent | CacheDataInvalidateEvent | CacheDataSetEvent | CachePersistenceLoadSuccessEvent | CachePersistenceLoadErrorEvent | CachePersistenceSaveSuccessEvent | CachePersistenceSaveErrorEvent | CachePersistenceClearSuccessEvent | CachePersistenceClearErrorEvent | CachePersistenceSyncEvent;
132
+ type CacheEventType = CacheEvent['type'];
133
+
134
+ declare class QueryCache {
135
+ private cache;
136
+ private queries;
137
+ private fetching;
138
+ private readonly defaultOptions;
139
+ private metrics;
140
+ private eventBus;
141
+ private gcTimer?;
142
+ private readonly persistenceId;
143
+ private persistenceUnsubscribe?;
144
+ private persistenceDebounceTimer?;
145
+ private isHandlingRemoteUpdate;
146
+ constructor(defaultOptions?: CacheOptions);
147
+ private initializePersistence;
148
+ private serializeCache;
149
+ private deserializeAndLoadCache;
150
+ private schedulePersistState;
151
+ private handleRemoteStateChange;
152
+ registerQuery<T>(key: string, fetchFunction: () => Promise<T>, options?: CacheOptions): void;
153
+ get<T>(key: string, options?: {
154
+ waitForFresh?: boolean;
155
+ throwOnError?: boolean;
156
+ }): Promise<T | undefined>;
157
+ peek<T>(key: string): T | undefined;
158
+ has(key: string): boolean;
159
+ private fetch;
160
+ private fetchAndWait;
161
+ private performFetchWithRetry;
162
+ private isStale;
163
+ invalidate(key: string, refetch?: boolean): Promise<void>;
164
+ invalidatePattern(pattern: RegExp, refetch?: boolean): Promise<void>;
165
+ prefetch(key: string): Promise<void>;
166
+ refresh<T>(key: string): Promise<T | undefined>;
167
+ setData<T>(key: string, data: T): void;
168
+ remove(key: string): boolean;
169
+ private enforceSizeLimit;
170
+ private startGarbageCollection;
171
+ garbageCollect(): number;
172
+ getStats(): {
173
+ size: number;
174
+ metrics: CacheMetrics;
175
+ hitRate: number;
176
+ staleHitRate: number;
177
+ entries: Array<{
178
+ key: string;
179
+ lastAccessed: number;
180
+ lastUpdated: number;
181
+ accessCount: number;
182
+ isStale: boolean;
183
+ isLoading?: boolean;
184
+ error?: boolean;
185
+ }>;
186
+ };
187
+ on<EType extends CacheEventType>(event: EType, listener: (ev: Extract<CacheEvent, {
188
+ type: EType;
189
+ }>) => void): () => void;
190
+ private emitEvent;
191
+ private updateMetrics;
192
+ private delay;
193
+ clear(): Promise<void>;
194
+ destroy(): void;
195
+ }
3
196
 
4
197
  interface StoreErrorDetails {
5
198
  code: string;
@@ -205,7 +398,9 @@ interface PagedQueryResult<T> {
205
398
  /** Function to fetch a specific page of data.
206
399
  * @param page The page number to fetch.
207
400
  */
208
- fetch: (page: number) => Promise<void>;
401
+ navigate: (page: number) => Promise<void>;
402
+ refresh: (delay?: number) => Promise<void>;
403
+ changeParams: (setter: (params: any) => any) => Promise<void>;
209
404
  }
210
405
  /**
211
406
  * Represents a basic record in the remote store.
@@ -230,26 +425,6 @@ type PaginationInfo = {
230
425
  * Represents the name of a resource in the store, e.g., 'todos', 'users'.
231
426
  */
232
427
  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
428
  /**
254
429
  * Internal interface representing an active subscription to a query.
255
430
  * @template T The type of the data being queried.
@@ -271,283 +446,80 @@ interface QuerySubscription<T> {
271
446
  result: QueryResult<T> | PagedQueryResult<T>;
272
447
  }
273
448
 
449
+ /**
450
+ * Represents the result of a store query with stable React integration.
451
+ * @template T The type of the result data.
452
+ */
453
+ interface StoreResult<T extends Record<string, any> = Record<string, any>, V = QueryResult<T> | PagedQueryResult<T>> {
454
+ /** Function that returns the current query state */
455
+ value: () => V;
456
+ /** Function to subscribe to state changes */
457
+ onValueChange: (callback: () => void) => () => void;
458
+ }
274
459
  /**
275
460
  * A reactive remote store that provides cached and observable access to data
276
461
  * from a `BaseStore`. It handles caching, invalidation, and real-time updates
277
462
  * via server-sent events (SSE).
278
- *
279
- * @template T The type of the records managed by the store, extending Record.
280
- * @template TFindOptions Options for the find operation.
281
- * @template TReadOptions Options for the read operation.
282
- * @template TListOptions Options for the list operation.
283
- * @template TDeleteOptions Options for the delete operation.
284
- * @template TUpdateOptions Options for the update operation.
285
- * @template TCreateOptions Options for the create operation.
286
- * @template TUploadOptions Options for the upload operation.
287
- * @template TStreamOptions Options for the stream operation.
288
463
  */
289
464
  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>> {
290
465
  private cache;
291
466
  private baseStore;
292
467
  private correlator?;
293
468
  private storeEventCorrelator?;
294
- private querySubscriptions;
469
+ private queryStates;
470
+ private stableResults;
471
+ private stablePaginationMethods;
472
+ private errorCache;
295
473
  private keyCache;
296
474
  private unsubscribeFromBaseStore;
297
- /**
298
- * Creates an instance of ReactiveRemoteStore.
299
- * @param cache The QueryCache instance to use for caching data.
300
- * @param baseStore The underlying BaseStore for actual data fetching and mutations.
301
- * @param correlator Optional function to determine which queries to invalidate after a mutation.
302
- * @param storeEventCorrelator Optional function to determine which queries to invalidate after a store event.
303
- */
475
+ private cacheEventCleanups;
304
476
  constructor(cache: QueryCache, baseStore: BaseStore<T, TFindOptions, TReadOptions, TListOptions, TDeleteOptions, TUpdateOptions, TCreateOptions, TUploadOptions, TStreamOptions>, correlator?: Correlator | undefined, storeEventCorrelator?: StoreEventCorrelator | undefined);
305
- /**
306
- * Creates a new query subscription and registers it with the cache.
307
- * @template T The type of the data for the subscription.
308
- * @param queryKey The unique key for the query.
309
- * @param operation The operation type (e.g., 'read', 'list').
310
- * @param params The parameters for the query.
311
- * @param selector A function that returns the current query result.
312
- * @param notificationCondition A function to determine if a cache event should trigger a notification.
313
- * @returns The created QuerySubscription.
314
- */
315
- private createSubscription;
316
- /**
317
- * Retrieves an existing subscription or creates a new one if it doesn't exist.
318
- * @template T The type of the data for the subscription.
319
- * @param queryKey The unique key for the query.
320
- * @param operation The operation type (e.g., 'read', 'list').
321
- * @param params The parameters for the query.
322
- * @param selector A function that returns the current query result.
323
- * @param notificationCondition A function to determine if a cache event should trigger a notification.
324
- * @returns The existing or newly created QuerySubscription.
325
- */
326
- private getOrCreateSubscription;
327
- /**
328
- * Builds a unique cache key for a given operation and its parameters.
329
- * Uses a WeakMap for caching keys of object parameters to avoid re-hashing.
330
- * @param operation The name of the operation (e.g., 'read', 'list').
331
- * @param params The parameters for the operation.
332
- * @returns A unique string key for the operation and parameters.
333
- */
477
+ private setupCacheEventListeners;
478
+ private handleCacheEvent;
334
479
  private buildKey;
335
- /**
336
- * Creates a selector function for a single record query.
337
- * @template T The type of the data being queried.
338
- * @param queryKey The unique key for the query.
339
- * @returns A function that returns the current QueryResult for the given key.
340
- */
341
- private createSelector;
342
- /**
343
- * Reads a single record from the store.
344
- * @param params The read options.
345
- * @returns An object containing the current QueryResult and a function to subscribe to changes.
346
- */
347
- /**
348
- * Retrieves a single record reactively.
349
- *
350
- * This method provides a reactive query result for a single record. The data is fetched from the cache if available,
351
- * otherwise it's fetched from the underlying `baseStore`. The method returns an object containing the current
352
- * query result and a function to subscribe to future updates.
353
- *
354
- * @param params The options for the read operation, used to identify the record.
355
- * @returns A `ReactiveQueryResult` object containing the reactive value and an `onValueChange` subscription function.
356
- */
357
- read(params: TReadOptions): ReactiveQueryResult<T>;
358
- /**
359
- * Creates a selector function for a paginated query (list or find).
360
- * @template TParams The type of the parameters for the query.
361
- * @param baseQueryKey The base unique key for the query.
362
- * @param baseParams The base parameters for the query.
363
- * @param fetchFn The function to call to fetch a page of data.
364
- * @returns A function that returns the current PagedQueryResult.
365
- */
366
- private createPagedSelector;
367
- /**
368
- * Sets up a paginated query (list or find) with caching and reactivity.
369
- * @template TParams The type of the parameters for the query.
370
- * @param type The type of the paginated query ('list' or 'find').
371
- * @param params The parameters for the query.
372
- * @param fetchFn The function to call to fetch a page of data.
373
- * @returns An object containing the current PagedQueryResult and a function to subscribe to changes.
374
- */
480
+ private getOrCreateError;
481
+ private getOrCreateStoreError;
482
+ private computeResult;
483
+ private scheduleBackgroundFetch;
484
+ private getCurrentPageForQuery;
485
+ private getOrCreatePaginationMethods;
486
+ private computeResultForParams;
487
+ hasActiveQuery(operation: string, params: any): boolean;
488
+ getActiveQuery<TResult extends Record<string, unknown> = T>(operation: string, params: any): StoreResult<TResult> | undefined;
489
+ read(params: TReadOptions): StoreResult<T, QueryResult<T>>;
490
+ list(params: TListOptions): StoreResult<T, PagedQueryResult<T>>;
491
+ find(params: TFindOptions): StoreResult<T, PagedQueryResult<T>>;
375
492
  private setupPagedQuery;
376
- /**
377
- * Lists records from the store with pagination and reactivity.
378
- * @param params The list options.
379
- * @returns An object containing the current PagedQueryResult and a function to subscribe to changes.
380
- */
381
- /**
382
- * Retrieves a paginated list of records reactively.
383
- *
384
- * This method provides a reactive query result for a list of records. It supports pagination and automatically
385
- * fetches data from the cache or the `baseStore`. The result includes methods for navigating to the next,
386
- * previous, or a specific page.
387
- *
388
- * @param params The options for the list operation, including pagination details.
389
- * @returns A `ReactivePagedQueryResult` object containing the reactive paged value and an `onValueChange` subscription function.
390
- */
391
- list(params: TListOptions): ReactivePagedQueryResult<T>;
392
- /**
393
- * Finds records from the store with pagination and reactivity.
394
- * @param params The find options.
395
- * @returns An object containing the current PagedQueryResult and a function to subscribe to changes.
396
- */
397
- /**
398
- * Finds and retrieves a paginated list of records reactively based on a query.
399
- *
400
- * Similar to `list`, this method provides a reactive query result for a set of records that match the given
401
- * find options. It supports pagination and provides methods for navigating through the pages.
402
- *
403
- * @param params The options for the find operation, used to query for specific records.
404
- * @returns A `ReactivePagedQueryResult` object containing the reactive paged value and an `onValueChange` subscription function.
405
- */
406
- find(params: TFindOptions): ReactivePagedQueryResult<T>;
407
- /**
408
- * Invalidates relevant queries in the cache based on a mutation operation.
409
- * If a correlator is provided, it will be used to determine which queries to invalidate.
410
- * Otherwise, it invalidates all 'list' and 'find' queries.
411
- * @param mutation An object describing the mutation operation and its parameters.
412
- */
413
493
  private invalidateQueries;
414
- /**
415
- * Handles incoming store events, invalidating relevant queries if a `storeEventCorrelator` is provided.
416
- * @param event The StoreEvent received.
417
- */
418
494
  private handleStoreEvent;
419
- /**
420
- * Creates a new record.
421
- *
422
- * This method sends a request to the `baseStore` to create a new record. Upon successful creation,
423
- * it updates the cache with the new record and invalidates relevant queries to ensure data consistency.
424
- *
425
- * @param params An object containing the data for the new record and optional create options.
426
- * @returns A promise that resolves to the newly created record, or `undefined` if the creation fails.
427
- * @throws An error if the create operation fails.
428
- */
429
495
  create(params: {
430
496
  data: Partial<T>;
431
497
  options?: TCreateOptions;
432
498
  }): Promise<T | undefined>;
433
- /**
434
- * Updates an existing record.
435
- *
436
- * This method sends a request to the `baseStore` to update a record. If the update is successful,
437
- * it updates the cache with the modified record and invalidates relevant queries.
438
- *
439
- * @param params An object containing the ID of the record to update, the partial data, and optional update options.
440
- * @returns A promise that resolves to the updated record, or `undefined` if the update fails.
441
- * @throws An error if the update operation fails.
442
- */
443
499
  update(params: {
444
500
  data: Partial<T>;
445
501
  options?: TUpdateOptions;
446
502
  }): Promise<T | undefined>;
447
- /**
448
- * Deletes a record.
449
- *
450
- * This method sends a request to the `baseStore` to delete a record. Upon successful deletion,
451
- * it removes the record from the cache and invalidates relevant queries.
452
- *
453
- * @param params The options for the delete operation, used to identify the record to be deleted.
454
- * @returns A promise that resolves when the deletion is complete.
455
- * @throws An error if the delete operation fails.
456
- */
457
503
  delete(params: TDeleteOptions): Promise<void>;
458
- /**
459
- * Sends a notification event to the base store.
460
- *
461
- * This can be used to trigger server-side events or other custom actions in the `baseStore`.
462
- *
463
- * @param event The `StoreEvent` to be sent.
464
- * @returns A promise that resolves when the notification has been processed.
465
- */
466
504
  notify(event: StoreEvent): Promise<void>;
467
- /**
468
- * Establishes a real-time data stream.
469
- *
470
- * This method delegates to the `baseStore`'s `stream` method to create a persistent connection for
471
- * receiving real-time updates. The provided callback will be invoked when new data is available.
472
- *
473
- * @param options The options for the stream operation.
474
- * @param onStreamChange A callback function that is executed when the stream's data changes.
475
- * @returns A promise that resolves with a function to close the stream.
476
- */
477
- stream(options: TStreamOptions, onStreamChange: () => void): Promise<{
505
+ stream(options: TStreamOptions, onStreamChange: () => void): {
478
506
  stream: () => AsyncIterable<T>;
479
507
  cancel: () => void;
480
508
  status: () => "active" | "cancelled" | "completed";
481
- }>;
482
- /**
483
- * Uploads a file and creates a new record associated with it.
484
- *
485
- * This method handles file uploads through the `baseStore`. After a successful upload, it updates the cache
486
- * with the new record and invalidates relevant queries.
487
- *
488
- * @param params An object containing the file to upload and optional upload options.
489
- * @returns A promise that resolves to the newly created record, or `undefined` if the upload fails.
490
- * @throws An error if the upload operation fails.
491
- */
509
+ };
510
+ subscribe(scope: string, callback: (event: StoreEvent) => void): Promise<() => void>;
492
511
  upload(params: {
493
512
  file: File;
494
513
  options?: TUploadOptions;
495
514
  }): Promise<T | undefined>;
496
- /**
497
- * Forces a refresh of a specific query.
498
- *
499
- * This method bypasses the cache's staleness checks and forces a refetch of the data from the `baseStore`.
500
- * The method is overloaded to support `read`, `list`, and `find` operations.
501
- *
502
- * @param operation The type of operation to refresh ('read', 'list', or 'find').
503
- * @param params The parameters for the operation.
504
- * @returns A promise that resolves to the refreshed data.
505
- */
506
515
  refresh(operation: 'read', params: TReadOptions): Promise<T | undefined>;
507
516
  refresh(operation: 'list', params: TListOptions): Promise<Page<T> | undefined>;
508
517
  refresh(operation: 'find', query: TFindOptions): Promise<Page<T> | undefined>;
509
- /**
510
- * Pre-fetches data for a query and caches it.
511
- *
512
- * This method is used to proactively fetch data that is likely to be needed soon. It fetches the data
513
- * and stores it in the cache, so that subsequent requests for the same data can be served instantly.
514
- * The method is overloaded for `read`, `list`, and `find` operations.
515
- *
516
- * @param operation The type of operation to prefetch ('read', 'list', or 'find').
517
- * @param params The parameters for the operation.
518
- */
519
518
  prefetch(operation: 'read', params: TReadOptions): void;
520
519
  prefetch(operation: 'list', params: TListOptions): void;
521
520
  prefetch(operation: 'find', params: TFindOptions): void;
522
- /**
523
- * Invalidates the cached data for a specific query.
524
- *
525
- * This marks the query's data as stale, forcing a refetch the next time it's accessed. This is useful
526
- * when you know the data has changed on the server, but the change was not triggered by a mutation
527
- * through this store.
528
- *
529
- * @param operation The type of operation to invalidate ('read', 'list', or 'find').
530
- * @param params The parameters for the operation.
531
- * @returns A promise that resolves when the invalidation is complete.
532
- */
533
521
  invalidate(operation: string, params: any): Promise<void>;
534
- /**
535
- * Invalidates all active queries in the store.
536
- *
537
- * This method marks all currently active queries as stale, forcing them to be refetched the next time
538
- * they are accessed. This is a more aggressive approach to cache invalidation.
539
- *
540
- * @returns A promise that resolves when all invalidations are complete.
541
- */
542
522
  invalidateAll(): Promise<void>;
543
- /**
544
- * Retrieves statistics about the store and its cache.
545
- *
546
- * This method returns an object containing statistics from the underlying cache, plus the number of
547
- * active subscriptions in the reactive store.
548
- *
549
- * @returns An object with cache statistics and the number of active subscriptions.
550
- */
551
523
  getStats(): {
552
524
  activeSubscriptions: number;
553
525
  size: number;
@@ -564,12 +536,6 @@ declare class ReactiveRemoteStore<T extends StoreRecord, TFindOptions = Record<s
564
536
  error?: boolean;
565
537
  }>;
566
538
  };
567
- /**
568
- * Cleans up all resources used by the store.
569
- *
570
- * This method should be called when the store is no longer needed. It unsubscribes from all cache events,
571
- * clears all query subscriptions, and unsubscribes from the base store's events.
572
- */
573
539
  destroy(): void;
574
540
  }
575
541
 
@@ -580,4 +546,4 @@ declare class ReactiveRemoteStore<T extends StoreRecord, TFindOptions = Record<s
580
546
  */
581
547
  declare function hash(data: any): string;
582
548
 
583
- 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, hash };
549
+ export { type ActiveQuery, type BaseStore, type Correlator, type MutationOperation, type Page, type PagedQueryResult, type PaginationInfo, type QueryResult, type QuerySubscription, ReactiveRemoteStore, type ResourceName, StoreError, type StoreErrorDetails, type StoreEvent, type StoreEventCorrelator, type StoreRecord, type StoreResult, hash };
package/index.d.ts CHANGED
@@ -1,5 +1,198 @@
1
- import { QueryCache, CacheMetrics } from '../cache/index.js';
2
- import '../types-DUZGkNEB.js';
1
+ interface SimplePersistence<T> {
2
+ /**
3
+ * Persists data to storage.
4
+ *
5
+ * @param id The **unique identifier of the *consumer instance*** making the change. This is NOT the ID of the data (`T`) itself.
6
+ * Think of it as the ID of the specific browser tab, component, or module that's currently interacting with the persistence layer.
7
+ * It should typically be a **UUID** generated once at the consumer instance's instantiation.
8
+ * This `id` is crucial for the `subscribe` method, helping to differentiate updates originating from the current instance versus other instances/tabs, thereby preventing self-triggered notification loops.
9
+ * @param state The state (of type T) to persist. This state is generally considered the **global or shared state** that all instances interact with.
10
+ * @returns `true` if the operation was successful, `false` if an error occurred. For asynchronous implementations (like `IndexedDBPersistence`), this returns a `Promise<boolean>`.
11
+ */
12
+ set(id: string, state: T): boolean | Promise<boolean>;
13
+ /**
14
+ * Retrieves the global persisted data from storage.
15
+ *
16
+ * @returns The retrieved state of type `T`, or `null` if no data is found or if an error occurs during retrieval/parsing.
17
+ * For asynchronous implementations, this returns a `Promise<T | null>`.
18
+ */
19
+ get(): (T | null) | (Promise<T | null>);
20
+ /**
21
+ * Subscribes to changes in the global persisted data that originate from *other* instances of your application (e.g., other tabs or independent components using the same persistence layer).
22
+ *
23
+ * @param id The **unique identifier of the *consumer instance* subscribing**. This allows the persistence implementation to filter out notifications that were initiated by the subscribing instance itself.
24
+ * @param callback The function to call when the global persisted data changes from *another* source. The new state (`T`) is passed as an argument to this callback.
25
+ * @returns A function that, when called, will unsubscribe the provided callback from future updates. Call this when your component or instance is no longer active to prevent memory leaks.
26
+ */
27
+ subscribe(id: string, callback: (state: T) => void): () => void;
28
+ /**
29
+ * Clears (removes) the entire global persisted data from storage.
30
+ *
31
+ * @returns `true` if the operation was successful, `false` if an error occurred. For asynchronous implementations, this returns a `Promise<boolean>`.
32
+ */
33
+ clear(): boolean | Promise<boolean>;
34
+ /**
35
+ * Returns metadata about the persistence layer.
36
+ *
37
+ * This is useful for distinguishing between multiple apps running on the same host
38
+ * (e.g., several apps served at `localhost:3000` that share the same storage key).
39
+ *
40
+ * @returns An object containing:
41
+ * - `version`: The semantic version string of the persistence schema or application.
42
+ * - `id`: A unique identifier for the application using this persistence instance.
43
+ */
44
+ stats(): {
45
+ version: string;
46
+ id: string;
47
+ };
48
+ }
49
+
50
+ interface CacheOptions {
51
+ staleTime?: number;
52
+ cacheTime?: number;
53
+ retryAttempts?: number;
54
+ retryDelay?: number;
55
+ maxSize?: number;
56
+ enableMetrics?: boolean;
57
+ persistence?: SimplePersistence<SerializableCacheState>;
58
+ persistenceId?: string;
59
+ serializeValue?: (value: any) => any;
60
+ deserializeValue?: (value: any) => any;
61
+ persistenceDebounceTime?: number;
62
+ }
63
+ interface CacheMetrics {
64
+ hits: number;
65
+ misses: number;
66
+ fetches: number;
67
+ errors: number;
68
+ evictions: number;
69
+ staleHits: number;
70
+ }
71
+ interface SerializableCacheEntry {
72
+ data: any;
73
+ lastUpdated: number;
74
+ lastAccessed: number;
75
+ accessCount: number;
76
+ error?: {
77
+ name: string;
78
+ message: string;
79
+ stack?: string;
80
+ };
81
+ }
82
+ type SerializableCacheState = Array<[string, SerializableCacheEntry]>;
83
+ type CacheEventBase<Type extends string, Payload = {}> = {
84
+ type: Type;
85
+ key: string;
86
+ timestamp: number;
87
+ } & Payload;
88
+ type CacheReadHitEvent<T = any> = CacheEventBase<'cache:read:hit', {
89
+ data: T;
90
+ isStale: boolean;
91
+ }>;
92
+ type CacheReadMissEvent = CacheEventBase<'cache:read:miss'>;
93
+ type CacheFetchStartEvent = CacheEventBase<'cache:fetch:start', {
94
+ attempt: number;
95
+ }>;
96
+ type CacheFetchSuccessEvent<T = any> = CacheEventBase<'cache:fetch:success', {
97
+ data: T;
98
+ }>;
99
+ type CacheFetchErrorEvent = CacheEventBase<'cache:fetch:error', {
100
+ error: Error;
101
+ attempt: number;
102
+ }>;
103
+ type CacheDataEvictEvent = CacheEventBase<'cache:data:evict', {
104
+ reason?: string;
105
+ }>;
106
+ type CacheDataInvalidateEvent = CacheEventBase<'cache:data:invalidate'>;
107
+ type CacheDataSetEvent<T = any> = CacheEventBase<'cache:data:set', {
108
+ newData: T;
109
+ oldData?: T;
110
+ }>;
111
+ type CachePersistenceLoadSuccessEvent = CacheEventBase<'cache:persistence:load:success', {
112
+ message?: string;
113
+ }>;
114
+ type CachePersistenceLoadErrorEvent = CacheEventBase<'cache:persistence:load:error', {
115
+ message?: string;
116
+ error?: any;
117
+ }>;
118
+ type CachePersistenceSaveSuccessEvent = CacheEventBase<'cache:persistence:save:success'>;
119
+ type CachePersistenceSaveErrorEvent = CacheEventBase<'cache:persistence:save:error', {
120
+ message?: string;
121
+ error?: any;
122
+ }>;
123
+ type CachePersistenceClearSuccessEvent = CacheEventBase<'cache:persistence:clear:success'>;
124
+ type CachePersistenceClearErrorEvent = CacheEventBase<'cache:persistence:clear:error', {
125
+ message?: string;
126
+ error?: any;
127
+ }>;
128
+ type CachePersistenceSyncEvent = CacheEventBase<'cache:persistence:sync', {
129
+ message?: string;
130
+ }>;
131
+ type CacheEvent = CacheReadHitEvent | CacheReadMissEvent | CacheFetchStartEvent | CacheFetchSuccessEvent | CacheFetchErrorEvent | CacheDataEvictEvent | CacheDataInvalidateEvent | CacheDataSetEvent | CachePersistenceLoadSuccessEvent | CachePersistenceLoadErrorEvent | CachePersistenceSaveSuccessEvent | CachePersistenceSaveErrorEvent | CachePersistenceClearSuccessEvent | CachePersistenceClearErrorEvent | CachePersistenceSyncEvent;
132
+ type CacheEventType = CacheEvent['type'];
133
+
134
+ declare class QueryCache {
135
+ private cache;
136
+ private queries;
137
+ private fetching;
138
+ private readonly defaultOptions;
139
+ private metrics;
140
+ private eventBus;
141
+ private gcTimer?;
142
+ private readonly persistenceId;
143
+ private persistenceUnsubscribe?;
144
+ private persistenceDebounceTimer?;
145
+ private isHandlingRemoteUpdate;
146
+ constructor(defaultOptions?: CacheOptions);
147
+ private initializePersistence;
148
+ private serializeCache;
149
+ private deserializeAndLoadCache;
150
+ private schedulePersistState;
151
+ private handleRemoteStateChange;
152
+ registerQuery<T>(key: string, fetchFunction: () => Promise<T>, options?: CacheOptions): void;
153
+ get<T>(key: string, options?: {
154
+ waitForFresh?: boolean;
155
+ throwOnError?: boolean;
156
+ }): Promise<T | undefined>;
157
+ peek<T>(key: string): T | undefined;
158
+ has(key: string): boolean;
159
+ private fetch;
160
+ private fetchAndWait;
161
+ private performFetchWithRetry;
162
+ private isStale;
163
+ invalidate(key: string, refetch?: boolean): Promise<void>;
164
+ invalidatePattern(pattern: RegExp, refetch?: boolean): Promise<void>;
165
+ prefetch(key: string): Promise<void>;
166
+ refresh<T>(key: string): Promise<T | undefined>;
167
+ setData<T>(key: string, data: T): void;
168
+ remove(key: string): boolean;
169
+ private enforceSizeLimit;
170
+ private startGarbageCollection;
171
+ garbageCollect(): number;
172
+ getStats(): {
173
+ size: number;
174
+ metrics: CacheMetrics;
175
+ hitRate: number;
176
+ staleHitRate: number;
177
+ entries: Array<{
178
+ key: string;
179
+ lastAccessed: number;
180
+ lastUpdated: number;
181
+ accessCount: number;
182
+ isStale: boolean;
183
+ isLoading?: boolean;
184
+ error?: boolean;
185
+ }>;
186
+ };
187
+ on<EType extends CacheEventType>(event: EType, listener: (ev: Extract<CacheEvent, {
188
+ type: EType;
189
+ }>) => void): () => void;
190
+ private emitEvent;
191
+ private updateMetrics;
192
+ private delay;
193
+ clear(): Promise<void>;
194
+ destroy(): void;
195
+ }
3
196
 
4
197
  interface StoreErrorDetails {
5
198
  code: string;
@@ -205,7 +398,9 @@ interface PagedQueryResult<T> {
205
398
  /** Function to fetch a specific page of data.
206
399
  * @param page The page number to fetch.
207
400
  */
208
- fetch: (page: number) => Promise<void>;
401
+ navigate: (page: number) => Promise<void>;
402
+ refresh: (delay?: number) => Promise<void>;
403
+ changeParams: (setter: (params: any) => any) => Promise<void>;
209
404
  }
210
405
  /**
211
406
  * Represents a basic record in the remote store.
@@ -230,26 +425,6 @@ type PaginationInfo = {
230
425
  * Represents the name of a resource in the store, e.g., 'todos', 'users'.
231
426
  */
232
427
  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
428
  /**
254
429
  * Internal interface representing an active subscription to a query.
255
430
  * @template T The type of the data being queried.
@@ -271,283 +446,80 @@ interface QuerySubscription<T> {
271
446
  result: QueryResult<T> | PagedQueryResult<T>;
272
447
  }
273
448
 
449
+ /**
450
+ * Represents the result of a store query with stable React integration.
451
+ * @template T The type of the result data.
452
+ */
453
+ interface StoreResult<T extends Record<string, any> = Record<string, any>, V = QueryResult<T> | PagedQueryResult<T>> {
454
+ /** Function that returns the current query state */
455
+ value: () => V;
456
+ /** Function to subscribe to state changes */
457
+ onValueChange: (callback: () => void) => () => void;
458
+ }
274
459
  /**
275
460
  * A reactive remote store that provides cached and observable access to data
276
461
  * from a `BaseStore`. It handles caching, invalidation, and real-time updates
277
462
  * via server-sent events (SSE).
278
- *
279
- * @template T The type of the records managed by the store, extending Record.
280
- * @template TFindOptions Options for the find operation.
281
- * @template TReadOptions Options for the read operation.
282
- * @template TListOptions Options for the list operation.
283
- * @template TDeleteOptions Options for the delete operation.
284
- * @template TUpdateOptions Options for the update operation.
285
- * @template TCreateOptions Options for the create operation.
286
- * @template TUploadOptions Options for the upload operation.
287
- * @template TStreamOptions Options for the stream operation.
288
463
  */
289
464
  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>> {
290
465
  private cache;
291
466
  private baseStore;
292
467
  private correlator?;
293
468
  private storeEventCorrelator?;
294
- private querySubscriptions;
469
+ private queryStates;
470
+ private stableResults;
471
+ private stablePaginationMethods;
472
+ private errorCache;
295
473
  private keyCache;
296
474
  private unsubscribeFromBaseStore;
297
- /**
298
- * Creates an instance of ReactiveRemoteStore.
299
- * @param cache The QueryCache instance to use for caching data.
300
- * @param baseStore The underlying BaseStore for actual data fetching and mutations.
301
- * @param correlator Optional function to determine which queries to invalidate after a mutation.
302
- * @param storeEventCorrelator Optional function to determine which queries to invalidate after a store event.
303
- */
475
+ private cacheEventCleanups;
304
476
  constructor(cache: QueryCache, baseStore: BaseStore<T, TFindOptions, TReadOptions, TListOptions, TDeleteOptions, TUpdateOptions, TCreateOptions, TUploadOptions, TStreamOptions>, correlator?: Correlator | undefined, storeEventCorrelator?: StoreEventCorrelator | undefined);
305
- /**
306
- * Creates a new query subscription and registers it with the cache.
307
- * @template T The type of the data for the subscription.
308
- * @param queryKey The unique key for the query.
309
- * @param operation The operation type (e.g., 'read', 'list').
310
- * @param params The parameters for the query.
311
- * @param selector A function that returns the current query result.
312
- * @param notificationCondition A function to determine if a cache event should trigger a notification.
313
- * @returns The created QuerySubscription.
314
- */
315
- private createSubscription;
316
- /**
317
- * Retrieves an existing subscription or creates a new one if it doesn't exist.
318
- * @template T The type of the data for the subscription.
319
- * @param queryKey The unique key for the query.
320
- * @param operation The operation type (e.g., 'read', 'list').
321
- * @param params The parameters for the query.
322
- * @param selector A function that returns the current query result.
323
- * @param notificationCondition A function to determine if a cache event should trigger a notification.
324
- * @returns The existing or newly created QuerySubscription.
325
- */
326
- private getOrCreateSubscription;
327
- /**
328
- * Builds a unique cache key for a given operation and its parameters.
329
- * Uses a WeakMap for caching keys of object parameters to avoid re-hashing.
330
- * @param operation The name of the operation (e.g., 'read', 'list').
331
- * @param params The parameters for the operation.
332
- * @returns A unique string key for the operation and parameters.
333
- */
477
+ private setupCacheEventListeners;
478
+ private handleCacheEvent;
334
479
  private buildKey;
335
- /**
336
- * Creates a selector function for a single record query.
337
- * @template T The type of the data being queried.
338
- * @param queryKey The unique key for the query.
339
- * @returns A function that returns the current QueryResult for the given key.
340
- */
341
- private createSelector;
342
- /**
343
- * Reads a single record from the store.
344
- * @param params The read options.
345
- * @returns An object containing the current QueryResult and a function to subscribe to changes.
346
- */
347
- /**
348
- * Retrieves a single record reactively.
349
- *
350
- * This method provides a reactive query result for a single record. The data is fetched from the cache if available,
351
- * otherwise it's fetched from the underlying `baseStore`. The method returns an object containing the current
352
- * query result and a function to subscribe to future updates.
353
- *
354
- * @param params The options for the read operation, used to identify the record.
355
- * @returns A `ReactiveQueryResult` object containing the reactive value and an `onValueChange` subscription function.
356
- */
357
- read(params: TReadOptions): ReactiveQueryResult<T>;
358
- /**
359
- * Creates a selector function for a paginated query (list or find).
360
- * @template TParams The type of the parameters for the query.
361
- * @param baseQueryKey The base unique key for the query.
362
- * @param baseParams The base parameters for the query.
363
- * @param fetchFn The function to call to fetch a page of data.
364
- * @returns A function that returns the current PagedQueryResult.
365
- */
366
- private createPagedSelector;
367
- /**
368
- * Sets up a paginated query (list or find) with caching and reactivity.
369
- * @template TParams The type of the parameters for the query.
370
- * @param type The type of the paginated query ('list' or 'find').
371
- * @param params The parameters for the query.
372
- * @param fetchFn The function to call to fetch a page of data.
373
- * @returns An object containing the current PagedQueryResult and a function to subscribe to changes.
374
- */
480
+ private getOrCreateError;
481
+ private getOrCreateStoreError;
482
+ private computeResult;
483
+ private scheduleBackgroundFetch;
484
+ private getCurrentPageForQuery;
485
+ private getOrCreatePaginationMethods;
486
+ private computeResultForParams;
487
+ hasActiveQuery(operation: string, params: any): boolean;
488
+ getActiveQuery<TResult extends Record<string, unknown> = T>(operation: string, params: any): StoreResult<TResult> | undefined;
489
+ read(params: TReadOptions): StoreResult<T, QueryResult<T>>;
490
+ list(params: TListOptions): StoreResult<T, PagedQueryResult<T>>;
491
+ find(params: TFindOptions): StoreResult<T, PagedQueryResult<T>>;
375
492
  private setupPagedQuery;
376
- /**
377
- * Lists records from the store with pagination and reactivity.
378
- * @param params The list options.
379
- * @returns An object containing the current PagedQueryResult and a function to subscribe to changes.
380
- */
381
- /**
382
- * Retrieves a paginated list of records reactively.
383
- *
384
- * This method provides a reactive query result for a list of records. It supports pagination and automatically
385
- * fetches data from the cache or the `baseStore`. The result includes methods for navigating to the next,
386
- * previous, or a specific page.
387
- *
388
- * @param params The options for the list operation, including pagination details.
389
- * @returns A `ReactivePagedQueryResult` object containing the reactive paged value and an `onValueChange` subscription function.
390
- */
391
- list(params: TListOptions): ReactivePagedQueryResult<T>;
392
- /**
393
- * Finds records from the store with pagination and reactivity.
394
- * @param params The find options.
395
- * @returns An object containing the current PagedQueryResult and a function to subscribe to changes.
396
- */
397
- /**
398
- * Finds and retrieves a paginated list of records reactively based on a query.
399
- *
400
- * Similar to `list`, this method provides a reactive query result for a set of records that match the given
401
- * find options. It supports pagination and provides methods for navigating through the pages.
402
- *
403
- * @param params The options for the find operation, used to query for specific records.
404
- * @returns A `ReactivePagedQueryResult` object containing the reactive paged value and an `onValueChange` subscription function.
405
- */
406
- find(params: TFindOptions): ReactivePagedQueryResult<T>;
407
- /**
408
- * Invalidates relevant queries in the cache based on a mutation operation.
409
- * If a correlator is provided, it will be used to determine which queries to invalidate.
410
- * Otherwise, it invalidates all 'list' and 'find' queries.
411
- * @param mutation An object describing the mutation operation and its parameters.
412
- */
413
493
  private invalidateQueries;
414
- /**
415
- * Handles incoming store events, invalidating relevant queries if a `storeEventCorrelator` is provided.
416
- * @param event The StoreEvent received.
417
- */
418
494
  private handleStoreEvent;
419
- /**
420
- * Creates a new record.
421
- *
422
- * This method sends a request to the `baseStore` to create a new record. Upon successful creation,
423
- * it updates the cache with the new record and invalidates relevant queries to ensure data consistency.
424
- *
425
- * @param params An object containing the data for the new record and optional create options.
426
- * @returns A promise that resolves to the newly created record, or `undefined` if the creation fails.
427
- * @throws An error if the create operation fails.
428
- */
429
495
  create(params: {
430
496
  data: Partial<T>;
431
497
  options?: TCreateOptions;
432
498
  }): Promise<T | undefined>;
433
- /**
434
- * Updates an existing record.
435
- *
436
- * This method sends a request to the `baseStore` to update a record. If the update is successful,
437
- * it updates the cache with the modified record and invalidates relevant queries.
438
- *
439
- * @param params An object containing the ID of the record to update, the partial data, and optional update options.
440
- * @returns A promise that resolves to the updated record, or `undefined` if the update fails.
441
- * @throws An error if the update operation fails.
442
- */
443
499
  update(params: {
444
500
  data: Partial<T>;
445
501
  options?: TUpdateOptions;
446
502
  }): Promise<T | undefined>;
447
- /**
448
- * Deletes a record.
449
- *
450
- * This method sends a request to the `baseStore` to delete a record. Upon successful deletion,
451
- * it removes the record from the cache and invalidates relevant queries.
452
- *
453
- * @param params The options for the delete operation, used to identify the record to be deleted.
454
- * @returns A promise that resolves when the deletion is complete.
455
- * @throws An error if the delete operation fails.
456
- */
457
503
  delete(params: TDeleteOptions): Promise<void>;
458
- /**
459
- * Sends a notification event to the base store.
460
- *
461
- * This can be used to trigger server-side events or other custom actions in the `baseStore`.
462
- *
463
- * @param event The `StoreEvent` to be sent.
464
- * @returns A promise that resolves when the notification has been processed.
465
- */
466
504
  notify(event: StoreEvent): Promise<void>;
467
- /**
468
- * Establishes a real-time data stream.
469
- *
470
- * This method delegates to the `baseStore`'s `stream` method to create a persistent connection for
471
- * receiving real-time updates. The provided callback will be invoked when new data is available.
472
- *
473
- * @param options The options for the stream operation.
474
- * @param onStreamChange A callback function that is executed when the stream's data changes.
475
- * @returns A promise that resolves with a function to close the stream.
476
- */
477
- stream(options: TStreamOptions, onStreamChange: () => void): Promise<{
505
+ stream(options: TStreamOptions, onStreamChange: () => void): {
478
506
  stream: () => AsyncIterable<T>;
479
507
  cancel: () => void;
480
508
  status: () => "active" | "cancelled" | "completed";
481
- }>;
482
- /**
483
- * Uploads a file and creates a new record associated with it.
484
- *
485
- * This method handles file uploads through the `baseStore`. After a successful upload, it updates the cache
486
- * with the new record and invalidates relevant queries.
487
- *
488
- * @param params An object containing the file to upload and optional upload options.
489
- * @returns A promise that resolves to the newly created record, or `undefined` if the upload fails.
490
- * @throws An error if the upload operation fails.
491
- */
509
+ };
510
+ subscribe(scope: string, callback: (event: StoreEvent) => void): Promise<() => void>;
492
511
  upload(params: {
493
512
  file: File;
494
513
  options?: TUploadOptions;
495
514
  }): Promise<T | undefined>;
496
- /**
497
- * Forces a refresh of a specific query.
498
- *
499
- * This method bypasses the cache's staleness checks and forces a refetch of the data from the `baseStore`.
500
- * The method is overloaded to support `read`, `list`, and `find` operations.
501
- *
502
- * @param operation The type of operation to refresh ('read', 'list', or 'find').
503
- * @param params The parameters for the operation.
504
- * @returns A promise that resolves to the refreshed data.
505
- */
506
515
  refresh(operation: 'read', params: TReadOptions): Promise<T | undefined>;
507
516
  refresh(operation: 'list', params: TListOptions): Promise<Page<T> | undefined>;
508
517
  refresh(operation: 'find', query: TFindOptions): Promise<Page<T> | undefined>;
509
- /**
510
- * Pre-fetches data for a query and caches it.
511
- *
512
- * This method is used to proactively fetch data that is likely to be needed soon. It fetches the data
513
- * and stores it in the cache, so that subsequent requests for the same data can be served instantly.
514
- * The method is overloaded for `read`, `list`, and `find` operations.
515
- *
516
- * @param operation The type of operation to prefetch ('read', 'list', or 'find').
517
- * @param params The parameters for the operation.
518
- */
519
518
  prefetch(operation: 'read', params: TReadOptions): void;
520
519
  prefetch(operation: 'list', params: TListOptions): void;
521
520
  prefetch(operation: 'find', params: TFindOptions): void;
522
- /**
523
- * Invalidates the cached data for a specific query.
524
- *
525
- * This marks the query's data as stale, forcing a refetch the next time it's accessed. This is useful
526
- * when you know the data has changed on the server, but the change was not triggered by a mutation
527
- * through this store.
528
- *
529
- * @param operation The type of operation to invalidate ('read', 'list', or 'find').
530
- * @param params The parameters for the operation.
531
- * @returns A promise that resolves when the invalidation is complete.
532
- */
533
521
  invalidate(operation: string, params: any): Promise<void>;
534
- /**
535
- * Invalidates all active queries in the store.
536
- *
537
- * This method marks all currently active queries as stale, forcing them to be refetched the next time
538
- * they are accessed. This is a more aggressive approach to cache invalidation.
539
- *
540
- * @returns A promise that resolves when all invalidations are complete.
541
- */
542
522
  invalidateAll(): Promise<void>;
543
- /**
544
- * Retrieves statistics about the store and its cache.
545
- *
546
- * This method returns an object containing statistics from the underlying cache, plus the number of
547
- * active subscriptions in the reactive store.
548
- *
549
- * @returns An object with cache statistics and the number of active subscriptions.
550
- */
551
523
  getStats(): {
552
524
  activeSubscriptions: number;
553
525
  size: number;
@@ -564,12 +536,6 @@ declare class ReactiveRemoteStore<T extends StoreRecord, TFindOptions = Record<s
564
536
  error?: boolean;
565
537
  }>;
566
538
  };
567
- /**
568
- * Cleans up all resources used by the store.
569
- *
570
- * This method should be called when the store is no longer needed. It unsubscribes from all cache events,
571
- * clears all query subscriptions, and unsubscribes from the base store's events.
572
- */
573
539
  destroy(): void;
574
540
  }
575
541
 
@@ -580,4 +546,4 @@ declare class ReactiveRemoteStore<T extends StoreRecord, TFindOptions = Record<s
580
546
  */
581
547
  declare function hash(data: any): string;
582
548
 
583
- 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, hash };
549
+ export { type ActiveQuery, type BaseStore, type Correlator, type MutationOperation, type Page, type PagedQueryResult, type PaginationInfo, type QueryResult, type QuerySubscription, ReactiveRemoteStore, type ResourceName, StoreError, type StoreErrorDetails, type StoreEvent, type StoreEventCorrelator, type StoreRecord, type StoreResult, hash };
package/index.js CHANGED
@@ -1 +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={operation:t,params:r,subscribers:i},c=()=>{const e=a();var r,s;("read"===t?(r=o.result,s=e,r&&s?r.data===s.data&&r.loading===s.loading&&r.stale===s.stale&&(r.error===s.error||r.error?.message===s.error?.message):r===s):function(e,t){return e&&t?e.page===t.page&&e.loading===t.loading&&e.stale===t.stale&&(e.error===t.error||e.error?.message===t.error?.message)&&e.hasNext===t.hasNext&&e.hasPrevious===t.hasPrevious:e===t}(o.result,e))||(o.result=e,i.forEach((e=>{try{e()}catch(e){console.error("ReactiveRemoteStore: Subscriber callback error:",e)}})))};return o.unsubscribeCallbacks=[this.cache.on("cache:fetch:success",(e=>{s(e.key)&&c()})),this.cache.on("cache:fetch:error",(e=>{s(e.key)&&c()})),this.cache.on("cache:data:set",(e=>{s(e.key)&&c()})),this.cache.on("cache:data:invalidate",(e=>{s(e.key)&&c()})),this.cache.on("cache:read:hit",(e=>{s(e.key)&&e.isStale&&c()}))],o.cachedSubscribe=t=>(i.add(t),()=>{i.delete(t),0===i.size&&(o.unsubscribeCallbacks.forEach((e=>e())),this.querySubscriptions.delete(e))}),o.cachedSelector=()=>o.result,o.result=a(),this.querySubscriptions.set(e,o),o}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);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)}));return{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){let s=r;const i=this,o={next:async()=>{const e=s.page||1,r=i.buildKey(t.split(":")[0],s),o=i.cache.peek(r);if(o&&e<o.page.pages){const r={...s,page:e+1},o=i.buildKey(t.split(":")[0],r);s=r,i.querySubscriptions.has(o)||i.cache.registerQuery(o,(()=>a(r))),await i.cache.get(o,{waitForFresh:!0})}},previous:async()=>{const e=s.page||1;if(e>1){const r={...s,page:e-1},o=i.buildKey(t.split(":")[0],r);s=r,i.querySubscriptions.has(o)||i.cache.registerQuery(o,(()=>a(r))),await i.cache.get(o,{waitForFresh:!0})}},fetch:async e=>{const r={...s,page:e},o=i.buildKey(t.split(":")[0],r);s=r,i.querySubscriptions.has(o)||i.cache.registerQuery(o,(()=>a(r))),await i.cache.get(o,{waitForFresh:!0})}};return()=>{const r=this.buildKey(t.split(":")[0],s),a=this.cache.getStats().entries.find((e=>e.key===r)),i=this.cache.peek(r);a?.isStale&&this.cache.get(r,{waitForFresh:!1}).catch((e=>{console.warn(`ReactiveRemoteStore: Background refetch failed for ${r}:`,e)})),void 0!==i&&this.cache.has(r)||this.cache.get(r,{waitForFresh:!1}).catch((e=>{console.warn(`ReactiveRemoteStore: Background fetch failed for ${r}:`,e)}));const c=s.page||1;return{page:i,loading:a?.isLoading??void 0===i,error:a?.error?e.fromError(new Error("Query failed"),"query"):void 0,stale:a?.isStale??!0,updated:a?.lastUpdated??0,hasNext:!!i&&c<i.page.pages,hasPrevious:c>1,...o}}}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),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,exports.hash=r;
1
+ "use strict";var e=require("./store"),t=require("./hash"),r=require("./error"),o=require("./types");Object.keys(e).forEach((function(t){"default"===t||Object.prototype.hasOwnProperty.call(exports,t)||Object.defineProperty(exports,t,{enumerable:!0,get:function(){return e[t]}})})),Object.keys(t).forEach((function(e){"default"===e||Object.prototype.hasOwnProperty.call(exports,e)||Object.defineProperty(exports,e,{enumerable:!0,get:function(){return t[e]}})})),Object.keys(r).forEach((function(e){"default"===e||Object.prototype.hasOwnProperty.call(exports,e)||Object.defineProperty(exports,e,{enumerable:!0,get:function(){return r[e]}})})),Object.keys(o).forEach((function(e){"default"===e||Object.prototype.hasOwnProperty.call(exports,e)||Object.defineProperty(exports,e,{enumerable:!0,get:function(){return o[e]}})}));
package/index.mjs CHANGED
@@ -1 +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={operation:t,params:r,subscribers:i},c=()=>{const e=a();var r,s;("read"===t?(r=o.result,s=e,r&&s?r.data===s.data&&r.loading===s.loading&&r.stale===s.stale&&(r.error===s.error||r.error?.message===s.error?.message):r===s):function(e,t){return e&&t?e.page===t.page&&e.loading===t.loading&&e.stale===t.stale&&(e.error===t.error||e.error?.message===t.error?.message)&&e.hasNext===t.hasNext&&e.hasPrevious===t.hasPrevious:e===t}(o.result,e))||(o.result=e,i.forEach((e=>{try{e()}catch(e){console.error("ReactiveRemoteStore: Subscriber callback error:",e)}})))};return o.unsubscribeCallbacks=[this.cache.on("cache:fetch:success",(e=>{s(e.key)&&c()})),this.cache.on("cache:fetch:error",(e=>{s(e.key)&&c()})),this.cache.on("cache:data:set",(e=>{s(e.key)&&c()})),this.cache.on("cache:data:invalidate",(e=>{s(e.key)&&c()})),this.cache.on("cache:read:hit",(e=>{s(e.key)&&e.isStale&&c()}))],o.cachedSubscribe=t=>(i.add(t),()=>{i.delete(t),0===i.size&&(o.unsubscribeCallbacks.forEach((e=>e())),this.querySubscriptions.delete(e))}),o.cachedSelector=()=>o.result,o.result=a(),this.querySubscriptions.set(e,o),o}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);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)}));return{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){let s=r;const i=this,o={next:async()=>{const e=s.page||1,r=i.buildKey(t.split(":")[0],s),o=i.cache.peek(r);if(o&&e<o.page.pages){const r={...s,page:e+1},o=i.buildKey(t.split(":")[0],r);s=r,i.querySubscriptions.has(o)||i.cache.registerQuery(o,(()=>a(r))),await i.cache.get(o,{waitForFresh:!0})}},previous:async()=>{const e=s.page||1;if(e>1){const r={...s,page:e-1},o=i.buildKey(t.split(":")[0],r);s=r,i.querySubscriptions.has(o)||i.cache.registerQuery(o,(()=>a(r))),await i.cache.get(o,{waitForFresh:!0})}},fetch:async e=>{const r={...s,page:e},o=i.buildKey(t.split(":")[0],r);s=r,i.querySubscriptions.has(o)||i.cache.registerQuery(o,(()=>a(r))),await i.cache.get(o,{waitForFresh:!0})}};return()=>{const r=this.buildKey(t.split(":")[0],s),a=this.cache.getStats().entries.find((e=>e.key===r)),i=this.cache.peek(r);a?.isStale&&this.cache.get(r,{waitForFresh:!1}).catch((e=>{console.warn(`ReactiveRemoteStore: Background refetch failed for ${r}:`,e)})),void 0!==i&&this.cache.has(r)||this.cache.get(r,{waitForFresh:!1}).catch((e=>{console.warn(`ReactiveRemoteStore: Background fetch failed for ${r}:`,e)}));const c=s.page||1;return{page:i,loading:a?.isLoading??void 0===i,error:a?.error?e.fromError(new Error("Query failed"),"query"):void 0,stale:a?.isStale??!0,updated:a?.lastUpdated??0,hasNext:!!i&&c<i.page.pages,hasPrevious:c>1,...o}}}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),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,r as hash};
1
+ export*from"./store";export*from"./hash";export*from"./error";export*from"./types";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asaidimu/utils-remote-store",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "A reactive store for remote data, built on top of @asaidimu/utils-cache",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",
@@ -9,7 +9,7 @@
9
9
  "./*"
10
10
  ],
11
11
  "dependencies": {
12
- "@asaidimu/utils-cache": "2.1.0",
12
+ "@asaidimu/utils-cache": "2.1.1",
13
13
  "eventsource": "^4.0.0"
14
14
  },
15
15
  "exports": {