lazypock 0.2.0 → 0.4.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/dist/index.d.ts CHANGED
@@ -50,6 +50,21 @@ interface RequestOptions {
50
50
  signal?: AbortSignal;
51
51
  /** Custom fetch implementation (for RN or test mocking) */
52
52
  fetch?: typeof globalThis.fetch;
53
+ /**
54
+ * Cache control for this request (see {@link CacheRequestOptions}).
55
+ * Resolved against the client's global cache config when unset.
56
+ */
57
+ cache?: boolean | number | {
58
+ ttl?: number;
59
+ key?: string;
60
+ };
61
+ /** Alias of `cache: <ms>` — cache this GET for `ttl` milliseconds. */
62
+ ttl?: number;
63
+ /**
64
+ * Extra cache namespaces to invalidate when this mutation succeeds.
65
+ * The current collection is always invalidated automatically.
66
+ */
67
+ invalidate?: string[];
53
68
  /**
54
69
  * Request identifier used by the auto-cancellation mechanism.
55
70
  *
@@ -72,6 +87,17 @@ interface RequestOptions {
72
87
  * Alias of `requestKey` (PocketBase `$cancelKey` compat).
73
88
  */
74
89
  cancelKey?: string;
90
+ /**
91
+ * Coalesce concurrent identical requests (same `requestKey`) onto a single
92
+ * in-flight promise instead of aborting the earlier one.
93
+ *
94
+ * When enabled, a request arriving while another with the same key is still
95
+ * pending awaits the same result — no duplicate network request, and the
96
+ * caller of the first request never sees an abort rejection.
97
+ *
98
+ * @default false (auto-cancellation aborts the earlier duplicate)
99
+ */
100
+ singleFlight?: boolean;
75
101
  }
76
102
  declare class ApiError extends Error {
77
103
  readonly data: unknown;
@@ -158,16 +184,160 @@ declare class AuthStore {
158
184
  private notify;
159
185
  }
160
186
 
187
+ /** Options for enabling/customising the client's query cache. */
188
+ interface CacheConfig {
189
+ /**
190
+ * Master switch. When `true`, readable GET requests are cached with the
191
+ * default TTL unless a request opts out via `{ cache: false }`.
192
+ *
193
+ * When `false` (default), caching is disabled unless a request opts in
194
+ * via `{ cache: true }` or `{ ttl: <ms> }`. Opt-in works regardless.
195
+ */
196
+ enabled?: boolean;
197
+ /**
198
+ * Default time-to-live for cached entries, in milliseconds.
199
+ * @default 60_000 (1 minute)
200
+ */
201
+ defaultTTL?: number;
202
+ /**
203
+ * Optional persistence backend (same interface as AuthStore's storage).
204
+ * Defaults to an in-memory Map — swap for `localStorage` / `AsyncStorage`
205
+ * to keep the cache across page reloads / app restarts.
206
+ */
207
+ store?: StorageAdapter;
208
+ /**
209
+ * Max number of entries to keep in memory (LRU eviction).
210
+ * @default 500
211
+ */
212
+ maxEntries?: number;
213
+ /**
214
+ * When true and the client has an active realtime subscription for a
215
+ * collection, inbound create/update/delete events invalidate that
216
+ * collection's cached entries automatically.
217
+ * @default false — only local mutations invalidate (explicit + predictable)
218
+ */
219
+ invalidateOnRealtime?: boolean;
220
+ }
221
+ /** Per-request cache controls (mixed into {@link RequestOptions}). */
222
+ interface CacheRequestOptions {
223
+ /**
224
+ * Cache control for this request:
225
+ * - `true` — cache with the default (or global) TTL
226
+ * - `false` — always fetch fresh, bypass cache (and don't store the result)
227
+ * - a number — cache with this TTL in milliseconds
228
+ * - an object — `{ ttl, key }` for finer control
229
+ *
230
+ * When unset, the global `cache.enabled` flag decides.
231
+ */
232
+ cache?: boolean | number | {
233
+ ttl?: number;
234
+ key?: string;
235
+ };
236
+ /** Alias of `cache: <ms>` (convenience, reads naturally). */
237
+ ttl?: number;
238
+ /**
239
+ * Extra cache namespaces to invalidate when this mutation succeeds.
240
+ * The current collection is always invalidated automatically.
241
+ * @example create({ ... }, { invalidate: ['users'] })
242
+ */
243
+ invalidate?: string[];
244
+ }
245
+ /** LRU-ish memory store + optional persistent adapter hybrid. */
246
+ declare class CacheStore {
247
+ private memory;
248
+ private readonly ttl;
249
+ private readonly persistence?;
250
+ private readonly maxEntries;
251
+ private hits;
252
+ private misses;
253
+ private namespaceEntries;
254
+ /** Key → set of prefix tags registered for that key (e.g. `getList:posts`). */
255
+ private prefixEntries;
256
+ constructor(config?: {
257
+ defaultTTL?: number;
258
+ store?: StorageAdapter;
259
+ maxEntries?: number;
260
+ });
261
+ /** Resolve the effective TTL: request override → global default. */
262
+ private resolveTTL;
263
+ /**
264
+ * Read a cached value. Fast sync path (memory) with async persistence
265
+ * fallback for adapters whose `get` returns a Promise.
266
+ * @param key Cache key (e.g. `"GET /posts?page=1"`).
267
+ * @returns The cached value, or undefined when absent/expired (the hit is
268
+ * cleared on expiry so a stale value is never served).
269
+ */
270
+ get<T = unknown>(key: string): Promise<T | undefined>;
271
+ /**
272
+ * Store a value.
273
+ * @param key Cache key.
274
+ * @param value The response payload.
275
+ * @param ttlOverride Optional TTL override (ms).
276
+ * @param namespace Optional namespace for group invalidation.
277
+ */
278
+ set(key: string, value: unknown, ttlOverride?: number, namespace?: string, tags?: string[]): void;
279
+ /**
280
+ * Invalidate entries belonging to a namespace (e.g. a collection name).
281
+ * Also clears the namespace index entry.
282
+ */
283
+ invalidate(namespace: string): void;
284
+ /** Remove a single key. */
285
+ delete(key: string): void;
286
+ /**
287
+ * Delete every entry whose key starts with `prefix`.
288
+ *
289
+ * Useful for fine-grained invalidation, e.g.:
290
+ * ```ts
291
+ * client.cache.deleteByPrefix('getList:posts'); // delete all getList cache
292
+ * client.cache.deleteByPrefix('getOne:posts'); // delete all getOne cache
293
+ * ```
294
+ */
295
+ deleteByPrefix(prefix: string): void;
296
+ /** Drop every cached entry (memory + persistence). */
297
+ clear(): void;
298
+ /** Cache hit/miss/entry statistics. */
299
+ stats(): {
300
+ hits: number;
301
+ misses: number;
302
+ entries: number;
303
+ };
304
+ private persistKey;
305
+ private readPersisted;
306
+ }
307
+ /** Resolve per-request cache options into a usable directive. */
308
+ declare function resolveCacheDirective(opts?: {
309
+ cache?: boolean | number | {
310
+ ttl?: number;
311
+ key?: string;
312
+ };
313
+ ttl?: number;
314
+ }): {
315
+ enabled: boolean;
316
+ ttl?: number;
317
+ key?: string;
318
+ } | null;
319
+
161
320
  declare class HttpClient {
162
321
  private baseUrl;
163
322
  private authStore;
164
323
  private defaultFetch;
324
+ /** Optional query cache store (wired when the client enables caching). */
325
+ private cache?;
326
+ /** Master switch resolved from CacheConfig.enabled. */
327
+ private cacheEnabled;
165
328
  /**
166
329
  * Abort controllers for in-flight requests, keyed by their cancellation key
167
330
  * (default `METHOD path`). A new request with the same key aborts the
168
331
  * previous one — PocketBase-style auto-cancellation of duplicated requests.
169
332
  */
170
333
  private cancelControllers;
334
+ /**
335
+ * In-flight request promises, keyed by cancellation key. When auto-cancellation
336
+ * would abort a pending duplicate, the newer request instead awaits the same
337
+ * promise — single-flight coalescing (no duplicate network request, no
338
+ * spurious abort rejection for the caller).
339
+ */
340
+ private inflight;
171
341
  /** Global toggle for the auto-cancellation behaviour (default: on). */
172
342
  private enableAutoCancellation;
173
343
  /**
@@ -175,6 +345,13 @@ declare class HttpClient {
175
345
  * @param authStore The auth store providing the token for Authorization headers.
176
346
  */
177
347
  constructor(baseUrl: string, authStore: AuthStore);
348
+ /**
349
+ * Attach a cache store + master switch.
350
+ * Called by the client constructor when cache config is present.
351
+ */
352
+ setCache(cache: CacheStore, enabled: boolean): void;
353
+ /** Whether the global cache flag is on (requests opt in/out individually too). */
354
+ get cacheIsEnabled(): boolean;
178
355
  private refreshAuth;
179
356
  /**
180
357
  * Globally enable or disable auto-cancellation of duplicated pending requests.
@@ -204,7 +381,31 @@ declare class HttpClient {
204
381
  * @throws {ApiError} On non-2xx responses or when the request is aborted
205
382
  * (aborted requests throw an `ApiError` with `isAbort === true`).
206
383
  */
384
+ /** Invalidate a namespace (collection name). No-op when cache is off. */
385
+ invalidateCache(namespace: string): void;
386
+ /** Current cache statistics (hits/misses/entries), or null when disabled. */
387
+ cacheStats(): {
388
+ hits: number;
389
+ misses: number;
390
+ entries: number;
391
+ } | null;
207
392
  request<T = unknown>(method: Method, path: string, body?: unknown, options?: RequestOptions): Promise<T | null>;
393
+ /**
394
+ * Execute the actual HTTP request (fetch + parse + cache). Called by {@link request}
395
+ * as the inner in-flight unit so single-flight callers can reuse the promise.
396
+ */
397
+ private doRequest;
398
+ /** Build a token-scoped cache key: `METHOD path|token-hash|params`. */
399
+ private cacheKeyFor;
400
+ /** Best-effort namespace (collection name) from a REST path. */
401
+ private namespaceFromPath;
402
+ /**
403
+ * Semantic prefix tags for `deleteByPrefix`, derived from the REST shape:
404
+ * - `/{collection}?...` → `getList:{collection}`
405
+ * - `/{collection}/{id}` → `getOne:{collection}`
406
+ * - `/collections?...` / `/collections/{id}` → `collections:getList` / `collections:getOne`
407
+ */
408
+ private cacheTagsFor;
208
409
  /**
209
410
  * HTTP GET.
210
411
  * @param path URL path.
@@ -681,6 +882,26 @@ declare function fieldTypeKind(field: SchemaField): FieldTypeKind;
681
882
  */
682
883
  declare function schemaFieldType(field: SchemaField): unknown;
683
884
 
885
+ /**
886
+ * Callable cache namespace: `client.cache(config)` configures, and
887
+ * `client.cache.deleteByPrefix(...)` etc. manage cached entries.
888
+ */
889
+ interface CacheController {
890
+ /** Configure the query cache at runtime. */
891
+ (config?: CacheConfig): LazypockClient;
892
+ /** Delete every entry whose key starts with `prefix` (e.g. `getList:posts`). */
893
+ deleteByPrefix(prefix: string): void;
894
+ /** Invalidate a collection's cached entries (alias of invalidateCache). */
895
+ invalidate(namespace: string): void;
896
+ /** Drop every cached entry. */
897
+ clear(): void;
898
+ /** Cache hit/miss/entry stats, or null when never configured. */
899
+ stats(): {
900
+ hits: number;
901
+ misses: number;
902
+ entries: number;
903
+ } | null;
904
+ }
684
905
  /** Options for constructing a {@link LazypockClient}. */
685
906
  interface LazypockClientOptions {
686
907
  /** API base URL (e.g. 'http://localhost:4000/api') */
@@ -691,6 +912,24 @@ interface LazypockClientOptions {
691
912
  authStore?: AuthStore;
692
913
  /** Real-time service for Phoenix Channel WebSocket subscriptions */
693
914
  realtime?: RealtimeService;
915
+ /**
916
+ * Query cache configuration. Disabled by default.
917
+ *
918
+ * ```ts
919
+ * const client = createClient({
920
+ * baseUrl: '...',
921
+ * cache: {
922
+ * enabled: true,
923
+ * defaultTTL: 30_000,
924
+ * store: myStorage, // optional persistence (same interface as auth)
925
+ * },
926
+ * });
927
+ * ```
928
+ *
929
+ * When enabled, readable GETs are cached. Requests can opt out via
930
+ * `{ cache: false }`, or opt in with a custom TTL via `{ ttl: ms }`.
931
+ */
932
+ cache?: CacheConfig;
694
933
  /**
695
934
  * Optional schema types for generating typed services at runtime.
696
935
  * When provided, `collection()` returns a service whose create/update
@@ -724,6 +963,9 @@ declare class LazypockClient {
724
963
  readonly files: FilesService;
725
964
  private collectionCache;
726
965
  private schemaByName?;
966
+ private cacheStore?;
967
+ /** Namespace → realtime unsubscribe; used for realtime-driven invalidation. */
968
+ private realtimeInvalidators;
727
969
  /**
728
970
  * Create a new Lazypock client.
729
971
  * @param options Configuration options.
@@ -778,6 +1020,47 @@ declare class LazypockClient {
778
1020
  cancelRequest(requestKey: string): this;
779
1021
  /** Abort all pending requests. */
780
1022
  cancelAllRequests(): this;
1023
+ /**
1024
+ * Configure the query cache at runtime (also a namespace for cache
1025
+ * management methods).
1026
+ *
1027
+ * ```ts
1028
+ * client.cache({ enabled: true, defaultTTL: 30_000 });
1029
+ * client.cache.deleteByPrefix('getList:posts'); // all list caches for posts
1030
+ * client.cache.deleteByPrefix('getOne:posts'); // all one-record caches
1031
+ * ```
1032
+ *
1033
+ * When enabled, GET requests cache their payload; mutations invalidate the
1034
+ * affected collection automatically. Individual requests can opt out with
1035
+ * `{ cache: false }` or override the TTL with `{ ttl: ms }`.
1036
+ */
1037
+ readonly cache: CacheController;
1038
+ /**
1039
+ * Drop every cached entry (all collections / namespaces).
1040
+ * Also disables realtime-driven invalidation subscriptions.
1041
+ */
1042
+ clearCache(): this;
1043
+ /**
1044
+ * Invalidate cached entries for a collection (or custom namespace).
1045
+ * Runs automatically on mutations — call explicitly when data changed
1046
+ * out-of-band (e.g. another client wrote to the same collection).
1047
+ */
1048
+ invalidateCache(namespace: string): this;
1049
+ /**
1050
+ * Cache hit/miss/entry statistics.
1051
+ * Returns null when caching was never configured.
1052
+ */
1053
+ cacheStats(): {
1054
+ hits: number;
1055
+ misses: number;
1056
+ entries: number;
1057
+ } | null;
1058
+ /**
1059
+ * Subscribe a collection's cache to realtime invalidation: any inbound
1060
+ * create/update/delete event for the collection clears its cached entries.
1061
+ * Returns an unsubscribe function.
1062
+ */
1063
+ invalidateCacheOnRealtime(collectionName: string): () => void;
781
1064
  /** Check whether any superuser exists (for login vs setup screen routing). */
782
1065
  checkSuperuser(): Promise<{
783
1066
  has_superuser: boolean;
@@ -870,4 +1153,4 @@ declare class TypedClient<TCollections extends LazypockCollections = LazypockCol
870
1153
  */
871
1154
  declare function createClient<TCollections extends LazypockCollections = LazypockCollections>(options: LazypockClientOptions): TypedClient<TCollections>;
872
1155
 
873
- export { ApiError, type ApiRecord, type AuthModel, AuthStore, type CollectionSchema, CollectionService, type CollectionsMessage, CollectionsService, type CreateData, type FileRecord, FilesService, HttpClient, LazypockClient, type LazypockClientOptions, type LazypockCollections, type ListResult, type RealtimeCallback, type RealtimeMessage, RealtimeService, type RecordShape, type RequestOptions, type SchemaField, type StorageAdapter, type SystemFields, TypedClient, type UpdateData, collectionTypeName, createClient, fieldTypeKind, fieldTypeScriptType, generateTypes, getFileUrl, getScaleUrl, getThumbUrl, schemaFieldType, wsUrlFromBaseUrl };
1156
+ export { ApiError, type ApiRecord, type AuthModel, AuthStore, type CacheConfig, type CacheRequestOptions, CacheStore, type CollectionSchema, CollectionService, type CollectionsMessage, CollectionsService, type CreateData, type FileRecord, FilesService, HttpClient, LazypockClient, type LazypockClientOptions, type LazypockCollections, type ListResult, type RealtimeCallback, type RealtimeMessage, RealtimeService, type RecordShape, type RequestOptions, type SchemaField, type StorageAdapter, type SystemFields, TypedClient, type UpdateData, collectionTypeName, createClient, fieldTypeKind, fieldTypeScriptType, generateTypes, getFileUrl, getScaleUrl, getThumbUrl, resolveCacheDirective, schemaFieldType, wsUrlFromBaseUrl };