astatus
v0.1.5
Published
Async status signal. No data, no fetch — just the stage.
Maintainers
Readme
astatus
Async status signal. No data, no fetch — just the stage.
import astatus from "astatus";
const AS = astatus();
AS.pending();
await fetchUser();
await processData(); // however many steps
AS.success(); // you decide whenMost async state libraries tie status to data or fetch logic. astatus doesn't.
It's a standalone signal you inject into any flow, at any point you choose.
Install
npm install astatusUsage
import astatus, { STATUS } from "astatus";
const loginAS = astatus({ name: "login" });
// Inject status at the right moment
loginAS.pending();
const data = await fetchUser();
await syncStore(data); // sync or async, as many steps as needed
loginAS.success();
// Read
console.log(loginAS.status); // 'success'
console.log(loginAS.isSuccess); // trueAPI
All examples use
const AS = astatus()unless otherwise noted.
Create
astatus({ name?, status? })| Option | Type | Default | Description |
| -------- | -------- | ----------- | ------------------- |
| name | string | null | Name for debug logs |
| status | string | 'initial' | Initial status |
Status stages
astatus manages four built-in stages. These are the core of the library — everything else builds on top of them.
| Stage | Description |
| --------- | ------------------------------------ |
| initial | Not yet started — the default |
| pending | In progress |
| success | Completed successfully |
| failure | Failed, with an optional error value |
You move between these four stages by calling the corresponding methods. That's the whole idea.
AS.pending();
await doSomething();
AS.success();For anything outside this lifecycle, AS.custom('uploading') accepts any string.
Inject
AS.initial()
AS.pending()
AS.success()
AS.failure(error?) // optional error value
AS.custom('uploading') // any string outside built-in stagesRead
AS.status; // 'initial' | 'pending' | 'success' | 'failure' | custom
AS.error; // value passed to failure(), otherwise null
AS.isInitial;
AS.isPending;
AS.isSuccess;
AS.isFailure;
AS.isCustom; // true if current status is not a built-in stage
AS.isLocked;Observe
// All changes
const unsub = AS.subscribe((curr, prev) => {
console.log(curr.status, prev.status)
})
// Specific status — triggers on both entry and exit
const unwatch = AS.watch('success', (curr, prev) => { ... })
AS.watch(['success', 'failure'], (curr, prev) => { ... })
// Entry only — triggers once on transition into the status
const unwatch = AS.when('success', (curr, prev) => { ... })
// Cleanup
unsub()
unwatch()Wait
// As a timing gate — wait for the right moment, then continue
await AS.wait();
await AS.wait("success");
// As a result — inspect what happened
const { status, error, timeout, immediate } = await AS.wait();
const result = await AS.wait(["success", "failure"], 15); // timeout in seconds| Field | Description |
| ----------- | ------------------------------------------------- |
| status | Status at resolve time (snapshot) |
| error | Error at resolve time |
| timeout | true if timed out |
| immediate | true if already in target status |
| destroyed | true if instance was destroyed before resolving |
If destroy() is called while a wait() is pending, it resolves immediately with destroyed: true.
Lock
AS.lock(); // block all status changes
AS.unlock(); // unblockReset / Destroy
AS.reset(); // back to initial, clears error and lock
AS.destroy(); // resolves all pending wait()s, clears all listeners, blocks all further changesSTATUS constant
import { STATUS } from "astatus";
STATUS.INITIAL; // 'initial'
STATUS.PENDING; // 'pending'
STATUS.SUCCESS; // 'success'
STATUS.FAILURE; // 'failure'Vue / Nuxt
astatus includes a Vue composable out of the box. No extra install needed.
import { useAstatus } from "astatus/vue";Basic usage
<script setup>
import { useAstatus } from "astatus/vue";
const loginAS = useAstatus({ name: "login" });
const handleLogin = async () => {
loginAS.pending();
try {
await fetchUser();
loginAS.success();
} catch (e) {
loginAS.failure(e);
}
};
</script>
<template>
<button :disabled="loginAS.isPending" @click="handleLogin">
{{ loginAS.isPending ? "Loading..." : "Login" }}
</button>
<p v-if="loginAS.isFailure">{{ loginAS.error }}</p>
</template>Reactivity
status and error are readonly refs. isInitial, isPending, isSuccess, isFailure, isCustom are computed values — all reactive and safe to use in templates.
Refs are automatically unwrapped inside templates, so .value is not needed there.
const AS = useAstatus();
// script: use .value
watch(AS.isPending, (val) => {
console.log("pending:", val);
});
// template: no .value needed
// <p v-if="AS.isPending">...</p>Note: Destructuring breaks reactivity. Always use the returned object directly.
// ✅ const loginAS = useAstatus(); // loginAS.isPending in template // ❌ reactivity lost const { isPending } = useAstatus();
Auto cleanup
The composable automatically calls destroy() on component unmount via onUnmounted. No manual cleanup needed in most cases.
If you use useAstatus outside of a component (e.g. in a Pinia store), auto cleanup does not apply — call destroy() manually when done.
// Pinia store
const AS = useAstatus();
// when no longer needed
AS.destroy();wait() in Nuxt
wait() uses setTimeout internally. In Nuxt, avoid calling it during SSR — use it inside onMounted or guard with import.meta.client.
// ✅ safe
onMounted(async () => {
await AS.wait();
});
// ✅ safe
if (import.meta.client) {
await AS.wait();
}Requirements
- Vue >= 3.3.0
Notes
subscribe/watch/whenreturn an unsubscribe function — call it to clean up.destroy()resolves all pendingwait()calls immediately, then clears all listeners.- Listener errors are caught and logged individually without breaking other listeners.
License
MIT
Readme partially edited with AI assistance.
