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.js CHANGED
@@ -17,6 +17,201 @@ var ApiError = class extends Error {
17
17
  }
18
18
  };
19
19
 
20
+ // src/cache.ts
21
+ var CacheStore = class {
22
+ constructor(config = {}) {
23
+ this.memory = /* @__PURE__ */ new Map();
24
+ this.hits = 0;
25
+ this.misses = 0;
26
+ this.namespaceEntries = /* @__PURE__ */ new Map();
27
+ /** Key → set of prefix tags registered for that key (e.g. `getList:posts`). */
28
+ this.prefixEntries = /* @__PURE__ */ new Map();
29
+ this.ttl = config.defaultTTL ?? 6e4;
30
+ this.persistence = config.store;
31
+ this.maxEntries = config.maxEntries ?? 500;
32
+ }
33
+ /** Resolve the effective TTL: request override → global default. */
34
+ resolveTTL(ttl) {
35
+ return ttl && ttl > 0 ? ttl : this.ttl;
36
+ }
37
+ /**
38
+ * Read a cached value. Fast sync path (memory) with async persistence
39
+ * fallback for adapters whose `get` returns a Promise.
40
+ * @param key Cache key (e.g. `"GET /posts?page=1"`).
41
+ * @returns The cached value, or undefined when absent/expired (the hit is
42
+ * cleared on expiry so a stale value is never served).
43
+ */
44
+ async get(key) {
45
+ const mem = this.memory.get(key);
46
+ if (mem !== void 0) {
47
+ if (Date.now() > mem.expiresAt) {
48
+ this.delete(key);
49
+ this.misses++;
50
+ return void 0;
51
+ }
52
+ this.memory.delete(key);
53
+ this.memory.set(key, mem);
54
+ this.hits++;
55
+ return mem.value;
56
+ }
57
+ if (this.persistence) {
58
+ const entry = await this.readPersisted(key);
59
+ if (entry) {
60
+ if (Date.now() > entry.expiresAt) {
61
+ this.delete(key);
62
+ this.misses++;
63
+ return void 0;
64
+ }
65
+ this.hits++;
66
+ return entry.value;
67
+ }
68
+ }
69
+ this.misses++;
70
+ return void 0;
71
+ }
72
+ /**
73
+ * Store a value.
74
+ * @param key Cache key.
75
+ * @param value The response payload.
76
+ * @param ttlOverride Optional TTL override (ms).
77
+ * @param namespace Optional namespace for group invalidation.
78
+ */
79
+ set(key, value, ttlOverride, namespace, tags) {
80
+ const expiresAt = Date.now() + this.resolveTTL(ttlOverride);
81
+ const entry = { value, expiresAt, namespace, tags };
82
+ this.memory.set(key, entry);
83
+ if (this.memory.size > this.maxEntries) {
84
+ const oldest = this.memory.keys().next().value;
85
+ if (oldest !== void 0) this.delete(oldest);
86
+ }
87
+ if (namespace) {
88
+ let keys = this.namespaceEntries.get(namespace);
89
+ if (!keys) {
90
+ keys = /* @__PURE__ */ new Set();
91
+ this.namespaceEntries.set(namespace, keys);
92
+ }
93
+ keys.add(key);
94
+ }
95
+ for (const tag of tags ?? []) {
96
+ let keys = this.prefixEntries.get(tag);
97
+ if (!keys) {
98
+ keys = /* @__PURE__ */ new Set();
99
+ this.prefixEntries.set(tag, keys);
100
+ }
101
+ keys.add(key);
102
+ }
103
+ if (this.persistence) {
104
+ void this.persistence.set(this.persistKey(key), JSON.stringify(entry));
105
+ }
106
+ }
107
+ /**
108
+ * Invalidate entries belonging to a namespace (e.g. a collection name).
109
+ * Also clears the namespace index entry.
110
+ */
111
+ invalidate(namespace) {
112
+ const keys = Array.from(this.namespaceEntries.get(namespace) ?? []);
113
+ for (const key of keys) this.delete(key);
114
+ this.namespaceEntries.delete(namespace);
115
+ }
116
+ /** Remove a single key. */
117
+ delete(key) {
118
+ const entry = this.memory.get(key);
119
+ if (entry?.namespace) {
120
+ const set = this.namespaceEntries.get(entry.namespace);
121
+ if (set) {
122
+ set.delete(key);
123
+ if (set.size === 0) this.namespaceEntries.delete(entry.namespace);
124
+ }
125
+ }
126
+ for (const tag of entry?.tags ?? []) {
127
+ const set = this.prefixEntries.get(tag);
128
+ if (set) {
129
+ set.delete(key);
130
+ if (set.size === 0) this.prefixEntries.delete(tag);
131
+ }
132
+ }
133
+ this.memory.delete(key);
134
+ if (this.persistence) {
135
+ void this.persistence.remove(this.persistKey(key));
136
+ }
137
+ }
138
+ /**
139
+ * Delete every entry whose key starts with `prefix`.
140
+ *
141
+ * Useful for fine-grained invalidation, e.g.:
142
+ * ```ts
143
+ * client.cache.deleteByPrefix('getList:posts'); // delete all getList cache
144
+ * client.cache.deleteByPrefix('getOne:posts'); // delete all getOne cache
145
+ * ```
146
+ */
147
+ deleteByPrefix(prefix) {
148
+ if (!prefix) return;
149
+ const tagged = this.prefixEntries.get(prefix);
150
+ if (tagged) {
151
+ for (const key of Array.from(tagged)) this.delete(key);
152
+ this.prefixEntries.delete(prefix);
153
+ return;
154
+ }
155
+ for (const key of Array.from(this.memory.keys())) {
156
+ if (key.startsWith(prefix)) this.delete(key);
157
+ }
158
+ }
159
+ /** Drop every cached entry (memory + persistence). */
160
+ clear() {
161
+ this.memory.clear();
162
+ this.namespaceEntries.clear();
163
+ this.prefixEntries.clear();
164
+ }
165
+ /** Cache hit/miss/entry statistics. */
166
+ stats() {
167
+ return { hits: this.hits, misses: this.misses, entries: this.memory.size };
168
+ }
169
+ persistKey(key) {
170
+ return "lazypock:cache:" + key;
171
+ }
172
+ async readPersisted(key) {
173
+ if (!this.persistence) return void 0;
174
+ const raw = await this.persistence.get(this.persistKey(key));
175
+ if (raw == null) return void 0;
176
+ try {
177
+ const entry = JSON.parse(raw);
178
+ this.memory.set(key, entry);
179
+ if (entry.namespace) {
180
+ let keys = this.namespaceEntries.get(entry.namespace);
181
+ if (!keys) {
182
+ keys = /* @__PURE__ */ new Set();
183
+ this.namespaceEntries.set(entry.namespace, keys);
184
+ }
185
+ keys.add(key);
186
+ }
187
+ for (const tag of entry.tags ?? []) {
188
+ let keys = this.prefixEntries.get(tag);
189
+ if (!keys) {
190
+ keys = /* @__PURE__ */ new Set();
191
+ this.prefixEntries.set(tag, keys);
192
+ }
193
+ keys.add(key);
194
+ }
195
+ return entry;
196
+ } catch {
197
+ void this.persistence.remove(this.persistKey(key));
198
+ return void 0;
199
+ }
200
+ }
201
+ };
202
+ function resolveCacheDirective(opts) {
203
+ if (!opts) return null;
204
+ if (typeof opts.ttl === "number" && opts.ttl > 0) {
205
+ return { enabled: true, ttl: opts.ttl };
206
+ }
207
+ const c = opts.cache;
208
+ if (c === void 0) return null;
209
+ if (c === true) return { enabled: true };
210
+ if (c === false) return { enabled: false };
211
+ if (typeof c === "number") return { enabled: true, ttl: c > 0 ? c : void 0 };
212
+ return { enabled: true, ttl: c.ttl, key: c.key };
213
+ }
214
+
20
215
  // src/http.ts
21
216
  function isAbortError(err) {
22
217
  return err instanceof Error && (err.name === "AbortError" || err.message === "Aborted");
@@ -27,18 +222,39 @@ var HttpClient = class {
27
222
  * @param authStore The auth store providing the token for Authorization headers.
28
223
  */
29
224
  constructor(baseUrl, authStore) {
225
+ /** Master switch resolved from CacheConfig.enabled. */
226
+ this.cacheEnabled = false;
30
227
  /**
31
228
  * Abort controllers for in-flight requests, keyed by their cancellation key
32
229
  * (default `METHOD path`). A new request with the same key aborts the
33
230
  * previous one — PocketBase-style auto-cancellation of duplicated requests.
34
231
  */
35
232
  this.cancelControllers = {};
233
+ /**
234
+ * In-flight request promises, keyed by cancellation key. When auto-cancellation
235
+ * would abort a pending duplicate, the newer request instead awaits the same
236
+ * promise — single-flight coalescing (no duplicate network request, no
237
+ * spurious abort rejection for the caller).
238
+ */
239
+ this.inflight = {};
36
240
  /** Global toggle for the auto-cancellation behaviour (default: on). */
37
241
  this.enableAutoCancellation = true;
38
242
  this.baseUrl = baseUrl.replace(/\/+$/, "");
39
243
  this.authStore = authStore;
40
244
  this.defaultFetch = globalThis.fetch.bind(globalThis);
41
245
  }
246
+ /**
247
+ * Attach a cache store + master switch.
248
+ * Called by the client constructor when cache config is present.
249
+ */
250
+ setCache(cache, enabled) {
251
+ this.cache = cache;
252
+ this.cacheEnabled = enabled;
253
+ }
254
+ /** Whether the global cache flag is on (requests opt in/out individually too). */
255
+ get cacheIsEnabled() {
256
+ return this.cacheEnabled;
257
+ }
42
258
  async refreshAuth() {
43
259
  const collection = this.authStore.collectionName;
44
260
  if (!collection) return null;
@@ -115,12 +331,33 @@ var HttpClient = class {
115
331
  * @throws {ApiError} On non-2xx responses or when the request is aborted
116
332
  * (aborted requests throw an `ApiError` with `isAbort === true`).
117
333
  */
334
+ /** Invalidate a namespace (collection name). No-op when cache is off. */
335
+ invalidateCache(namespace) {
336
+ this.cache?.invalidate(namespace);
337
+ }
338
+ /** Current cache statistics (hits/misses/entries), or null when disabled. */
339
+ cacheStats() {
340
+ return this.cache ? this.cache.stats() : null;
341
+ }
118
342
  async request(method, path, body, options) {
119
343
  if (this.authStore.isExpired && this.authStore.collectionName) {
120
344
  await this.refreshAuth();
121
345
  }
346
+ const cacheDirective = resolveCacheDirective(options);
347
+ const wantCache = cacheDirective !== null ? cacheDirective.enabled : this.cacheEnabled;
348
+ const cacheKey = method === "GET" && this.cache && wantCache ? this.cacheKeyFor(method, path, options?.params) : null;
349
+ if (cacheKey !== null) {
350
+ const hit = await this.cache?.get(cacheKey);
351
+ if (hit !== void 0) return hit;
352
+ }
122
353
  let requestKey = options?.requestKey === void 0 ? options?.cancelKey ?? `${method} ${path}` : options.requestKey;
123
354
  if (options?.autoCancel === false) requestKey = null;
355
+ if (options?.singleFlight && requestKey !== null) {
356
+ const pending = this.inflight[requestKey];
357
+ if (pending !== void 0) {
358
+ return pending;
359
+ }
360
+ }
124
361
  let controller = null;
125
362
  const externalSignal = options?.signal;
126
363
  if (requestKey !== null) {
@@ -138,6 +375,38 @@ var HttpClient = class {
138
375
  }
139
376
  }
140
377
  const signal = controller?.signal ?? externalSignal;
378
+ const perform = async () => {
379
+ try {
380
+ return await this.doRequest(
381
+ method,
382
+ path,
383
+ body,
384
+ options,
385
+ signal,
386
+ requestKey,
387
+ controller,
388
+ cacheKey,
389
+ cacheDirective
390
+ );
391
+ } finally {
392
+ if (requestKey !== null) {
393
+ if (this.inflight[requestKey] === promise) {
394
+ delete this.inflight[requestKey];
395
+ }
396
+ }
397
+ }
398
+ };
399
+ const promise = perform();
400
+ if (requestKey !== null) {
401
+ this.inflight[requestKey] = promise;
402
+ }
403
+ return promise;
404
+ }
405
+ /**
406
+ * Execute the actual HTTP request (fetch + parse + cache). Called by {@link request}
407
+ * as the inner in-flight unit so single-flight callers can reuse the promise.
408
+ */
409
+ async doRequest(method, path, body, options, signal, requestKey, controller, cacheKey, cacheDirective) {
141
410
  let url = this.baseUrl + path;
142
411
  if (options?.params) {
143
412
  const qs = new URLSearchParams(options.params).toString();
@@ -202,8 +471,63 @@ var HttpClient = class {
202
471
  res.status
203
472
  );
204
473
  }
474
+ if (cacheKey !== null && this.cache) {
475
+ const namespace = this.namespaceFromPath(path);
476
+ const ttl = cacheDirective?.ttl;
477
+ const tags = this.cacheTagsFor(path, namespace);
478
+ this.cache.set(
479
+ cacheKey,
480
+ data,
481
+ ttl,
482
+ namespace ?? void 0,
483
+ tags
484
+ );
485
+ }
486
+ if (method !== "GET" && this.cache) {
487
+ const namespaces = /* @__PURE__ */ new Set();
488
+ const ns = this.namespaceFromPath(path);
489
+ if (ns) namespaces.add(ns);
490
+ for (const extra of options?.invalidate ?? []) {
491
+ if (extra) namespaces.add(extra);
492
+ }
493
+ for (const nsName of namespaces) this.cache.invalidate(nsName);
494
+ }
205
495
  return data;
206
496
  }
497
+ // ── Cache key/namespace helpers ──
498
+ /** Build a token-scoped cache key: `METHOD path|token-hash|params`. */
499
+ cacheKeyFor(method, path, params) {
500
+ const token = this.authStore.token || "anon";
501
+ const qs = params ? "?" + new URLSearchParams(params).toString() : "";
502
+ return `${method} ${path}${qs}|${token}`;
503
+ }
504
+ /** Best-effort namespace (collection name) from a REST path. */
505
+ namespaceFromPath(path) {
506
+ const clean = path.split("?")[0];
507
+ const parts = clean.split("/").filter(Boolean);
508
+ if (parts.length === 0) return void 0;
509
+ if (parts[0] === "collections" || parts[0] === "_superusers") {
510
+ return parts[0];
511
+ }
512
+ return parts[0];
513
+ }
514
+ /**
515
+ * Semantic prefix tags for `deleteByPrefix`, derived from the REST shape:
516
+ * - `/{collection}?...` → `getList:{collection}`
517
+ * - `/{collection}/{id}` → `getOne:{collection}`
518
+ * - `/collections?...` / `/collections/{id}` → `collections:getList` / `collections:getOne`
519
+ */
520
+ cacheTagsFor(path, namespace) {
521
+ if (!namespace) return [];
522
+ const clean = path.split("?")[0];
523
+ const parts = clean.split("/").filter(Boolean);
524
+ if (parts[0] === "collections" || parts[0] === "_superusers") {
525
+ const op2 = parts.length >= 2 ? "getOne" : "getList";
526
+ return [`${namespace}:${op2}`];
527
+ }
528
+ const op = parts.length >= 2 ? "getOne" : "getList";
529
+ return [`${op}:${namespace}`];
530
+ }
207
531
  /**
208
532
  * HTTP GET.
209
533
  * @param path URL path.
@@ -379,6 +703,30 @@ var memoryStorage = {
379
703
  };
380
704
 
381
705
  // src/collection.ts
706
+ function stableStringify(value) {
707
+ const seen = /* @__PURE__ */ new Set();
708
+ const sort = (v) => {
709
+ if (Array.isArray(v)) return v.map(sort);
710
+ if (v && typeof v === "object") {
711
+ if (seen.has(v)) return "[Circular]";
712
+ seen.add(v);
713
+ const out = {};
714
+ for (const k of Object.keys(v).sort()) {
715
+ if (k === "requestKey" || k === "singleFlight" || k === "fetch" || k === "signal") {
716
+ continue;
717
+ }
718
+ out[k] = sort(v[k]);
719
+ }
720
+ return out;
721
+ }
722
+ return v;
723
+ };
724
+ try {
725
+ return JSON.stringify(sort(value));
726
+ } catch {
727
+ return String(value);
728
+ }
729
+ }
382
730
  function normalizeAction(event, rawAction) {
383
731
  if (typeof rawAction === "string") {
384
732
  const a = rawAction.toLowerCase();
@@ -410,19 +758,44 @@ var CollectionService = class {
410
758
  * @param options Query params (`filter`, `sort`, `expand`, `fields`) + request options.
411
759
  */
412
760
  getList(page = 1, perPage = 30, options) {
413
- const { requestKey, autoCancel, cancelKey, ...rest } = options ?? {};
761
+ const {
762
+ requestKey,
763
+ autoCancel,
764
+ cancelKey,
765
+ fetch,
766
+ headers,
767
+ signal,
768
+ cache,
769
+ ttl,
770
+ invalidate,
771
+ singleFlight,
772
+ params,
773
+ ...queryParams
774
+ } = options ?? {};
414
775
  const qs = new URLSearchParams(
415
776
  Object.fromEntries(
416
777
  Object.entries({
417
778
  page: String(page),
418
779
  perPage: String(perPage),
419
- ...rest
780
+ ...queryParams
420
781
  }).map(([k, v]) => [k, String(v)])
421
782
  )
422
783
  ).toString();
423
784
  return this.http.get(
424
785
  "/" + this.encodeId(this.collectionName) + "?" + qs,
425
- { requestKey, autoCancel, cancelKey }
786
+ {
787
+ requestKey,
788
+ autoCancel,
789
+ cancelKey,
790
+ fetch,
791
+ headers,
792
+ signal,
793
+ cache,
794
+ ttl,
795
+ invalidate,
796
+ singleFlight,
797
+ params
798
+ }
426
799
  );
427
800
  }
428
801
  /**
@@ -433,6 +806,7 @@ var CollectionService = class {
433
806
  */
434
807
  async getFullList(options) {
435
808
  const { batch = 1e3, ...rest } = options ?? {};
809
+ const effectiveKey = typeof rest.requestKey === "string" ? rest.requestKey : `getFullList:${this.collectionName}:${stableStringify(rest)}`;
436
810
  const items = [];
437
811
  let page = 1;
438
812
  for (; ; ) {
@@ -440,9 +814,9 @@ var CollectionService = class {
440
814
  page,
441
815
  batch,
442
816
  {
443
- // disable auto-cancellation across pages — each page request is unique
444
817
  ...rest,
445
- requestKey: null
818
+ requestKey: effectiveKey,
819
+ singleFlight: true
446
820
  }
447
821
  );
448
822
  if (!res || !res.items || res.items.length === 0) break;
@@ -965,14 +1339,40 @@ var CollectionsService = class {
965
1339
  async getFullList(options) {
966
1340
  if (!this.http) return [];
967
1341
  const { batch = 1e3, ...rest } = options ?? {};
1342
+ const {
1343
+ requestKey: reqKey,
1344
+ singleFlight: _singleFlight,
1345
+ fetch: fetchFn,
1346
+ headers: hdrs,
1347
+ signal: sig,
1348
+ cache: cacheOpt,
1349
+ ttl: ttlOpt,
1350
+ invalidate: inval,
1351
+ params: passthroughParams,
1352
+ ...queryParams
1353
+ } = rest;
1354
+ const effectiveKey = typeof reqKey === "string" ? reqKey : `getFullList:collections:${stableStringify(rest)}`;
968
1355
  const items = [];
969
1356
  let page = 1;
970
1357
  for (; ; ) {
971
- const res = await this.getList({
972
- ...rest,
973
- page,
974
- perPage: batch
975
- });
1358
+ const res = await this.getList(
1359
+ {
1360
+ ...queryParams,
1361
+ ...passthroughParams ?? {},
1362
+ page,
1363
+ perPage: batch
1364
+ },
1365
+ {
1366
+ requestKey: effectiveKey,
1367
+ singleFlight: true,
1368
+ ...fetchFn ? { fetch: fetchFn } : {},
1369
+ ...hdrs ? { headers: hdrs } : {},
1370
+ ...sig ? { signal: sig } : {},
1371
+ ...cacheOpt !== void 0 ? { cache: cacheOpt } : {},
1372
+ ...ttlOpt !== void 0 ? { ttl: ttlOpt } : {},
1373
+ ...inval ? { invalidate: inval } : {}
1374
+ }
1375
+ );
976
1376
  if (!res || !res.items || res.items.length === 0) break;
977
1377
  items.push(...res.items);
978
1378
  if (page >= (res.totalPages ?? page)) break;
@@ -1071,6 +1471,50 @@ var LazypockClient = class {
1071
1471
  */
1072
1472
  constructor(options) {
1073
1473
  this.collectionCache = /* @__PURE__ */ new Map();
1474
+ /** Namespace → realtime unsubscribe; used for realtime-driven invalidation. */
1475
+ this.realtimeInvalidators = /* @__PURE__ */ new Map();
1476
+ // ── Query cache (opt-in by default; opt-out per request) ──
1477
+ /**
1478
+ * Configure the query cache at runtime (also a namespace for cache
1479
+ * management methods).
1480
+ *
1481
+ * ```ts
1482
+ * client.cache({ enabled: true, defaultTTL: 30_000 });
1483
+ * client.cache.deleteByPrefix('getList:posts'); // all list caches for posts
1484
+ * client.cache.deleteByPrefix('getOne:posts'); // all one-record caches
1485
+ * ```
1486
+ *
1487
+ * When enabled, GET requests cache their payload; mutations invalidate the
1488
+ * affected collection automatically. Individual requests can opt out with
1489
+ * `{ cache: false }` or override the TTL with `{ ttl: ms }`.
1490
+ */
1491
+ this.cache = Object.assign(
1492
+ ((config) => {
1493
+ if (!this.cacheStore) {
1494
+ this.cacheStore = new CacheStore({
1495
+ defaultTTL: config?.defaultTTL,
1496
+ store: config?.store,
1497
+ maxEntries: config?.maxEntries
1498
+ });
1499
+ this.http.setCache(this.cacheStore, config?.enabled ?? true);
1500
+ } else {
1501
+ if (config?.enabled !== void 0) {
1502
+ this.http.setCache(this.cacheStore, config.enabled);
1503
+ }
1504
+ }
1505
+ return this;
1506
+ }),
1507
+ {
1508
+ deleteByPrefix: (prefix) => this.cacheStore?.deleteByPrefix(prefix),
1509
+ invalidate: (namespace) => this.cacheStore?.invalidate(namespace),
1510
+ clear: () => {
1511
+ this.cacheStore?.clear();
1512
+ for (const unsub of this.realtimeInvalidators.values()) unsub();
1513
+ this.realtimeInvalidators.clear();
1514
+ },
1515
+ stats: () => this.cacheStore ? this.cacheStore.stats() : null
1516
+ }
1517
+ );
1074
1518
  const baseUrl = options.baseUrl.replace(/\/+$/, "");
1075
1519
  this.authStore = options.authStore ?? new AuthStore(options.storage ?? memoryStorage);
1076
1520
  this.http = new HttpClient(baseUrl, this.authStore);
@@ -1080,6 +1524,14 @@ var LazypockClient = class {
1080
1524
  }
1081
1525
  this.files = new FilesService(this.http);
1082
1526
  this.collections = new CollectionsService(this.http, this.realtime);
1527
+ if (options.cache) {
1528
+ this.cacheStore = new CacheStore({
1529
+ defaultTTL: options.cache.defaultTTL,
1530
+ store: options.cache.store,
1531
+ maxEntries: options.cache.maxEntries
1532
+ });
1533
+ this.http.setCache(this.cacheStore, options.cache.enabled ?? false);
1534
+ }
1083
1535
  if (options.types?.schemas) {
1084
1536
  this.schemaByName = new Map(
1085
1537
  options.types.schemas.map((s) => [s.name, s])
@@ -1160,6 +1612,49 @@ var LazypockClient = class {
1160
1612
  this.http.cancelAllRequests();
1161
1613
  return this;
1162
1614
  }
1615
+ /**
1616
+ * Drop every cached entry (all collections / namespaces).
1617
+ * Also disables realtime-driven invalidation subscriptions.
1618
+ */
1619
+ clearCache() {
1620
+ this.cacheStore?.clear();
1621
+ for (const unsub of this.realtimeInvalidators.values()) unsub();
1622
+ this.realtimeInvalidators.clear();
1623
+ return this;
1624
+ }
1625
+ /**
1626
+ * Invalidate cached entries for a collection (or custom namespace).
1627
+ * Runs automatically on mutations — call explicitly when data changed
1628
+ * out-of-band (e.g. another client wrote to the same collection).
1629
+ */
1630
+ invalidateCache(namespace) {
1631
+ this.cacheStore?.invalidate(namespace);
1632
+ return this;
1633
+ }
1634
+ /**
1635
+ * Cache hit/miss/entry statistics.
1636
+ * Returns null when caching was never configured.
1637
+ */
1638
+ cacheStats() {
1639
+ return this.cacheStore ? this.cacheStore.stats() : null;
1640
+ }
1641
+ /**
1642
+ * Subscribe a collection's cache to realtime invalidation: any inbound
1643
+ * create/update/delete event for the collection clears its cached entries.
1644
+ * Returns an unsubscribe function.
1645
+ */
1646
+ invalidateCacheOnRealtime(collectionName) {
1647
+ if (!this.cacheStore) {
1648
+ this.cache({ enabled: false });
1649
+ }
1650
+ const existing = this.realtimeInvalidators.get(collectionName);
1651
+ if (existing) return existing;
1652
+ const unsub = this.collection(collectionName).subscribe(() => {
1653
+ this.cacheStore?.invalidate(collectionName);
1654
+ });
1655
+ this.realtimeInvalidators.set(collectionName, unsub);
1656
+ return unsub;
1657
+ }
1163
1658
  // ── Auth ──
1164
1659
  /** Check whether any superuser exists (for login vs setup screen routing). */
1165
1660
  async checkSuperuser() {
@@ -1296,6 +1791,7 @@ function createClient(options) {
1296
1791
  export {
1297
1792
  ApiError,
1298
1793
  AuthStore,
1794
+ CacheStore,
1299
1795
  CollectionService,
1300
1796
  CollectionsService,
1301
1797
  FilesService,
@@ -1311,6 +1807,7 @@ export {
1311
1807
  getFileUrl,
1312
1808
  getScaleUrl,
1313
1809
  getThumbUrl,
1810
+ resolveCacheDirective,
1314
1811
  schemaFieldType,
1315
1812
  wsUrlFromBaseUrl
1316
1813
  };