astatus 0.1.0

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/README.md ADDED
@@ -0,0 +1,178 @@
1
+ # astatus
2
+
3
+ Async status signal. No data, no fetch — just the stage.
4
+
5
+ ```js
6
+ import astatus from "astatus";
7
+
8
+ const AS = astatus();
9
+
10
+ AS.pending();
11
+ await fetchUser();
12
+ await processData(); // however many steps
13
+ AS.success(); // you decide when
14
+ ```
15
+
16
+ Most async state libraries tie status to data or fetch logic. `astatus` doesn't.
17
+ It's a standalone signal you inject into any flow, at any point you choose.
18
+
19
+ ---
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ npm install astatus
25
+ ```
26
+
27
+ ---
28
+
29
+ ## Usage
30
+
31
+ ```js
32
+ import astatus, { STATUS } from "astatus";
33
+
34
+ const loginAS = astatus({ name: "login" });
35
+
36
+ // Inject status at the right moment
37
+ loginAS.pending();
38
+ const data = await fetchUser();
39
+ await syncStore(data); // sync or async, as many steps as needed
40
+ loginAS.success();
41
+
42
+ // Read
43
+ console.log(loginAS.status); // 'success'
44
+ console.log(loginAS.isSuccess); // true
45
+ ```
46
+
47
+ ---
48
+
49
+ ## API
50
+
51
+ > All examples use `const AS = astatus()` unless otherwise noted.
52
+
53
+ ### Create
54
+
55
+ ```js
56
+ astatus({ name?, status? })
57
+ ```
58
+
59
+ | Option | Type | Default | Description |
60
+ | -------- | -------- | ----------- | ------------------- |
61
+ | `name` | `string` | `null` | Name for debug logs |
62
+ | `status` | `string` | `'initial'` | Initial status |
63
+
64
+ ---
65
+
66
+ ### Inject
67
+
68
+ ```js
69
+ AS.initial()
70
+ AS.pending()
71
+ AS.success()
72
+ AS.failure(error?) // optional error value
73
+ AS.custom('uploading') // any string outside built-in stages
74
+ ```
75
+
76
+ ---
77
+
78
+ ### Read
79
+
80
+ ```js
81
+ AS.status; // 'initial' | 'pending' | 'success' | 'failure' | custom
82
+ AS.error; // value passed to failure(), otherwise null
83
+ AS.isInitial;
84
+ AS.isPending;
85
+ AS.isSuccess;
86
+ AS.isFailure;
87
+ AS.isCustom; // true if current status is not a built-in stage
88
+ AS.isLocked;
89
+ ```
90
+
91
+ ---
92
+
93
+ ### Observe
94
+
95
+ ```js
96
+ // All changes
97
+ const unsub = AS.subscribe((curr, prev) => {
98
+ console.log(curr.status, prev.status)
99
+ })
100
+
101
+ // Specific status — triggers on both entry and exit
102
+ const unwatch = AS.watch('success', (curr, prev) => { ... })
103
+ AS.watch(['success', 'failure'], (curr, prev) => { ... })
104
+
105
+ // Entry only — triggers once on transition into the status
106
+ const unwatch = AS.when('success', (curr, prev) => { ... })
107
+
108
+ // Cleanup
109
+ unsub()
110
+ unwatch()
111
+ ```
112
+
113
+ ---
114
+
115
+ ### Wait
116
+
117
+ ```js
118
+ // As a timing gate — wait for the right moment, then continue
119
+ await AS.wait();
120
+ await AS.wait("success");
121
+
122
+ // As a result — inspect what happened
123
+ const { status, error, timeout, immediate } = await AS.wait();
124
+ const result = await AS.wait(["success", "failure"], 15); // timeout in seconds
125
+ ```
126
+
127
+ | Field | Description |
128
+ | ----------- | ---------------------------------- |
129
+ | `status` | Status at resolve time (snapshot) |
130
+ | `error` | Error at resolve time |
131
+ | `timeout` | `true` if timed out |
132
+ | `immediate` | `true` if already in target status |
133
+ | `destroyed` | `true` if instance was destroyed |
134
+
135
+ ---
136
+
137
+ ### Lock
138
+
139
+ ```js
140
+ AS.lock(); // block all status changes
141
+ AS.unlock(); // unblock
142
+ ```
143
+
144
+ ---
145
+
146
+ ### Reset / Destroy
147
+
148
+ ```js
149
+ AS.reset(); // back to initial, clears lock
150
+ AS.destroy(); // clears all listeners, blocks all further changes
151
+ ```
152
+
153
+ ---
154
+
155
+ ### STATUS constant
156
+
157
+ ```js
158
+ import { STATUS } from "astatus";
159
+
160
+ STATUS.INITIAL; // 'initial'
161
+ STATUS.PENDING; // 'pending'
162
+ STATUS.SUCCESS; // 'success'
163
+ STATUS.FAILURE; // 'failure'
164
+ ```
165
+
166
+ ---
167
+
168
+ ## Notes
169
+
170
+ - `subscribe` / `watch` / `when` return an unsubscribe function — call it to clean up.
171
+ - `destroy()` clears all listeners at once. Useful on route change or component unmount.
172
+ - Listener errors are caught and logged individually without breaking other listeners.
173
+
174
+ ---
175
+
176
+ ## License
177
+
178
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,199 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+
19
+ // src/index.ts
20
+ var index_exports = {};
21
+ __export(index_exports, {
22
+ STATUS: () => STATUS,
23
+ default: () => index_default
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+ var STATUS = Object.freeze({
27
+ INITIAL: "initial",
28
+ PENDING: "pending",
29
+ SUCCESS: "success",
30
+ FAILURE: "failure"
31
+ });
32
+ var astatus = (options = {}) => {
33
+ const { name = null } = options;
34
+ const _prefix = `[astatus${name ? `:${name}` : ""}]`;
35
+ const _builtinStatuses = new Set(Object.values(STATUS));
36
+ let _status = options.status ?? STATUS.INITIAL;
37
+ let _error = null;
38
+ let _locked = false;
39
+ let _destroyed = false;
40
+ const _listeners = /* @__PURE__ */ new Set();
41
+ const _notify = (prevStatus, prevError) => {
42
+ const curr = { status: _status, error: _error };
43
+ const prev = { status: prevStatus, error: prevError };
44
+ _listeners.forEach((fn) => {
45
+ try {
46
+ fn(curr, prev);
47
+ } catch (e) {
48
+ console.error(`${_prefix} subscriber error:`, e);
49
+ }
50
+ });
51
+ };
52
+ const _set = (newStatus, newError = null) => {
53
+ if (_locked || _destroyed) return;
54
+ if (_status === newStatus && _error === newError) return;
55
+ const prevStatus = _status;
56
+ const prevError = _error;
57
+ _status = newStatus;
58
+ _error = newError;
59
+ _notify(prevStatus, prevError);
60
+ };
61
+ const isInitial = () => _status === STATUS.INITIAL;
62
+ const isPending = () => _status === STATUS.PENDING;
63
+ const isSuccess = () => _status === STATUS.SUCCESS;
64
+ const isFailure = () => _status === STATUS.FAILURE;
65
+ const isCustom = () => !_builtinStatuses.has(_status);
66
+ const initial = () => _set(STATUS.INITIAL);
67
+ const pending = () => _set(STATUS.PENDING);
68
+ const success = () => _set(STATUS.SUCCESS);
69
+ const failure = (error = null) => _set(STATUS.FAILURE, error);
70
+ const custom = (type) => {
71
+ if (!type) {
72
+ console.error(`${_prefix} custom() requires a non-empty string`);
73
+ return;
74
+ }
75
+ _set(type);
76
+ };
77
+ const lock = () => {
78
+ _locked = true;
79
+ };
80
+ const unlock = () => {
81
+ _locked = false;
82
+ };
83
+ const subscribe = (fn) => {
84
+ if (_destroyed) return () => {
85
+ };
86
+ _listeners.add(fn);
87
+ return () => _listeners.delete(fn);
88
+ };
89
+ const watch = (types, fn) => {
90
+ const _types = Array.isArray(types) ? types : [types];
91
+ return subscribe((curr, prev) => {
92
+ if (_types.includes(curr.status) || _types.includes(prev.status))
93
+ fn(curr, prev);
94
+ });
95
+ };
96
+ const when = (type, fn) => {
97
+ return subscribe((curr, prev) => {
98
+ if (curr.status === type && prev.status !== type) fn(curr, prev);
99
+ });
100
+ };
101
+ const wait = (types = [STATUS.SUCCESS, STATUS.FAILURE], timeoutSec = 10) => {
102
+ const _types = typeof types === "string" ? [types] : types;
103
+ if (_destroyed) {
104
+ return Promise.resolve({
105
+ timeout: false,
106
+ immediate: true,
107
+ destroyed: true,
108
+ status: _status,
109
+ error: _error
110
+ });
111
+ }
112
+ return new Promise((resolve) => {
113
+ if (_types.includes(_status)) {
114
+ resolve({
115
+ timeout: false,
116
+ immediate: true,
117
+ status: _status,
118
+ error: _error
119
+ });
120
+ return;
121
+ }
122
+ let unsubscribe;
123
+ const timer = setTimeout(() => {
124
+ unsubscribe();
125
+ resolve({
126
+ timeout: true,
127
+ immediate: false,
128
+ status: _status,
129
+ error: _error
130
+ });
131
+ }, timeoutSec * 1e3);
132
+ unsubscribe = subscribe(({ status, error }) => {
133
+ if (_types.includes(status)) {
134
+ clearTimeout(timer);
135
+ unsubscribe();
136
+ resolve({
137
+ timeout: false,
138
+ immediate: false,
139
+ status,
140
+ error
141
+ });
142
+ }
143
+ });
144
+ });
145
+ };
146
+ const reset = () => {
147
+ if (_destroyed) return;
148
+ _locked = false;
149
+ _set(STATUS.INITIAL);
150
+ };
151
+ const destroy = () => {
152
+ _listeners.clear();
153
+ _destroyed = true;
154
+ };
155
+ return {
156
+ get status() {
157
+ return _status;
158
+ },
159
+ get error() {
160
+ return _error;
161
+ },
162
+ get isInitial() {
163
+ return isInitial();
164
+ },
165
+ get isPending() {
166
+ return isPending();
167
+ },
168
+ get isSuccess() {
169
+ return isSuccess();
170
+ },
171
+ get isFailure() {
172
+ return isFailure();
173
+ },
174
+ get isCustom() {
175
+ return isCustom();
176
+ },
177
+ get isLocked() {
178
+ return _locked;
179
+ },
180
+ initial,
181
+ pending,
182
+ success,
183
+ failure,
184
+ custom,
185
+ lock,
186
+ unlock,
187
+ subscribe,
188
+ watch,
189
+ when,
190
+ wait,
191
+ reset,
192
+ destroy
193
+ };
194
+ };
195
+ var index_default = astatus;
196
+ // Annotate the CommonJS export names for ESM import in node:
197
+ 0 && (module.exports = {
198
+ STATUS
199
+ });
@@ -0,0 +1,60 @@
1
+ declare const STATUS: Readonly<{
2
+ readonly INITIAL: "initial";
3
+ readonly PENDING: "pending";
4
+ readonly SUCCESS: "success";
5
+ readonly FAILURE: "failure";
6
+ }>;
7
+ type StatusValue = (typeof STATUS)[keyof typeof STATUS];
8
+ interface AstatusState {
9
+ status: string;
10
+ error: unknown;
11
+ }
12
+ type AstatusListener = (curr: AstatusState, prev: AstatusState) => void;
13
+ interface AstatusOptions {
14
+ status?: string;
15
+ name?: string;
16
+ }
17
+ interface AstatusWaitResult {
18
+ timeout: boolean;
19
+ immediate: boolean;
20
+ destroyed?: boolean;
21
+ status: string;
22
+ error: unknown;
23
+ }
24
+ interface Astatus {
25
+ readonly status: string;
26
+ readonly error: unknown;
27
+ readonly isInitial: boolean;
28
+ readonly isPending: boolean;
29
+ readonly isSuccess: boolean;
30
+ readonly isFailure: boolean;
31
+ readonly isCustom: boolean;
32
+ readonly isLocked: boolean;
33
+ initial: () => void;
34
+ pending: () => void;
35
+ success: () => void;
36
+ failure: (error?: unknown) => void;
37
+ custom: (type: string) => void;
38
+ lock: () => void;
39
+ unlock: () => void;
40
+ subscribe: (fn: AstatusListener) => () => void;
41
+ watch: (types: string | string[], fn: AstatusListener) => () => void;
42
+ when: (type: string, fn: AstatusListener) => () => void;
43
+ wait: (types?: string | string[], timeoutSec?: number) => Promise<AstatusWaitResult>;
44
+ reset: () => void;
45
+ destroy: () => void;
46
+ }
47
+ /**
48
+ * astatus
49
+ *
50
+ * A signal that manages only the stage of an async operation
51
+ * (initial / pending / success / failure).
52
+ * Completely decoupled from data and fetch logic —
53
+ * you decide when and where to inject each status.
54
+ *
55
+ * @param options.status - Initial status (default: 'initial')
56
+ * @param options.name - Name for debug logs
57
+ */
58
+ declare const astatus: (options?: AstatusOptions) => Astatus;
59
+
60
+ export { type Astatus, type AstatusListener, type AstatusOptions, type AstatusState, type AstatusWaitResult, STATUS, type StatusValue, astatus as default };
@@ -0,0 +1,60 @@
1
+ declare const STATUS: Readonly<{
2
+ readonly INITIAL: "initial";
3
+ readonly PENDING: "pending";
4
+ readonly SUCCESS: "success";
5
+ readonly FAILURE: "failure";
6
+ }>;
7
+ type StatusValue = (typeof STATUS)[keyof typeof STATUS];
8
+ interface AstatusState {
9
+ status: string;
10
+ error: unknown;
11
+ }
12
+ type AstatusListener = (curr: AstatusState, prev: AstatusState) => void;
13
+ interface AstatusOptions {
14
+ status?: string;
15
+ name?: string;
16
+ }
17
+ interface AstatusWaitResult {
18
+ timeout: boolean;
19
+ immediate: boolean;
20
+ destroyed?: boolean;
21
+ status: string;
22
+ error: unknown;
23
+ }
24
+ interface Astatus {
25
+ readonly status: string;
26
+ readonly error: unknown;
27
+ readonly isInitial: boolean;
28
+ readonly isPending: boolean;
29
+ readonly isSuccess: boolean;
30
+ readonly isFailure: boolean;
31
+ readonly isCustom: boolean;
32
+ readonly isLocked: boolean;
33
+ initial: () => void;
34
+ pending: () => void;
35
+ success: () => void;
36
+ failure: (error?: unknown) => void;
37
+ custom: (type: string) => void;
38
+ lock: () => void;
39
+ unlock: () => void;
40
+ subscribe: (fn: AstatusListener) => () => void;
41
+ watch: (types: string | string[], fn: AstatusListener) => () => void;
42
+ when: (type: string, fn: AstatusListener) => () => void;
43
+ wait: (types?: string | string[], timeoutSec?: number) => Promise<AstatusWaitResult>;
44
+ reset: () => void;
45
+ destroy: () => void;
46
+ }
47
+ /**
48
+ * astatus
49
+ *
50
+ * A signal that manages only the stage of an async operation
51
+ * (initial / pending / success / failure).
52
+ * Completely decoupled from data and fetch logic —
53
+ * you decide when and where to inject each status.
54
+ *
55
+ * @param options.status - Initial status (default: 'initial')
56
+ * @param options.name - Name for debug logs
57
+ */
58
+ declare const astatus: (options?: AstatusOptions) => Astatus;
59
+
60
+ export { type Astatus, type AstatusListener, type AstatusOptions, type AstatusState, type AstatusWaitResult, STATUS, type StatusValue, astatus as default };
package/dist/index.js ADDED
@@ -0,0 +1,175 @@
1
+ // src/index.ts
2
+ var STATUS = Object.freeze({
3
+ INITIAL: "initial",
4
+ PENDING: "pending",
5
+ SUCCESS: "success",
6
+ FAILURE: "failure"
7
+ });
8
+ var astatus = (options = {}) => {
9
+ const { name = null } = options;
10
+ const _prefix = `[astatus${name ? `:${name}` : ""}]`;
11
+ const _builtinStatuses = new Set(Object.values(STATUS));
12
+ let _status = options.status ?? STATUS.INITIAL;
13
+ let _error = null;
14
+ let _locked = false;
15
+ let _destroyed = false;
16
+ const _listeners = /* @__PURE__ */ new Set();
17
+ const _notify = (prevStatus, prevError) => {
18
+ const curr = { status: _status, error: _error };
19
+ const prev = { status: prevStatus, error: prevError };
20
+ _listeners.forEach((fn) => {
21
+ try {
22
+ fn(curr, prev);
23
+ } catch (e) {
24
+ console.error(`${_prefix} subscriber error:`, e);
25
+ }
26
+ });
27
+ };
28
+ const _set = (newStatus, newError = null) => {
29
+ if (_locked || _destroyed) return;
30
+ if (_status === newStatus && _error === newError) return;
31
+ const prevStatus = _status;
32
+ const prevError = _error;
33
+ _status = newStatus;
34
+ _error = newError;
35
+ _notify(prevStatus, prevError);
36
+ };
37
+ const isInitial = () => _status === STATUS.INITIAL;
38
+ const isPending = () => _status === STATUS.PENDING;
39
+ const isSuccess = () => _status === STATUS.SUCCESS;
40
+ const isFailure = () => _status === STATUS.FAILURE;
41
+ const isCustom = () => !_builtinStatuses.has(_status);
42
+ const initial = () => _set(STATUS.INITIAL);
43
+ const pending = () => _set(STATUS.PENDING);
44
+ const success = () => _set(STATUS.SUCCESS);
45
+ const failure = (error = null) => _set(STATUS.FAILURE, error);
46
+ const custom = (type) => {
47
+ if (!type) {
48
+ console.error(`${_prefix} custom() requires a non-empty string`);
49
+ return;
50
+ }
51
+ _set(type);
52
+ };
53
+ const lock = () => {
54
+ _locked = true;
55
+ };
56
+ const unlock = () => {
57
+ _locked = false;
58
+ };
59
+ const subscribe = (fn) => {
60
+ if (_destroyed) return () => {
61
+ };
62
+ _listeners.add(fn);
63
+ return () => _listeners.delete(fn);
64
+ };
65
+ const watch = (types, fn) => {
66
+ const _types = Array.isArray(types) ? types : [types];
67
+ return subscribe((curr, prev) => {
68
+ if (_types.includes(curr.status) || _types.includes(prev.status))
69
+ fn(curr, prev);
70
+ });
71
+ };
72
+ const when = (type, fn) => {
73
+ return subscribe((curr, prev) => {
74
+ if (curr.status === type && prev.status !== type) fn(curr, prev);
75
+ });
76
+ };
77
+ const wait = (types = [STATUS.SUCCESS, STATUS.FAILURE], timeoutSec = 10) => {
78
+ const _types = typeof types === "string" ? [types] : types;
79
+ if (_destroyed) {
80
+ return Promise.resolve({
81
+ timeout: false,
82
+ immediate: true,
83
+ destroyed: true,
84
+ status: _status,
85
+ error: _error
86
+ });
87
+ }
88
+ return new Promise((resolve) => {
89
+ if (_types.includes(_status)) {
90
+ resolve({
91
+ timeout: false,
92
+ immediate: true,
93
+ status: _status,
94
+ error: _error
95
+ });
96
+ return;
97
+ }
98
+ let unsubscribe;
99
+ const timer = setTimeout(() => {
100
+ unsubscribe();
101
+ resolve({
102
+ timeout: true,
103
+ immediate: false,
104
+ status: _status,
105
+ error: _error
106
+ });
107
+ }, timeoutSec * 1e3);
108
+ unsubscribe = subscribe(({ status, error }) => {
109
+ if (_types.includes(status)) {
110
+ clearTimeout(timer);
111
+ unsubscribe();
112
+ resolve({
113
+ timeout: false,
114
+ immediate: false,
115
+ status,
116
+ error
117
+ });
118
+ }
119
+ });
120
+ });
121
+ };
122
+ const reset = () => {
123
+ if (_destroyed) return;
124
+ _locked = false;
125
+ _set(STATUS.INITIAL);
126
+ };
127
+ const destroy = () => {
128
+ _listeners.clear();
129
+ _destroyed = true;
130
+ };
131
+ return {
132
+ get status() {
133
+ return _status;
134
+ },
135
+ get error() {
136
+ return _error;
137
+ },
138
+ get isInitial() {
139
+ return isInitial();
140
+ },
141
+ get isPending() {
142
+ return isPending();
143
+ },
144
+ get isSuccess() {
145
+ return isSuccess();
146
+ },
147
+ get isFailure() {
148
+ return isFailure();
149
+ },
150
+ get isCustom() {
151
+ return isCustom();
152
+ },
153
+ get isLocked() {
154
+ return _locked;
155
+ },
156
+ initial,
157
+ pending,
158
+ success,
159
+ failure,
160
+ custom,
161
+ lock,
162
+ unlock,
163
+ subscribe,
164
+ watch,
165
+ when,
166
+ wait,
167
+ reset,
168
+ destroy
169
+ };
170
+ };
171
+ var index_default = astatus;
172
+ export {
173
+ STATUS,
174
+ index_default as default
175
+ };
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "astatus",
3
+ "version": "0.1.0",
4
+ "description": "Async status signal. No data, no fetch — just the stage.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.cjs"
13
+ }
14
+ },
15
+ "types": "./dist/index.d.ts",
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup src/index.ts --format esm,cjs --dts",
21
+ "test": "vitest"
22
+ },
23
+ "keywords": [
24
+ "async",
25
+ "status",
26
+ "state",
27
+ "signal",
28
+ "framework-agnostic"
29
+ ],
30
+ "license": "MIT",
31
+ "devDependencies": {
32
+ "tsup": "latest",
33
+ "typescript": "^5.9.3",
34
+ "vitest": "latest"
35
+ }
36
+ }