svelte-ag 1.0.56 → 1.0.57

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.
@@ -1 +1 @@
1
- {"version":3,"file":"query.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/api/query/query.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,kBAAkB,EAAE,cAAc,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AAInH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAG5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAEnE,qBAAa,KAAK,CAChB,GAAG,SAAS,YAAY,EACxB,IAAI,SAAS,GAAG,CAAC,MAAM,CAAC,EACxB,MAAM,SAAS,OAAO,CAAC,GAAG,EAAE;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,CAAC,CAAC,QAAQ,CAAC;;gBAyBzC,EACV,IAAI,EACJ,MAAM,EACN,KAAK,EACL,SAAS,EACT,KAAK,EACN,EAAE;QACD,IAAI,EAAE,IAAI,CAAC;QACX,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;QACnC,SAAS,EAAE,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;QACxC,KAAK,EAAE,KAAK,CAAC;QACb,IAAI,CAAC,EAAE;YACL,KAAK,CAAC,EAAE,UAAU,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;SAC1C,CAAC;KACH;IAgBK,OAAO,IAAI,OAAO,CAAC,WAAW,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IA+BxD,IAAI,QAAQ,WAEX;IACD,IAAI,QAAQ,YAEX;IACD,UAAU;IAMV,IAAI,MAAM,gBAET;IACD,IAAI,IAAI,IAAI,cAAc,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,IAAI,CAEnD;IACD,IAAI,SAAS,IAAI,YAAY,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,IAAI,CAEtD;CACF;AAED,qBAAa,SAAS,CACpB,GAAG,SAAS,YAAY,EACxB,IAAI,SAAS,GAAG,CAAC,MAAM,CAAC,EACxB,MAAM,SAAS,OAAO,CAAC,GAAG,EAAE;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,CAAC,CAAC,QAAQ,CAAC;;gBA6BnD,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,kBAAkB,CAAC,GAAG,CAAC,EAChC,MAAM,EAAE,KAAK,EACb,YAAY,CAAC,EAAE,YAAY,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC;YAmBlC,KAAK;IAOnB;;;OAGG;YACW,eAAe;IAqBvB,OAAO,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;CAkB3F"}
1
+ {"version":3,"file":"query.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/api/query/query.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,kBAAkB,EAAE,cAAc,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AAGnH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAG5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGxD,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAEnE,qBAAa,KAAK,CAChB,GAAG,SAAS,YAAY,EACxB,IAAI,SAAS,GAAG,CAAC,MAAM,CAAC,EACxB,MAAM,SAAS,OAAO,CAAC,GAAG,EAAE;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,CAAC,CAAC,QAAQ,CAAC;;gBAyBzC,EACV,IAAI,EACJ,MAAM,EACN,KAAK,EACL,SAAS,EACT,KAAK,EACN,EAAE;QACD,IAAI,EAAE,IAAI,CAAC;QACX,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;QACnC,SAAS,EAAE,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;QACxC,KAAK,EAAE,KAAK,CAAC;QACb,IAAI,CAAC,EAAE;YACL,KAAK,CAAC,EAAE,UAAU,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;SAC1C,CAAC;KACH;IAgBK,OAAO,IAAI,OAAO,CAAC,WAAW,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IA+BxD,IAAI,QAAQ,WAEX;IACD,IAAI,QAAQ,YAEX;IACD,UAAU;IAMV,IAAI,MAAM,gBAET;IACD,IAAI,IAAI,IAAI,cAAc,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,IAAI,CAEnD;IACD,IAAI,SAAS,IAAI,YAAY,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,IAAI,CAEtD;CACF;AAED,qBAAa,SAAS,CACpB,GAAG,SAAS,YAAY,EACxB,IAAI,SAAS,GAAG,CAAC,MAAM,CAAC,EACxB,MAAM,SAAS,OAAO,CAAC,GAAG,EAAE;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,CAAC,CAAC,QAAQ,CAAC;;gBA6BnD,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,kBAAkB,CAAC,GAAG,CAAC,EAChC,MAAM,EAAE,KAAK,EACb,YAAY,CAAC,EAAE,YAAY,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC;YAelC,KAAK;IAOnB;;;OAGG;YACW,eAAe;IAqBvB,OAAO,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;CAkB3F"}
@@ -1,6 +1,6 @@
1
1
  import { stringify } from 'devalue';
2
- import PQueue from 'p-queue';
3
2
  import { cacheKey } from './utils.svelte.js';
3
+ import { RateLimiter } from './rate.svelte';
4
4
  export class Query {
5
5
  // -------- Constants --------
6
6
  #TIMEOUT = 1000 * 60 * 5; // 5 minutes
@@ -97,11 +97,7 @@ export class Requestor {
97
97
  this.#path = path;
98
98
  this.#method = method;
99
99
  this.#request = request;
100
- this.#limiter = new PQueue({
101
- concurrency: 5,
102
- interval: 100,
103
- intervalCap: 1
104
- });
100
+ this.#limiter = new RateLimiter();
105
101
  // this.#cache = cache;
106
102
  // TODO
107
103
  this.#canBatch = batchDetails ? batchDetails.canBatch : () => false;
@@ -0,0 +1,5 @@
1
+ export declare class RateLimiter {
2
+ #private;
3
+ add<T>(fn: () => Promise<T>): Promise<T>;
4
+ }
5
+ //# sourceMappingURL=rate.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/api/query/rate.svelte.ts"],"names":[],"mappings":"AAAA,qBAAa,WAAW;;IAItB,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAezC"}
@@ -0,0 +1,16 @@
1
+ export class RateLimiter {
2
+ #nextStart = 0;
3
+ #interval = 100;
4
+ add(fn) {
5
+ const now = Date.now();
6
+ const startAt = Math.max(now, this.#nextStart);
7
+ this.#nextStart = startAt + this.#interval;
8
+ return (async () => {
9
+ const wait = Math.max(0, startAt - Date.now());
10
+ if (wait > 0) {
11
+ await new Promise((resolve) => setTimeout(resolve, wait));
12
+ }
13
+ return await fn();
14
+ })();
15
+ }
16
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=rate.unit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate.unit.test.d.ts","sourceRoot":"","sources":["../../../../src/lib/api/query/rate.unit.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,109 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { sleep } from 'radash';
3
+ import { RateLimiter } from './rate.svelte.js';
4
+ describe('RateLimiter', () => {
5
+ beforeEach(() => {
6
+ vi.useFakeTimers();
7
+ vi.setSystemTime(0);
8
+ });
9
+ afterEach(() => {
10
+ vi.useRealTimers();
11
+ vi.restoreAllMocks();
12
+ });
13
+ it('starts the first request immediately', async () => {
14
+ const limiter = new RateLimiter();
15
+ const fn = vi.fn().mockResolvedValue('ok');
16
+ const promise = limiter.add(fn);
17
+ await Promise.resolve();
18
+ expect(fn).toHaveBeenCalledTimes(1);
19
+ await expect(promise).resolves.toBe('ok');
20
+ });
21
+ it('spaces simultaneous requests by at least 100ms', async () => {
22
+ const limiter = new RateLimiter();
23
+ const starts = [];
24
+ const makeTask = (label) => vi.fn().mockImplementation(async () => {
25
+ starts.push(Date.now());
26
+ await sleep(105);
27
+ return label;
28
+ });
29
+ const p1 = limiter.add(makeTask('a'));
30
+ const p2 = limiter.add(makeTask('b'));
31
+ const p3 = limiter.add(makeTask('c'));
32
+ const p4 = limiter.add(makeTask('d'));
33
+ const p5 = limiter.add(makeTask('e'));
34
+ await Promise.resolve();
35
+ expect(starts).toEqual([0]);
36
+ await vi.advanceTimersByTimeAsync(99);
37
+ expect(starts).toEqual([0]);
38
+ await vi.advanceTimersByTimeAsync(1);
39
+ expect(starts).toEqual([0, 100]);
40
+ await vi.advanceTimersByTimeAsync(100);
41
+ expect(starts).toEqual([0, 100, 200]);
42
+ await vi.advanceTimersByTimeAsync(100);
43
+ expect(starts).toEqual([0, 100, 200, 300]);
44
+ await vi.advanceTimersByTimeAsync(100);
45
+ expect(starts).toEqual([0, 100, 200, 300, 400]);
46
+ await vi.runAllTimersAsync();
47
+ await expect(Promise.all([p1, p2, p3, p4, p5])).resolves.toEqual(['a', 'b', 'c', 'd', 'e']);
48
+ });
49
+ it('does not bunch requests together after waiting', async () => {
50
+ const limiter = new RateLimiter();
51
+ const starts = [];
52
+ const task = () => limiter.add(async () => {
53
+ starts.push(Date.now());
54
+ await sleep(105);
55
+ });
56
+ const p1 = task();
57
+ const p2 = task();
58
+ const p3 = task();
59
+ await Promise.resolve();
60
+ expect(starts).toEqual([0]);
61
+ await vi.advanceTimersByTimeAsync(100);
62
+ expect(starts).toEqual([0, 100]);
63
+ await vi.advanceTimersByTimeAsync(100);
64
+ expect(starts).toEqual([0, 100, 200]);
65
+ await vi.runAllTimersAsync();
66
+ await Promise.all([p1, p2, p3]);
67
+ });
68
+ it('keeps working after a request fails', async () => {
69
+ const limiter = new RateLimiter();
70
+ const starts = [];
71
+ const p1 = limiter.add(async () => {
72
+ starts.push(Date.now());
73
+ await sleep(200);
74
+ throw new Error('boom');
75
+ });
76
+ const p1Expectation = expect(p1).rejects.toThrow('boom');
77
+ const p2 = limiter.add(async () => {
78
+ starts.push(Date.now());
79
+ return 'ok';
80
+ });
81
+ await Promise.resolve();
82
+ expect(starts).toEqual([0]);
83
+ await vi.advanceTimersByTimeAsync(100);
84
+ expect(starts).toEqual([0, 100]);
85
+ await vi.advanceTimersByTimeAsync(100);
86
+ await p1Expectation;
87
+ await expect(p2).resolves.toBe('ok');
88
+ });
89
+ it('only limits start time, not completion time', async () => {
90
+ const limiter = new RateLimiter();
91
+ const starts = [];
92
+ const p1 = limiter.add(async () => {
93
+ starts.push(Date.now());
94
+ await new Promise((r) => setTimeout(r, 1000));
95
+ return 'slow';
96
+ });
97
+ const p2 = limiter.add(async () => {
98
+ starts.push(Date.now());
99
+ return 'fast';
100
+ });
101
+ await Promise.resolve();
102
+ expect(starts).toEqual([0]);
103
+ await vi.advanceTimersByTimeAsync(200);
104
+ expect(starts).toEqual([0, 100]);
105
+ await vi.advanceTimersByTimeAsync(900);
106
+ await expect(p2).resolves.toBe('fast');
107
+ await expect(p1).resolves.toBe('slow');
108
+ });
109
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-ag",
3
- "version": "1.0.56",
3
+ "version": "1.0.57",
4
4
  "description": "Useful svelte components",
5
5
  "bugs": "https://github.com/ageorgeh/svelte-ag/issues",
6
6
  "repository": {
@@ -39,6 +39,7 @@
39
39
  "lint:fix": "eslint --fix",
40
40
  "prepare": "husky",
41
41
  "svelte:sync": "svelte-kit sync",
42
+ "test": "vitest --run",
42
43
  "test:e2e": "playwright test",
43
44
  "test:e2e:ui": "playwright test --ui",
44
45
  "test:e2e:update": "playwright test --update-snapshots",
@@ -53,7 +54,6 @@
53
54
  "devalue": "^5.6.4",
54
55
  "embla-carousel-svelte": "8.6.0",
55
56
  "formsnap": "2.0.1",
56
- "p-queue": "^9.1.0",
57
57
  "radash": "12.1.1",
58
58
  "runed": "0.37.1",
59
59
  "svelte-toolbelt": "^0.10.6",
@@ -1,11 +1,11 @@
1
1
  import type { ApiEndpoints, ApiInput, ApiRequestFunction, ApiSuccessBody, ApiErrorBody, ApiResponse } from 'ts-ag';
2
2
 
3
3
  import { stringify } from 'devalue';
4
- import PQueue from 'p-queue';
5
4
  import type { Cache } from './cache.svelte';
6
5
 
7
6
  import { cacheKey } from './utils.svelte.js';
8
7
  import type { BatchDetails } from './entrypoint.svelte';
8
+ import { RateLimiter } from './rate.svelte';
9
9
 
10
10
  export type QueryStatus = 'idle' | 'loading' | 'success' | 'error';
11
11
 
@@ -139,7 +139,7 @@ export class Requestor<
139
139
  #batchInput: BatchDetails<API, Path, Method>['batchInput'];
140
140
  #unBatchOutput: BatchDetails<API, Path, Method>['unBatchOutput'];
141
141
 
142
- #limiter: PQueue;
142
+ #limiter: RateLimiter;
143
143
  // #cache: Cache;
144
144
 
145
145
  // -------- State --------
@@ -163,11 +163,7 @@ export class Requestor<
163
163
  this.#path = path;
164
164
  this.#method = method;
165
165
  this.#request = request;
166
- this.#limiter = new PQueue({
167
- concurrency: 5,
168
- interval: 100,
169
- intervalCap: 1
170
- });
166
+ this.#limiter = new RateLimiter();
171
167
  // this.#cache = cache;
172
168
 
173
169
  // TODO
@@ -0,0 +1,20 @@
1
+ export class RateLimiter {
2
+ #nextStart = 0;
3
+ #interval = 100;
4
+
5
+ add<T>(fn: () => Promise<T>): Promise<T> {
6
+ const now = Date.now();
7
+ const startAt = Math.max(now, this.#nextStart);
8
+ this.#nextStart = startAt + this.#interval;
9
+
10
+ return (async () => {
11
+ const wait = Math.max(0, startAt - Date.now());
12
+
13
+ if (wait > 0) {
14
+ await new Promise((resolve) => setTimeout(resolve, wait));
15
+ }
16
+
17
+ return await fn();
18
+ })();
19
+ }
20
+ }
@@ -0,0 +1,149 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { sleep } from 'radash';
3
+ import { RateLimiter } from './rate.svelte.js';
4
+
5
+ describe('RateLimiter', () => {
6
+ beforeEach(() => {
7
+ vi.useFakeTimers();
8
+ vi.setSystemTime(0);
9
+ });
10
+
11
+ afterEach(() => {
12
+ vi.useRealTimers();
13
+ vi.restoreAllMocks();
14
+ });
15
+
16
+ it('starts the first request immediately', async () => {
17
+ const limiter = new RateLimiter();
18
+ const fn = vi.fn().mockResolvedValue('ok');
19
+
20
+ const promise = limiter.add(fn);
21
+
22
+ await Promise.resolve();
23
+
24
+ expect(fn).toHaveBeenCalledTimes(1);
25
+
26
+ await expect(promise).resolves.toBe('ok');
27
+ });
28
+
29
+ it('spaces simultaneous requests by at least 100ms', async () => {
30
+ const limiter = new RateLimiter();
31
+ const starts: number[] = [];
32
+
33
+ const makeTask = (label: string) =>
34
+ vi.fn().mockImplementation(async () => {
35
+ starts.push(Date.now());
36
+ await sleep(105);
37
+ return label;
38
+ });
39
+
40
+ const p1 = limiter.add(makeTask('a'));
41
+ const p2 = limiter.add(makeTask('b'));
42
+ const p3 = limiter.add(makeTask('c'));
43
+ const p4 = limiter.add(makeTask('d'));
44
+ const p5 = limiter.add(makeTask('e'));
45
+
46
+ await Promise.resolve();
47
+ expect(starts).toEqual([0]);
48
+
49
+ await vi.advanceTimersByTimeAsync(99);
50
+ expect(starts).toEqual([0]);
51
+
52
+ await vi.advanceTimersByTimeAsync(1);
53
+ expect(starts).toEqual([0, 100]);
54
+
55
+ await vi.advanceTimersByTimeAsync(100);
56
+ expect(starts).toEqual([0, 100, 200]);
57
+
58
+ await vi.advanceTimersByTimeAsync(100);
59
+ expect(starts).toEqual([0, 100, 200, 300]);
60
+
61
+ await vi.advanceTimersByTimeAsync(100);
62
+ expect(starts).toEqual([0, 100, 200, 300, 400]);
63
+
64
+ await vi.runAllTimersAsync();
65
+ await expect(Promise.all([p1, p2, p3, p4, p5])).resolves.toEqual(['a', 'b', 'c', 'd', 'e']);
66
+ });
67
+
68
+ it('does not bunch requests together after waiting', async () => {
69
+ const limiter = new RateLimiter();
70
+ const starts: number[] = [];
71
+
72
+ const task = () =>
73
+ limiter.add(async () => {
74
+ starts.push(Date.now());
75
+ await sleep(105);
76
+ });
77
+
78
+ const p1 = task();
79
+ const p2 = task();
80
+ const p3 = task();
81
+
82
+ await Promise.resolve();
83
+ expect(starts).toEqual([0]);
84
+
85
+ await vi.advanceTimersByTimeAsync(100);
86
+ expect(starts).toEqual([0, 100]);
87
+
88
+ await vi.advanceTimersByTimeAsync(100);
89
+ expect(starts).toEqual([0, 100, 200]);
90
+
91
+ await vi.runAllTimersAsync();
92
+ await Promise.all([p1, p2, p3]);
93
+ });
94
+
95
+ it('keeps working after a request fails', async () => {
96
+ const limiter = new RateLimiter();
97
+ const starts: number[] = [];
98
+
99
+ const p1 = limiter.add(async () => {
100
+ starts.push(Date.now());
101
+ await sleep(200);
102
+ throw new Error('boom');
103
+ });
104
+ const p1Expectation = expect(p1).rejects.toThrow('boom');
105
+
106
+ const p2 = limiter.add(async () => {
107
+ starts.push(Date.now());
108
+ return 'ok';
109
+ });
110
+
111
+ await Promise.resolve();
112
+ expect(starts).toEqual([0]);
113
+
114
+ await vi.advanceTimersByTimeAsync(100);
115
+ expect(starts).toEqual([0, 100]);
116
+
117
+ await vi.advanceTimersByTimeAsync(100);
118
+
119
+ await p1Expectation;
120
+ await expect(p2).resolves.toBe('ok');
121
+ });
122
+
123
+ it('only limits start time, not completion time', async () => {
124
+ const limiter = new RateLimiter();
125
+ const starts: number[] = [];
126
+
127
+ const p1 = limiter.add(async () => {
128
+ starts.push(Date.now());
129
+ await new Promise((r) => setTimeout(r, 1000));
130
+ return 'slow';
131
+ });
132
+
133
+ const p2 = limiter.add(async () => {
134
+ starts.push(Date.now());
135
+ return 'fast';
136
+ });
137
+
138
+ await Promise.resolve();
139
+ expect(starts).toEqual([0]);
140
+
141
+ await vi.advanceTimersByTimeAsync(200);
142
+ expect(starts).toEqual([0, 100]);
143
+
144
+ await vi.advanceTimersByTimeAsync(900);
145
+
146
+ await expect(p2).resolves.toBe('fast');
147
+ await expect(p1).resolves.toBe('slow');
148
+ });
149
+ });