@yoltra/core 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.
@@ -0,0 +1,219 @@
1
+ /**
2
+ * @module @yoltra/core
3
+ */
4
+ /**
5
+ * Flexible, synchronous pub/sub bus that supports **exact** and **pattern** event subscriptions.
6
+ *
7
+ * @typeParam C - Channel name type (defaults to `string`).
8
+ * @typeParam T - Event type name type (defaults to `string`). Types are treated as **dot-separated paths** (e.g. `"a.b.c"`).
9
+ * @typeParam P - Payload type for all events (defaults to `any`).
10
+ *
11
+ * @remarks
12
+ * - **Exact handlers** subscribe to a specific `(channel, type)` pair. Type keys are **normalized** by stripping a single leading dot (`".foo"` → `"foo"`).
13
+ * - **Pattern handlers** subscribe using wildcards over dot-separated segments:
14
+ * - `*` matches **one** segment.
15
+ * - `**` matches **zero or more** segments (greedy).
16
+ * - On {@link LooseEventBus.emit | `emit`}, exact handlers fire first, then any matching pattern handlers.
17
+ * - Handlers are **de-duplicated**: if the same function is both exact and pattern-registered, it is called **once**.
18
+ * - Handler invocation is **synchronous**. Exceptions are caught and logged; remaining handlers still run.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * type C = 'ui' | 'data';
23
+ * type T = string;
24
+ * type P = unknown;
25
+ *
26
+ * const bus = new LooseEventBus<C, T, P>();
27
+ *
28
+ * // Exact
29
+ * const offA = bus.on('ui', 'panel.open', () => console.log('panel opened'));
30
+ *
31
+ * // Patterns
32
+ * const offB = bus.on('ui', 'panel.*', () => console.log('any single sub-event under panel'));
33
+ * const offC = bus.on('ui', 'panel.**', () => console.log('any depth under panel'));
34
+ *
35
+ * bus.emit('ui', 'panel.open', null);
36
+ * // => exact fires, then 'panel.*', then 'panel.**'
37
+ *
38
+ * offA(); offB(); offC(); // unsubscribe
39
+ * ```
40
+ *
41
+ * @public
42
+ */
43
+ export declare class LooseEventBus<C extends string = string, T extends string = string, P = any> {
44
+ /**
45
+ * Exact handlers: `channel → type → [handlers]`.
46
+ * @internal
47
+ */
48
+ private handlers;
49
+ /**
50
+ * Pattern handlers with `*` and `**`: `channel → pattern(string) → [handlers]`.
51
+ * @internal
52
+ */
53
+ private patternHandlers;
54
+ /**
55
+ * Subscribes a handler to either an **exact** type or a **pattern**.
56
+ *
57
+ * @param channel - Channel to subscribe on.
58
+ * @param type - Exact event type (e.g. `"a.b"`) or pattern (contains `*`/`**`).
59
+ * @param handler - Function invoked with the emitted payload.
60
+ * @returns An **unsubscribe** function that removes this handler.
61
+ *
62
+ * @remarks
63
+ * - Exact subscriptions are stored under a **normalized** key (leading `.` removed).
64
+ * - Pattern subscriptions are stored **as provided**; matching normalizes the subject.
65
+ *
66
+ * @example Exact subscription
67
+ * ```ts
68
+ * const off = bus.on('data', 'items.loaded', ({ count }) => {
69
+ * console.log('Loaded', count);
70
+ * });
71
+ * // Later
72
+ * off();
73
+ * ```
74
+ *
75
+ * @example Pattern subscription
76
+ * ```ts
77
+ * // Match any single sub-event: 'panel.open', 'panel.close', etc.
78
+ * const offStar = bus.on('ui', 'panel.*', () => {});
79
+ *
80
+ * // Match any depth: 'panel.open', 'panel.items.add', 'panel', etc.
81
+ * const offGlob = bus.on('ui', 'panel.**', () => {});
82
+ * ```
83
+ *
84
+ * @public
85
+ */
86
+ on(channel: C, type: T, handler: (payload: P) => void): () => void;
87
+ /**
88
+ * Unsubscribes an **exact** handler. The `type` key is normalized internally,
89
+ * so callers can pass `"foo"` or `".foo"` interchangeably.
90
+ *
91
+ * @param channel - Channel name.
92
+ * @param type - Exact event type key to remove (normalization applied).
93
+ * @param handler - The same handler reference previously passed to {@link LooseEventBus.on | `on`}.
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * const h = () => {};
98
+ * bus.on('ui', 'panel.open', h);
99
+ * // Remove it (with or without leading dot)
100
+ * bus.off('ui', '.panel.open', h);
101
+ * ```
102
+ *
103
+ * @public
104
+ */
105
+ off(channel: C, type: T, handler: (payload: P) => void): void;
106
+ /**
107
+ * Internal exact unsubscription using an already **normalized** type key.
108
+ *
109
+ * @param channel - Channel name.
110
+ * @param normalizedType - Event type key with leading dot removed.
111
+ * @param handler - Handler to remove.
112
+ * @internal
113
+ */
114
+ private offExactNormalized;
115
+ /**
116
+ * Internal removal for a **pattern** subscription. No-ops if missing.
117
+ *
118
+ * @param channel - Channel name.
119
+ * @param pattern - Pattern string as originally subscribed.
120
+ * @param handler - Handler to remove.
121
+ * @internal
122
+ */
123
+ private offPattern;
124
+ /**
125
+ * Emits an event to all exact subscribers first, then to **matching pattern** subscribers.
126
+ * Duplicate handler references are called **once** (de-duped).
127
+ *
128
+ * @param channel - Channel to emit on.
129
+ * @param type - Event type (subject). A leading dot is ignored for matching.
130
+ * @param payload - Payload delivered to handlers.
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * // Suppose:
135
+ * // - on('ui', 'panel.open', h)
136
+ * // - on('ui', 'panel.*', h) // same handler ref!
137
+ * // - on('ui', 'panel.**', other)
138
+ * bus.emit('ui', 'panel.open', { id: 1 });
139
+ * // => 'h' runs once (de-duped), then 'other'
140
+ * ```
141
+ *
142
+ * @public
143
+ */
144
+ emit(channel: C, type: T, payload: P): void;
145
+ /**
146
+ * Determines if a string is a **pattern** (contains `*`).
147
+ * @param s - Event type or pattern string.
148
+ * @returns `true` if it contains at least one `*`, else `false`.
149
+ * @internal
150
+ */
151
+ private isPattern;
152
+ /**
153
+ * Normalizes event type keys for exact matching by stripping a **single** leading dot.
154
+ *
155
+ * @param s - Event type key.
156
+ * @returns Normalized key without a leading dot.
157
+ * @example
158
+ * ```ts
159
+ * normalizeTypeKey('.a.b') // 'a.b'
160
+ * normalizeTypeKey('a.b') // 'a.b'
161
+ * ```
162
+ * @internal
163
+ */
164
+ private normalizeTypeKey;
165
+ /**
166
+ * Splits a path into dot-separated segments after normalization and removes empties.
167
+ * @param p - Event type or pattern string.
168
+ * @internal
169
+ */
170
+ private splitPath;
171
+ /**
172
+ * Pattern matcher over dot-separated segments.
173
+ *
174
+ * Rules:
175
+ * - **literal**: exact match.
176
+ * - `*` : matches exactly **one** segment.
177
+ * - `**` : matches **zero or more** remaining segments (including empty).
178
+ *
179
+ * @param pattern - Pattern (may include `*`/`**`).
180
+ * @param path - Subject event type key to test.
181
+ * @returns `true` if the pattern matches the path; otherwise `false`.
182
+ *
183
+ * @example
184
+ * ```ts
185
+ * matchPattern('a.*', 'a.b') // true
186
+ * matchPattern('a.*', 'a.b.c') // false
187
+ * matchPattern('a.**', 'a') // true
188
+ * matchPattern('a.**', 'a.b.c') // true
189
+ * matchPattern('**.end', 'x.y.end') // true
190
+ * ```
191
+ *
192
+ * @internal
193
+ */
194
+ private matchPattern;
195
+ /**
196
+ * Removes **all** listeners (exact and pattern). Useful for tests/HMR teardown.
197
+ *
198
+ * @example
199
+ * ```ts
200
+ * afterEach(() => bus.clear());
201
+ * ```
202
+ *
203
+ * @public
204
+ */
205
+ clear(): void;
206
+ /**
207
+ * Returns a snapshot of all registered subscriptions for DevTools introspection.
208
+ *
209
+ * @returns An array of `{ channel, type, count }` entries for each distinct
210
+ * (channel, type/pattern) pair with at least one handler.
211
+ *
212
+ * @internal
213
+ */
214
+ __introspect(): Array<{
215
+ channel: string;
216
+ type: string;
217
+ count: number;
218
+ }>;
219
+ }
@@ -0,0 +1,2 @@
1
+ export * from './EventBus';
2
+ export * from './LooseEventBus';
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @module @yoltra/core
3
+ *
4
+ * yoltra Core - Channel/Event-driven state management
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+ export { EventBus } from './eventBus/EventBus';
9
+ export { LooseEventBus } from './eventBus/LooseEventBus';
10
+ export { Reducer } from './reducer/Reducer';
11
+ export { Store, createStore, typedEvents } from './store/Store';
12
+ export { detectChangedProps } from './utils/detectChangedProps';
13
+ export { freezeState } from './utils/immutability';
14
+ export { eventKeys } from './types';
15
+ export type { EventMapBase, EventKey, Event, EventUnion, Change, Emit, Unsubscribe, StoreSpec, StoreInstance, ReducerSpec, ReducerFunction, EffectSpec, EffectFunction, MiddlewareFunction, MiddlewareSpec, MiddlewareInput, DeepReadonly, DeepRO, Primitive, Path, PathValue, WithGlob, Dotted, EventPhase, EventSubscriptionHandler, NarrowedEventHandler, When, EventFromWhen, EventConsumerType, EventConsumerMeta, } from './types';
@@ -0,0 +1,81 @@
1
+ import { EventMapBase, EventUnion, ReducerFunction } from '../types';
2
+ /**
3
+ * Thin wrapper around a pure reducer function (stateful event consumer):
4
+ * given a state `S` and an event (from {@link EventUnion | `EventUnion<EM>`}),
5
+ * returns the next state `S`.
6
+ *
7
+ * @typeParam S - State shape handled by this reducer.
8
+ * @typeParam EM - Event map describing the valid event keys and payload types.
9
+ *
10
+ * @remarks
11
+ * - The reducer function is expected to be **pure** and **side-effect free**.
12
+ * - Use this class when you want to pass a reducer around as a value, or to
13
+ * unify the reducer interface across the core API.
14
+ *
15
+ * @example Basic counter
16
+ * ```ts
17
+ * type State = { count: number };
18
+ * type EM = { math: { add: number; set: number } };
19
+ *
20
+ * const rf: ReducerFunction<State, EM> = (s, evt) => {
21
+ * if (evt.channel === 'math' && evt.type === 'add') {
22
+ * return { count: s.count + evt.payload };
23
+ * }
24
+ * if (evt.channel === 'math' && evt.type === 'set') {
25
+ * return { count: evt.payload };
26
+ * }
27
+ * return s;
28
+ * };
29
+ *
30
+ * const r = new Reducer<State, EM>(rf);
31
+ *
32
+ * const s0 = { count: 0 };
33
+ * const s1 = r.reduce(s0, {
34
+ * channel: 'math',
35
+ * type: 'add',
36
+ * payload: 2,
37
+ * id: crypto.randomUUID()
38
+ * } as EventUnion<EM>);
39
+ * // s1.count === 2
40
+ * ```
41
+ *
42
+ * @public
43
+ */
44
+ export declare class Reducer<S, EM extends EventMapBase = EventMapBase> {
45
+ /**
46
+ * The underlying pure reducer function.
47
+ * @internal
48
+ */
49
+ private readonly _reduce;
50
+ /**
51
+ * Creates a new {@link Reducer} from a pure reducer function.
52
+ *
53
+ * @param reduce - A function `(state, event) => nextState` that implements your update logic.
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * const reducer = new Reducer<MyState, MyEM>((state, event) => {
58
+ * // implement your transitions here
59
+ * return state;
60
+ * });
61
+ * ```
62
+ *
63
+ * @public
64
+ */
65
+ constructor(reduce: ReducerFunction<S, EM>);
66
+ /**
67
+ * Applies the reducer to produce the next state.
68
+ *
69
+ * @param state - Current state.
70
+ * @param event - An event drawn from {@link EventUnion | `EventUnion<EM>`}.
71
+ * @returns The next state produced by the underlying reducer function.
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * const next = reducer.reduce(curr, someEvent as EventUnion<MyEM>);
76
+ * ```
77
+ *
78
+ * @public
79
+ */
80
+ reduce(state: S, event: EventUnion<EM>): S;
81
+ }