@choksheak/react-utils 0.2.2 → 0.2.3

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.
Files changed (39) hide show
  1. package/docs/assets/highlight.css +49 -0
  2. package/docs/assets/search.js +1 -1
  3. package/docs/classes/sharedQuery.SharedQuery.html +11 -6
  4. package/docs/classes/sharedState.SharedState.html +2 -2
  5. package/docs/classes/utils_storage.InMemoryStorageAdapter.html +2 -2
  6. package/docs/classes/utils_storage.IndexedDbAdapter.html +2 -2
  7. package/docs/classes/utils_storage.LocalStorageAdapter.html +2 -2
  8. package/docs/functions/sharedQuery.sharedQuery.html +5 -1
  9. package/docs/functions/sharedQuery.useSharedQuery.html +5 -3
  10. package/docs/functions/sharedState.sharedState.html +1 -1
  11. package/docs/functions/sharedState.useSharedState.html +1 -1
  12. package/docs/functions/sharedState.useSharedStateSetter.html +1 -1
  13. package/docs/functions/sharedState.useSharedStateValue.html +1 -1
  14. package/docs/functions/utils_storage.getStorageAdapter.html +1 -1
  15. package/docs/functions/utils_stringify.stringifyDeterministicForKeys.html +1 -1
  16. package/docs/types/sharedQuery.FetchFn.html +1 -1
  17. package/docs/types/sharedQuery.PersistTo.html +1 -1
  18. package/docs/types/sharedQuery.QueryStateValue.html +5 -2
  19. package/docs/types/sharedQuery.SharedQueryOptions.html +1 -1
  20. package/docs/types/sharedQuery.SharedQueryState.html +1 -1
  21. package/docs/types/sharedQuery.UseQueryResult.html +1 -1
  22. package/docs/types/sharedState.SharedStateOptions.html +1 -1
  23. package/docs/types/utils_storage.StorageAdapter.html +2 -2
  24. package/docs/types/utils_storage.StorageOptions.html +2 -2
  25. package/docs/variables/fetcher.fetcher.html +1 -1
  26. package/docs/variables/fetcher.fetcherWithRetries.html +1 -1
  27. package/docs/variables/sharedQuery.SharedQueryDefaults.html +2 -2
  28. package/docs/variables/sharedState.SharedStateDefaults.html +1 -1
  29. package/package.json +33 -32
  30. package/sharedQuery.d.mts +30 -1
  31. package/sharedQuery.d.ts +30 -1
  32. package/sharedQuery.js +105 -4
  33. package/sharedQuery.mjs +95 -4
  34. package/src/sharedQuery.ts +159 -5
  35. package/src/utils/stringify.test.ts +0 -46
  36. package/utils/stringify.test.d.mts +0 -2
  37. package/utils/stringify.test.d.ts +0 -2
  38. package/utils/stringify.test.js +0 -69
  39. package/utils/stringify.test.mjs +0 -86
package/sharedQuery.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/sharedQuery.ts
2
2
  import { MS_PER_DAY as MS_PER_DAY2 } from "@choksheak/ts-utils/timeConstants";
3
+ import getByteSize from "object-sizeof";
3
4
  import { useCallback, useEffect, useMemo, useRef } from "react";
4
5
 
5
6
  // src/sharedState.ts
@@ -269,7 +270,11 @@ function stringifyDeterministicForKeys(params) {
269
270
  var SharedQueryDefaults = {
270
271
  staleMs: 0,
271
272
  expiryMs: 30 * MS_PER_DAY2,
272
- persistTo: void 0
273
+ persistTo: void 0,
274
+ maxSize: 100,
275
+ // 100 is not too large, but please tweak accordingly
276
+ maxBytes: 1e5
277
+ // 100kb before GC
273
278
  };
274
279
  var SharedQuery = class {
275
280
  inflightPromises = /* @__PURE__ */ new Map();
@@ -279,6 +284,8 @@ var SharedQuery = class {
279
284
  expiryMs;
280
285
  staleMs;
281
286
  refetchOnStale;
287
+ maxSize;
288
+ maxBytes;
282
289
  constructor(options) {
283
290
  this.queryName = options.queryName;
284
291
  this.queryFn = options.queryFn;
@@ -291,6 +298,8 @@ var SharedQuery = class {
291
298
  } else if (persistTo === "indexedDb") {
292
299
  options.indexedDbKey = options.queryName;
293
300
  }
301
+ this.maxSize = options?.maxSize ?? SharedQueryDefaults.maxSize;
302
+ this.maxBytes = options?.maxBytes ?? SharedQueryDefaults.maxBytes;
294
303
  this.queryState = sharedState(
295
304
  {},
296
305
  {
@@ -359,11 +368,13 @@ var SharedQuery = class {
359
368
  /** Set the data directly if the user obtained it from somewhere else. */
360
369
  async setData(queryKey, data, dataUpdatedMs = Date.now()) {
361
370
  const entry = {
371
+ lastUpdatedMs: Date.now(),
362
372
  loading: false,
363
373
  data,
364
374
  dataUpdatedMs
365
375
  };
366
376
  this.queryState.setValue((prev) => ({ ...prev, [queryKey]: entry }));
377
+ this.enforceSizeLimit();
367
378
  }
368
379
  /** Do a new fetch even when the data is already cached. */
369
380
  async updateFromSource(...args) {
@@ -384,6 +395,78 @@ var SharedQuery = class {
384
395
  this.queryState.delete();
385
396
  this.inflightPromises.clear();
386
397
  }
398
+ /**
399
+ * Discard the oldest entries if the size exceeds the limit. The last
400
+ * remaining record cannot be deleted no matter what the limit is.
401
+ */
402
+ enforceSizeLimit() {
403
+ if (!this.maxSize && !this.maxBytes) {
404
+ return;
405
+ }
406
+ const oldState = this.queryState.getSnapshot();
407
+ const oldSize = Object.keys(oldState).length;
408
+ if (oldSize <= 1) {
409
+ return;
410
+ }
411
+ const oldByteSize = getByteSize(oldState);
412
+ let newState = oldState;
413
+ let needUpdate = false;
414
+ if (this.maxSize) {
415
+ const numToCut = oldSize - this.maxSize;
416
+ if (numToCut > 0) {
417
+ console.log(
418
+ `enforceSizeLimit for ${this.queryName} needed due to size ${oldSize} > ${this.maxSize}`
419
+ );
420
+ newState = { ...newState };
421
+ needUpdate = true;
422
+ Object.entries(newState).sort(
423
+ (entry1, entry2) => (
424
+ // Sort descending.
425
+ entry2[1].lastUpdatedMs - entry1[1].lastUpdatedMs
426
+ )
427
+ ).slice(-numToCut).forEach(([key]) => {
428
+ delete newState[key];
429
+ });
430
+ }
431
+ }
432
+ if (this.maxBytes && Object.keys(newState).length > 1) {
433
+ const currentByteSize = needUpdate ? getByteSize(newState) : oldByteSize;
434
+ let bytesToCut = currentByteSize - this.maxBytes;
435
+ if (bytesToCut > 0) {
436
+ console.log(
437
+ `enforceSizeLimit for ${this.queryName} needed due to byte size ${currentByteSize.toLocaleString()} > ${this.maxBytes.toLocaleString()}`
438
+ );
439
+ if (!needUpdate) {
440
+ newState = { ...newState };
441
+ needUpdate = true;
442
+ }
443
+ const entriesByTimeAscending = Object.entries(newState).sort(
444
+ (entry1, entry2) => entry1[1].lastUpdatedMs - entry2[1].lastUpdatedMs
445
+ );
446
+ for (
447
+ let i = 0;
448
+ // Leave the last record entriesByTimeAscending[len-1] untouched.
449
+ i < entriesByTimeAscending.length - 1 && bytesToCut > 0;
450
+ i++
451
+ ) {
452
+ const key = entriesByTimeAscending[i][0];
453
+ const thisSize = getByteSize(key) + getByteSize(newState[key]);
454
+ delete newState[key];
455
+ bytesToCut -= thisSize;
456
+ }
457
+ }
458
+ }
459
+ if (needUpdate) {
460
+ this.queryState.setValue(newState);
461
+ console.log(
462
+ `Trimmed data for ${this.queryName}: size=(${oldSize} -> ${Object.keys(newState).length}) (limit=${this.maxSize}), byteSize=(${oldByteSize.toLocaleString()} -> ${getByteSize(newState).toLocaleString()}) (limit=${this.maxBytes.toLocaleString()})`
463
+ );
464
+ return;
465
+ }
466
+ console.log(
467
+ `No need to trim data for ${this.queryName}: size=${oldSize} (limit=${this.maxSize}), byteSize=${oldByteSize.toLocaleString()} (limit=${this.maxBytes.toLocaleString()})`
468
+ );
469
+ }
387
470
  };
388
471
  var seenQueryNames = /* @__PURE__ */ new Set();
389
472
  function sharedQuery(options) {
@@ -393,7 +476,10 @@ function sharedQuery(options) {
393
476
  seenQueryNames.add(options.queryName);
394
477
  return new SharedQuery(options);
395
478
  }
396
- var DEFAULT_QUERY_STATE_ENTRY = { loading: true };
479
+ var DEFAULT_QUERY_STATE_ENTRY = {
480
+ lastUpdatedMs: 0,
481
+ loading: true
482
+ };
397
483
  function useSharedQuery(query, args = []) {
398
484
  const [queryState, setQueryState] = useSharedState(query.queryState);
399
485
  const isMounted = useRef(true);
@@ -404,6 +490,7 @@ function useSharedQuery(query, args = []) {
404
490
  const clone = { ...prev };
405
491
  clone[queryKey] = {
406
492
  ...clone[queryKey] ?? {},
493
+ lastUpdatedMs: Date.now(),
407
494
  loading: true
408
495
  };
409
496
  return clone;
@@ -413,11 +500,13 @@ function useSharedQuery(query, args = []) {
413
500
  if (isMounted.current) {
414
501
  setQueryState((prev) => {
415
502
  const clone = { ...prev };
503
+ const now = Date.now();
416
504
  clone[queryKey] = {
417
505
  // Don't keep old error.
506
+ lastUpdatedMs: now,
418
507
  loading: false,
419
508
  data,
420
- dataUpdatedMs: Date.now()
509
+ dataUpdatedMs: now
421
510
  };
422
511
  return clone;
423
512
  });
@@ -427,12 +516,14 @@ function useSharedQuery(query, args = []) {
427
516
  if (isMounted.current) {
428
517
  setQueryState((prev) => {
429
518
  const clone = { ...prev };
519
+ const now = Date.now();
430
520
  clone[queryKey] = {
431
521
  // Keep old data.
432
522
  ...clone[queryKey] ?? {},
523
+ lastUpdatedMs: now,
433
524
  loading: false,
434
525
  error,
435
- errorUpdatedMs: Date.now()
526
+ errorUpdatedMs: now
436
527
  };
437
528
  return clone;
438
529
  });
@@ -1,4 +1,5 @@
1
1
  import { MS_PER_DAY } from "@choksheak/ts-utils/timeConstants";
2
+ import getByteSize from "object-sizeof";
2
3
  import { useCallback, useEffect, useMemo, useRef } from "react";
3
4
 
4
5
  import {
@@ -14,6 +15,12 @@ export type FetchFn<TArgs extends unknown[], TData> = (
14
15
  ) => Promise<TData> | TData;
15
16
 
16
17
  export type QueryStateValue<TData> = {
18
+ /**
19
+ * When this record was last updated. Used for LRU determination. If you want
20
+ * the data age, please use `dataUpdatedMs` instead.
21
+ */
22
+ lastUpdatedMs: number;
23
+
17
24
  loading: boolean;
18
25
  data?: TData;
19
26
  dataUpdatedMs?: number;
@@ -43,7 +50,7 @@ export type SharedQueryOptions<TArgs extends unknown[], TData> = {
43
50
  // Function to fetch data.
44
51
  queryFn: FetchFn<TArgs, TData>;
45
52
 
46
- // ms before data considered stale; 0 means never stale.
53
+ // ms before data considered stale; 0 means always stale.
47
54
  staleMs?: number;
48
55
 
49
56
  // ms before data is removed from cache; 0 means never expire.
@@ -56,6 +63,14 @@ export type SharedQueryOptions<TArgs extends unknown[], TData> = {
56
63
  // queryName. This is important because it avoids requiring the user to
57
64
  // specify the same queryName twice everytime.
58
65
  persistTo?: PersistTo;
66
+
67
+ // Max number of entries to keep. The oldest entries will be discarded.
68
+ // The last record cannot be discarded. 0 means no limit.
69
+ maxSize?: number;
70
+
71
+ // Max number of bytes to keep. The oldest entries will be discarded.
72
+ // The last record cannot be discarded. 0 means no limit.
73
+ maxBytes?: number;
59
74
  } & SharedStateOptions<SharedQueryState<TData>>;
60
75
 
61
76
  /** Users can override these values globally. */
@@ -63,6 +78,8 @@ export const SharedQueryDefaults = {
63
78
  staleMs: 0,
64
79
  expiryMs: 30 * MS_PER_DAY,
65
80
  persistTo: undefined as PersistTo | undefined,
81
+ maxSize: 100, // 100 is not too large, but please tweak accordingly
82
+ maxBytes: 100_000, // 100kb before GC
66
83
  };
67
84
 
68
85
  export class SharedQuery<TArgs extends unknown[], TData> {
@@ -74,6 +91,8 @@ export class SharedQuery<TArgs extends unknown[], TData> {
74
91
  public readonly expiryMs: number;
75
92
  public readonly staleMs: number;
76
93
  public readonly refetchOnStale: boolean;
94
+ public readonly maxSize: number;
95
+ public readonly maxBytes: number;
77
96
 
78
97
  public constructor(options: SharedQueryOptions<TArgs, TData>) {
79
98
  this.queryName = options.queryName;
@@ -101,6 +120,9 @@ export class SharedQuery<TArgs extends unknown[], TData> {
101
120
  options.indexedDbKey = options.queryName;
102
121
  }
103
122
 
123
+ this.maxSize = options?.maxSize ?? SharedQueryDefaults.maxSize;
124
+ this.maxBytes = options?.maxBytes ?? SharedQueryDefaults.maxBytes;
125
+
104
126
  this.queryState = sharedState<SharedQueryState<TData>>(
105
127
  {},
106
128
  {
@@ -196,12 +218,15 @@ export class SharedQuery<TArgs extends unknown[], TData> {
196
218
  dataUpdatedMs = Date.now(),
197
219
  ): Promise<void> {
198
220
  const entry: QueryStateValue<TData> = {
221
+ lastUpdatedMs: Date.now(),
199
222
  loading: false,
200
223
  data,
201
224
  dataUpdatedMs,
202
225
  };
203
226
 
204
227
  this.queryState.setValue((prev) => ({ ...prev, [queryKey]: entry }));
228
+
229
+ this.enforceSizeLimit();
205
230
  }
206
231
 
207
232
  /** Do a new fetch even when the data is already cached. */
@@ -227,6 +252,108 @@ export class SharedQuery<TArgs extends unknown[], TData> {
227
252
  this.queryState.delete();
228
253
  this.inflightPromises.clear();
229
254
  }
255
+
256
+ /**
257
+ * Discard the oldest entries if the size exceeds the limit. The last
258
+ * remaining record cannot be deleted no matter what the limit is.
259
+ */
260
+ public enforceSizeLimit(): void {
261
+ // Skip if there are no limits.
262
+ if (!this.maxSize && !this.maxBytes) {
263
+ return;
264
+ }
265
+
266
+ const oldState = this.queryState.getSnapshot();
267
+ const oldSize = Object.keys(oldState).length;
268
+
269
+ // Cannot do GC if size is 1 or less.
270
+ if (oldSize <= 1) {
271
+ return;
272
+ }
273
+
274
+ const oldByteSize = getByteSize(oldState);
275
+
276
+ let newState = oldState;
277
+ let needUpdate = false;
278
+
279
+ // Limit by number of records.
280
+ if (this.maxSize) {
281
+ const numToCut = oldSize - this.maxSize;
282
+
283
+ if (numToCut > 0) {
284
+ console.log(
285
+ `enforceSizeLimit for ${this.queryName} needed due to size ${oldSize} > ${this.maxSize}`,
286
+ );
287
+
288
+ newState = { ...newState }; // shallow clone
289
+ needUpdate = true;
290
+
291
+ Object.entries(newState)
292
+ .sort(
293
+ (entry1, entry2) =>
294
+ // Sort descending.
295
+ entry2[1].lastUpdatedMs - entry1[1].lastUpdatedMs,
296
+ )
297
+ .slice(-numToCut)
298
+ .forEach(([key]) => {
299
+ delete newState[key];
300
+ });
301
+ }
302
+ }
303
+
304
+ // Limit by byte size.
305
+ if (this.maxBytes && Object.keys(newState).length > 1) {
306
+ // Recompute only if changed by above code.
307
+ const currentByteSize = needUpdate ? getByteSize(newState) : oldByteSize;
308
+
309
+ let bytesToCut = currentByteSize - this.maxBytes;
310
+
311
+ if (bytesToCut > 0) {
312
+ console.log(
313
+ `enforceSizeLimit for ${this.queryName} needed due to byte size ${currentByteSize.toLocaleString()} > ${this.maxBytes.toLocaleString()}`,
314
+ );
315
+
316
+ // Shallow clone only if not already cloned above.
317
+ if (!needUpdate) {
318
+ newState = { ...newState };
319
+ needUpdate = true;
320
+ }
321
+
322
+ const entriesByTimeAscending = Object.entries(newState).sort(
323
+ (entry1, entry2) => entry1[1].lastUpdatedMs - entry2[1].lastUpdatedMs,
324
+ );
325
+
326
+ for (
327
+ let i = 0;
328
+ // Leave the last record entriesByTimeAscending[len-1] untouched.
329
+ i < entriesByTimeAscending.length - 1 && bytesToCut > 0;
330
+ i++
331
+ ) {
332
+ const key = entriesByTimeAscending[i][0];
333
+ const thisSize = getByteSize(key) + getByteSize(newState[key]);
334
+ delete newState[key];
335
+ bytesToCut -= thisSize;
336
+ }
337
+ }
338
+ }
339
+
340
+ // Update the state.
341
+ if (needUpdate) {
342
+ this.queryState.setValue(newState);
343
+
344
+ // Log to inform user that the data was trimmed.
345
+ console.log(
346
+ `Trimmed data for ${this.queryName}: size=(${oldSize} -> ${Object.keys(newState).length}) (limit=${this.maxSize}), byteSize=(${oldByteSize.toLocaleString()} -> ${getByteSize(newState).toLocaleString()}) (limit=${this.maxBytes.toLocaleString()})`,
347
+ );
348
+
349
+ return;
350
+ }
351
+
352
+ // Log to indicate why trimming was not needed.
353
+ console.log(
354
+ `No need to trim data for ${this.queryName}: size=${oldSize} (limit=${this.maxSize}), byteSize=${oldByteSize.toLocaleString()} (limit=${this.maxBytes.toLocaleString()})`,
355
+ );
356
+ }
230
357
  }
231
358
 
232
359
  const seenQueryNames = new Set<string>();
@@ -237,6 +364,16 @@ const seenQueryNames = new Set<string>();
237
364
  *
238
365
  * Each `queryName` must be unique, and can only be declared once, most often
239
366
  * in the top level scope. The queryName is used for logging only.
367
+ *
368
+ * Example:
369
+ * ```
370
+ * export const usersQuery = sharedQuery({
371
+ * queryName: "users",
372
+ * queryFn: listUsers,
373
+ * persistTo: "indexedDb",
374
+ * staleMs: MS_PER_DAY,
375
+ * });
376
+ * ```
240
377
  */
241
378
  export function sharedQuery<TArgs extends unknown[], TData>(
242
379
  // The name could have been auto-generated, but we let the user give us a
@@ -251,12 +388,18 @@ export function sharedQuery<TArgs extends unknown[], TData>(
251
388
  return new SharedQuery(options);
252
389
  }
253
390
 
254
- const DEFAULT_QUERY_STATE_ENTRY: QueryStateValue<unknown> = { loading: true };
391
+ const DEFAULT_QUERY_STATE_ENTRY: QueryStateValue<unknown> = {
392
+ lastUpdatedMs: 0,
393
+ loading: true,
394
+ };
255
395
 
256
396
  /**
257
397
  * React hook to make use of a shared query inside any React component.
258
398
  *
259
- * const users = useSharedQuery(usersQuery, ["123"]);
399
+ * Example:
400
+ * ```
401
+ * const users = useSharedQuery(usersQuery, ["123"]);
402
+ * ```
260
403
  */
261
404
  export function useSharedQuery<TArgs extends unknown[], TData>(
262
405
  query: SharedQuery<TArgs, TData>,
@@ -278,10 +421,13 @@ export function useSharedQuery<TArgs extends unknown[], TData>(
278
421
 
279
422
  setQueryState((prev) => {
280
423
  const clone = { ...prev };
424
+
281
425
  clone[queryKey] = {
282
426
  ...(clone[queryKey] ?? {}),
427
+ lastUpdatedMs: Date.now(),
283
428
  loading: true,
284
429
  };
430
+
285
431
  return clone;
286
432
  });
287
433
 
@@ -291,12 +437,16 @@ export function useSharedQuery<TArgs extends unknown[], TData>(
291
437
  if (isMounted.current) {
292
438
  setQueryState((prev) => {
293
439
  const clone = { ...prev };
440
+ const now = Date.now();
441
+
294
442
  clone[queryKey] = {
295
443
  // Don't keep old error.
444
+ lastUpdatedMs: now,
296
445
  loading: false,
297
446
  data,
298
- dataUpdatedMs: Date.now(),
447
+ dataUpdatedMs: now,
299
448
  };
449
+
300
450
  return clone;
301
451
  });
302
452
  }
@@ -309,13 +459,17 @@ export function useSharedQuery<TArgs extends unknown[], TData>(
309
459
  if (isMounted.current) {
310
460
  setQueryState((prev) => {
311
461
  const clone = { ...prev };
462
+ const now = Date.now();
463
+
312
464
  clone[queryKey] = {
313
465
  // Keep old data.
314
466
  ...(clone[queryKey] ?? {}),
467
+ lastUpdatedMs: now,
315
468
  loading: false,
316
469
  error,
317
- errorUpdatedMs: Date.now(),
470
+ errorUpdatedMs: now,
318
471
  };
472
+
319
473
  return clone;
320
474
  });
321
475
  }
@@ -1,46 +0,0 @@
1
- import { stringifyDeterministicForKeys } from "./stringify";
2
-
3
- describe("stringifyDeterministicForKeys", () => {
4
- test("happy", () => {
5
- const expected = `{"a":1,"b":2,"c":{"e":1,"f":2}}`;
6
-
7
- const actual1 = stringifyDeterministicForKeys({
8
- a: 1,
9
- b: 2,
10
- c: { e: 1, f: 2 },
11
- });
12
- expect(actual1).toBe(expected);
13
-
14
- const actual2 = stringifyDeterministicForKeys({
15
- c: { f: 2, e: 1 },
16
- b: 2,
17
- a: 1,
18
- });
19
- expect(actual2).toBe(expected);
20
- });
21
-
22
- test("circular", () => {
23
- const circular = { b: {}, a: 1 };
24
- circular.b = circular;
25
-
26
- const actual = stringifyDeterministicForKeys(circular);
27
-
28
- expect(actual).toBe(`{"a":1,"b":"[Circular]"}`);
29
- });
30
-
31
- test("function", () => {
32
- const fn = { b: {}, a: () => {} };
33
-
34
- expect(() => stringifyDeterministicForKeys(fn)).toThrow(
35
- `Functions should not be passed into query parameters`,
36
- );
37
- });
38
-
39
- test("array", () => {
40
- const a = [1, "a", { b: 2 }, [true]];
41
-
42
- const actual = stringifyDeterministicForKeys(a);
43
-
44
- expect(actual).toBe(`[1,"a",{"b":2},[true]]`);
45
- });
46
- });
@@ -1,2 +0,0 @@
1
-
2
- export { }
@@ -1,2 +0,0 @@
1
-
2
- export { }
@@ -1,69 +0,0 @@
1
- "use strict";
2
-
3
- // src/utils/stringify.ts
4
- function stringifyDeterministicForKeys(params) {
5
- const cache = /* @__PURE__ */ new Set();
6
- return JSON.stringify(params, (_, value) => {
7
- if (typeof value === "function") {
8
- throw new Error(`Functions should not be passed into query parameters`);
9
- }
10
- if (!value || typeof value !== "object" || Array.isArray(value)) {
11
- return value;
12
- }
13
- if (cache.has(value)) {
14
- return "[Circular]";
15
- }
16
- cache.add(value);
17
- if (value !== params) {
18
- try {
19
- JSON.stringify(value);
20
- } catch {
21
- return "[Unserializable]";
22
- }
23
- }
24
- return Object.keys(value).sort().reduce(
25
- (result, key) => {
26
- result[key] = value[key];
27
- return result;
28
- },
29
- {}
30
- );
31
- });
32
- }
33
-
34
- // src/utils/stringify.test.ts
35
- describe("stringifyDeterministicForKeys", () => {
36
- test("happy", () => {
37
- const expected = `{"a":1,"b":2,"c":{"e":1,"f":2}}`;
38
- const actual1 = stringifyDeterministicForKeys({
39
- a: 1,
40
- b: 2,
41
- c: { e: 1, f: 2 }
42
- });
43
- expect(actual1).toBe(expected);
44
- const actual2 = stringifyDeterministicForKeys({
45
- c: { f: 2, e: 1 },
46
- b: 2,
47
- a: 1
48
- });
49
- expect(actual2).toBe(expected);
50
- });
51
- test("circular", () => {
52
- const circular = { b: {}, a: 1 };
53
- circular.b = circular;
54
- const actual = stringifyDeterministicForKeys(circular);
55
- expect(actual).toBe(`{"a":1,"b":"[Circular]"}`);
56
- });
57
- test("function", () => {
58
- const fn = { b: {}, a: () => {
59
- } };
60
- expect(() => stringifyDeterministicForKeys(fn)).toThrow(
61
- `Functions should not be passed into query parameters`
62
- );
63
- });
64
- test("array", () => {
65
- const a = [1, "a", { b: 2 }, [true]];
66
- const actual = stringifyDeterministicForKeys(a);
67
- expect(actual).toBe(`[1,"a",{"b":2},[true]]`);
68
- });
69
- });
@@ -1,86 +0,0 @@
1
- var __getOwnPropNames = Object.getOwnPropertyNames;
2
- var __esm = (fn, res) => function __init() {
3
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
4
- };
5
- var __commonJS = (cb, mod) => function __require() {
6
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
7
- };
8
-
9
- // src/utils/stringify.ts
10
- function stringifyDeterministicForKeys(params) {
11
- const cache = /* @__PURE__ */ new Set();
12
- return JSON.stringify(params, (_, value) => {
13
- if (typeof value === "function") {
14
- throw new Error(`Functions should not be passed into query parameters`);
15
- }
16
- if (!value || typeof value !== "object" || Array.isArray(value)) {
17
- return value;
18
- }
19
- if (cache.has(value)) {
20
- return "[Circular]";
21
- }
22
- cache.add(value);
23
- if (value !== params) {
24
- try {
25
- JSON.stringify(value);
26
- } catch {
27
- return "[Unserializable]";
28
- }
29
- }
30
- return Object.keys(value).sort().reduce(
31
- (result, key) => {
32
- result[key] = value[key];
33
- return result;
34
- },
35
- {}
36
- );
37
- });
38
- }
39
- var init_stringify = __esm({
40
- "src/utils/stringify.ts"() {
41
- "use strict";
42
- }
43
- });
44
-
45
- // src/utils/stringify.test.ts
46
- var require_stringify_test = __commonJS({
47
- "src/utils/stringify.test.ts"() {
48
- init_stringify();
49
- describe("stringifyDeterministicForKeys", () => {
50
- test("happy", () => {
51
- const expected = `{"a":1,"b":2,"c":{"e":1,"f":2}}`;
52
- const actual1 = stringifyDeterministicForKeys({
53
- a: 1,
54
- b: 2,
55
- c: { e: 1, f: 2 }
56
- });
57
- expect(actual1).toBe(expected);
58
- const actual2 = stringifyDeterministicForKeys({
59
- c: { f: 2, e: 1 },
60
- b: 2,
61
- a: 1
62
- });
63
- expect(actual2).toBe(expected);
64
- });
65
- test("circular", () => {
66
- const circular = { b: {}, a: 1 };
67
- circular.b = circular;
68
- const actual = stringifyDeterministicForKeys(circular);
69
- expect(actual).toBe(`{"a":1,"b":"[Circular]"}`);
70
- });
71
- test("function", () => {
72
- const fn = { b: {}, a: () => {
73
- } };
74
- expect(() => stringifyDeterministicForKeys(fn)).toThrow(
75
- `Functions should not be passed into query parameters`
76
- );
77
- });
78
- test("array", () => {
79
- const a = [1, "a", { b: 2 }, [true]];
80
- const actual = stringifyDeterministicForKeys(a);
81
- expect(actual).toBe(`[1,"a",{"b":2},[true]]`);
82
- });
83
- });
84
- }
85
- });
86
- export default require_stringify_test();