@cplieger/actions 1.0.0 → 1.0.1

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.
Files changed (80) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +0 -0
  3. package/jsr.json +8 -12
  4. package/package.json +7 -9
  5. package/src/__test-helpers__/action-test-setup.ts +34 -0
  6. package/src/api-idempotency.test.ts +163 -0
  7. package/src/api.test.ts +96 -0
  8. package/src/api.ts +0 -0
  9. package/src/cleanup.test.ts +102 -0
  10. package/src/cleanup.ts +0 -0
  11. package/src/configure-api.test.ts +219 -0
  12. package/src/cross-action-coordination.test.ts +176 -0
  13. package/src/cross-action-races-extra.test.ts +231 -0
  14. package/src/cross-action-races.test.ts +213 -0
  15. package/src/cross-action-scope-serial.test.ts +121 -0
  16. package/src/debounce.test.ts +121 -0
  17. package/src/debounce.ts +0 -0
  18. package/src/define-helpers.ts +0 -0
  19. package/src/define.property.test.ts +133 -0
  20. package/src/define.test.ts +476 -0
  21. package/src/define.ts +0 -0
  22. package/src/dispatch-callbacks.test.ts +141 -0
  23. package/src/dispatch-options.test.ts +197 -0
  24. package/src/edge-cases.test.ts +81 -0
  25. package/src/error.test.ts +268 -0
  26. package/src/error.ts +0 -0
  27. package/src/index.ts +0 -0
  28. package/src/leak-stress.test.ts +117 -0
  29. package/src/loading.test.ts +146 -0
  30. package/src/loading.ts +0 -0
  31. package/src/new-features.test.ts +246 -0
  32. package/src/notifier.ts +0 -0
  33. package/src/poll.test.ts +385 -0
  34. package/src/poll.ts +0 -0
  35. package/src/primitive-edge-cases.test.ts +105 -0
  36. package/src/primitive-interactions.test.ts +112 -0
  37. package/src/primitives.test.ts +215 -0
  38. package/src/race-dedupe-cancel.test.ts +88 -0
  39. package/src/redteam.test.ts +254 -0
  40. package/src/redteam2.test.ts +337 -0
  41. package/src/redteam3.test.ts +454 -0
  42. package/src/registry.test.ts +271 -0
  43. package/src/registry.ts +0 -0
  44. package/src/retry-network-delay.test.ts +155 -0
  45. package/src/retry.ts +0 -0
  46. package/src/transport.test.ts +139 -0
  47. package/src/transport.ts +0 -0
  48. package/src/types.ts +0 -0
  49. package/.editorconfig +0 -19
  50. package/.htmlvalidate.json +0 -28
  51. package/.stylelintrc.json +0 -67
  52. package/dist/src/api.d.ts +0 -56
  53. package/dist/src/api.js +0 -158
  54. package/dist/src/cleanup.d.ts +0 -11
  55. package/dist/src/cleanup.js +0 -63
  56. package/dist/src/debounce.d.ts +0 -28
  57. package/dist/src/debounce.js +0 -91
  58. package/dist/src/define-helpers.d.ts +0 -20
  59. package/dist/src/define-helpers.js +0 -83
  60. package/dist/src/define.d.ts +0 -14
  61. package/dist/src/define.js +0 -627
  62. package/dist/src/error.d.ts +0 -42
  63. package/dist/src/error.js +0 -174
  64. package/dist/src/index.d.ts +0 -17
  65. package/dist/src/index.js +0 -22
  66. package/dist/src/loading.d.ts +0 -15
  67. package/dist/src/loading.js +0 -114
  68. package/dist/src/notifier.d.ts +0 -21
  69. package/dist/src/notifier.js +0 -21
  70. package/dist/src/poll.d.ts +0 -23
  71. package/dist/src/poll.js +0 -120
  72. package/dist/src/registry.d.ts +0 -18
  73. package/dist/src/registry.js +0 -210
  74. package/dist/src/retry.d.ts +0 -9
  75. package/dist/src/retry.js +0 -78
  76. package/dist/src/transport.d.ts +0 -46
  77. package/dist/src/transport.js +0 -70
  78. package/dist/src/types.d.ts +0 -157
  79. package/dist/src/types.js +0 -7
  80. package/eslint.config.base.mjs +0 -186
@@ -1,210 +0,0 @@
1
- // Action registry: in-memory log of all dispatched actions with a
2
- // subscribe API. Fires per state transition. Bounded to a recent
3
- // window so memory usage stays flat over a long session.
4
- //
5
- // Performance: eviction uses a head-pointer + tombstones instead of
6
- // splice + O(n) index re-computation. record() is O(1) amortized.
7
- // ---------------------------------------------------------------------------
8
- const MAX_LOG_SIZE = 200;
9
- const MAX_LOG_HARD = MAX_LOG_SIZE * 5;
10
- const log = [];
11
- const idMap = new Map();
12
- const listeners = new Set();
13
- const namedListeners = new Map();
14
- const pendingByName = new Map();
15
- let _pendingTotal = 0;
16
- let _liveCount = 0;
17
- let _head = 0;
18
- function compact() {
19
- while (_head < log.length && log[_head] === null) {
20
- _head++;
21
- }
22
- if (_head > 256) {
23
- log.splice(0, _head);
24
- for (const entry of idMap.values()) {
25
- entry.index -= _head;
26
- }
27
- _head = 0;
28
- }
29
- }
30
- /** Record a state transition. Called by define.ts. */
31
- export function record(instance) {
32
- const existing = idMap.get(instance.id);
33
- if (existing !== undefined) {
34
- const prev = existing.instance;
35
- if (prev.status === "pending" && instance.status !== "pending") {
36
- _pendingTotal--;
37
- const s = pendingByName.get(prev.name);
38
- if (s !== undefined) {
39
- s.delete(instance.id);
40
- if (s.size === 0) {
41
- pendingByName.delete(prev.name);
42
- }
43
- }
44
- }
45
- else if (prev.status !== "pending" && instance.status === "pending") {
46
- _pendingTotal++;
47
- let s = pendingByName.get(instance.name);
48
- if (s === undefined) {
49
- s = new Set();
50
- pendingByName.set(instance.name, s);
51
- }
52
- s.add(instance.id);
53
- }
54
- log[existing.index] = instance;
55
- existing.instance = instance;
56
- if (instance.status !== "pending" && _liveCount > MAX_LOG_SIZE) {
57
- for (let i = _head; i < log.length; i++) {
58
- const entry = log[i];
59
- if (entry !== null &&
60
- entry !== undefined &&
61
- entry.status !== "pending" &&
62
- entry.id !== instance.id) {
63
- idMap.delete(entry.id);
64
- log[i] = null;
65
- _liveCount--;
66
- if (_liveCount <= MAX_LOG_SIZE) {
67
- break;
68
- }
69
- }
70
- }
71
- compact();
72
- }
73
- }
74
- else {
75
- const idx = log.length;
76
- log.push(instance);
77
- idMap.set(instance.id, { instance, index: idx });
78
- _liveCount++;
79
- if (instance.status === "pending") {
80
- _pendingTotal++;
81
- let s = pendingByName.get(instance.name);
82
- if (s === undefined) {
83
- s = new Set();
84
- pendingByName.set(instance.name, s);
85
- }
86
- s.add(instance.id);
87
- }
88
- if (_liveCount > MAX_LOG_SIZE) {
89
- for (let i = _head; i < log.length; i++) {
90
- const entry = log[i];
91
- if (entry !== null && entry !== undefined && entry.status !== "pending") {
92
- idMap.delete(entry.id);
93
- log[i] = null;
94
- _liveCount--;
95
- break;
96
- }
97
- }
98
- }
99
- if (_liveCount > MAX_LOG_HARD) {
100
- for (let i = _head; i < log.length; i++) {
101
- const entry = log[i];
102
- if (entry !== null && entry !== undefined) {
103
- if (entry.status === "pending") {
104
- _pendingTotal--;
105
- const s = pendingByName.get(entry.name);
106
- if (s !== undefined) {
107
- s.delete(entry.id);
108
- if (s.size === 0) {
109
- pendingByName.delete(entry.name);
110
- }
111
- }
112
- }
113
- idMap.delete(entry.id);
114
- log[i] = null;
115
- _liveCount--;
116
- break;
117
- }
118
- }
119
- }
120
- compact();
121
- }
122
- if (_pendingTotal < 0) {
123
- console.warn("[actions] _pendingTotal went negative — invariant violation; clamping to 0");
124
- _pendingTotal = 0;
125
- }
126
- for (const fn of listeners) {
127
- try {
128
- fn(instance);
129
- }
130
- catch (e) {
131
- console.error("[actions] registry listener threw", e);
132
- }
133
- }
134
- const named = namedListeners.get(instance.name);
135
- if (named !== undefined) {
136
- for (const fn of named) {
137
- try {
138
- fn(instance);
139
- }
140
- catch (e) {
141
- console.error("[actions] registry listener threw", e);
142
- }
143
- }
144
- }
145
- }
146
- /** Subscribe to all action lifecycle events. */
147
- export function subscribe(fn) {
148
- listeners.add(fn);
149
- return () => listeners.delete(fn);
150
- }
151
- /** Subscribe to lifecycle events for a single action name. */
152
- export function subscribeByName(name, fn) {
153
- let set = namedListeners.get(name);
154
- if (set === undefined) {
155
- set = new Set();
156
- namedListeners.set(name, set);
157
- }
158
- set.add(fn);
159
- const captured = set;
160
- return () => {
161
- captured.delete(fn);
162
- if (captured.size === 0 && namedListeners.get(name) === captured) {
163
- namedListeners.delete(name);
164
- }
165
- };
166
- }
167
- /** @internal Test-only public surface. */
168
- export function recentLog() {
169
- const result = [];
170
- for (let i = _head; i < log.length; i++) {
171
- const entry = log[i];
172
- if (entry != null) {
173
- result.push(entry);
174
- }
175
- }
176
- return result;
177
- }
178
- /** Read the recent action log. Useful for devtools integration and
179
- * debugging panels. Returns a snapshot of all live entries. */
180
- export const getActionLog = recentLog;
181
- /** O(1) check: true if at least one instance of the named action is pending. */
182
- export function isPending(name) {
183
- const s = pendingByName.get(name);
184
- return s !== undefined && s.size > 0;
185
- }
186
- /** Pending count for action(s). */
187
- export function pendingCount(names) {
188
- if (names === undefined) {
189
- return _pendingTotal;
190
- }
191
- let total = 0;
192
- for (const name of names) {
193
- const s = pendingByName.get(name);
194
- if (s !== undefined) {
195
- total += s.size;
196
- }
197
- }
198
- return total;
199
- }
200
- /** Test-only: clear log + listeners. */
201
- export function _resetForTest() {
202
- log.length = 0;
203
- _head = 0;
204
- _liveCount = 0;
205
- idMap.clear();
206
- pendingByName.clear();
207
- listeners.clear();
208
- namedListeners.clear();
209
- _pendingTotal = 0;
210
- }
@@ -1,9 +0,0 @@
1
- /** Abort-aware sleep. Rejects with AbortError if the signal fires
2
- * before the timeout elapses. Resolves immediately for ms <= 0. */
3
- export declare function sleep(ms: number, signal: AbortSignal): Promise<void>;
4
- /** Wait for the browser to come back online, or for the signal to abort. */
5
- export declare function waitForOnline(signal: AbortSignal): Promise<void>;
6
- /** Attach attempt count to a thrown error (non-enumerable property). */
7
- export declare function attachAttempts(e: unknown, attempts: number): void;
8
- /** Read the attempt count attached by runWithRetry, or undefined. */
9
- export declare function readAttempts(e: unknown): number | undefined;
package/dist/src/retry.js DELETED
@@ -1,78 +0,0 @@
1
- // retry.ts — extracted retry/backoff primitives from define.ts.
2
- // These are pure utility functions with no dependency on the action
3
- // framework, making them independently testable.
4
- // ---------------------------------------------------------------------------
5
- /** Abort-aware sleep. Rejects with AbortError if the signal fires
6
- * before the timeout elapses. Resolves immediately for ms <= 0. */
7
- export function sleep(ms, signal) {
8
- if (signal.aborted) {
9
- return Promise.reject(new DOMException("aborted", "AbortError"));
10
- }
11
- if (ms <= 0) {
12
- return Promise.resolve();
13
- }
14
- return new Promise((resolve, reject) => {
15
- const t = setTimeout(() => {
16
- signal.removeEventListener("abort", onAbort);
17
- resolve();
18
- }, ms);
19
- const onAbort = () => {
20
- clearTimeout(t);
21
- reject(new DOMException("aborted", "AbortError"));
22
- };
23
- signal.addEventListener("abort", onAbort, { once: true });
24
- });
25
- }
26
- /** Wait for the browser to come back online, or for the signal to abort. */
27
- export function waitForOnline(signal) {
28
- if (typeof navigator === "undefined" || navigator.onLine) {
29
- return Promise.resolve();
30
- }
31
- if (signal.aborted) {
32
- return Promise.reject(new DOMException("aborted", "AbortError"));
33
- }
34
- return new Promise((resolve, reject) => {
35
- const onOnline = () => {
36
- cleanup();
37
- resolve();
38
- };
39
- const onAbort = () => {
40
- cleanup();
41
- reject(new DOMException("aborted", "AbortError"));
42
- };
43
- function cleanup() {
44
- if (typeof window !== "undefined") {
45
- window.removeEventListener("online", onOnline);
46
- }
47
- signal.removeEventListener("abort", onAbort);
48
- }
49
- if (typeof window !== "undefined") {
50
- window.addEventListener("online", onOnline, { once: true });
51
- }
52
- signal.addEventListener("abort", onAbort, { once: true });
53
- });
54
- }
55
- /** Attach attempt count to a thrown error (non-enumerable property). */
56
- export function attachAttempts(e, attempts) {
57
- if (typeof e === "object" && e !== null) {
58
- try {
59
- Object.defineProperty(e, "_attempts", { value: attempts, configurable: true });
60
- }
61
- catch {
62
- /* frozen/sealed object — skip */
63
- }
64
- }
65
- }
66
- /** Read the attempt count attached by runWithRetry, or undefined. */
67
- export function readAttempts(e) {
68
- try {
69
- if (typeof e === "object" && e !== null && "_attempts" in e) {
70
- const val = e._attempts;
71
- return typeof val === "number" ? val : undefined;
72
- }
73
- }
74
- catch {
75
- /* Proxy or getter threw — skip */
76
- }
77
- return undefined;
78
- }
@@ -1,46 +0,0 @@
1
- import type { Action, ActionDefinition } from "./types.js";
2
- /** Result returned by the transport send function.
3
- * `ok: true` means the command was accepted; `ok: false` triggers the
4
- * action's error branch (rollback + notification). */
5
- export interface TransportSendResult {
6
- readonly ok: boolean;
7
- readonly status: number;
8
- readonly error?: string;
9
- readonly code?: string;
10
- }
11
- /** A command object sent via the transport layer. Must include a `type`
12
- * discriminator; additional fields carry the command payload. */
13
- export interface TransportCommand {
14
- readonly type: string;
15
- [key: string]: unknown;
16
- }
17
- /** Signature of the consumer-provided send function.
18
- * Receives the command and an options bag with an AbortSignal for cancellation. */
19
- export type TransportSendFn = (cmd: TransportCommand, opts: {
20
- signal: AbortSignal;
21
- }) => Promise<TransportSendResult>;
22
- /**
23
- * Configure the global transport adapter. Call once at app boot.
24
- * Only needed if using `transportAction`.
25
- */
26
- export declare function configureTransport(fn: TransportSendFn): void;
27
- /** @internal Test-only: reset the transport to unconfigured state. */
28
- export declare function _resetTransportForTest(): void;
29
- /** Caller-facing shape of a transportAction definition. `command`
30
- * replaces `run`. Result is `void` because transport.send does not
31
- * return a payload (the response arrives later via SSE events). */
32
- interface TransportActionDefinition<TArgs, TOp = unknown> extends Omit<ActionDefinition<TArgs, void, TOp>, "run"> {
33
- /** Build the command for this dispatch. Re-evaluated per-dispatch. */
34
- command: (args: TArgs) => TransportCommand;
35
- }
36
- /**
37
- * Build an Action from a transport command descriptor. The generated
38
- * `run()` calls the configured transport send function and throws
39
- * {@link ActionError} on `!ok`, so the dispatcher's error branch
40
- * (notification + rollback) fires consistently.
41
- *
42
- * @param def - Transport action definition where `command` replaces `run`.
43
- * @returns An {@link Action} backed by the configured transport adapter.
44
- */
45
- export declare function transportAction<TArgs, TOp = unknown>(def: TransportActionDefinition<TArgs, TOp>): Action<TArgs, void>;
46
- export {};
@@ -1,70 +0,0 @@
1
- // Transport injection seam: consumer-provided adapter for streaming/SSE
2
- // command dispatch. Mirrors the notifier.ts pattern — call
3
- // configureTransport() at boot to wire up the send function.
4
- // ---------------------------------------------------------------------------
5
- import { defineAction } from "./define.js";
6
- import { ActionError } from "./error.js";
7
- let _send;
8
- /**
9
- * Configure the global transport adapter. Call once at app boot.
10
- * Only needed if using `transportAction`.
11
- */
12
- export function configureTransport(fn) {
13
- _send = fn;
14
- }
15
- /** @internal Test-only: reset the transport to unconfigured state. */
16
- export function _resetTransportForTest() {
17
- _send = undefined;
18
- }
19
- /**
20
- * Build an Action from a transport command descriptor. The generated
21
- * `run()` calls the configured transport send function and throws
22
- * {@link ActionError} on `!ok`, so the dispatcher's error branch
23
- * (notification + rollback) fires consistently.
24
- *
25
- * @param def - Transport action definition where `command` replaces `run`.
26
- * @returns An {@link Action} backed by the configured transport adapter.
27
- */
28
- export function transportAction(def) {
29
- const { command, ...rest } = def;
30
- // eslint-disable-next-line @typescript-eslint/no-invalid-void-type -- void used as generic type argument
31
- return defineAction({
32
- ...rest,
33
- run: async (args, signal, ctx) => {
34
- if (_send === undefined) {
35
- throw new ActionError("Transport not configured — call configureTransport() at boot", {
36
- code: "transport_not_configured",
37
- });
38
- }
39
- const raw = command(args);
40
- let cmd;
41
- if (ctx?.idempotencyKey !== undefined) {
42
- cmd = { ...raw, idempotency_key: ctx.idempotencyKey };
43
- }
44
- else {
45
- cmd = raw;
46
- }
47
- const r = await _send(cmd, { signal });
48
- if (!r.ok) {
49
- if (signal.aborted || r.code === "cancelled") {
50
- throw new ActionError("cancelled", { code: "cancelled" });
51
- }
52
- if (r.code === "timeout") {
53
- throw new ActionError(r.error ?? "Request timed out", {
54
- status: r.status,
55
- code: "timeout",
56
- });
57
- }
58
- if (r.code === "network") {
59
- throw new ActionError(r.error ?? "network error", { status: r.status, code: "network" });
60
- }
61
- const errOpts = { status: r.status };
62
- if (r.code !== undefined) {
63
- errOpts.code = r.code;
64
- }
65
- throw new ActionError(r.error ?? `send failed (${String(r.status)})`, errOpts);
66
- }
67
- return undefined;
68
- },
69
- });
70
- }
@@ -1,157 +0,0 @@
1
- /** Lifecycle status of a single dispatched action instance.
2
- *
3
- * - `"pending"` — optimistic ran (if any), run() in flight
4
- * - `"success"` — run() resolved
5
- * - `"error"` — run() threw; rollback ran
6
- * - `"cancelled"` — action.cancel() called or signal aborted externally */
7
- export type ActionLifecycleStatus = "pending" | "success" | "error" | "cancelled";
8
- /** Errors thrown by an action's run() function. ActionError subclass
9
- * in error.ts attaches HTTP status + server error code metadata. */
10
- export interface ActionErrorLike {
11
- readonly message: string;
12
- /** HTTP status if applicable. */
13
- readonly status?: number;
14
- /** Server-side error code. */
15
- readonly code?: string;
16
- readonly cause?: unknown;
17
- }
18
- /** Snapshot of a single in-flight or historical action invocation.
19
- * Stored in the registry log for observability. */
20
- export interface ActionInstance<TArgs = unknown, TResult = unknown> {
21
- /** Unique per dispatch. */
22
- readonly id: string;
23
- /** Matches ActionDefinition.name. */
24
- readonly name: string;
25
- readonly status: ActionLifecycleStatus;
26
- readonly args: TArgs;
27
- /** Date.now() when dispatch() was called. */
28
- readonly dispatchedAt: number;
29
- /** Date.now() when run() begins (after scope queue). */
30
- readonly startedAt: number;
31
- /** Date.now() at terminal state. */
32
- readonly completedAt?: number;
33
- /** Present iff status === "success". */
34
- readonly result?: TResult;
35
- /** Present iff status === "error". */
36
- readonly error?: ActionErrorLike;
37
- /** Total run() invocations (1 = no retry; >1 = retries fired). */
38
- readonly attempts?: number;
39
- }
40
- /** Notification wiring: either a literal string (used as-is), or a function
41
- * computed from action args + result/error at call time. Pass false
42
- * to opt out of the default notification for that branch. */
43
- export type NotificationSpec<TArgs, TPayload> = string | ((args: TArgs, payload: TPayload) => string) | false;
44
- /** @deprecated Use {@link NotificationSpec} instead. */
45
- export type ToastSpec<TArgs, TPayload> = NotificationSpec<TArgs, TPayload>;
46
- /** Per-dispatch context passed to run() as the 3rd argument. Mostly
47
- * populated by the framework so adapters (apiAction, transportAction)
48
- * can read out values like the idempotency key without the caller
49
- * having to plumb them. */
50
- export interface ActionContext {
51
- /** Stable identifier for this dispatch (matches the registry's
52
- * ActionInstance.id). */
53
- readonly instanceID: string;
54
- /** Set when ActionDefinition.idempotencyKey is configured. The
55
- * framework generates this once per dispatch (not per retry). */
56
- readonly idempotencyKey?: string;
57
- }
58
- /** Configuration for automatic retry of transient failures. */
59
- export interface RetryConfig {
60
- /** Additional attempts beyond the first (e.g. 2 = up to 3 total). */
61
- readonly count: number;
62
- /** Milliseconds before each retry. Number form: exponential backoff via
63
- * `delay × factor^(attempt-1)`, capped at 5s. Function form: full
64
- * control — receives the attempt number and the triggering error. */
65
- readonly delay: number | ((attempt: number, err: ActionErrorLike) => number);
66
- /** Backoff multiplier per retry. Default 2. Ignored when `delay` is a function. */
67
- readonly factor?: number;
68
- }
69
- /** Standard retry config: 2 retries, 300ms initial delay. */
70
- export declare const RETRY_STANDARD: RetryConfig;
71
- export interface ActionDefinition<TArgs, TResult, TOp = unknown> {
72
- /** Stable identifier, e.g. "chat.delete", "files.create".
73
- * Used in the registry log + as a default notification prefix. */
74
- readonly name: string;
75
- /** The work the action performs. Must throw ActionError on failure
76
- * (or any Error — wrappers will normalise). */
77
- run: (args: TArgs, signal: AbortSignal, ctx?: ActionContext) => Promise<TResult>;
78
- /** Optional optimistic mutation. Runs synchronously before run(). */
79
- optimistic?: (args: TArgs) => TOp | undefined;
80
- /** Undo the optimistic mutation. Called only if run() throws. */
81
- rollback?: (args: TArgs, op: TOp | undefined, err: ActionErrorLike) => void;
82
- /** Notification on success. Default: no notification. */
83
- success?: NotificationSpec<TArgs, TResult>;
84
- /** Notification on error. Default: action name humanised + error message. */
85
- error?: NotificationSpec<TArgs, ActionErrorLike>;
86
- /** Definition-level success callback. Fires on every successful dispatch.
87
- * Mirrors TanStack Query's mutation-level onSuccess. */
88
- onSuccess?: (result: TResult, args: TArgs) => void;
89
- /** Definition-level error callback. Fires on every failed dispatch.
90
- * Mirrors TanStack Query's mutation-level onError. */
91
- onError?: (err: ActionErrorLike, args: TArgs) => void;
92
- /** Definition-level settled callback. Fires after every dispatch
93
- * (success, error, or cancellation). Mirrors TanStack Query's onSettled. */
94
- onSettled?: (args: TArgs) => void;
95
- /** Classify whether an error qualifies for retry. */
96
- retryable?: (err: ActionErrorLike) => boolean;
97
- /** Auto-retry transient failures before surfacing the error notification. */
98
- retry?: RetryConfig;
99
- /** Auto-retry behavior when offline. Default `"online"`. */
100
- networkMode?: "online" | "always";
101
- /** Timeout in milliseconds for the run() function. Uses AbortSignal.timeout()
102
- * composed with the cancellation signal. If run() exceeds this duration,
103
- * the signal aborts with a TimeoutError. */
104
- timeout?: number;
105
- /** Serialize concurrent dispatches sharing the same scope key. */
106
- scope?: string | ((args: TArgs) => string);
107
- /** Generate an idempotency key per dispatch. */
108
- idempotencyKey?: boolean | ((args: TArgs) => string);
109
- /** Collapse concurrent dispatches with matching key into one in-flight promise. */
110
- dedupe?: boolean | ((args: TArgs) => string);
111
- }
112
- /** A registered action, returned by defineAction(). Can be dispatched
113
- * many times; each dispatch is a separate instance with its own
114
- * cancellation token. */
115
- export interface Action<TArgs, TResult> {
116
- readonly name: string;
117
- /** Run the action. Returns a DispatchHandle with the result promise
118
- * and a per-dispatch abort() method (RTK pattern). */
119
- dispatch(args: TArgs, opts?: DispatchOptions<TArgs, TResult>): DispatchHandle<TResult>;
120
- /** Cancel all in-flight instances. Each instance moves to status
121
- * "cancelled" and run()'s signal aborts. */
122
- cancel(): void;
123
- }
124
- /** Handle returned by dispatch(). An augmented Promise with an abort()
125
- * method for per-dispatch cancellation (mirrors RTK createAsyncThunk).
126
- * Can be awaited directly as a Promise. */
127
- export interface DispatchHandle<TResult> extends Promise<TResult | null> {
128
- /** Abort this specific dispatch. Other in-flight dispatches of the
129
- * same action are unaffected. Mirrors RTK's promise.abort(). */
130
- abort(): void;
131
- }
132
- /** Per-dispatch overrides. */
133
- export interface DispatchOptions<TArgs = unknown, TResult = unknown> {
134
- /** Suppress the success notification for this call. Errors still notify. */
135
- readonly silent?: boolean;
136
- /** Per-call success callback. Fires after the action-level notification. */
137
- readonly onSuccess?: (result: TResult, args: TArgs) => void;
138
- /** Per-call error callback. Fires after the action-level error notification. */
139
- readonly onError?: (err: ActionErrorLike, args: TArgs) => void;
140
- /** Per-call settled callback. Fires for success, error, AND cancellation. */
141
- readonly onSettled?: (args: TArgs) => void;
142
- }
143
- /** HTTP request descriptor used by apiAction(). GET is included for
144
- * read actions that want notification/cancellation semantics. */
145
- export type RequestSpec = {
146
- readonly method: "GET";
147
- readonly path: string;
148
- readonly headers?: Readonly<Record<string, string>>;
149
- } | {
150
- readonly method: "POST" | "PUT" | "PATCH" | "DELETE";
151
- readonly path: string;
152
- readonly body?: unknown;
153
- readonly headers?: Readonly<Record<string, string>>;
154
- };
155
- /** Subscriber callback for the registry. Fires once per state
156
- * transition (pending -> success/error/cancelled). */
157
- export type RegistryListener = (instance: ActionInstance) => void;
package/dist/src/types.js DELETED
@@ -1,7 +0,0 @@
1
- // Action framework types: defines the contract between callers, the
2
- // dispatcher, and observers. Pure types — no imports, no runtime
3
- // behavior — so any module in the codebase can depend on this without
4
- // pulling in transport / notification / store.
5
- // ---------------------------------------------------------------------------
6
- /** Standard retry config: 2 retries, 300ms initial delay. */
7
- export const RETRY_STANDARD = { count: 2, delay: 300 };