lazypock 0.2.0 → 0.3.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/src/lazypock.ts CHANGED
@@ -32,6 +32,8 @@ import { CollectionsService } from "./collections";
32
32
  import type { CollectionSchema, SchemaField } from "./schema";
33
33
  import { generateTypes, collectionTypeName } from "./codegen";
34
34
  import { fieldTypeScriptType, fieldTypeKind, schemaFieldType } from "./typegen";
35
+ import { CacheStore, type CacheConfig, type CacheRequestOptions } from "./cache";
36
+ import { resolveCacheDirective } from "./cache";
35
37
 
36
38
  export {
37
39
  AuthStore,
@@ -65,6 +67,25 @@ export type {
65
67
  CollectionSchema,
66
68
  SchemaField,
67
69
  };
70
+ export type { CacheConfig, CacheRequestOptions };
71
+ export { CacheStore, resolveCacheDirective };
72
+
73
+ /**
74
+ * Callable cache namespace: `client.cache(config)` configures, and
75
+ * `client.cache.deleteByPrefix(...)` etc. manage cached entries.
76
+ */
77
+ export interface CacheController {
78
+ /** Configure the query cache at runtime. */
79
+ (config?: CacheConfig): LazypockClient;
80
+ /** Delete every entry whose key starts with `prefix` (e.g. `getList:posts`). */
81
+ deleteByPrefix(prefix: string): void;
82
+ /** Invalidate a collection's cached entries (alias of invalidateCache). */
83
+ invalidate(namespace: string): void;
84
+ /** Drop every cached entry. */
85
+ clear(): void;
86
+ /** Cache hit/miss/entry stats, or null when never configured. */
87
+ stats(): { hits: number; misses: number; entries: number } | null;
88
+ }
68
89
 
69
90
  /** Options for constructing a {@link LazypockClient}. */
70
91
  export interface LazypockClientOptions {
@@ -76,6 +97,24 @@ export interface LazypockClientOptions {
76
97
  authStore?: AuthStore;
77
98
  /** Real-time service for Phoenix Channel WebSocket subscriptions */
78
99
  realtime?: RealtimeService;
100
+ /**
101
+ * Query cache configuration. Disabled by default.
102
+ *
103
+ * ```ts
104
+ * const client = createClient({
105
+ * baseUrl: '...',
106
+ * cache: {
107
+ * enabled: true,
108
+ * defaultTTL: 30_000,
109
+ * store: myStorage, // optional persistence (same interface as auth)
110
+ * },
111
+ * });
112
+ * ```
113
+ *
114
+ * When enabled, readable GETs are cached. Requests can opt out via
115
+ * `{ cache: false }`, or opt in with a custom TTL via `{ ttl: ms }`.
116
+ */
117
+ cache?: CacheConfig;
79
118
  /**
80
119
  * Optional schema types for generating typed services at runtime.
81
120
  * When provided, `collection()` returns a service whose create/update
@@ -110,6 +149,9 @@ export class LazypockClient {
110
149
  readonly files: FilesService;
111
150
  private collectionCache = new Map<string, CollectionService>();
112
151
  private schemaByName?: Map<string, CollectionSchema>;
152
+ private cacheStore?: CacheStore;
153
+ /** Namespace → realtime unsubscribe; used for realtime-driven invalidation. */
154
+ private realtimeInvalidators = new Map<string, () => void>();
113
155
 
114
156
  /**
115
157
  * Create a new Lazypock client.
@@ -127,6 +169,14 @@ export class LazypockClient {
127
169
  }
128
170
  this.files = new FilesService(this.http);
129
171
  this.collections = new CollectionsService(this.http, this.realtime);
172
+ if (options.cache) {
173
+ this.cacheStore = new CacheStore({
174
+ defaultTTL: options.cache.defaultTTL,
175
+ store: options.cache.store,
176
+ maxEntries: options.cache.maxEntries,
177
+ });
178
+ this.http.setCache(this.cacheStore, options.cache.enabled ?? false);
179
+ }
130
180
  if (options.types?.schemas) {
131
181
  this.schemaByName = new Map(
132
182
  options.types.schemas.map((s) => [s.name, s]),
@@ -215,6 +265,100 @@ export class LazypockClient {
215
265
  return this;
216
266
  }
217
267
 
268
+ // ── Query cache (opt-in by default; opt-out per request) ──
269
+
270
+ /**
271
+ * Configure the query cache at runtime (also a namespace for cache
272
+ * management methods).
273
+ *
274
+ * ```ts
275
+ * client.cache({ enabled: true, defaultTTL: 30_000 });
276
+ * client.cache.deleteByPrefix('getList:posts'); // all list caches for posts
277
+ * client.cache.deleteByPrefix('getOne:posts'); // all one-record caches
278
+ * ```
279
+ *
280
+ * When enabled, GET requests cache their payload; mutations invalidate the
281
+ * affected collection automatically. Individual requests can opt out with
282
+ * `{ cache: false }` or override the TTL with `{ ttl: ms }`.
283
+ */
284
+ readonly cache: CacheController = Object.assign(
285
+ ((config?: CacheConfig) => {
286
+ if (!this.cacheStore) {
287
+ // Lazy-create so `.cache({ enabled: true })` works even when the
288
+ // constructor wasn't given cache config.
289
+ this.cacheStore = new CacheStore({
290
+ defaultTTL: config?.defaultTTL,
291
+ store: config?.store,
292
+ maxEntries: config?.maxEntries,
293
+ });
294
+ this.http.setCache(this.cacheStore, config?.enabled ?? true);
295
+ } else {
296
+ if (config?.enabled !== undefined) {
297
+ this.http.setCache(this.cacheStore, config.enabled);
298
+ }
299
+ }
300
+ return this;
301
+ }) as (config?: CacheConfig) => LazypockClient,
302
+ {
303
+ deleteByPrefix: (prefix: string) => this.cacheStore?.deleteByPrefix(prefix),
304
+ invalidate: (namespace: string) => this.cacheStore?.invalidate(namespace),
305
+ clear: () => {
306
+ this.cacheStore?.clear();
307
+ for (const unsub of this.realtimeInvalidators.values()) unsub();
308
+ this.realtimeInvalidators.clear();
309
+ },
310
+ stats: () => (this.cacheStore ? this.cacheStore.stats() : null),
311
+ },
312
+ );
313
+
314
+ /**
315
+ * Drop every cached entry (all collections / namespaces).
316
+ * Also disables realtime-driven invalidation subscriptions.
317
+ */
318
+ clearCache(): this {
319
+ this.cacheStore?.clear();
320
+ for (const unsub of this.realtimeInvalidators.values()) unsub();
321
+ this.realtimeInvalidators.clear();
322
+ return this;
323
+ }
324
+
325
+ /**
326
+ * Invalidate cached entries for a collection (or custom namespace).
327
+ * Runs automatically on mutations — call explicitly when data changed
328
+ * out-of-band (e.g. another client wrote to the same collection).
329
+ */
330
+ invalidateCache(namespace: string): this {
331
+ this.cacheStore?.invalidate(namespace);
332
+ return this;
333
+ }
334
+
335
+ /**
336
+ * Cache hit/miss/entry statistics.
337
+ * Returns null when caching was never configured.
338
+ */
339
+ cacheStats(): { hits: number; misses: number; entries: number } | null {
340
+ return this.cacheStore ? this.cacheStore.stats() : null;
341
+ }
342
+
343
+ /**
344
+ * Subscribe a collection's cache to realtime invalidation: any inbound
345
+ * create/update/delete event for the collection clears its cached entries.
346
+ * Returns an unsubscribe function.
347
+ */
348
+ invalidateCacheOnRealtime(collectionName: string): () => void {
349
+ if (!this.cacheStore) {
350
+ // ensure a store exists so invalidation has somewhere to go
351
+ this.cache({ enabled: false });
352
+ }
353
+ const existing = this.realtimeInvalidators.get(collectionName);
354
+ if (existing) return existing;
355
+ const unsub = this.collection(collectionName).subscribe(() => {
356
+ this.cacheStore?.invalidate(collectionName);
357
+ });
358
+ this.realtimeInvalidators.set(collectionName, unsub);
359
+ return unsub;
360
+ }
361
+
218
362
  // ── Auth ──
219
363
 
220
364
  /** Check whether any superuser exists (for login vs setup screen routing). */
package/src/types.ts CHANGED
@@ -65,6 +65,18 @@ export interface RequestOptions {
65
65
  signal?: AbortSignal;
66
66
  /** Custom fetch implementation (for RN or test mocking) */
67
67
  fetch?: typeof globalThis.fetch;
68
+ /**
69
+ * Cache control for this request (see {@link CacheRequestOptions}).
70
+ * Resolved against the client's global cache config when unset.
71
+ */
72
+ cache?: boolean | number | { ttl?: number; key?: string };
73
+ /** Alias of `cache: <ms>` — cache this GET for `ttl` milliseconds. */
74
+ ttl?: number;
75
+ /**
76
+ * Extra cache namespaces to invalidate when this mutation succeeds.
77
+ * The current collection is always invalidated automatically.
78
+ */
79
+ invalidate?: string[];
68
80
  /**
69
81
  * Request identifier used by the auto-cancellation mechanism.
70
82
  *