@jskit-ai/http-web 0.1.59 → 0.1.60

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/http-web",
3
- "version": "0.1.59",
3
+ "version": "0.1.60",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -44,14 +44,14 @@
44
44
  "./client/support/contractGuards": "./src/client/support/contractGuards.js"
45
45
  },
46
46
  "dependencies": {
47
- "@jskit-ai/resource-crud-core": "0.1.156",
47
+ "@jskit-ai/resource-crud-core": "0.1.157",
48
48
  "@mdi/js": "^7.4.47"
49
49
  },
50
50
  "peerDependencies": {
51
- "@jskit-ai/http-runtime": "0.1.212",
52
- "@jskit-ai/kernel": "0.1.214",
53
- "@jskit-ai/realtime": "0.1.211",
54
- "@jskit-ai/shell-web": "0.1.218",
51
+ "@jskit-ai/http-runtime": "0.1.213",
52
+ "@jskit-ai/kernel": "0.1.215",
53
+ "@jskit-ai/realtime": "0.1.212",
54
+ "@jskit-ai/shell-web": "0.1.219",
55
55
  "@tanstack/vue-query": "^5.90.5",
56
56
  "vue": "^3.5.13",
57
57
  "vue-router": "^5.0.4",
@@ -91,7 +91,7 @@ function useEndpointResource({
91
91
 
92
92
  const query = useQuery({
93
93
  queryKey,
94
- queryFn: () => {
94
+ queryFn: ({ signal }) => {
95
95
  const requestPath = normalizedPath.value;
96
96
  if (!requestPath) {
97
97
  throw new Error("Resource path is required.");
@@ -102,7 +102,8 @@ function useEndpointResource({
102
102
  method: readMethod,
103
103
  query: readQuery,
104
104
  transport
105
- })
105
+ }),
106
+ signal
106
107
  });
107
108
  },
108
109
  enabled: queryEnabled,
@@ -79,20 +79,15 @@ function useListCore({
79
79
  queryKey,
80
80
  initialPageParam,
81
81
  enabled: queryEnabled,
82
- queryFn: async ({ pageParam }) => {
82
+ queryFn: async ({ pageParam, signal }) => {
83
83
  const requestPath = normalizedPath.value;
84
84
  if (!requestPath) {
85
85
  throw new Error("List path is required.");
86
86
  }
87
87
 
88
- return activeClient.request(
89
- requestPath,
90
- buildListRequestOptions({
91
- requestOptions,
92
- transport,
93
- pageParam
94
- })
95
- );
88
+ const options = buildListRequestOptions({ requestOptions, transport, pageParam });
89
+ options.signal = options.signal ? AbortSignal.any([signal, options.signal]) : signal;
90
+ return activeClient.request(requestPath, options);
96
91
  },
97
92
  getNextPageParam,
98
93
  selectItems,
@@ -14,7 +14,7 @@ import {
14
14
  configureHttpWebClient,
15
15
  resetHttpWebClientForTests
16
16
  } from "../src/client/lib/httpClient.js";
17
- import { buildListRequestOptions } from "../src/client/composables/runtime/useListCore.js";
17
+ import { buildListRequestOptions, useListCore } from "../src/client/composables/runtime/useListCore.js";
18
18
  import {
19
19
  resolveOperationRealtimeOptions
20
20
  } from "../src/client/composables/useRealtimeQueryInvalidation.js";
@@ -293,6 +293,75 @@ test("endpoint resource reads attach request recovery metadata to query options"
293
293
  });
294
294
  });
295
295
 
296
+ for (const kind of ["endpoint", "list", "list with caller signal"]) {
297
+ test(`repeated ${kind} refreshes abort superseded HTTP requests`, async (t) => {
298
+ const paginated = kind !== "endpoint";
299
+ const caller = kind === "list with caller signal" ? new AbortController() : null;
300
+ const queryClient = new QueryClient();
301
+ const queryKey = ["endpoint-cancellation"];
302
+ const requests = [];
303
+ const active = new Set();
304
+ let resource;
305
+ const payload = (revision) => paginated
306
+ ? { pages: [{ items: [revision] }], pageParams: [null] }
307
+ : { revision };
308
+ queryClient.setQueryData(queryKey, payload(0));
309
+ const app = createSSRApp({
310
+ setup() {
311
+ resource = (paginated ? useListCore : useEndpointResource)({
312
+ queryKey,
313
+ path: "/api/endpoint-cancellation",
314
+ requestOptions: caller ? { signal: caller.signal } : null,
315
+ queryOptions: { staleTime: Infinity, retry: false },
316
+ client: {
317
+ request(_path, { signal }) {
318
+ return new Promise((resolve, reject) => {
319
+ const request = { signal, resolve };
320
+ requests.push(request);
321
+ active.add(request);
322
+ signal?.addEventListener("abort", () => {
323
+ active.delete(request);
324
+ reject(new DOMException("Query cancelled", "AbortError"));
325
+ }, { once: true });
326
+ });
327
+ }
328
+ }
329
+ });
330
+ return () => h("div");
331
+ }
332
+ });
333
+ app.use(VueQueryPlugin, { queryClient });
334
+ t.after(() => {
335
+ queryClient.clear();
336
+ for (const request of requests) request.resolve({ revision: -1 });
337
+ });
338
+ await renderToString(app);
339
+
340
+ const refreshes = [];
341
+ for (let revision = 1; revision <= 10; revision += 1) {
342
+ // SSR has no mounted observer; include its cached query in invalidation.
343
+ refreshes.push(revision % 2 === 0
344
+ ? queryClient.invalidateQueries({ queryKey, refetchType: "all" })
345
+ : resource.reload());
346
+ await new Promise((resolve) => setImmediate(resolve));
347
+ assert.equal(requests.length, revision);
348
+ assert.equal(active.size, 1, "only the latest refresh should keep an HTTP request alive");
349
+ }
350
+ assert.equal(requests[0].signal.aborted, true);
351
+ assert.equal(requests.at(-1).signal.aborted, false);
352
+ requests.at(-1).resolve(paginated ? { items: [10] } : { revision: 10 });
353
+ await Promise.all(refreshes);
354
+ assert.deepEqual(queryClient.getQueryData(queryKey), payload(10));
355
+ if (caller) {
356
+ const pending = resource.reload();
357
+ caller.abort();
358
+ assert.equal(requests.at(-1).signal.aborted, true);
359
+ await pending;
360
+ assert.equal(resource.query.error.value.name, "AbortError");
361
+ }
362
+ });
363
+ }
364
+
296
365
  test("endpoint resources use the configured http-web HTTP client by default", async () => {
297
366
  const queryClient = new QueryClient();
298
367
  const calls = [];
@@ -256,10 +256,14 @@ test("route-synchronized lists make one initial request with the hydrated route
256
256
  });
257
257
 
258
258
  assert.equal(runtime.calls.length, 1);
259
+ const { signal } = runtime.calls[0].options;
260
+ assert.ok(signal instanceof AbortSignal);
261
+ assert.equal(signal.aborted, false);
259
262
  assert.deepEqual(runtime.calls[0], {
260
263
  path: "/contacts",
261
264
  options: {
262
265
  method: "GET",
266
+ signal,
263
267
  query: {
264
268
  currentness: "archived",
265
269
  limit: 20