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/README.md
CHANGED
|
@@ -358,6 +358,34 @@ client.cancelRequest('GET /api/posts?page=1');
|
|
|
358
358
|
client.cancelAllRequests();
|
|
359
359
|
```
|
|
360
360
|
|
|
361
|
+
#### Single-flight dedup (`getFullList`)
|
|
362
|
+
|
|
363
|
+
`getFullList()` (and `collections.getFullList()`) are **single-flight**: concurrent
|
|
364
|
+
calls with the same effective options share one in-flight request instead of
|
|
365
|
+
firing duplicates. This means the common pattern below results in **one**
|
|
366
|
+
network request, and **both** callers resolve with the same data — no abort
|
|
367
|
+
rejection:
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
const [a, b] = await Promise.all([
|
|
371
|
+
client.collection('posts').getFullList(),
|
|
372
|
+
client.collection('posts').getFullList(),
|
|
373
|
+
]);
|
|
374
|
+
// one GET fired; a === b
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Calls with **different** options (e.g. different `sort`/`filter`) are still
|
|
378
|
+
distinct requests. Multi-page fetches continue to work normally — each page
|
|
379
|
+
request is unique (page number is part of the URL), so pages never cancel each
|
|
380
|
+
other.
|
|
381
|
+
|
|
382
|
+
The underlying `singleFlight` option is also available on any request when you
|
|
383
|
+
want to coalesce concurrent identical calls yourself:
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
await client.collection('posts').getList(1, 20, { singleFlight: true });
|
|
387
|
+
```
|
|
388
|
+
|
|
361
389
|
## Query Cache
|
|
362
390
|
|
|
363
391
|
Lazypock has a built-in query cache for **GET** requests — disabled by default.
|
package/dist/index.cjs
CHANGED
|
@@ -268,6 +268,13 @@ var HttpClient = class {
|
|
|
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 HttpClient = class {
|
|
|
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 HttpClient = class {
|
|
|
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 memoryStorage = {
|
|
|
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 CollectionService = class {
|
|
|
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 CollectionService = class {
|
|
|
761
831
|
cache,
|
|
762
832
|
ttl,
|
|
763
833
|
invalidate,
|
|
834
|
+
singleFlight,
|
|
764
835
|
params
|
|
765
836
|
}
|
|
766
837
|
);
|
|
@@ -773,6 +844,7 @@ var CollectionService = class {
|
|
|
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 CollectionService = class {
|
|
|
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 CollectionsService = class {
|
|
|
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;
|