svelte-ag 1.0.58 → 1.0.59

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;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,EACL,IAAI,EACL,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;IAyCxD,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;IA2BvB,OAAO,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;CAmB3F"}
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;AA2BnE,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,EACL,IAAI,EACL,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;IAyCxD,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;IA2BvB,OAAO,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;CAmB3F"}
@@ -1,12 +1,22 @@
1
1
  import { stringify } from 'devalue';
2
2
  import { cacheKey } from './utils.svelte.js';
3
3
  import { RateLimiter } from './rate.svelte';
4
+ function copyOwnOverrides(source, target) {
5
+ for (const key of Reflect.ownKeys(source)) {
6
+ const descriptor = Object.getOwnPropertyDescriptor(source, key);
7
+ if (!descriptor)
8
+ continue;
9
+ Object.defineProperty(target, key, descriptor);
10
+ }
11
+ return target;
12
+ }
4
13
  function cloneResponse(response) {
5
14
  if (response !== null &&
6
15
  typeof response === 'object' &&
7
16
  'clone' in response &&
8
17
  typeof response.clone === 'function') {
9
- return response.clone();
18
+ const source = response;
19
+ return copyOwnOverrides(source, response.clone());
10
20
  }
11
21
  return response;
12
22
  }
@@ -1,6 +1,9 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { createApiRequest } from 'ts-ag';
2
3
  import { Cache } from './cache.svelte.js';
3
4
  import { Query, Requestor } from './query.svelte.js';
5
+ import { stringify } from 'devalue';
6
+ import * as v from 'valibot';
4
7
  function getSingleId(input) {
5
8
  return 'id' in input ? input.id : input.ids[0];
6
9
  }
@@ -10,6 +13,23 @@ function jsonResponse(body, status = 200) {
10
13
  headers: { 'content-type': 'application/json' }
11
14
  });
12
15
  }
16
+ function devalueFetchResponse(body, status = 200) {
17
+ return new Response(stringify(body), {
18
+ status,
19
+ headers: { 'content-type': 'application/devalue' }
20
+ });
21
+ }
22
+ function withResponseOverrides(response) {
23
+ Object.defineProperty(response, 'extra', {
24
+ configurable: true,
25
+ value: () => 'copied'
26
+ });
27
+ Object.defineProperty(response, 'meta', {
28
+ configurable: true,
29
+ value: { source: 'custom' }
30
+ });
31
+ return response;
32
+ }
13
33
  function deferred() {
14
34
  let resolve;
15
35
  let reject;
@@ -27,6 +47,7 @@ describe('Requestor', () => {
27
47
  afterEach(() => {
28
48
  vi.useRealTimers();
29
49
  vi.restoreAllMocks();
50
+ vi.unstubAllGlobals();
30
51
  });
31
52
  it('passes through non-batched requests', async () => {
32
53
  const requestMock = vi.fn(async () => jsonResponse({ id: 1 }));
@@ -37,6 +58,23 @@ describe('Requestor', () => {
37
58
  expect(requestMock).toHaveBeenCalledWith('/users', 'GET', { id: 1 });
38
59
  await expect(response.json()).resolves.toEqual({ id: 1 });
39
60
  });
61
+ it('devalue response', async () => {
62
+ const fetchMock = vi.fn(async () => devalueFetchResponse({ id: 1 }));
63
+ vi.stubGlobal('fetch', fetchMock);
64
+ const request = createApiRequest({
65
+ '/users': {
66
+ GET: v.object({ id: v.number() })
67
+ }
68
+ }, 'https://api.example.test', 'test');
69
+ const requestor = new Requestor('/users', 'GET', request, new Cache());
70
+ const response = await requestor.request({ id: 1 });
71
+ expect(fetchMock).toHaveBeenCalledTimes(1);
72
+ expect(fetchMock).toHaveBeenCalledWith('https://api.example.test//users?id=1', expect.objectContaining({
73
+ method: 'GET',
74
+ credentials: 'include'
75
+ }));
76
+ await expect(response.json()).resolves.toEqual({ id: 1 });
77
+ });
40
78
  it('batches requests with the same batch id and preserves response order', async () => {
41
79
  const requestMock = vi.fn(async () => jsonResponse({ ok: true }));
42
80
  const request = requestMock;
@@ -164,6 +202,58 @@ describe('Query', () => {
164
202
  await expect(first.json()).resolves.toEqual({ id: 1, name: 'Ada' });
165
203
  await expect(second.json()).resolves.toEqual({ id: 1, name: 'Ada' });
166
204
  });
205
+ it('preserves devalue parsing for query state, returned responses, and cache hits', async () => {
206
+ const fetchMock = vi.fn(async () => devalueFetchResponse({ id: 1, createdAt: new Date('2024-01-01T00:00:00.000Z') }));
207
+ vi.stubGlobal('fetch', fetchMock);
208
+ const request = createApiRequest({
209
+ '/users': {
210
+ GET: v.object({ id: v.number() })
211
+ }
212
+ }, 'https://api.example.test', 'test');
213
+ const requestor = new Requestor('/users', 'GET', request, new Cache());
214
+ const query = new Query({
215
+ path: '/users',
216
+ method: 'GET',
217
+ input: { id: 1 },
218
+ requestor,
219
+ cache: new Cache()
220
+ });
221
+ const firstResponse = await query.request();
222
+ expect(fetchMock).toHaveBeenCalledTimes(1);
223
+ expect(query.status).toBe('success');
224
+ expect(query.data).toEqual({ id: 1, createdAt: new Date('2024-01-01T00:00:00.000Z') });
225
+ await expect(firstResponse.json()).resolves.toEqual({
226
+ id: 1,
227
+ createdAt: new Date('2024-01-01T00:00:00.000Z')
228
+ });
229
+ const cachedResponse = await query.request();
230
+ expect(fetchMock).toHaveBeenCalledTimes(1);
231
+ await expect(cachedResponse.json()).resolves.toEqual({
232
+ id: 1,
233
+ createdAt: new Date('2024-01-01T00:00:00.000Z')
234
+ });
235
+ });
236
+ it('preserves arbitrary response overrides across query clones and cache hits', async () => {
237
+ const customResponse = withResponseOverrides(jsonResponse({ id: 1 }));
238
+ const requestMock = vi.fn().mockResolvedValue(customResponse);
239
+ const requestor = {
240
+ request: requestMock
241
+ };
242
+ const query = new Query({
243
+ path: '/users',
244
+ method: 'GET',
245
+ input: { id: 1 },
246
+ requestor,
247
+ cache: new Cache()
248
+ });
249
+ const first = (await query.request());
250
+ const second = (await query.request());
251
+ expect(requestMock).toHaveBeenCalledTimes(1);
252
+ expect(first.extra()).toBe('copied');
253
+ expect(first.meta).toEqual({ source: 'custom' });
254
+ expect(second.extra()).toBe('copied');
255
+ expect(second.meta).toEqual({ source: 'custom' });
256
+ });
167
257
  it('updates success state from successful responses', async () => {
168
258
  const requestMock = vi.fn().mockResolvedValue(jsonResponse({ id: 1, active: true }));
169
259
  const requestor = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-ag",
3
- "version": "1.0.58",
3
+ "version": "1.0.59",
4
4
  "description": "Useful svelte components",
5
5
  "bugs": "https://github.com/ageorgeh/svelte-ag/issues",
6
6
  "repository": {
@@ -9,6 +9,17 @@ import { RateLimiter } from './rate.svelte';
9
9
 
10
10
  export type QueryStatus = 'idle' | 'loading' | 'success' | 'error';
11
11
 
12
+ function copyOwnOverrides<T extends object>(source: T, target: T): T {
13
+ for (const key of Reflect.ownKeys(source)) {
14
+ const descriptor = Object.getOwnPropertyDescriptor(source, key);
15
+ if (!descriptor) continue;
16
+
17
+ Object.defineProperty(target, key, descriptor);
18
+ }
19
+
20
+ return target;
21
+ }
22
+
12
23
  function cloneResponse<T>(response: T): T {
13
24
  if (
14
25
  response !== null &&
@@ -16,7 +27,8 @@ function cloneResponse<T>(response: T): T {
16
27
  'clone' in response &&
17
28
  typeof response.clone === 'function'
18
29
  ) {
19
- return response.clone();
30
+ const source = response as T & object;
31
+ return copyOwnOverrides(source, response.clone() as T & object) as T;
20
32
  }
21
33
 
22
34
  return response;
@@ -240,8 +252,8 @@ export class Requestor<
240
252
  if (batchId !== false) {
241
253
  return new Promise((resolve, reject) => {
242
254
  if (!this.#batchQueue[batchId]) this.#batchQueue[batchId] = [];
243
-
244
255
  this.#batchQueue[batchId].push({ input, resolve, reject });
256
+
245
257
  if (!this.#batchTimers[batchId]) {
246
258
  this.#batchTimers[batchId] = setTimeout(() => {
247
259
  void this.flushBatchQueue(batchId).finally(() => {
@@ -1,7 +1,9 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
- import type { ApiEndpoints, ApiRequestFunction } from 'ts-ag';
2
+ import { createApiRequest, type ApiEndpoints, type ApiRequestFunction } from 'ts-ag';
3
3
  import { Cache } from './cache.svelte.js';
4
4
  import { Query, Requestor } from './query.svelte.js';
5
+ import { stringify } from 'devalue';
6
+ import * as v from 'valibot';
5
7
 
6
8
  type PlainUserInput = { id: number };
7
9
  type BatchedUserInput = { id: number; group?: string };
@@ -40,6 +42,34 @@ function jsonResponse(body: unknown, status = 200): TestResponse {
40
42
  }) as TestResponse;
41
43
  }
42
44
 
45
+ function devalueFetchResponse(body: unknown, status = 200): Response {
46
+ return new Response(stringify(body), {
47
+ status,
48
+ headers: { 'content-type': 'application/devalue' }
49
+ });
50
+ }
51
+
52
+ function withResponseOverrides<T extends Response>(
53
+ response: T
54
+ ): T & {
55
+ extra: () => string;
56
+ meta: { source: string };
57
+ } {
58
+ Object.defineProperty(response, 'extra', {
59
+ configurable: true,
60
+ value: () => 'copied'
61
+ });
62
+ Object.defineProperty(response, 'meta', {
63
+ configurable: true,
64
+ value: { source: 'custom' }
65
+ });
66
+
67
+ return response as T & {
68
+ extra: () => string;
69
+ meta: { source: string };
70
+ };
71
+ }
72
+
43
73
  function deferred<T>() {
44
74
  let resolve!: (value: T | PromiseLike<T>) => void;
45
75
  let reject!: (reason?: unknown) => void;
@@ -60,6 +90,7 @@ describe('Requestor', () => {
60
90
  afterEach(() => {
61
91
  vi.useRealTimers();
62
92
  vi.restoreAllMocks();
93
+ vi.unstubAllGlobals();
63
94
  });
64
95
 
65
96
  it('passes through non-batched requests', async () => {
@@ -74,6 +105,34 @@ describe('Requestor', () => {
74
105
  await expect(response.json()).resolves.toEqual({ id: 1 });
75
106
  });
76
107
 
108
+ it('devalue response', async () => {
109
+ const fetchMock = vi.fn(async () => devalueFetchResponse({ id: 1 }));
110
+ vi.stubGlobal('fetch', fetchMock);
111
+
112
+ const request = createApiRequest<PlainUsersApi>(
113
+ {
114
+ '/users': {
115
+ GET: v.object({ id: v.number() })
116
+ }
117
+ },
118
+ 'https://api.example.test',
119
+ 'test'
120
+ );
121
+ const requestor = new Requestor<PlainUsersApi, '/users', 'GET'>('/users', 'GET', request, new Cache());
122
+
123
+ const response = await requestor.request({ id: 1 });
124
+
125
+ expect(fetchMock).toHaveBeenCalledTimes(1);
126
+ expect(fetchMock).toHaveBeenCalledWith(
127
+ 'https://api.example.test//users?id=1',
128
+ expect.objectContaining({
129
+ method: 'GET',
130
+ credentials: 'include'
131
+ })
132
+ );
133
+ await expect(response.json()).resolves.toEqual({ id: 1 });
134
+ });
135
+
77
136
  it('batches requests with the same batch id and preserves response order', async () => {
78
137
  const requestMock = vi.fn(async () => jsonResponse({ ok: true }));
79
138
  const request = requestMock as unknown as BatchedUsersRequest;
@@ -235,6 +294,80 @@ describe('Query', () => {
235
294
  await expect(second.json()).resolves.toEqual({ id: 1, name: 'Ada' });
236
295
  });
237
296
 
297
+ it('preserves devalue parsing for query state, returned responses, and cache hits', async () => {
298
+ const fetchMock = vi.fn(async () =>
299
+ devalueFetchResponse({ id: 1, createdAt: new Date('2024-01-01T00:00:00.000Z') })
300
+ );
301
+ vi.stubGlobal('fetch', fetchMock);
302
+
303
+ const request = createApiRequest<PlainUsersApi>(
304
+ {
305
+ '/users': {
306
+ GET: v.object({ id: v.number() })
307
+ }
308
+ },
309
+ 'https://api.example.test',
310
+ 'test'
311
+ );
312
+ const requestor = new Requestor<PlainUsersApi, '/users', 'GET'>('/users', 'GET', request, new Cache());
313
+ const query = new Query<PlainUsersApi, '/users', 'GET'>({
314
+ path: '/users',
315
+ method: 'GET',
316
+ input: { id: 1 },
317
+ requestor,
318
+ cache: new Cache()
319
+ });
320
+
321
+ const firstResponse = await query.request();
322
+
323
+ expect(fetchMock).toHaveBeenCalledTimes(1);
324
+ expect(query.status).toBe('success');
325
+ expect(query.data).toEqual({ id: 1, createdAt: new Date('2024-01-01T00:00:00.000Z') });
326
+ await expect(firstResponse.json()).resolves.toEqual({
327
+ id: 1,
328
+ createdAt: new Date('2024-01-01T00:00:00.000Z')
329
+ });
330
+
331
+ const cachedResponse = await query.request();
332
+
333
+ expect(fetchMock).toHaveBeenCalledTimes(1);
334
+ await expect(cachedResponse.json()).resolves.toEqual({
335
+ id: 1,
336
+ createdAt: new Date('2024-01-01T00:00:00.000Z')
337
+ });
338
+ });
339
+
340
+ it('preserves arbitrary response overrides across query clones and cache hits', async () => {
341
+ const customResponse = withResponseOverrides(jsonResponse({ id: 1 }) as Response);
342
+ const requestMock = vi.fn().mockResolvedValue(customResponse);
343
+ const requestor = {
344
+ request: requestMock as PlainUsersRequest
345
+ } as unknown as PlainUsersRequestor;
346
+
347
+ const query = new Query<PlainUsersApi, '/users', 'GET'>({
348
+ path: '/users',
349
+ method: 'GET',
350
+ input: { id: 1 },
351
+ requestor,
352
+ cache: new Cache()
353
+ });
354
+
355
+ const first = (await query.request()) as Response & {
356
+ extra: () => string;
357
+ meta: { source: string };
358
+ };
359
+ const second = (await query.request()) as Response & {
360
+ extra: () => string;
361
+ meta: { source: string };
362
+ };
363
+
364
+ expect(requestMock).toHaveBeenCalledTimes(1);
365
+ expect(first.extra()).toBe('copied');
366
+ expect(first.meta).toEqual({ source: 'custom' });
367
+ expect(second.extra()).toBe('copied');
368
+ expect(second.meta).toEqual({ source: 'custom' });
369
+ });
370
+
238
371
  it('updates success state from successful responses', async () => {
239
372
  const requestMock = vi.fn().mockResolvedValue(jsonResponse({ id: 1, active: true }));
240
373
  const requestor = {