lazypock 0.3.0 → 0.5.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,7 +22,6 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  ApiError: () => ApiError,
24
24
  AuthStore: () => AuthStore,
25
- CacheStore: () => CacheStore,
26
25
  CollectionService: () => CollectionService,
27
26
  CollectionsService: () => CollectionsService,
28
27
  FilesService: () => FilesService,
@@ -38,7 +37,6 @@ __export(index_exports, {
38
37
  getFileUrl: () => getFileUrl,
39
38
  getScaleUrl: () => getScaleUrl,
40
39
  getThumbUrl: () => getThumbUrl,
41
- resolveCacheDirective: () => resolveCacheDirective,
42
40
  schemaFieldType: () => schemaFieldType,
43
41
  wsUrlFromBaseUrl: () => wsUrlFromBaseUrl
44
42
  });
@@ -55,201 +53,6 @@ var ApiError = class extends Error {
55
53
  }
56
54
  };
57
55
 
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
-
253
56
  // src/http.ts
254
57
  function isAbortError(err) {
255
58
  return err instanceof Error && (err.name === "AbortError" || err.message === "Aborted");
@@ -260,32 +63,25 @@ var HttpClient = class {
260
63
  * @param authStore The auth store providing the token for Authorization headers.
261
64
  */
262
65
  constructor(baseUrl, authStore) {
263
- /** Master switch resolved from CacheConfig.enabled. */
264
- this.cacheEnabled = false;
265
66
  /**
266
67
  * Abort controllers for in-flight requests, keyed by their cancellation key
267
68
  * (default `METHOD path`). A new request with the same key aborts the
268
69
  * previous one — PocketBase-style auto-cancellation of duplicated requests.
269
70
  */
270
71
  this.cancelControllers = {};
72
+ /**
73
+ * In-flight request promises, keyed by cancellation key. When auto-cancellation
74
+ * would abort a pending duplicate, the newer request instead awaits the same
75
+ * promise — single-flight coalescing (no duplicate network request, no
76
+ * spurious abort rejection for the caller).
77
+ */
78
+ this.inflight = {};
271
79
  /** Global toggle for the auto-cancellation behaviour (default: on). */
272
80
  this.enableAutoCancellation = true;
273
81
  this.baseUrl = baseUrl.replace(/\/+$/, "");
274
82
  this.authStore = authStore;
275
83
  this.defaultFetch = globalThis.fetch.bind(globalThis);
276
84
  }
277
- /**
278
- * Attach a cache store + master switch.
279
- * Called by the client constructor when cache config is present.
280
- */
281
- setCache(cache, enabled) {
282
- this.cache = cache;
283
- this.cacheEnabled = enabled;
284
- }
285
- /** Whether the global cache flag is on (requests opt in/out individually too). */
286
- get cacheIsEnabled() {
287
- return this.cacheEnabled;
288
- }
289
85
  async refreshAuth() {
290
86
  const collection = this.authStore.collectionName;
291
87
  if (!collection) return null;
@@ -362,27 +158,18 @@ var HttpClient = class {
362
158
  * @throws {ApiError} On non-2xx responses or when the request is aborted
363
159
  * (aborted requests throw an `ApiError` with `isAbort === true`).
364
160
  */
365
- /** Invalidate a namespace (collection name). No-op when cache is off. */
366
- invalidateCache(namespace) {
367
- this.cache?.invalidate(namespace);
368
- }
369
- /** Current cache statistics (hits/misses/entries), or null when disabled. */
370
- cacheStats() {
371
- return this.cache ? this.cache.stats() : null;
372
- }
373
161
  async request(method, path, body, options) {
374
162
  if (this.authStore.isExpired && this.authStore.collectionName) {
375
163
  await this.refreshAuth();
376
164
  }
377
- const cacheDirective = resolveCacheDirective(options);
378
- const wantCache = cacheDirective !== null ? cacheDirective.enabled : this.cacheEnabled;
379
- const cacheKey = method === "GET" && this.cache && wantCache ? this.cacheKeyFor(method, path, options?.params) : null;
380
- if (cacheKey !== null) {
381
- const hit = await this.cache?.get(cacheKey);
382
- if (hit !== void 0) return hit;
383
- }
384
165
  let requestKey = options?.requestKey === void 0 ? options?.cancelKey ?? `${method} ${path}` : options.requestKey;
385
166
  if (options?.autoCancel === false) requestKey = null;
167
+ if (options?.singleFlight && requestKey !== null) {
168
+ const pending = this.inflight[requestKey];
169
+ if (pending !== void 0) {
170
+ return pending;
171
+ }
172
+ }
386
173
  let controller = null;
387
174
  const externalSignal = options?.signal;
388
175
  if (requestKey !== null) {
@@ -400,6 +187,36 @@ var HttpClient = class {
400
187
  }
401
188
  }
402
189
  const signal = controller?.signal ?? externalSignal;
190
+ const perform = async () => {
191
+ try {
192
+ return await this.doRequest(
193
+ method,
194
+ path,
195
+ body,
196
+ options,
197
+ signal,
198
+ requestKey,
199
+ controller
200
+ );
201
+ } finally {
202
+ if (requestKey !== null) {
203
+ if (this.inflight[requestKey] === promise) {
204
+ delete this.inflight[requestKey];
205
+ }
206
+ }
207
+ }
208
+ };
209
+ const promise = perform();
210
+ if (requestKey !== null) {
211
+ this.inflight[requestKey] = promise;
212
+ }
213
+ return promise;
214
+ }
215
+ /**
216
+ * Execute the actual HTTP request (fetch + parse). Called by {@link request}
217
+ * as the inner in-flight unit so single-flight callers can reuse the promise.
218
+ */
219
+ async doRequest(method, path, body, options, signal, requestKey, controller) {
403
220
  let url = this.baseUrl + path;
404
221
  if (options?.params) {
405
222
  const qs = new URLSearchParams(options.params).toString();
@@ -464,63 +281,8 @@ var HttpClient = class {
464
281
  res.status
465
282
  );
466
283
  }
467
- if (cacheKey !== null && this.cache) {
468
- const namespace = this.namespaceFromPath(path);
469
- const ttl = cacheDirective?.ttl;
470
- const tags = this.cacheTagsFor(path, namespace);
471
- this.cache.set(
472
- cacheKey,
473
- data,
474
- ttl,
475
- namespace ?? void 0,
476
- tags
477
- );
478
- }
479
- if (method !== "GET" && this.cache) {
480
- const namespaces = /* @__PURE__ */ new Set();
481
- const ns = this.namespaceFromPath(path);
482
- if (ns) namespaces.add(ns);
483
- for (const extra of options?.invalidate ?? []) {
484
- if (extra) namespaces.add(extra);
485
- }
486
- for (const nsName of namespaces) this.cache.invalidate(nsName);
487
- }
488
284
  return data;
489
285
  }
490
- // ── Cache key/namespace helpers ──
491
- /** Build a token-scoped cache key: `METHOD path|token-hash|params`. */
492
- cacheKeyFor(method, path, params) {
493
- const token = this.authStore.token || "anon";
494
- const qs = params ? "?" + new URLSearchParams(params).toString() : "";
495
- return `${method} ${path}${qs}|${token}`;
496
- }
497
- /** Best-effort namespace (collection name) from a REST path. */
498
- namespaceFromPath(path) {
499
- const clean = path.split("?")[0];
500
- const parts = clean.split("/").filter(Boolean);
501
- if (parts.length === 0) return void 0;
502
- if (parts[0] === "collections" || parts[0] === "_superusers") {
503
- return parts[0];
504
- }
505
- return parts[0];
506
- }
507
- /**
508
- * Semantic prefix tags for `deleteByPrefix`, derived from the REST shape:
509
- * - `/{collection}?...` → `getList:{collection}`
510
- * - `/{collection}/{id}` → `getOne:{collection}`
511
- * - `/collections?...` / `/collections/{id}` → `collections:getList` / `collections:getOne`
512
- */
513
- cacheTagsFor(path, namespace) {
514
- if (!namespace) return [];
515
- const clean = path.split("?")[0];
516
- const parts = clean.split("/").filter(Boolean);
517
- if (parts[0] === "collections" || parts[0] === "_superusers") {
518
- const op2 = parts.length >= 2 ? "getOne" : "getList";
519
- return [`${namespace}:${op2}`];
520
- }
521
- const op = parts.length >= 2 ? "getOne" : "getList";
522
- return [`${op}:${namespace}`];
523
- }
524
286
  /**
525
287
  * HTTP GET.
526
288
  * @param path URL path.
@@ -696,6 +458,30 @@ var memoryStorage = {
696
458
  };
697
459
 
698
460
  // src/collection.ts
461
+ function stableStringify(value) {
462
+ const seen = /* @__PURE__ */ new Set();
463
+ const sort = (v) => {
464
+ if (Array.isArray(v)) return v.map(sort);
465
+ if (v && typeof v === "object") {
466
+ if (seen.has(v)) return "[Circular]";
467
+ seen.add(v);
468
+ const out = {};
469
+ for (const k of Object.keys(v).sort()) {
470
+ if (k === "requestKey" || k === "singleFlight" || k === "fetch" || k === "signal") {
471
+ continue;
472
+ }
473
+ out[k] = sort(v[k]);
474
+ }
475
+ return out;
476
+ }
477
+ return v;
478
+ };
479
+ try {
480
+ return JSON.stringify(sort(value));
481
+ } catch {
482
+ return String(value);
483
+ }
484
+ }
699
485
  function normalizeAction(event, rawAction) {
700
486
  if (typeof rawAction === "string") {
701
487
  const a = rawAction.toLowerCase();
@@ -708,17 +494,104 @@ function normalizeAction(event, rawAction) {
708
494
  if (event === "delete" || event === "record_delete") return "delete";
709
495
  return "update";
710
496
  }
711
- var CollectionService = class {
497
+ var CollectionService = class _CollectionService {
712
498
  /** @internal */
713
- constructor(http, collectionName, authStore, realtime) {
499
+ constructor(http, collectionName, authStore, realtime, schema) {
714
500
  this.http = http;
715
501
  this.collectionName = collectionName;
716
502
  this.authStore = authStore;
717
503
  this.realtime = realtime;
504
+ this.schema = schema;
718
505
  }
719
506
  encodeId(id) {
720
507
  return encodeURIComponent(id);
721
508
  }
509
+ // ── Field projection (`.select`) ──
510
+ /**
511
+ * Project list/read responses to the given fields (PocketBase `fields`
512
+ * param). Field names are type-checked when this service is typed
513
+ * (codegen / `typed<T>()`).
514
+ *
515
+ * Returns a derived service — the original is untouched.
516
+ *
517
+ * @example
518
+ * ```ts
519
+ * const t = await client.collection("posts").select("id", "title").getList();
520
+ * // GET /api/posts?fields=id,title
521
+ *
522
+ * // All visible fields (hidden fields are excluded server-side)
523
+ * const all = await client.collection("posts").select("*").getList();
524
+ * // GET /api/posts?fields=id,title,published,… (schema known)
525
+ * ```
526
+ *
527
+ * When no schema is known, the default (no `select()` call) sends no
528
+ * `fields` param — the server returns every non-password field.
529
+ * Pass `select("*")` to restore the default after a projection.
530
+ */
531
+ select(...fields) {
532
+ const preset = fields.length === 0 ? "*" : fields.join(",");
533
+ const derived = new _CollectionService(
534
+ this.http,
535
+ this.collectionName,
536
+ this.authStore,
537
+ this.realtime,
538
+ this.schema
539
+ );
540
+ derived.fieldsPreset = preset;
541
+ if (this.schema && preset !== "*") {
542
+ const known = new Set(this.schema.fields?.map((f) => f.name) ?? []);
543
+ for (const f of fields) {
544
+ if (!known.has(f)) {
545
+ console.warn(
546
+ `[lazypock] select("${f}"): unknown field on collection "${this.collectionName}"`
547
+ );
548
+ }
549
+ }
550
+ }
551
+ return derived;
552
+ }
553
+ /**
554
+ * Fields to send with reads:
555
+ * 1. explicit `options.fields` (caller wins)
556
+ * 2. `select()` preset
557
+ * 3. schema default — all visible (non-hidden, non-system) fields
558
+ */
559
+ effectiveFields(optionsFields) {
560
+ if (optionsFields !== void 0) return optionsFields;
561
+ if (this.fieldsPreset !== void 0) {
562
+ if (this.fieldsPreset === "*") {
563
+ return this.visibleFields();
564
+ }
565
+ return this.fieldsPreset;
566
+ }
567
+ return this.visibleFields();
568
+ }
569
+ /**
570
+ * All non-hidden, non-system, non-password field names from the schema.
571
+ * `undefined` when no schema is available (server decides).
572
+ */
573
+ visibleFields() {
574
+ if (!this.schema?.fields) return void 0;
575
+ const visible = this.schema.fields.filter(
576
+ (f) => !f.hidden && f.type !== "password" && f.type !== "autodate"
577
+ ).map((f) => f.name);
578
+ return visible.length > 0 ? visible.join(",") : void 0;
579
+ }
580
+ /** Warn once when an expand field is not a relation (schema known). */
581
+ validateExpand(expand) {
582
+ if (!expand || !this.schema?.fields) return;
583
+ const relations = new Set(
584
+ this.schema.fields.filter((f) => f.type === "relation").map((f) => f.name)
585
+ );
586
+ for (const f of expand.split(",")) {
587
+ const name = f.trim();
588
+ if (name && !relations.has(name)) {
589
+ console.warn(
590
+ `[lazypock] expand("${name}"): field is not a relation on collection "${this.collectionName}"`
591
+ );
592
+ }
593
+ }
594
+ }
722
595
  /**
723
596
  * Fetch a paginated list of records (PocketBase `getList`).
724
597
  *
@@ -734,12 +607,17 @@ var CollectionService = class {
734
607
  fetch,
735
608
  headers,
736
609
  signal,
737
- cache,
738
- ttl,
739
- invalidate,
610
+ singleFlight,
740
611
  params,
741
612
  ...queryParams
742
613
  } = options ?? {};
614
+ const fields = this.effectiveFields(queryParams.fields);
615
+ if (fields !== void 0 && queryParams.fields === void 0) {
616
+ queryParams.fields = fields;
617
+ }
618
+ if (typeof queryParams.expand === "string") {
619
+ this.validateExpand(queryParams.expand);
620
+ }
743
621
  const qs = new URLSearchParams(
744
622
  Object.fromEntries(
745
623
  Object.entries({
@@ -758,9 +636,7 @@ var CollectionService = class {
758
636
  fetch,
759
637
  headers,
760
638
  signal,
761
- cache,
762
- ttl,
763
- invalidate,
639
+ singleFlight,
764
640
  params
765
641
  }
766
642
  );
@@ -773,6 +649,7 @@ var CollectionService = class {
773
649
  */
774
650
  async getFullList(options) {
775
651
  const { batch = 1e3, ...rest } = options ?? {};
652
+ const effectiveKey = typeof rest.requestKey === "string" ? rest.requestKey : `getFullList:${this.collectionName}:${stableStringify(rest)}`;
776
653
  const items = [];
777
654
  let page = 1;
778
655
  for (; ; ) {
@@ -780,9 +657,9 @@ var CollectionService = class {
780
657
  page,
781
658
  batch,
782
659
  {
783
- // disable auto-cancellation across pages — each page request is unique
784
660
  ...rest,
785
- requestKey: null
661
+ requestKey: effectiveKey,
662
+ singleFlight: true
786
663
  }
787
664
  );
788
665
  if (!res || !res.items || res.items.length === 0) break;
@@ -811,8 +688,16 @@ var CollectionService = class {
811
688
  * @param options Optional request options.
812
689
  */
813
690
  getOne(id, options) {
691
+ const fields = this.effectiveFields(options?.fields);
692
+ if (typeof options?.expand === "string") {
693
+ this.validateExpand(options.expand);
694
+ }
695
+ const qs = new URLSearchParams();
696
+ if (fields !== void 0) qs.set("fields", fields);
697
+ if (typeof options?.expand === "string") qs.set("expand", options.expand);
698
+ const qsStr = qs.toString();
814
699
  return this.http.get(
815
- "/" + this.encodeId(this.collectionName) + "/" + this.encodeId(id),
700
+ "/" + this.encodeId(this.collectionName) + "/" + this.encodeId(id) + (qsStr ? "?" + qsStr : ""),
816
701
  options
817
702
  );
818
703
  }
@@ -887,6 +772,22 @@ var CollectionService = class {
887
772
  typed() {
888
773
  return this;
889
774
  }
775
+ /**
776
+ * Explicitly bind a schema for this service (overrides the client-level
777
+ * `types.schemas`). Enables schema-aware defaults: hidden fields are
778
+ * excluded from responses, select/expand are validated.
779
+ */
780
+ withSchema(schema) {
781
+ const derived = new _CollectionService(
782
+ this.http,
783
+ this.collectionName,
784
+ this.authStore,
785
+ this.realtime,
786
+ schema
787
+ );
788
+ derived.fieldsPreset = this.fieldsPreset;
789
+ return derived;
790
+ }
890
791
  // ── Realtime Subscriptions (PocketBase-style) ──
891
792
  /**
892
793
  * Subscribe to realtime changes for this collection.
@@ -1305,14 +1206,34 @@ var CollectionsService = class {
1305
1206
  async getFullList(options) {
1306
1207
  if (!this.http) return [];
1307
1208
  const { batch = 1e3, ...rest } = options ?? {};
1209
+ const {
1210
+ requestKey: reqKey,
1211
+ singleFlight: _singleFlight,
1212
+ fetch: fetchFn,
1213
+ headers: hdrs,
1214
+ signal: sig,
1215
+ params: passthroughParams,
1216
+ ...queryParams
1217
+ } = rest;
1218
+ const effectiveKey = typeof reqKey === "string" ? reqKey : `getFullList:collections:${stableStringify(rest)}`;
1308
1219
  const items = [];
1309
1220
  let page = 1;
1310
1221
  for (; ; ) {
1311
- const res = await this.getList({
1312
- ...rest,
1313
- page,
1314
- perPage: batch
1315
- });
1222
+ const res = await this.getList(
1223
+ {
1224
+ ...queryParams,
1225
+ ...passthroughParams ?? {},
1226
+ page,
1227
+ perPage: batch
1228
+ },
1229
+ {
1230
+ requestKey: effectiveKey,
1231
+ singleFlight: true,
1232
+ ...fetchFn ? { fetch: fetchFn } : {},
1233
+ ...hdrs ? { headers: hdrs } : {},
1234
+ ...sig ? { signal: sig } : {}
1235
+ }
1236
+ );
1316
1237
  if (!res || !res.items || res.items.length === 0) break;
1317
1238
  items.push(...res.items);
1318
1239
  if (page >= (res.totalPages ?? page)) break;
@@ -1551,15 +1472,39 @@ function generateTypes(collections, options = {}) {
1551
1472
  sections.push(`export interface LazypockCollections {
1552
1473
  ${mapEntries}
1553
1474
  }`);
1475
+ const schemaEntries = collections.map(
1476
+ (c) => ` {
1477
+ id: ${JSON.stringify(c.id ?? null)},
1478
+ name: ${JSON.stringify(c.name)},
1479
+ type: ${JSON.stringify(c.type)},
1480
+ system: ${JSON.stringify(c.system ?? false)},
1481
+ fields: ${JSON.stringify(c.fields ?? [], null, 2)},
1482
+ rules: ${JSON.stringify(c.rules ?? {})},
1483
+ options: ${JSON.stringify(c.options ?? {})},
1484
+ }`
1485
+ ).join(",\n");
1486
+ sections.push(`// Schema snapshot (runtime) \u2014 hidden-field exclusion + query validation.
1487
+ export const lazypockSchema: import("${packageName}").CollectionSchema[] = [
1488
+ ${schemaEntries}
1489
+ ];`);
1554
1490
  sections.push(`import { LazypockClient, type LazypockClientOptions, type CollectionService } from "${packageName}";
1555
1491
 
1556
1492
  /**
1557
1493
  * Create a Lazypock client typed against this schema snapshot.
1558
1494
  * Collection access is fully type-checked:
1559
1495
  * client.collection("posts").create({ title: "x" }) // title must exist
1496
+ *
1497
+ * The schema snapshot is wired into the client automatically, so hidden
1498
+ * fields are excluded from responses and select/expand are validated.
1560
1499
  */
1561
1500
  export function createClient(options: LazypockClientOptions): TypedClient {
1562
- return new TypedClient(options);
1501
+ return new TypedClient({
1502
+ ...options,
1503
+ types: {
1504
+ ...options.types,
1505
+ schemas: options.types?.schemas ?? lazypockSchema,
1506
+ },
1507
+ });
1563
1508
  }
1564
1509
 
1565
1510
  export class TypedClient extends LazypockClient {
@@ -1580,6 +1525,7 @@ ${opts.body}
1580
1525
  }`;
1581
1526
  }
1582
1527
  function memberLine(f) {
1528
+ if (f.hidden) return "";
1583
1529
  const key = fieldKey(f.name);
1584
1530
  const req = f.required || f.type === "password" ? "" : "?";
1585
1531
  const type = fieldTypeScriptType(f);
@@ -1595,50 +1541,6 @@ var LazypockClient = class {
1595
1541
  */
1596
1542
  constructor(options) {
1597
1543
  this.collectionCache = /* @__PURE__ */ new Map();
1598
- /** Namespace → realtime unsubscribe; used for realtime-driven invalidation. */
1599
- this.realtimeInvalidators = /* @__PURE__ */ new Map();
1600
- // ── Query cache (opt-in by default; opt-out per request) ──
1601
- /**
1602
- * Configure the query cache at runtime (also a namespace for cache
1603
- * management methods).
1604
- *
1605
- * ```ts
1606
- * client.cache({ enabled: true, defaultTTL: 30_000 });
1607
- * client.cache.deleteByPrefix('getList:posts'); // all list caches for posts
1608
- * client.cache.deleteByPrefix('getOne:posts'); // all one-record caches
1609
- * ```
1610
- *
1611
- * When enabled, GET requests cache their payload; mutations invalidate the
1612
- * affected collection automatically. Individual requests can opt out with
1613
- * `{ cache: false }` or override the TTL with `{ ttl: ms }`.
1614
- */
1615
- this.cache = Object.assign(
1616
- ((config) => {
1617
- if (!this.cacheStore) {
1618
- this.cacheStore = new CacheStore({
1619
- defaultTTL: config?.defaultTTL,
1620
- store: config?.store,
1621
- maxEntries: config?.maxEntries
1622
- });
1623
- this.http.setCache(this.cacheStore, config?.enabled ?? true);
1624
- } else {
1625
- if (config?.enabled !== void 0) {
1626
- this.http.setCache(this.cacheStore, config.enabled);
1627
- }
1628
- }
1629
- return this;
1630
- }),
1631
- {
1632
- deleteByPrefix: (prefix) => this.cacheStore?.deleteByPrefix(prefix),
1633
- invalidate: (namespace) => this.cacheStore?.invalidate(namespace),
1634
- clear: () => {
1635
- this.cacheStore?.clear();
1636
- for (const unsub of this.realtimeInvalidators.values()) unsub();
1637
- this.realtimeInvalidators.clear();
1638
- },
1639
- stats: () => this.cacheStore ? this.cacheStore.stats() : null
1640
- }
1641
- );
1642
1544
  const baseUrl = options.baseUrl.replace(/\/+$/, "");
1643
1545
  this.authStore = options.authStore ?? new AuthStore(options.storage ?? memoryStorage);
1644
1546
  this.http = new HttpClient(baseUrl, this.authStore);
@@ -1648,14 +1550,6 @@ var LazypockClient = class {
1648
1550
  }
1649
1551
  this.files = new FilesService(this.http);
1650
1552
  this.collections = new CollectionsService(this.http, this.realtime);
1651
- if (options.cache) {
1652
- this.cacheStore = new CacheStore({
1653
- defaultTTL: options.cache.defaultTTL,
1654
- store: options.cache.store,
1655
- maxEntries: options.cache.maxEntries
1656
- });
1657
- this.http.setCache(this.cacheStore, options.cache.enabled ?? false);
1658
- }
1659
1553
  if (options.types?.schemas) {
1660
1554
  this.schemaByName = new Map(
1661
1555
  options.types.schemas.map((s) => [s.name, s])
@@ -1673,14 +1567,16 @@ var LazypockClient = class {
1673
1567
  * @param name The collection name.
1674
1568
  * @returns A {@link CollectionService} instance.
1675
1569
  */
1676
- collection(name) {
1570
+ collection(name, schema) {
1677
1571
  let svc = this.collectionCache.get(name);
1678
1572
  if (!svc) {
1679
1573
  svc = new CollectionService(
1680
1574
  this.http,
1681
1575
  name,
1682
1576
  this.authStore,
1683
- this.realtime
1577
+ this.realtime,
1578
+ // Prefer the explicit schema, then the client-level `types.schemas`.
1579
+ schema ?? this.schemaByName?.get(name)
1684
1580
  );
1685
1581
  this.collectionCache.set(name, svc);
1686
1582
  }
@@ -1736,49 +1632,6 @@ var LazypockClient = class {
1736
1632
  this.http.cancelAllRequests();
1737
1633
  return this;
1738
1634
  }
1739
- /**
1740
- * Drop every cached entry (all collections / namespaces).
1741
- * Also disables realtime-driven invalidation subscriptions.
1742
- */
1743
- clearCache() {
1744
- this.cacheStore?.clear();
1745
- for (const unsub of this.realtimeInvalidators.values()) unsub();
1746
- this.realtimeInvalidators.clear();
1747
- return this;
1748
- }
1749
- /**
1750
- * Invalidate cached entries for a collection (or custom namespace).
1751
- * Runs automatically on mutations — call explicitly when data changed
1752
- * out-of-band (e.g. another client wrote to the same collection).
1753
- */
1754
- invalidateCache(namespace) {
1755
- this.cacheStore?.invalidate(namespace);
1756
- return this;
1757
- }
1758
- /**
1759
- * Cache hit/miss/entry statistics.
1760
- * Returns null when caching was never configured.
1761
- */
1762
- cacheStats() {
1763
- return this.cacheStore ? this.cacheStore.stats() : null;
1764
- }
1765
- /**
1766
- * Subscribe a collection's cache to realtime invalidation: any inbound
1767
- * create/update/delete event for the collection clears its cached entries.
1768
- * Returns an unsubscribe function.
1769
- */
1770
- invalidateCacheOnRealtime(collectionName) {
1771
- if (!this.cacheStore) {
1772
- this.cache({ enabled: false });
1773
- }
1774
- const existing = this.realtimeInvalidators.get(collectionName);
1775
- if (existing) return existing;
1776
- const unsub = this.collection(collectionName).subscribe(() => {
1777
- this.cacheStore?.invalidate(collectionName);
1778
- });
1779
- this.realtimeInvalidators.set(collectionName, unsub);
1780
- return unsub;
1781
- }
1782
1635
  // ── Auth ──
1783
1636
  /** Check whether any superuser exists (for login vs setup screen routing). */
1784
1637
  async checkSuperuser() {
@@ -1916,7 +1769,6 @@ function createClient(options) {
1916
1769
  0 && (module.exports = {
1917
1770
  ApiError,
1918
1771
  AuthStore,
1919
- CacheStore,
1920
1772
  CollectionService,
1921
1773
  CollectionsService,
1922
1774
  FilesService,
@@ -1932,7 +1784,6 @@ function createClient(options) {
1932
1784
  getFileUrl,
1933
1785
  getScaleUrl,
1934
1786
  getThumbUrl,
1935
- resolveCacheDirective,
1936
1787
  schemaFieldType,
1937
1788
  wsUrlFromBaseUrl
1938
1789
  });