@alwatr/action 9.14.0 → 9.17.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/dist/method.d.ts CHANGED
@@ -2,18 +2,22 @@ import type { Awaitable } from '@alwatr/type-helper';
2
2
  import type { SubscribeResult } from '@alwatr/signal';
3
3
  import { type ModifierHandler, type PayloadResolver } from './registry.js';
4
4
  import type { ActionRecord } from './action-record.js';
5
+ import type { Action } from './action.js';
5
6
  export type { ModifierHandler, PayloadResolver };
7
+ export type { Action };
6
8
  /**
7
9
  * Subscribes to a named action dispatched anywhere in the application.
8
10
  *
9
- * `actionId` must be a key of `ActionRecord`. The handler's `payload` parameter
10
- * is automatically typed to the corresponding `ActionRecord` value — no manual
11
- * generic annotation needed:
11
+ * `type` must be a key of `ActionRecord`. The handler receives the full
12
+ * `Action<K>` object giving access to `payload`, `context`, and `meta`
13
+ * in one place. No manual generic annotation is needed; the compiler infers
14
+ * the correct `payload` type from `ActionRecord`:
12
15
  *
13
16
  * ```ts
14
- * // ActionRecord declares: 'add-to-cart': {productId: number; qty: number}
15
- * onAction('add-to-cart', (item) => {
16
- * cartService.add(item.productId, item.qty); // fully typed, no `!` needed
17
+ * // ActionRecord declares: 'add_to_cart': {productId: number; qty: number}
18
+ * onAction('add_to_cart', (action) => {
19
+ * cartService.add(action.payload.productId, action.payload.qty); // fully typed
20
+ * console.log(action.context); // e.g. 'product-list' (from DOM) or undefined
17
21
  * });
18
22
  * ```
19
23
  *
@@ -24,7 +28,7 @@ export type { ModifierHandler, PayloadResolver };
24
28
  * // src/action-record.ts
25
29
  * declare module '@alwatr/action' {
26
30
  * interface ActionRecord {
27
- * 'open-drawer': string;
31
+ * 'open_drawer': string;
28
32
  * }
29
33
  * }
30
34
  * ```
@@ -32,83 +36,87 @@ export type { ModifierHandler, PayloadResolver };
32
36
  * Internally delegates to `ChannelSignal.on()` for **O(1) routing** — dispatching
33
37
  * action `'A'` never invokes handlers registered for action `'B'`.
34
38
  *
35
- * @param actionId - A key of `ActionRecord`.
36
- * @param handler - Callback invoked with the typed payload on each dispatch.
39
+ * @param type - A key of `ActionRecord`.
40
+ * @param handler - Callback invoked with the full `Action<K>` on each dispatch.
37
41
  * @returns A `SubscribeResult` with an `unsubscribe()` method for cleanup.
38
42
  *
39
43
  * @example
40
44
  * ```ts
41
45
  * import {onAction} from '@alwatr/action';
42
46
  *
43
- * const sub = onAction('page-ready', (pageId) => {
44
- * router.setPage(pageId); // pageId: string — inferred from ActionRecord
47
+ * const sub = onAction('page_ready', (action) => {
48
+ * router.setPage(action.payload); // payload: string — inferred from ActionRecord
45
49
  * });
46
50
  *
47
51
  * sub.unsubscribe(); // stop listening when no longer needed
48
52
  * ```
49
53
  */
50
- export declare function onAction<K extends keyof ActionRecord>(actionId: K, handler: (payload: ActionRecord[K]) => Awaitable<void>): SubscribeResult;
54
+ export declare function onAction<K extends keyof ActionRecord>(type: K, handler: (action: Action<K>) => Awaitable<void>): SubscribeResult;
51
55
  /**
52
- * Dispatches a named action to all `onAction` subscribers with a matching `actionId`.
56
+ * Dispatches an action to all `onAction` subscribers with a matching `type`.
53
57
  *
54
- * `actionId` must be a key of `ActionRecord`. The `payload` parameter is
55
- * automatically typed — passing the wrong type is a **compile error**:
58
+ * Accepts a full `Action<K>` object. The `payload` field is automatically
59
+ * typed from `ActionRecord[K]` — passing the wrong shape is a **compile error**:
56
60
  *
57
61
  * ```ts
58
- * // ActionRecord declares: 'add-to-cart': {productId: number; qty: number}
59
- * dispatchAction('add-to-cart', {productId: 42, qty: 1}); // ✅
60
- * dispatchAction('add-to-cart', 'wrong'); // ❌ compile error
61
- * dispatchAction('unknown-action', 'x'); // ❌ compile error
62
+ * // ActionRecord declares: 'add_to_cart': {productId: number; qty: number}
63
+ * dispatchAction({type: 'add_to_cart', payload: {productId: 42, qty: 1}}); // ✅
64
+ * dispatchAction({type: 'add_to_cart', payload: 'wrong'}); // ❌ compile error
65
+ * dispatchAction({type: 'unknown_action', payload: 'x'}); // ❌ compile error
62
66
  * ```
63
67
  *
64
- * Register new actions by extending `ActionRecord` via declaration merging:
65
- *
66
- * ```ts
67
- * // src/action-record.ts
68
- * declare module '@alwatr/action' {
69
- * interface ActionRecord {
70
- * 'navigate': string;
71
- * 'logout': void;
72
- * }
73
- * }
74
- * ```
68
+ * The `context` and `meta` fields are optional. When dispatching from code
69
+ * (not from the DOM), omit `context` — it is only meaningful for DOM-originated
70
+ * actions where an `[action-context]` ancestor exists.
75
71
  *
76
72
  * Use `dispatchAction` when triggering an action from code — e.g. after an
77
73
  * async operation, from a service layer, or in tests. For DOM-driven actions,
78
- * use the `on-action` HTML attribute with `setupActionDelegation`.
74
+ * use the `on-<eventType>` HTML attribute with `setupActionDelegation`.
79
75
  *
80
- * @param actionId - A key of `ActionRecord`.
81
- * @param actionPayload - The payload; type is enforced by `ActionRecord`.
76
+ * @param action - A full `Action<K>` object with at minimum `type` and `payload`.
82
77
  *
83
78
  * @example — with payload
84
79
  * ```ts
85
80
  * import {dispatchAction} from '@alwatr/action';
86
81
  *
87
- * dispatchAction('page-ready', 'home');
88
- * dispatchAction('navigate', '/dashboard');
82
+ * dispatchAction({type: 'navigate', payload: '/dashboard'});
83
+ * dispatchAction({type: 'add_to_cart', payload: {productId: 42, qty: 1}});
89
84
  * ```
90
85
  *
91
- * @example — void payload (no second argument)
86
+ * @example — void payload
92
87
  * ```ts
93
- * dispatchAction('logout');
88
+ * dispatchAction({type: 'logout', payload: undefined});
89
+ * ```
90
+ *
91
+ * @example — with context and meta
92
+ * ```ts
93
+ * dispatchAction({
94
+ * type: 'slider_change',
95
+ * payload: 75,
96
+ * context: 'volume_slider',
97
+ * meta: {traceId: 'abc-123'},
98
+ * });
94
99
  * ```
95
100
  */
96
- export declare function dispatchAction<K extends keyof ActionRecord>(...args: ActionRecord[K] extends void | undefined ? [actionId: K] : [actionId: K, actionPayload: ActionRecord[K]]): void;
101
+ export declare function dispatchAction<K extends keyof ActionRecord>(action: Action<K>): void;
97
102
  /**
98
- * Registers a custom modifier that can be used in `on-action` attribute syntax.
103
+ * Registers a custom modifier that can be used in `on-<eventType>` attribute syntax.
99
104
  *
100
- * A modifier is a dot-chained token placed after the event type
101
- * (e.g. `click.mymod->action-id`). Its handler runs before the payload is
105
+ * A modifier is a comma-separated token placed after the `;` separator
106
+ * (e.g. `on-click="action-id; mymod"`). Its handler runs before the payload is
102
107
  * resolved and the action is dispatched. Returning `false` cancels the dispatch.
103
108
  *
104
- * Built-in modifiers (`prevent`, `stop`, `validate`, `once`) are always
105
- * available. This function lets you add domain-specific ones.
109
+ * The handler also receives the **mutable** `action` object being built, so it
110
+ * can attach data to `action.meta` before the action reaches subscribers.
111
+ *
112
+ * Built-in modifiers (`prevent`, `validate`, `once`) are always available.
113
+ * This function lets you add domain-specific ones.
106
114
  *
107
115
  * Registering the same name twice logs an accident and overwrites the previous
108
116
  * handler — avoid duplicate registrations in production code.
109
117
  *
110
- * @param name - The modifier token (lowercase, no dots or arrows).
111
- * @param handler - A `ModifierHandler` receiving `(event, element)`.
118
+ * @param name - The modifier token (lowercase, no special characters).
119
+ * @param handler - A `ModifierHandler` receiving `(event, element, action)`.
112
120
  *
113
121
  * @example — a `confirm` modifier that shows a browser dialog
114
122
  * ```ts
@@ -117,20 +125,29 @@ export declare function dispatchAction<K extends keyof ActionRecord>(...args: Ac
117
125
  * registerModifier('confirm', () => window.confirm('Are you sure?'));
118
126
  * ```
119
127
  * ```html
120
- * <button on-action="click.confirm->delete-item:42">Delete</button>
128
+ * <button on-click="delete_item:42; confirm">Delete</button>
129
+ * ```
130
+ *
131
+ * @example — a `trace` modifier that stamps a trace ID into meta
132
+ * ```ts
133
+ * registerModifier('trace', (_event, _element, action) => {
134
+ * action.meta ??= {};
135
+ * action.meta['traceId'] = crypto.randomUUID();
136
+ * return true;
137
+ * });
121
138
  * ```
122
139
  */
123
140
  export declare function registerModifier(name: string, handler: ModifierHandler): void;
124
141
  /**
125
- * Registers a custom payload resolver that can be used in `on-action` attribute syntax.
142
+ * Registers a custom payload resolver that can be used in `on-<eventType>` attribute syntax.
126
143
  *
127
- * A payload resolver is a colon-suffixed token in the attribute value
128
- * (e.g. `click->action-id:$mytoken`). Its function is called at dispatch time
129
- * with an `ActionContext` as `this` and the DOM event as the argument.
130
- * The return value becomes the `actionPayload` passed to `onAction` subscribers.
144
+ * A payload resolver is a colon-prefixed token in the attribute value
145
+ * (e.g. `on-click="action-id:$mytoken"`). Its function is called at dispatch time
146
+ * with the DOM event and the element. The return value becomes the `payload`
147
+ * field of the `Action` object passed to `onAction` subscribers.
131
148
  *
132
- * Built-in resolvers (`$value`, `$formdata`) are always available. This function
133
- * lets you add domain-specific ones.
149
+ * Built-in resolvers (`$value`, `$formdata`, `$checked`) are always available.
150
+ * This function lets you add domain-specific ones.
134
151
  *
135
152
  * Registering the same name twice logs an accident and overwrites the previous
136
153
  * resolver — avoid duplicate registrations in production code.
@@ -138,16 +155,16 @@ export declare function registerModifier(name: string, handler: ModifierHandler)
138
155
  * @param name - The resolver token (should start with `$` by convention).
139
156
  * @param resolver - A `PayloadResolver` receiving `(event, element)`.
140
157
  *
141
- * @example — a `$checked` resolver for checkbox state
158
+ * @example — a `$data-id` resolver that reads a data attribute
142
159
  * ```ts
143
160
  * import {registerPayloadResolver} from '@alwatr/action';
144
161
  *
145
- * registerPayloadResolver('$checked', (_event, element) => {
146
- * return (element as HTMLInputElement).checked;
162
+ * registerPayloadResolver('$data-id', (_event, element) => {
163
+ * return (element as HTMLElement).dataset.id ?? null;
147
164
  * });
148
165
  * ```
149
166
  * ```html
150
- * <input type="checkbox" on-action="change->toggle-feature:$checked" />
167
+ * <button on-click="select_item:$data-id" data-id="42">Select</button>
151
168
  * ```
152
169
  */
153
170
  export declare function registerPayloadResolver(name: string, resolver: PayloadResolver): void;
@@ -1 +1 @@
1
- {"version":3,"file":"method.d.ts","sourceRoot":"","sources":["../src/method.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAEnD,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAAoC,KAAK,eAAe,EAAE,KAAK,eAAe,EAAC,MAAM,eAAe,CAAC;AAC5G,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAC;AAGrD,YAAY,EAAC,eAAe,EAAE,eAAe,EAAC,CAAC;AAI/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,MAAM,YAAY,EACnD,QAAQ,EAAE,CAAC,EACX,OAAO,EAAE,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,IAAI,CAAC,GACrD,eAAe,CAGjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,YAAY,EACzD,GAAG,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,SAAS,IAAI,GAAG,SAAS,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,EAAE,aAAa,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC,GAChH,IAAI,CAIN;AAID;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CAM7E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,GAAG,IAAI,CAMrF"}
1
+ {"version":3,"file":"method.d.ts","sourceRoot":"","sources":["../src/method.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AACnD,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,gBAAgB,CAAC;AAGpD,OAAO,EAAoC,KAAK,eAAe,EAAE,KAAK,eAAe,EAAC,MAAM,eAAe,CAAC;AAC5G,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,aAAa,CAAC;AAGxC,YAAY,EAAC,eAAe,EAAE,eAAe,EAAC,CAAC;AAC/C,YAAY,EAAC,MAAM,EAAC,CAAC;AAIrB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,MAAM,YAAY,EACnD,IAAI,EAAE,CAAC,EACP,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,IAAI,CAAC,GAC9C,eAAe,CAMjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,YAAY,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,CAGpF;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CAM7E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,GAAG,IAAI,CAMrF"}
@@ -1,35 +1,47 @@
1
+ import type { Action } from './action.js';
1
2
  /**
2
- * A modifier handler used in `on-action` attribute syntax.
3
+ * A modifier handler used in `on-<eventType>` attribute syntax.
3
4
  *
4
- * Receives the triggering DOM `event` and the `element` that owns the
5
- * `on-action` attribute. Return `true` (or any truthy value) to allow the
6
- * action to proceed, or `false` to cancel the dispatch.
5
+ * Receives the triggering DOM `event`, the `element` that owns the
6
+ * `on-<eventType>` attribute, and the **mutable** `action` object being built.
7
+ * The handler may mutate `action.meta` to attach cross-cutting data (e.g. a
8
+ * trace ID, a timestamp, or an A/B flag) before the action reaches subscribers.
9
+ *
10
+ * Return `true` (or any truthy value) to allow the action to proceed, or
11
+ * `false` to cancel the dispatch entirely.
7
12
  *
8
13
  * Using explicit parameters instead of `this` binding makes handlers
9
14
  * compatible with arrow functions and easier to test in isolation.
10
15
  *
11
- * @example
16
+ * @example — a modifier that stamps a timestamp into meta
17
+ * ```ts
18
+ * const timestampHandler: ModifierHandler = (_event, _element, action) => {
19
+ * action.meta ??= {};
20
+ * action.meta['timestamp'] = Date.now();
21
+ * return true;
22
+ * };
23
+ * ```
24
+ *
25
+ * @example — a modifier that cancels dispatch when the element is disabled
12
26
  * ```ts
13
- * // A modifier that only allows the action when the element is not disabled
14
27
  * const notDisabledHandler: ModifierHandler = (_event, element) => {
15
28
  * return !(element as HTMLButtonElement).disabled;
16
29
  * };
17
30
  * ```
18
31
  */
19
- export type ModifierHandler = (event: Event, element: HTMLElement) => boolean;
32
+ export type ModifierHandler = (event: Event, element: HTMLElement, action: Action) => boolean;
20
33
  /**
21
- * A payload resolver used in `on-action` attribute syntax.
34
+ * A payload resolver used in `on-<eventType>` attribute syntax.
22
35
  *
23
36
  * Receives the triggering DOM `event` and the `element` that owns the
24
- * `on-action` attribute. The return value becomes the `actionPayload` passed
25
- * to `onAction` subscribers. Use this to compute dynamic payloads from DOM state.
37
+ * `on-<eventType>` attribute. The return value becomes the `payload` field of
38
+ * the `Action` object passed to `onAction` subscribers.
26
39
  *
27
40
  * Using explicit parameters instead of `this` binding makes resolvers
28
41
  * compatible with arrow functions and easier to test in isolation.
29
42
  *
30
- * @example
43
+ * @example — a resolver that returns the element's dataset id
31
44
  * ```ts
32
- * // A resolver that returns the element's dataset id
33
45
  * const dataIdResolver: PayloadResolver = (_event, element) => {
34
46
  * return (element as HTMLElement).dataset.id ?? null;
35
47
  * };
@@ -39,8 +51,8 @@ export type PayloadResolver = (event: Event, element: HTMLElement) => unknown;
39
51
  /**
40
52
  * Registry of all named modifier handlers.
41
53
  *
42
- * Keys are modifier names used in the `on-action` attribute syntax
43
- * (e.g. `click.prevent->action-id`). Values are `ModifierHandler` functions.
54
+ * Keys are modifier names used in the `on-<eventType>` attribute syntax
55
+ * (e.g. `on-click="action-id; prevent"`). Values are `ModifierHandler` functions.
44
56
  * Populated at module load with built-in modifiers; extended at runtime via
45
57
  * `registerModifier`.
46
58
  *
@@ -50,8 +62,8 @@ export declare const modifierRegistry: Map<string, ModifierHandler>;
50
62
  /**
51
63
  * Registry of all named payload resolvers.
52
64
  *
53
- * Keys are resolver tokens used in the `on-action` attribute syntax
54
- * (e.g. `click->action-id:$value`). Values are `PayloadResolver` functions.
65
+ * Keys are resolver tokens used in the `on-<eventType>` attribute syntax
66
+ * (e.g. `on-input="search_query:$value"`). Values are `PayloadResolver` functions.
55
67
  * Populated at module load with built-in resolvers; extended at runtime via
56
68
  * `registerPayloadResolver`.
57
69
  *
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC;AAE9E;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC;AAI9E;;;;;;;;;GASG;AACH,eAAO,MAAM,gBAAgB,8BAAqC,CAAC;AAEnE;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,8BAAqC,CAAC"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,aAAa,CAAC;AAIxC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;AAE9F;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC;AAI9E;;;;;;;;;GASG;AACH,eAAO,MAAM,gBAAgB,8BAAqC,CAAC;AAEnE;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,8BAAqC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alwatr/action",
3
- "version": "9.14.0",
3
+ "version": "9.17.0",
4
4
  "description": "Declarative DOM action-dispatch — bridge HTML attributes to typed signal handlers.",
5
5
  "license": "MPL-2.0",
6
6
  "author": "S. Ali Mihandoost <ali.mihandoost@gmail.com> (https://ali.mihandoost.com)",
@@ -21,12 +21,12 @@
21
21
  },
22
22
  "sideEffects": false,
23
23
  "dependencies": {
24
- "@alwatr/logger": "9.14.0",
25
- "@alwatr/signal": "9.14.0"
24
+ "@alwatr/logger": "9.16.0",
25
+ "@alwatr/signal": "9.16.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@alwatr/nano-build": "9.14.0",
29
- "@alwatr/standard": "9.14.0",
29
+ "@alwatr/standard": "9.16.0",
30
30
  "@alwatr/type-helper": "9.14.0",
31
31
  "typescript": "^6.0.3"
32
32
  },
@@ -79,5 +79,5 @@
79
79
  "vanilla-js",
80
80
  "web-development"
81
81
  ],
82
- "gitHead": "4e499b23191d4460ea60f34cde8a99b472741f1a"
82
+ "gitHead": "782563375f55c55d29719cbcfebaca251d69ddcd"
83
83
  }
@@ -13,8 +13,8 @@
13
13
  * // In your package: src/action-record.ts
14
14
  * declare module '@alwatr/action' {
15
15
  * interface ActionRecord {
16
- * 'open-drawer': string;
17
- * 'add-to-cart': {productId: number; qty: number};
16
+ * 'open_drawer': string;
17
+ * 'add_to_cart': {productId: number; qty: number};
18
18
  * 'logout': void;
19
19
  * }
20
20
  * }
@@ -43,8 +43,8 @@
43
43
  * // pkg/my-feature/src/action-record.ts
44
44
  * declare module '@alwatr/action' {
45
45
  * interface ActionRecord {
46
- * 'open-drawer': string;
47
- * 'add-to-cart': {productId: number; qty: number};
46
+ * 'open_drawer': string;
47
+ * 'add_to_cart': {productId: number; qty: number};
48
48
  * 'logout': void;
49
49
  * }
50
50
  * }
package/src/action.ts ADDED
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @file action.ts
3
+ *
4
+ * Defines the Alwatr Flux Standard Action (AFSA) — the single, unified data
5
+ * structure that flows through the entire action bus for both dispatch and
6
+ * subscription.
7
+ *
8
+ * ## Why a single Action object?
9
+ *
10
+ * Previously, `dispatchAction(id, payload)` and `onAction(id, handler(payload))`
11
+ * treated the action as two separate concerns: an identifier and a raw payload.
12
+ * This made it impossible to carry cross-cutting metadata (context, trace IDs,
13
+ * timestamps) without breaking every call site.
14
+ *
15
+ * AFSA wraps everything into one object:
16
+ * - `type` — the action identifier (replaces the first positional argument)
17
+ * - `payload` — the business data (replaces the second positional argument)
18
+ * - `context` — the DOM context extracted from the nearest `[action-context]`
19
+ * ancestor at delegation time; `undefined` for programmatic dispatches
20
+ * - `meta` — open-ended bag for future cross-cutting concerns (trace IDs,
21
+ * timestamps, A/B flags, etc.) without breaking the typed API
22
+ *
23
+ * Modifiers in the delegation pipeline can also mutate `meta` before the action
24
+ * reaches subscribers — e.g. a `trace` modifier could stamp a request ID.
25
+ */
26
+
27
+ import type {DictionaryOpt} from '@alwatr/type-helper';
28
+ import type {ActionRecord} from './action-record.js';
29
+
30
+ /**
31
+ * Alwatr Flux Standard Action (AFSA).
32
+ *
33
+ * The single, canonical object passed to every `dispatchAction` call and
34
+ * received by every `onAction` handler. Keeping all action data in one
35
+ * structure makes the bus extensible without breaking existing call sites.
36
+ *
37
+ * @template K - A key of `ActionRecord`; constrains `type` and `payload` together.
38
+ *
39
+ * @example — dispatching
40
+ * ```ts
41
+ * dispatchAction({type: 'add_to_cart', payload: {productId: 42, qty: 1}});
42
+ * ```
43
+ *
44
+ * @example — subscribing
45
+ * ```ts
46
+ * onAction('add_to_cart', (action) => {
47
+ * console.log(action.type); // 'add_to_cart'
48
+ * console.log(action.payload); // {productId: 42, qty: 1}
49
+ * console.log(action.context); // e.g. 'product-list' (from DOM) or undefined
50
+ * });
51
+ * ```
52
+ */
53
+ export interface Action<K extends keyof ActionRecord = keyof ActionRecord> {
54
+ /**
55
+ * Unique action identifier — must be a key of `ActionRecord`.
56
+ *
57
+ * @example 'cart:add-item', 'open_drawer', 'logout'
58
+ */
59
+ readonly type: K;
60
+
61
+ /**
62
+ * The DOM context in which the action was triggered.
63
+ *
64
+ * Extracted at delegation time from the nearest ancestor element that carries
65
+ * an `action-context` attribute. Useful for scoping the same action type to
66
+ * different UI regions (e.g. two sliders on the same page both dispatching
67
+ * `'slider:change'` but with different context values).
68
+ *
69
+ * `undefined` when the action is dispatched programmatically (no DOM involved)
70
+ * or when no `[action-context]` ancestor exists.
71
+ *
72
+ * @example 'slider-123', 'product-list', 'checkout-form'
73
+ */
74
+ readonly context?: string;
75
+
76
+ /**
77
+ * The pure business payload carried by this action.
78
+ *
79
+ * Type is inferred from `ActionRecord[K]` — the compiler enforces the correct
80
+ * shape at every call site. No manual generic annotation is needed.
81
+ */
82
+ readonly payload: ActionRecord[K];
83
+
84
+ /**
85
+ * Open-ended metadata bag for cross-cutting concerns.
86
+ *
87
+ * Intentionally untyped so that future infrastructure layers (tracing,
88
+ * analytics, A/B testing) can attach data without touching the typed API.
89
+ * Modifiers in the delegation pipeline may also write to `meta` before the
90
+ * action reaches subscribers.
91
+ *
92
+ * Treat values here as `unknown` and validate before use.
93
+ *
94
+ * @example {traceId: 'abc-123', timestamp: Date.now()}
95
+ */
96
+ meta?: DictionaryOpt<unknown>;
97
+ }