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/README.md +28 -0
- package/dist/index.cjs +105 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.global.js +105 -7
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +105 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/collection.ts +45 -2
- package/src/collections.ts +41 -5
- package/src/http.ts +64 -0
- package/src/types.ts +11 -0
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. */
|
package/dist/index.global.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
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;
|