Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.1.0] - 2026-09-30

### Added

- `read()`, `submit()` and `remove()` now return an awaitable object (a `PromiseLike`, like VueUse's `useFetch`): awaiting it waits for the execution started by the call and resolves to the same plain snapshot `execute()` resolves to (`isSuccess`, `isError`, `error`, `aborted`, ...). `const { isSuccess } = await remove({ id })` now does what it reads: before, `await` returned the object at once, before the request ended, and `isSuccess` was a `ComputedRef`, always truthy, so the success branch also ran on failed requests;
- awaiting never rejects (a failure resolves with `isError: true`, an aborted request with `aborted: true`), settles on the execution started by the call only (the first one with `autoExecute`), resolves at once to the current snapshot when the call starts no execution (`immediate: false`, or `executeWhen` false) and never starts a request itself, so `Promise.resolve()` or a `return` from an async function cannot run the action twice;
- the reactive fields and `execute()` are unchanged, and the provider components' template refs are not thenable.

### Changed

- Code that awaited an action, or returned it from an async function, and then used its refs now gets plain values: `const { data } = await read()` followed by `data.value` is now a type error, and the same destructuring after a top-level `await` in `<script setup>` renders once and no longer updates. Drop the `await`, or keep the returned object and await it on its own (`const users = read(); await users`);
- actions are cleaned up when the effect scope that created them is disposed, instead of on component unmount only: components behave as before, and `effectScope()` and Pinia setup stores are now covered too;
- dependencies update: minor/patch updates to `@types/node`, `eslint`, `unplugin-dts`, `vite` and `vitest`; `packageManager` bumped to `pnpm@12.8.1`.

### Fix

- An action created outside any effect scope (an event handler, a plain async function) was never cleaned up, so its query, hash and items stayed in the store for ever: awaiting it now releases its query once the awaited execution settles, unless `keepAlive` is `true`;
- a request that could not be built (for example a `RepositoryHttp` template with a missing path parameter) made `execute()` reject, with an unhandled rejection on the immediate execution, and left the query idle; the query now gets the error and `execute()` resolves with `isError: true`;
- a `read()` joining an in-flight request with the same params rejected, with an unhandled rejection, when that request failed, and its query never showed the error; it now joins the query and resolves with the error state, keeps the shared error, and resolves with `aborted: true` when the shared request is aborted.

## [2.0.8] - 2026-09-17

### Changed
Expand Down
97 changes: 91 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ const {
} = read()
```

The returned object is also awaitable, see [Await](#await).

### Parameters

`read()` accepts an optional parameters map.
Expand Down Expand Up @@ -385,8 +387,9 @@ const {
*/
directory: false,
/*
* Keep the query alive when
* the component is unmounted (default: false)
* Keep the query alive when the component (or the effect scope)
* that created it is disposed and, with no owner, after an awaited
* execution (default: false)
*/
keepAlive: false,
/*
Expand Down Expand Up @@ -510,6 +513,8 @@ const {
} = submit()
```

The returned object is also awaitable, see [Await](#await).

### Options

`submit()` accepts the following options:
Expand All @@ -531,8 +536,9 @@ const {
*/
name: undefined,
/*
* Keep the query alive when
* the component is unmounted (default: false)
* Keep the query alive when the component (or the effect scope)
* that created it is disposed and, with no owner, after an awaited
* execution (default: false)
*/
keepAlive: false,
/*
Expand Down Expand Up @@ -686,6 +692,8 @@ const {
})
```

The returned object is also awaitable, see [Await](#await).

### Options

`remove()` accepts the following options:
Expand All @@ -705,8 +713,9 @@ const {
*/
name: undefined,
/*
* Keep the query alive when
* the component is unmounted (default: false)
* Keep the query alive when the component (or the effect scope)
* that created it is disposed and, with no owner, after an awaited
* execution (default: false)
*/
keepAlive: false,
/* Execute the `remove()` action immediately (default: true) */
Expand All @@ -720,6 +729,82 @@ const {
)
```

## Await

The object returned by `read()`, `submit()` and `remove()` is awaitable, like the one returned by VueUse's `useFetch`. Awaiting it waits for the execution started by the call and resolves to the same plain snapshot that `execute()` resolves to: booleans and values, not refs.

```ts
const { remove } = useUsersStore()

async function deleteUser(id: string) {
const { isSuccess, error } = await remove({ id })
if (isSuccess) {
// the DELETE request succeeded
}
else {
// the DELETE request failed, `error` tells why
console.error(error)
}
}
```

The snapshot contains:

```ts
const {
/* Boolean that indicates if the request has succeeded */
isSuccess,
/* Boolean that indicates if the request has failed */
isError,
/* Boolean that indicates if the request has been aborted */
aborted,
/* The first error, if any */
error,
/* The array of errors */
errors,
/* The query object */
query,
/* The metadata object returned by the repository */
metadata,
/* `read()` and `submit()` only: the data array returned by the repository */
data,
/* `read()` and `submit()` only: the first item of the `data` array */
item,
} = await read()
```

Awaiting follows these rules:

- it never rejects: a failed request (also one that cannot be built, for example because of a missing path parameter) resolves with `isError: true` and its `error`, an aborted request (for example one replaced by an `execute()` with other parameters) resolves with `aborted: true`. With `RepositoryHttp` from `@volverjs/data` 2.0.x, a `read()` that fails without an HTTP response (a network error, an invalid JSON body) is reported as aborted, so do not treat `aborted` as a success;
- it settles on the execution started by the call only: with `autoExecute`, the executions started later by a parameters change do not change the result, await `execute()` to wait for one of them;
- when the call starts no execution (`immediate: false`, or `executeWhen` false when the action is called), it resolves at once to the current snapshot and sends no request, so `isSuccess` stays `false` until an execution succeeds;
- awaiting never starts a request, so `Promise.resolve()`, `Promise.all()` or returning the object from an `async` function do not execute the action twice.

The awaited values are plain, they do not update. Keep the returned object to use both the result and the reactive fields:

```ts
const users = read()
const { isSuccess } = await users
// `isSuccess` is a boolean, `users.data` is still a reactive array
```

Before `2.1.0` awaiting an action returned the object itself at once. If your code awaits an action and then uses its refs (`data.value`, or binds them in the template after a top-level `await` in `<script setup>`), drop the `await` or keep the returned object and await it on its own as above.

To execute an action later and wait for it, create it with `immediate: false` and await `execute()`:

```ts
const { execute } = remove(undefined, { immediate: false })

async function deleteUser(id: string) {
const { isSuccess } = await execute({ id })
// ...
}
```

The template refs of the provider components are not awaitable. If you expose the object returned by an action with `defineExpose()`, leave `then` out the same way (`const { then, ...exposed } = read()`), otherwise an `async` function that returns the component instance resolves to the snapshot instead.

An action is cleaned up when the effect scope that created it is disposed: the component `setup()` when the component unmounts, but also an `effectScope()` or a Pinia setup store. An action created with no active scope, for example in an event handler as above, has no owner: when it is awaited, its query is released as soon as the awaited execution settles (unless `keepAlive` is `true`), and the next clean up drops it together with the items only that query referenced, so the reactive fields of the returned object go back to their empty values. If you create an action outside a scope and do not await it, call `cleanup()` when you no longer need it.

## Components

`@volverjs/query-vue` also exposes some useful components:
Expand Down
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@volverjs/query-vue",
"type": "module",
"version": "0.0.0",
"packageManager": "pnpm@12.4.2",
"packageManager": "pnpm@12.8.1",
"description": "Repository pattern applied to a pinia store.",
"author": "8 Wave S.r.l.",
"license": "MIT",
Expand Down Expand Up @@ -66,21 +66,21 @@
"@antfu/eslint-config": "^9.5.1",
"@nabla/vite-plugin-eslint": "^3.0.1",
"@pinia/testing": "^2.0.1",
"@types/node": "^26.6.1",
"@types/node": "^26.6.3",
"@vitejs/plugin-vue": "^6.0.9",
"@volverjs/data": "^2.0.5",
"@vue/language-core": "^3.3.11",
"@vue/shared": "^3.5.43",
"@vue/test-utils": "^2.5.1",
"@vueuse/core": "^15.0.0",
"copy": "^0.3.2",
"eslint": "^10.10.0",
"eslint": "^10.11.0",
"happy-dom": "^20.14.5",
"pinia": "^4.0.3",
"typescript": "6.0.3",
"unplugin-dts": "1.1.0",
"vite": "^8.3.0",
"vitest": "^5.0.1",
"unplugin-dts": "1.1.1",
"vite": "^8.3.1",
"vitest": "^5.0.2",
"vitest-fetch-mock": "^0.4.5",
"vue": "^3.5.43",
"vue-tsc": "^3.3.11"
Expand Down
Loading
Loading