lazypock 0.3.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.cts CHANGED
@@ -87,6 +87,17 @@ interface RequestOptions {
87
87
  * Alias of `requestKey` (PocketBase `$cancelKey` compat).
88
88
  */
89
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;
90
101
  }
91
102
  declare class ApiError extends Error {
92
103
  readonly data: unknown;
@@ -320,6 +331,13 @@ declare class HttpClient {
320
331
  * previous one — PocketBase-style auto-cancellation of duplicated requests.
321
332
  */
322
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;
323
341
  /** Global toggle for the auto-cancellation behaviour (default: on). */
324
342
  private enableAutoCancellation;
325
343
  /**
@@ -372,6 +390,11 @@ declare class HttpClient {
372
390
  entries: number;
373
391
  } | null;
374
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;
375
398
  /** Build a token-scoped cache key: `METHOD path|token-hash|params`. */
376
399
  private cacheKeyFor;
377
400
  /** Best-effort namespace (collection name) from a REST path. */
package/dist/index.d.ts CHANGED
@@ -87,6 +87,17 @@ interface RequestOptions {
87
87
  * Alias of `requestKey` (PocketBase `$cancelKey` compat).
88
88
  */
89
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;
90
101
  }
91
102
  declare class ApiError extends Error {
92
103
  readonly data: unknown;
@@ -320,6 +331,13 @@ declare class HttpClient {
320
331
  * previous one — PocketBase-style auto-cancellation of duplicated requests.
321
332
  */
322
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;
323
341
  /** Global toggle for the auto-cancellation behaviour (default: on). */
324
342
  private enableAutoCancellation;
325
343
  /**
@@ -372,6 +390,11 @@ declare class HttpClient {
372
390
  entries: number;
373
391
  } | null;
374
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;
375
398
  /** Build a token-scoped cache key: `METHOD path|token-hash|params`. */
376
399
  private cacheKeyFor;
377
400
  /** Best-effort namespace (collection name) from a REST path. */
@@ -268,6 +268,13 @@ var Lazypock = (() => {
268
268
  * previous one — PocketBase-style auto-cancellation of duplicated requests.
269
269
  */
270
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 = {};
271
278
  /** Global toggle for the auto-cancellation behaviour (default: on). */
272
279
  this.enableAutoCancellation = true;
273
280
  this.baseUrl = baseUrl.replace(/\/+$/, "");
@@ -383,6 +390,12 @@ var Lazypock = (() => {
383
390
  }
384
391
  let requestKey = options?.requestKey === void 0 ? options?.cancelKey ?? `${method} ${path}` : options.requestKey;
385
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
+ }
386
399
  let controller = null;
387
400
  const externalSignal = options?.signal;
388
401
  if (requestKey !== null) {
@@ -400,6 +413,38 @@ var Lazypock = (() => {
400
413
  }
401
414
  }
402
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) {
403
448
  let url = this.baseUrl + path;
404
449
  if (options?.params) {
405
450
  const qs = new URLSearchParams(options.params).toString();
@@ -696,6 +741,30 @@ var Lazypock = (() => {
696
741
  };
697
742
 
698
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
+ }
699
768
  function normalizeAction(event, rawAction) {
700
769
  if (typeof rawAction === "string") {
701
770
  const a = rawAction.toLowerCase();
@@ -737,6 +806,7 @@ var Lazypock = (() => {
737
806
  cache,
738
807
  ttl,
739
808
  invalidate,
809
+ singleFlight,
740
810
  params,
741
811
  ...queryParams
742
812
  } = options ?? {};
@@ -761,6 +831,7 @@ var Lazypock = (() => {
761
831
  cache,
762
832
  ttl,
763
833
  invalidate,
834
+ singleFlight,
764
835
  params
765
836
  }
766
837
  );
@@ -773,6 +844,7 @@ var Lazypock = (() => {
773
844
  */
774
845
  async getFullList(options) {
775
846
  const { batch = 1e3, ...rest } = options ?? {};
847
+ const effectiveKey = typeof rest.requestKey === "string" ? rest.requestKey : `getFullList:${this.collectionName}:${stableStringify(rest)}`;
776
848
  const items = [];
777
849
  let page = 1;
778
850
  for (; ; ) {
@@ -780,9 +852,9 @@ var Lazypock = (() => {
780
852
  page,
781
853
  batch,
782
854
  {
783
- // disable auto-cancellation across pages — each page request is unique
784
855
  ...rest,
785
- requestKey: null
856
+ requestKey: effectiveKey,
857
+ singleFlight: true
786
858
  }
787
859
  );
788
860
  if (!res || !res.items || res.items.length === 0) break;
@@ -1305,14 +1377,40 @@ var Lazypock = (() => {
1305
1377
  async getFullList(options) {
1306
1378
  if (!this.http) return [];
1307
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)}`;
1308
1393
  const items = [];
1309
1394
  let page = 1;
1310
1395
  for (; ; ) {
1311
- const res = await this.getList({
1312
- ...rest,
1313
- page,
1314
- perPage: batch
1315
- });
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
+ );
1316
1414
  if (!res || !res.items || res.items.length === 0) break;
1317
1415
  items.push(...res.items);
1318
1416
  if (page >= (res.totalPages ?? page)) break;