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.cjs CHANGED
@@ -22,6 +22,7 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  ApiError: () => ApiError,
24
24
  AuthStore: () => AuthStore,
25
+ CacheStore: () => CacheStore,
25
26
  CollectionService: () => CollectionService,
26
27
  CollectionsService: () => CollectionsService,
27
28
  FilesService: () => FilesService,
@@ -37,6 +38,7 @@ __export(index_exports, {
37
38
  getFileUrl: () => getFileUrl,
38
39
  getScaleUrl: () => getScaleUrl,
39
40
  getThumbUrl: () => getThumbUrl,
41
+ resolveCacheDirective: () => resolveCacheDirective,
40
42
  schemaFieldType: () => schemaFieldType,
41
43
  wsUrlFromBaseUrl: () => wsUrlFromBaseUrl
42
44
  });
@@ -53,6 +55,201 @@ var ApiError = class extends Error {
53
55
  }
54
56
  };
55
57
 
58
+ // src/cache.ts
59
+ var CacheStore = class {
60
+ constructor(config = {}) {
61
+ this.memory = /* @__PURE__ */ new Map();
62
+ this.hits = 0;
63
+ this.misses = 0;
64
+ this.namespaceEntries = /* @__PURE__ */ new Map();
65
+ /** Key → set of prefix tags registered for that key (e.g. `getList:posts`). */
66
+ this.prefixEntries = /* @__PURE__ */ new Map();
67
+ this.ttl = config.defaultTTL ?? 6e4;
68
+ this.persistence = config.store;
69
+ this.maxEntries = config.maxEntries ?? 500;
70
+ }
71
+ /** Resolve the effective TTL: request override → global default. */
72
+ resolveTTL(ttl) {
73
+ return ttl && ttl > 0 ? ttl : this.ttl;
74
+ }
75
+ /**
76
+ * Read a cached value. Fast sync path (memory) with async persistence
77
+ * fallback for adapters whose `get` returns a Promise.
78
+ * @param key Cache key (e.g. `"GET /posts?page=1"`).
79
+ * @returns The cached value, or undefined when absent/expired (the hit is
80
+ * cleared on expiry so a stale value is never served).
81
+ */
82
+ async get(key) {
83
+ const mem = this.memory.get(key);
84
+ if (mem !== void 0) {
85
+ if (Date.now() > mem.expiresAt) {
86
+ this.delete(key);
87
+ this.misses++;
88
+ return void 0;
89
+ }
90
+ this.memory.delete(key);
91
+ this.memory.set(key, mem);
92
+ this.hits++;
93
+ return mem.value;
94
+ }
95
+ if (this.persistence) {
96
+ const entry = await this.readPersisted(key);
97
+ if (entry) {
98
+ if (Date.now() > entry.expiresAt) {
99
+ this.delete(key);
100
+ this.misses++;
101
+ return void 0;
102
+ }
103
+ this.hits++;
104
+ return entry.value;
105
+ }
106
+ }
107
+ this.misses++;
108
+ return void 0;
109
+ }
110
+ /**
111
+ * Store a value.
112
+ * @param key Cache key.
113
+ * @param value The response payload.
114
+ * @param ttlOverride Optional TTL override (ms).
115
+ * @param namespace Optional namespace for group invalidation.
116
+ */
117
+ set(key, value, ttlOverride, namespace, tags) {
118
+ const expiresAt = Date.now() + this.resolveTTL(ttlOverride);
119
+ const entry = { value, expiresAt, namespace, tags };
120
+ this.memory.set(key, entry);
121
+ if (this.memory.size > this.maxEntries) {
122
+ const oldest = this.memory.keys().next().value;
123
+ if (oldest !== void 0) this.delete(oldest);
124
+ }
125
+ if (namespace) {
126
+ let keys = this.namespaceEntries.get(namespace);
127
+ if (!keys) {
128
+ keys = /* @__PURE__ */ new Set();
129
+ this.namespaceEntries.set(namespace, keys);
130
+ }
131
+ keys.add(key);
132
+ }
133
+ for (const tag of tags ?? []) {
134
+ let keys = this.prefixEntries.get(tag);
135
+ if (!keys) {
136
+ keys = /* @__PURE__ */ new Set();
137
+ this.prefixEntries.set(tag, keys);
138
+ }
139
+ keys.add(key);
140
+ }
141
+ if (this.persistence) {
142
+ void this.persistence.set(this.persistKey(key), JSON.stringify(entry));
143
+ }
144
+ }
145
+ /**
146
+ * Invalidate entries belonging to a namespace (e.g. a collection name).
147
+ * Also clears the namespace index entry.
148
+ */
149
+ invalidate(namespace) {
150
+ const keys = Array.from(this.namespaceEntries.get(namespace) ?? []);
151
+ for (const key of keys) this.delete(key);
152
+ this.namespaceEntries.delete(namespace);
153
+ }
154
+ /** Remove a single key. */
155
+ delete(key) {
156
+ const entry = this.memory.get(key);
157
+ if (entry?.namespace) {
158
+ const set = this.namespaceEntries.get(entry.namespace);
159
+ if (set) {
160
+ set.delete(key);
161
+ if (set.size === 0) this.namespaceEntries.delete(entry.namespace);
162
+ }
163
+ }
164
+ for (const tag of entry?.tags ?? []) {
165
+ const set = this.prefixEntries.get(tag);
166
+ if (set) {
167
+ set.delete(key);
168
+ if (set.size === 0) this.prefixEntries.delete(tag);
169
+ }
170
+ }
171
+ this.memory.delete(key);
172
+ if (this.persistence) {
173
+ void this.persistence.remove(this.persistKey(key));
174
+ }
175
+ }
176
+ /**
177
+ * Delete every entry whose key starts with `prefix`.
178
+ *
179
+ * Useful for fine-grained invalidation, e.g.:
180
+ * ```ts
181
+ * client.cache.deleteByPrefix('getList:posts'); // delete all getList cache
182
+ * client.cache.deleteByPrefix('getOne:posts'); // delete all getOne cache
183
+ * ```
184
+ */
185
+ deleteByPrefix(prefix) {
186
+ if (!prefix) return;
187
+ const tagged = this.prefixEntries.get(prefix);
188
+ if (tagged) {
189
+ for (const key of Array.from(tagged)) this.delete(key);
190
+ this.prefixEntries.delete(prefix);
191
+ return;
192
+ }
193
+ for (const key of Array.from(this.memory.keys())) {
194
+ if (key.startsWith(prefix)) this.delete(key);
195
+ }
196
+ }
197
+ /** Drop every cached entry (memory + persistence). */
198
+ clear() {
199
+ this.memory.clear();
200
+ this.namespaceEntries.clear();
201
+ this.prefixEntries.clear();
202
+ }
203
+ /** Cache hit/miss/entry statistics. */
204
+ stats() {
205
+ return { hits: this.hits, misses: this.misses, entries: this.memory.size };
206
+ }
207
+ persistKey(key) {
208
+ return "lazypock:cache:" + key;
209
+ }
210
+ async readPersisted(key) {
211
+ if (!this.persistence) return void 0;
212
+ const raw = await this.persistence.get(this.persistKey(key));
213
+ if (raw == null) return void 0;
214
+ try {
215
+ const entry = JSON.parse(raw);
216
+ this.memory.set(key, entry);
217
+ if (entry.namespace) {
218
+ let keys = this.namespaceEntries.get(entry.namespace);
219
+ if (!keys) {
220
+ keys = /* @__PURE__ */ new Set();
221
+ this.namespaceEntries.set(entry.namespace, keys);
222
+ }
223
+ keys.add(key);
224
+ }
225
+ for (const tag of entry.tags ?? []) {
226
+ let keys = this.prefixEntries.get(tag);
227
+ if (!keys) {
228
+ keys = /* @__PURE__ */ new Set();
229
+ this.prefixEntries.set(tag, keys);
230
+ }
231
+ keys.add(key);
232
+ }
233
+ return entry;
234
+ } catch {
235
+ void this.persistence.remove(this.persistKey(key));
236
+ return void 0;
237
+ }
238
+ }
239
+ };
240
+ function resolveCacheDirective(opts) {
241
+ if (!opts) return null;
242
+ if (typeof opts.ttl === "number" && opts.ttl > 0) {
243
+ return { enabled: true, ttl: opts.ttl };
244
+ }
245
+ const c = opts.cache;
246
+ if (c === void 0) return null;
247
+ if (c === true) return { enabled: true };
248
+ if (c === false) return { enabled: false };
249
+ if (typeof c === "number") return { enabled: true, ttl: c > 0 ? c : void 0 };
250
+ return { enabled: true, ttl: c.ttl, key: c.key };
251
+ }
252
+
56
253
  // src/http.ts
57
254
  function isAbortError(err) {
58
255
  return err instanceof Error && (err.name === "AbortError" || err.message === "Aborted");
@@ -63,18 +260,39 @@ var HttpClient = class {
63
260
  * @param authStore The auth store providing the token for Authorization headers.
64
261
  */
65
262
  constructor(baseUrl, authStore) {
263
+ /** Master switch resolved from CacheConfig.enabled. */
264
+ this.cacheEnabled = false;
66
265
  /**
67
266
  * Abort controllers for in-flight requests, keyed by their cancellation key
68
267
  * (default `METHOD path`). A new request with the same key aborts the
69
268
  * previous one — PocketBase-style auto-cancellation of duplicated requests.
70
269
  */
71
270
  this.cancelControllers = {};
271
+ /**
272
+ * In-flight request promises, keyed by cancellation key. When auto-cancellation
273
+ * would abort a pending duplicate, the newer request instead awaits the same
274
+ * promise — single-flight coalescing (no duplicate network request, no
275
+ * spurious abort rejection for the caller).
276
+ */
277
+ this.inflight = {};
72
278
  /** Global toggle for the auto-cancellation behaviour (default: on). */
73
279
  this.enableAutoCancellation = true;
74
280
  this.baseUrl = baseUrl.replace(/\/+$/, "");
75
281
  this.authStore = authStore;
76
282
  this.defaultFetch = globalThis.fetch.bind(globalThis);
77
283
  }
284
+ /**
285
+ * Attach a cache store + master switch.
286
+ * Called by the client constructor when cache config is present.
287
+ */
288
+ setCache(cache, enabled) {
289
+ this.cache = cache;
290
+ this.cacheEnabled = enabled;
291
+ }
292
+ /** Whether the global cache flag is on (requests opt in/out individually too). */
293
+ get cacheIsEnabled() {
294
+ return this.cacheEnabled;
295
+ }
78
296
  async refreshAuth() {
79
297
  const collection = this.authStore.collectionName;
80
298
  if (!collection) return null;
@@ -151,12 +369,33 @@ var HttpClient = class {
151
369
  * @throws {ApiError} On non-2xx responses or when the request is aborted
152
370
  * (aborted requests throw an `ApiError` with `isAbort === true`).
153
371
  */
372
+ /** Invalidate a namespace (collection name). No-op when cache is off. */
373
+ invalidateCache(namespace) {
374
+ this.cache?.invalidate(namespace);
375
+ }
376
+ /** Current cache statistics (hits/misses/entries), or null when disabled. */
377
+ cacheStats() {
378
+ return this.cache ? this.cache.stats() : null;
379
+ }
154
380
  async request(method, path, body, options) {
155
381
  if (this.authStore.isExpired && this.authStore.collectionName) {
156
382
  await this.refreshAuth();
157
383
  }
384
+ const cacheDirective = resolveCacheDirective(options);
385
+ const wantCache = cacheDirective !== null ? cacheDirective.enabled : this.cacheEnabled;
386
+ const cacheKey = method === "GET" && this.cache && wantCache ? this.cacheKeyFor(method, path, options?.params) : null;
387
+ if (cacheKey !== null) {
388
+ const hit = await this.cache?.get(cacheKey);
389
+ if (hit !== void 0) return hit;
390
+ }
158
391
  let requestKey = options?.requestKey === void 0 ? options?.cancelKey ?? `${method} ${path}` : options.requestKey;
159
392
  if (options?.autoCancel === false) requestKey = null;
393
+ if (options?.singleFlight && requestKey !== null) {
394
+ const pending = this.inflight[requestKey];
395
+ if (pending !== void 0) {
396
+ return pending;
397
+ }
398
+ }
160
399
  let controller = null;
161
400
  const externalSignal = options?.signal;
162
401
  if (requestKey !== null) {
@@ -174,6 +413,38 @@ var HttpClient = class {
174
413
  }
175
414
  }
176
415
  const signal = controller?.signal ?? externalSignal;
416
+ const perform = async () => {
417
+ try {
418
+ return await this.doRequest(
419
+ method,
420
+ path,
421
+ body,
422
+ options,
423
+ signal,
424
+ requestKey,
425
+ controller,
426
+ cacheKey,
427
+ cacheDirective
428
+ );
429
+ } finally {
430
+ if (requestKey !== null) {
431
+ if (this.inflight[requestKey] === promise) {
432
+ delete this.inflight[requestKey];
433
+ }
434
+ }
435
+ }
436
+ };
437
+ const promise = perform();
438
+ if (requestKey !== null) {
439
+ this.inflight[requestKey] = promise;
440
+ }
441
+ return promise;
442
+ }
443
+ /**
444
+ * Execute the actual HTTP request (fetch + parse + cache). Called by {@link request}
445
+ * as the inner in-flight unit so single-flight callers can reuse the promise.
446
+ */
447
+ async doRequest(method, path, body, options, signal, requestKey, controller, cacheKey, cacheDirective) {
177
448
  let url = this.baseUrl + path;
178
449
  if (options?.params) {
179
450
  const qs = new URLSearchParams(options.params).toString();
@@ -238,8 +509,63 @@ var HttpClient = class {
238
509
  res.status
239
510
  );
240
511
  }
512
+ if (cacheKey !== null && this.cache) {
513
+ const namespace = this.namespaceFromPath(path);
514
+ const ttl = cacheDirective?.ttl;
515
+ const tags = this.cacheTagsFor(path, namespace);
516
+ this.cache.set(
517
+ cacheKey,
518
+ data,
519
+ ttl,
520
+ namespace ?? void 0,
521
+ tags
522
+ );
523
+ }
524
+ if (method !== "GET" && this.cache) {
525
+ const namespaces = /* @__PURE__ */ new Set();
526
+ const ns = this.namespaceFromPath(path);
527
+ if (ns) namespaces.add(ns);
528
+ for (const extra of options?.invalidate ?? []) {
529
+ if (extra) namespaces.add(extra);
530
+ }
531
+ for (const nsName of namespaces) this.cache.invalidate(nsName);
532
+ }
241
533
  return data;
242
534
  }
535
+ // ── Cache key/namespace helpers ──
536
+ /** Build a token-scoped cache key: `METHOD path|token-hash|params`. */
537
+ cacheKeyFor(method, path, params) {
538
+ const token = this.authStore.token || "anon";
539
+ const qs = params ? "?" + new URLSearchParams(params).toString() : "";
540
+ return `${method} ${path}${qs}|${token}`;
541
+ }
542
+ /** Best-effort namespace (collection name) from a REST path. */
543
+ namespaceFromPath(path) {
544
+ const clean = path.split("?")[0];
545
+ const parts = clean.split("/").filter(Boolean);
546
+ if (parts.length === 0) return void 0;
547
+ if (parts[0] === "collections" || parts[0] === "_superusers") {
548
+ return parts[0];
549
+ }
550
+ return parts[0];
551
+ }
552
+ /**
553
+ * Semantic prefix tags for `deleteByPrefix`, derived from the REST shape:
554
+ * - `/{collection}?...` → `getList:{collection}`
555
+ * - `/{collection}/{id}` → `getOne:{collection}`
556
+ * - `/collections?...` / `/collections/{id}` → `collections:getList` / `collections:getOne`
557
+ */
558
+ cacheTagsFor(path, namespace) {
559
+ if (!namespace) return [];
560
+ const clean = path.split("?")[0];
561
+ const parts = clean.split("/").filter(Boolean);
562
+ if (parts[0] === "collections" || parts[0] === "_superusers") {
563
+ const op2 = parts.length >= 2 ? "getOne" : "getList";
564
+ return [`${namespace}:${op2}`];
565
+ }
566
+ const op = parts.length >= 2 ? "getOne" : "getList";
567
+ return [`${op}:${namespace}`];
568
+ }
243
569
  /**
244
570
  * HTTP GET.
245
571
  * @param path URL path.
@@ -415,6 +741,30 @@ var memoryStorage = {
415
741
  };
416
742
 
417
743
  // src/collection.ts
744
+ function stableStringify(value) {
745
+ const seen = /* @__PURE__ */ new Set();
746
+ const sort = (v) => {
747
+ if (Array.isArray(v)) return v.map(sort);
748
+ if (v && typeof v === "object") {
749
+ if (seen.has(v)) return "[Circular]";
750
+ seen.add(v);
751
+ const out = {};
752
+ for (const k of Object.keys(v).sort()) {
753
+ if (k === "requestKey" || k === "singleFlight" || k === "fetch" || k === "signal") {
754
+ continue;
755
+ }
756
+ out[k] = sort(v[k]);
757
+ }
758
+ return out;
759
+ }
760
+ return v;
761
+ };
762
+ try {
763
+ return JSON.stringify(sort(value));
764
+ } catch {
765
+ return String(value);
766
+ }
767
+ }
418
768
  function normalizeAction(event, rawAction) {
419
769
  if (typeof rawAction === "string") {
420
770
  const a = rawAction.toLowerCase();
@@ -446,19 +796,44 @@ var CollectionService = class {
446
796
  * @param options Query params (`filter`, `sort`, `expand`, `fields`) + request options.
447
797
  */
448
798
  getList(page = 1, perPage = 30, options) {
449
- const { requestKey, autoCancel, cancelKey, ...rest } = options ?? {};
799
+ const {
800
+ requestKey,
801
+ autoCancel,
802
+ cancelKey,
803
+ fetch,
804
+ headers,
805
+ signal,
806
+ cache,
807
+ ttl,
808
+ invalidate,
809
+ singleFlight,
810
+ params,
811
+ ...queryParams
812
+ } = options ?? {};
450
813
  const qs = new URLSearchParams(
451
814
  Object.fromEntries(
452
815
  Object.entries({
453
816
  page: String(page),
454
817
  perPage: String(perPage),
455
- ...rest
818
+ ...queryParams
456
819
  }).map(([k, v]) => [k, String(v)])
457
820
  )
458
821
  ).toString();
459
822
  return this.http.get(
460
823
  "/" + this.encodeId(this.collectionName) + "?" + qs,
461
- { requestKey, autoCancel, cancelKey }
824
+ {
825
+ requestKey,
826
+ autoCancel,
827
+ cancelKey,
828
+ fetch,
829
+ headers,
830
+ signal,
831
+ cache,
832
+ ttl,
833
+ invalidate,
834
+ singleFlight,
835
+ params
836
+ }
462
837
  );
463
838
  }
464
839
  /**
@@ -469,6 +844,7 @@ var CollectionService = class {
469
844
  */
470
845
  async getFullList(options) {
471
846
  const { batch = 1e3, ...rest } = options ?? {};
847
+ const effectiveKey = typeof rest.requestKey === "string" ? rest.requestKey : `getFullList:${this.collectionName}:${stableStringify(rest)}`;
472
848
  const items = [];
473
849
  let page = 1;
474
850
  for (; ; ) {
@@ -476,9 +852,9 @@ var CollectionService = class {
476
852
  page,
477
853
  batch,
478
854
  {
479
- // disable auto-cancellation across pages — each page request is unique
480
855
  ...rest,
481
- requestKey: null
856
+ requestKey: effectiveKey,
857
+ singleFlight: true
482
858
  }
483
859
  );
484
860
  if (!res || !res.items || res.items.length === 0) break;
@@ -1001,14 +1377,40 @@ var CollectionsService = class {
1001
1377
  async getFullList(options) {
1002
1378
  if (!this.http) return [];
1003
1379
  const { batch = 1e3, ...rest } = options ?? {};
1380
+ const {
1381
+ requestKey: reqKey,
1382
+ singleFlight: _singleFlight,
1383
+ fetch: fetchFn,
1384
+ headers: hdrs,
1385
+ signal: sig,
1386
+ cache: cacheOpt,
1387
+ ttl: ttlOpt,
1388
+ invalidate: inval,
1389
+ params: passthroughParams,
1390
+ ...queryParams
1391
+ } = rest;
1392
+ const effectiveKey = typeof reqKey === "string" ? reqKey : `getFullList:collections:${stableStringify(rest)}`;
1004
1393
  const items = [];
1005
1394
  let page = 1;
1006
1395
  for (; ; ) {
1007
- const res = await this.getList({
1008
- ...rest,
1009
- page,
1010
- perPage: batch
1011
- });
1396
+ const res = await this.getList(
1397
+ {
1398
+ ...queryParams,
1399
+ ...passthroughParams ?? {},
1400
+ page,
1401
+ perPage: batch
1402
+ },
1403
+ {
1404
+ requestKey: effectiveKey,
1405
+ singleFlight: true,
1406
+ ...fetchFn ? { fetch: fetchFn } : {},
1407
+ ...hdrs ? { headers: hdrs } : {},
1408
+ ...sig ? { signal: sig } : {},
1409
+ ...cacheOpt !== void 0 ? { cache: cacheOpt } : {},
1410
+ ...ttlOpt !== void 0 ? { ttl: ttlOpt } : {},
1411
+ ...inval ? { invalidate: inval } : {}
1412
+ }
1413
+ );
1012
1414
  if (!res || !res.items || res.items.length === 0) break;
1013
1415
  items.push(...res.items);
1014
1416
  if (page >= (res.totalPages ?? page)) break;
@@ -1291,6 +1693,50 @@ var LazypockClient = class {
1291
1693
  */
1292
1694
  constructor(options) {
1293
1695
  this.collectionCache = /* @__PURE__ */ new Map();
1696
+ /** Namespace → realtime unsubscribe; used for realtime-driven invalidation. */
1697
+ this.realtimeInvalidators = /* @__PURE__ */ new Map();
1698
+ // ── Query cache (opt-in by default; opt-out per request) ──
1699
+ /**
1700
+ * Configure the query cache at runtime (also a namespace for cache
1701
+ * management methods).
1702
+ *
1703
+ * ```ts
1704
+ * client.cache({ enabled: true, defaultTTL: 30_000 });
1705
+ * client.cache.deleteByPrefix('getList:posts'); // all list caches for posts
1706
+ * client.cache.deleteByPrefix('getOne:posts'); // all one-record caches
1707
+ * ```
1708
+ *
1709
+ * When enabled, GET requests cache their payload; mutations invalidate the
1710
+ * affected collection automatically. Individual requests can opt out with
1711
+ * `{ cache: false }` or override the TTL with `{ ttl: ms }`.
1712
+ */
1713
+ this.cache = Object.assign(
1714
+ ((config) => {
1715
+ if (!this.cacheStore) {
1716
+ this.cacheStore = new CacheStore({
1717
+ defaultTTL: config?.defaultTTL,
1718
+ store: config?.store,
1719
+ maxEntries: config?.maxEntries
1720
+ });
1721
+ this.http.setCache(this.cacheStore, config?.enabled ?? true);
1722
+ } else {
1723
+ if (config?.enabled !== void 0) {
1724
+ this.http.setCache(this.cacheStore, config.enabled);
1725
+ }
1726
+ }
1727
+ return this;
1728
+ }),
1729
+ {
1730
+ deleteByPrefix: (prefix) => this.cacheStore?.deleteByPrefix(prefix),
1731
+ invalidate: (namespace) => this.cacheStore?.invalidate(namespace),
1732
+ clear: () => {
1733
+ this.cacheStore?.clear();
1734
+ for (const unsub of this.realtimeInvalidators.values()) unsub();
1735
+ this.realtimeInvalidators.clear();
1736
+ },
1737
+ stats: () => this.cacheStore ? this.cacheStore.stats() : null
1738
+ }
1739
+ );
1294
1740
  const baseUrl = options.baseUrl.replace(/\/+$/, "");
1295
1741
  this.authStore = options.authStore ?? new AuthStore(options.storage ?? memoryStorage);
1296
1742
  this.http = new HttpClient(baseUrl, this.authStore);
@@ -1300,6 +1746,14 @@ var LazypockClient = class {
1300
1746
  }
1301
1747
  this.files = new FilesService(this.http);
1302
1748
  this.collections = new CollectionsService(this.http, this.realtime);
1749
+ if (options.cache) {
1750
+ this.cacheStore = new CacheStore({
1751
+ defaultTTL: options.cache.defaultTTL,
1752
+ store: options.cache.store,
1753
+ maxEntries: options.cache.maxEntries
1754
+ });
1755
+ this.http.setCache(this.cacheStore, options.cache.enabled ?? false);
1756
+ }
1303
1757
  if (options.types?.schemas) {
1304
1758
  this.schemaByName = new Map(
1305
1759
  options.types.schemas.map((s) => [s.name, s])
@@ -1380,6 +1834,49 @@ var LazypockClient = class {
1380
1834
  this.http.cancelAllRequests();
1381
1835
  return this;
1382
1836
  }
1837
+ /**
1838
+ * Drop every cached entry (all collections / namespaces).
1839
+ * Also disables realtime-driven invalidation subscriptions.
1840
+ */
1841
+ clearCache() {
1842
+ this.cacheStore?.clear();
1843
+ for (const unsub of this.realtimeInvalidators.values()) unsub();
1844
+ this.realtimeInvalidators.clear();
1845
+ return this;
1846
+ }
1847
+ /**
1848
+ * Invalidate cached entries for a collection (or custom namespace).
1849
+ * Runs automatically on mutations — call explicitly when data changed
1850
+ * out-of-band (e.g. another client wrote to the same collection).
1851
+ */
1852
+ invalidateCache(namespace) {
1853
+ this.cacheStore?.invalidate(namespace);
1854
+ return this;
1855
+ }
1856
+ /**
1857
+ * Cache hit/miss/entry statistics.
1858
+ * Returns null when caching was never configured.
1859
+ */
1860
+ cacheStats() {
1861
+ return this.cacheStore ? this.cacheStore.stats() : null;
1862
+ }
1863
+ /**
1864
+ * Subscribe a collection's cache to realtime invalidation: any inbound
1865
+ * create/update/delete event for the collection clears its cached entries.
1866
+ * Returns an unsubscribe function.
1867
+ */
1868
+ invalidateCacheOnRealtime(collectionName) {
1869
+ if (!this.cacheStore) {
1870
+ this.cache({ enabled: false });
1871
+ }
1872
+ const existing = this.realtimeInvalidators.get(collectionName);
1873
+ if (existing) return existing;
1874
+ const unsub = this.collection(collectionName).subscribe(() => {
1875
+ this.cacheStore?.invalidate(collectionName);
1876
+ });
1877
+ this.realtimeInvalidators.set(collectionName, unsub);
1878
+ return unsub;
1879
+ }
1383
1880
  // ── Auth ──
1384
1881
  /** Check whether any superuser exists (for login vs setup screen routing). */
1385
1882
  async checkSuperuser() {
@@ -1517,6 +2014,7 @@ function createClient(options) {
1517
2014
  0 && (module.exports = {
1518
2015
  ApiError,
1519
2016
  AuthStore,
2017
+ CacheStore,
1520
2018
  CollectionService,
1521
2019
  CollectionsService,
1522
2020
  FilesService,
@@ -1532,6 +2030,7 @@ function createClient(options) {
1532
2030
  getFileUrl,
1533
2031
  getScaleUrl,
1534
2032
  getThumbUrl,
2033
+ resolveCacheDirective,
1535
2034
  schemaFieldType,
1536
2035
  wsUrlFromBaseUrl
1537
2036
  });