@yoltra/core 0.3.0 → 0.5.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.es.md CHANGED
@@ -1,4 +1,4 @@
1
- ![yoltra logo](../../assets/yoltra-logo.png)
1
+ ![Yoltra logo](https://yoltra.dev/assets/yoltra-logo.png)
2
2
 
3
3
  # @yoltra/core
4
4
 
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- ![yoltra logo](../../assets/yoltra-logo.png)
1
+ ![Yoltra logo](https://yoltra.dev/assets/yoltra-logo.png)
2
2
 
3
3
  # @yoltra/core
4
4
 
@@ -87,6 +87,63 @@ store.connect({ reducer: "todos", property: "items.**" }, (change) =>
87
87
  );
88
88
  ```
89
89
 
90
+ ### Slices that hold a single value
91
+
92
+ A slice does not have to be an object. A primitive, a `Map`, a `Set` or a `Date` is a valid
93
+ slice state, and it commits like any other:
94
+
95
+ ```typescript
96
+ const store = createStore({
97
+ name: "session",
98
+ reducer: {
99
+ token: {
100
+ state: null as string | null,
101
+ when: { keys: [["auth", "login"]] },
102
+ reducer: (_state, event) => event.payload.token,
103
+ },
104
+ },
105
+ });
106
+
107
+ await store.emit("auth", "login", { token: "abc123" });
108
+ store.getState().token; // "abc123"
109
+ ```
110
+
111
+ Such a slice has no property beneath it, so its changes are reported at the **slice root** —
112
+ the empty path. Subscribe to it with `property: ""`:
113
+
114
+ ```typescript
115
+ store.connect({ reducer: "token", property: "" }, (change) =>
116
+ console.log("token:", change.oldValue, " --> ", change.newValue),
117
+ );
118
+ ```
119
+
120
+ The types know the difference. `property` on a root-value slice accepts `""` and nothing else —
121
+ there is no key to address — and the value comes back correctly typed:
122
+
123
+ ```typescript
124
+ const token = useAtomicProp({ reducer: "token", property: "" }); // string | null
125
+ ```
126
+
127
+ ### `""` versus `"**"` — watching a whole slice
128
+
129
+ Two subscriptions sound alike and are not:
130
+
131
+ | Pattern | Fires when |
132
+ |---|---|
133
+ | `""` | the slice's **whole value** is replaced — a primitive changes, a `Map` is rebuilt, an object slice becomes `null` |
134
+ | `"**"` | **anything** in the slice changes, at any depth. Matches the root too, since `**` matches zero segments |
135
+ | `"*"` | one level down, exactly. Never matches the root |
136
+
137
+ **`"**"` is the whole-slice subscription, and it works for every slice regardless of shape.**
138
+ Reach for `""` only when you mean the root value itself; on an object slice it stays quiet,
139
+ because such a slice reports its changes at their leaves.
140
+
141
+ `Map` and `Set` are compared by reference, not by entry: a reducer returning a new `Map` is a
142
+ change, mutating one in place is not. That follows from the immutability contract rather than
143
+ being a special case — build a new collection instead of mutating the stored one. It is also why
144
+ they have no paths beneath them: `"byId"` is subscribable, `"byId.get"` is not, and the types
145
+ say so.
146
+
90
147
  ### Immutability
91
148
 
92
149
  State is deep-frozen before committing. Mutations throw in strict mode:
@@ -426,14 +483,119 @@ store.registerEffect({
426
483
 
427
484
  ---
428
485
 
486
+ ## Saving and restoring state
487
+
488
+ Two functions, because the halves happen on opposite sides of the store's existence.
489
+ `hydrate` produces *initial slice state*, so the store is born with it:
490
+
491
+ ```ts
492
+ import { createStore, createWebStorageAdapter, hydrate, persist, withHydration } from '@yoltra/core';
493
+
494
+ const adapter = createWebStorageAdapter(localStorage);
495
+ const hydration = await hydrate({ key: 'app', adapter, version: 3 });
496
+
497
+ const store = createStore({
498
+ name: 'App',
499
+ reducer: withHydration({ todos: todosSpec, ui: uiSpec }, hydration),
500
+ });
501
+
502
+ const stop = persist(store, { key: 'app', adapter, version: 3, slices: ['todos'] });
503
+ ```
504
+
505
+ Restoring *after* construction is the obvious alternative and the wrong one: applying a
506
+ snapshot to a live store emits a change across every path, which on boot is a flash, a burst
507
+ of instrumentation entries describing changes nobody made, and effects observing a transition
508
+ that never happened.
509
+
510
+ **Nothing throws on boot.** A missing, unparseable or unmigratable payload falls back to your
511
+ declared defaults and reports through `onError`. A store that will not start because storage
512
+ holds stale JSON is worse than one that starts fresh — and a full disk should not take down a
513
+ page, so write failures are reported the same way rather than raised.
514
+
515
+ **Version mismatches are refused, not trusted.** Reducers change, and a snapshot written
516
+ against an older shape may not be valid state for this build at all. Supply `migrate` to
517
+ upgrade it, or it is discarded.
518
+
519
+ Writes are driven by instrumentation, so a change confined to a slice you are not persisting
520
+ costs nothing, and a burst is coalesced into one write. `Map`, `Set`, `Date`, `BigInt`,
521
+ `undefined` and circular references all survive the round trip: `JSON.stringify` does not fail
522
+ on those, it silently destroys them.
523
+
524
+ For a server render, `dehydrate(store, { version })` produces the payload and
525
+ `hydrate({ source, version })` consumes it.
526
+
527
+ ## Lists that reorder
528
+
529
+ Path notification is positional for arrays. `items.0.title` names a *slot*, not a thing, so
530
+ `unshift`, `splice(0, 1)` and `sort` move nearly every element into a different slot — and the
531
+ diff correctly reports that nearly every leaf changed. Inserting one row at the front of a
532
+ thousand wakes a thousand subscribers.
533
+
534
+ That is honest rather than noisy: with positional paths the value at almost every index really
535
+ did change. The remedy is the shape of the state, not a diff that stays quiet.
536
+
537
+ ```ts
538
+ import { createEntityAdapter } from '@yoltra/core';
539
+
540
+ const todos = createEntityAdapter<Todo>();
541
+
542
+ // state is { ids: [...], entities: { abc: {...} } }
543
+ todos.updateOne(state, { id: 'abc', changes: { done: true } });
544
+
545
+ // and the adapter hands out the paths, so they are never typed by hand
546
+ todos.pathTo('abc', 'title'); // "entities.abc.title"
547
+ todos.idsPath; // "ids"
548
+ ```
549
+
550
+ `entities.abc.title` survives insert, remove and reorder. A list container subscribes to `ids`
551
+ and reorders its children; rows subscribe to their own entity and stay asleep through a sort.
552
+
553
+ `ids` is still an array, so a reorder still reports `ids.0`, `ids.1` and so on — that cost is
554
+ confined, not removed. What you get is cost proportional to what actually changed.
555
+
556
+ For a small list that only ever grows at the end, `items.0.title` is fine and simpler. The
557
+ adapter is for collections that reorder, or that are large enough for the difference to show.
558
+
559
+ ### What it costs, measured
560
+
561
+ At 1000 rows, diffing after an insert at the front costs 1200 µs for an array and 371 µs
562
+ normalised, and the array reports roughly a thousand changed paths against two. That is the
563
+ case the adapter is for.
564
+
565
+ A single-field update runs the other way: 20 µs for the array against 470 µs normalised.
566
+ `detectChangedProps` indexes an array but enumerates an object's keys — building two key
567
+ arrays and a `Set` per comparison — so a wide entity map is more expensive to walk even when
568
+ almost nothing in it moved. The numbers are in `benchmarks/`, and closing that gap is tracked
569
+ work rather than a property of normalising as such.
570
+
571
+ So: normalise collections that reorder or churn. A large collection that only ever has
572
+ individual fields edited is better off as an array today.
573
+
429
574
  ## Performance
430
575
 
431
- | Metric | Value |
432
- | ------------------ | ------------------------------ |
433
- | **Bundle size** | ~8KB (minified + gzipped) |
434
- | **Tree-shakeable** | Yes (ES modules) |
435
- | **Dependencies** | Zero |
436
- | **TypeScript** | Full type definitions included |
576
+ | Metric | Value |
577
+ | ------------------ | ----------------------------------------- |
578
+ | **Bundle size** | 6.7 KB for the store (minified + gzipped) |
579
+ | **Tree-shakeable** | Yes (ES modules) |
580
+ | **Dependencies** | Zero |
581
+ | **TypeScript** | Full type definitions included |
582
+
583
+ Bundle size is checked, not asserted: `rush size` bundles the package the way a consumer
584
+ would — tree-shaken, minified, gzipped — and fails when it exceeds the budget declared in
585
+ `package.json`.
586
+
587
+ The number that matters is what you import, not what the package exports:
588
+
589
+ | Import | Size |
590
+ | ----------------------------------- | ------ |
591
+ | `{ createStore }` | 6.7 KB |
592
+ | `{ createStore, hydrate, persist }` | 8.2 KB |
593
+ | everything | 9.5 KB |
594
+
595
+ Persistence and the entity adapter cost nothing to anyone who does not import them — the
596
+ first row has not moved as either was added, which is the tree-shaking claim being checked
597
+ rather than repeated. The last row is a growth tripwire; `import * as all` is not something
598
+ anybody writes.
437
599
 
438
600
  ---
439
601
 
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Normalised collections, so a list stops paying O(N) for an O(1) change.
3
+ *
4
+ * @remarks
5
+ * Path notification is positional for arrays. `detectChangedProps` walks indices and reports
6
+ * `items.0.title`, which names a *slot*, not a thing. So `unshift`, `splice(0, 1)` and `sort`
7
+ * move nearly every element into a different slot, and the diff correctly reports that nearly
8
+ * every leaf changed. Inserting one row at the front of a thousand wakes a thousand
9
+ * subscribers.
10
+ *
11
+ * The remedy is the state shape, not a quieter diff. A key-stable array diff would need an
12
+ * identity key the diff has no business knowing, and even then the *paths* would still be
13
+ * positional — `items.0.title` names position zero, and so does the RFC-6902 pointer the
14
+ * devtools agents build from it.
15
+ *
16
+ * Normalising to `{ ids, entities }` makes `entities.abc.title` stable across insert, remove
17
+ * and reorder.
18
+ *
19
+ * **What this does not do:** `ids` is still an array, so a reorder still reports `ids.0`,
20
+ * `ids.1` and so on. That cost is confined rather than removed. A list container subscribes to
21
+ * `ids` and reorders its children; rows subscribe to `entities.<id>.<field>` and stay asleep.
22
+ * The promise is cost proportional to what actually changed.
23
+ *
24
+ * @module @yoltra/core
25
+ */
26
+ /** What an entity may be keyed by. */
27
+ export type EntityId = string | number;
28
+ /**
29
+ * A normalised collection.
30
+ *
31
+ * @typeParam T - The entity.
32
+ * @typeParam Id - Its key type.
33
+ *
34
+ * @public
35
+ */
36
+ export interface EntityState<T, Id extends EntityId = string> {
37
+ /** Order. Reordering touches this and nothing under `entities`. */
38
+ readonly ids: readonly Id[];
39
+ /** Identity-keyed, so a path to one entity survives every change to the others. */
40
+ readonly entities: Readonly<Record<Id, T>>;
41
+ }
42
+ /** A change to apply to one entity. */
43
+ export interface EntityUpdate<T, Id extends EntityId> {
44
+ readonly id: Id;
45
+ readonly changes: Partial<T>;
46
+ }
47
+ /** How an adapter identifies and orders its entities. */
48
+ export interface EntityAdapterOptions<T, Id extends EntityId> {
49
+ /** Defaults to reading `id`. */
50
+ readonly selectId?: (entity: T) => Id;
51
+ /**
52
+ * Keeps `ids` sorted.
53
+ *
54
+ * @remarks
55
+ * Omit it and `ids` holds insertion order, which is cheaper: with a comparer, any change
56
+ * that could affect position re-sorts. The sorted array is only adopted when it actually
57
+ * differs, so a sort that changes nothing reports nothing.
58
+ */
59
+ readonly sortComparer?: (a: T, b: T) => number;
60
+ }
61
+ /**
62
+ * Reducer helpers, selectors, and the subscription paths that make the shape worth having.
63
+ *
64
+ * @public
65
+ */
66
+ export interface EntityAdapter<T, Id extends EntityId = string> {
67
+ getInitialState(): EntityState<T, Id>;
68
+ getInitialState<Extra extends object>(extra: Extra): EntityState<T, Id> & Extra;
69
+ /** Adds an entity. Existing ids are left alone — this is not an upsert. */
70
+ addOne<S extends EntityState<T, Id>>(state: S, entity: T): S;
71
+ addMany<S extends EntityState<T, Id>>(state: S, entities: readonly T[]): S;
72
+ /** Adds or replaces one entity wholesale. */
73
+ setOne<S extends EntityState<T, Id>>(state: S, entity: T): S;
74
+ setMany<S extends EntityState<T, Id>>(state: S, entities: readonly T[]): S;
75
+ /** Replaces the whole collection. */
76
+ setAll<S extends EntityState<T, Id>>(state: S, entities: readonly T[]): S;
77
+ /** Merges `changes` into one entity. Unknown ids are ignored. */
78
+ updateOne<S extends EntityState<T, Id>>(state: S, update: EntityUpdate<T, Id>): S;
79
+ updateMany<S extends EntityState<T, Id>>(state: S, updates: readonly EntityUpdate<T, Id>[]): S;
80
+ /** Adds, or merges into an existing entity. */
81
+ upsertOne<S extends EntityState<T, Id>>(state: S, entity: T): S;
82
+ upsertMany<S extends EntityState<T, Id>>(state: S, entities: readonly T[]): S;
83
+ removeOne<S extends EntityState<T, Id>>(state: S, id: Id): S;
84
+ removeMany<S extends EntityState<T, Id>>(state: S, ids: readonly Id[]): S;
85
+ removeAll<S extends EntityState<T, Id>>(state: S): S;
86
+ selectIds(state: EntityState<T, Id>): readonly Id[];
87
+ selectEntities(state: EntityState<T, Id>): Readonly<Record<Id, T>>;
88
+ selectAll(state: EntityState<T, Id>): readonly T[];
89
+ selectById(state: EntityState<T, Id>, id: Id): T | undefined;
90
+ selectTotal(state: EntityState<T, Id>): number;
91
+ /** Path to the order array. Subscribe here for a list that reorders. */
92
+ readonly idsPath: string;
93
+ /** Path to one entity, or to a field of it. */
94
+ pathTo(id: Id, field?: string): string;
95
+ /** Wildcard across every entity's `field`, for the loose subscription registry. */
96
+ anyField(field: string): string;
97
+ }
98
+ /**
99
+ * Builds an adapter for one entity type.
100
+ *
101
+ * @example
102
+ * ```ts
103
+ * const todos = createEntityAdapter<Todo>();
104
+ *
105
+ * const spec: ReducerSpec<EntityState<Todo>, EM> = {
106
+ * state: todos.getInitialState(),
107
+ * when: { keys: eventKeys<EM>()([['todos', 'toggled']]) },
108
+ * reducer: (state, event) =>
109
+ * todos.updateOne(state, { id: event.payload.id, changes: { done: event.payload.done } }),
110
+ * };
111
+ *
112
+ * // and in a component
113
+ * useAtomicProp({ reducer: 'todos', property: todos.pathTo(id, 'title') });
114
+ * ```
115
+ *
116
+ * @public
117
+ */
118
+ export declare function createEntityAdapter<T, Id extends EntityId = string>(options?: EntityAdapterOptions<T, Id>): EntityAdapter<T, Id>;
@@ -1,4 +1,4 @@
1
- import { EventMapBase } from '../types';
1
+ import { Event, EventMapBase } from '../types.js';
2
2
  /**
3
3
  * Minimal, synchronous pub/sub event bus keyed by **channel** and **type**.
4
4
  *
@@ -53,7 +53,10 @@ export declare class EventBus<EM extends EventMapBase> {
53
53
  * @typeParam T - Type key within channel `C` (must be a string key of `EM[C]`).
54
54
  * @param channel - Channel name to subscribe to.
55
55
  * @param type - Event type within the channel.
56
- * @param handler - Function invoked with the payload type `EM[C][T]`.
56
+ * @param handler - Function invoked with the payload type `EM[C][T]`. It optionally
57
+ * receives the **source event** as a second argument when the emitter supplies one, so
58
+ * subscribers can read the true `id` (and any `meta`) instead of reconstructing an event
59
+ * from the payload alone. Handlers that declare only `payload` remain valid.
57
60
  * @returns An **unsubscribe** function that removes this handler.
58
61
  *
59
62
  * @example
@@ -66,9 +69,16 @@ export declare class EventBus<EM extends EventMapBase> {
66
69
  * off();
67
70
  * ```
68
71
  *
72
+ * @example Reading the source event
73
+ * ```ts
74
+ * bus.on('data', 'loaded', (payload, event) => {
75
+ * console.log('event id:', event?.id);
76
+ * });
77
+ * ```
78
+ *
69
79
  * @public
70
80
  */
71
- on<C extends keyof EM & string, T extends keyof EM[C] & string>(channel: C, type: T, handler: (payload: EM[C][T]) => void): () => void;
81
+ on<C extends keyof EM & string, T extends keyof EM[C] & string>(channel: C, type: T, handler: (payload: EM[C][T], event?: Event<EM, C, T>) => void): () => void;
72
82
  /**
73
83
  * Removes a specific handler previously added with {@link EventBus.on | `on`}.
74
84
  *
@@ -89,7 +99,7 @@ export declare class EventBus<EM extends EventMapBase> {
89
99
  *
90
100
  * @public
91
101
  */
92
- off<C extends keyof EM & string, T extends keyof EM[C] & string>(channel: C, type: T, handler: (payload: EM[C][T]) => void): void;
102
+ off<C extends keyof EM & string, T extends keyof EM[C] & string>(channel: C, type: T, handler: (payload: EM[C][T], event?: Event<EM, C, T>) => void): void;
93
103
  /**
94
104
  * Emits an event to all subscribers of the exact `(channel, type)`.
95
105
  *
@@ -101,6 +111,9 @@ export declare class EventBus<EM extends EventMapBase> {
101
111
  * @param channel - Channel name to emit on.
102
112
  * @param type - Event type to emit.
103
113
  * @param payload - Payload matching `EM[C][T]`.
114
+ * @param event - Optional **source event**, forwarded to handlers as a second argument.
115
+ * Supply it whenever the caller already holds the real event so subscribers observe its
116
+ * true `id` rather than reconstructing one; omitting it keeps the original behaviour.
104
117
  *
105
118
  * @example
106
119
  * ```ts
@@ -109,7 +122,7 @@ export declare class EventBus<EM extends EventMapBase> {
109
122
  *
110
123
  * @public
111
124
  */
112
- emit<C extends keyof EM & string, T extends keyof EM[C] & string>(channel: C, type: T, payload: EM[C][T]): void;
125
+ emit<C extends keyof EM & string, T extends keyof EM[C] & string>(channel: C, type: T, payload: EM[C][T], event?: Event<EM, C, T>): void;
113
126
  /**
114
127
  * Clears **all** listeners across all channels/types.
115
128
  *
@@ -1,45 +1,6 @@
1
1
  /**
2
2
  * @module @yoltra/core
3
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
4
  export declare class LooseEventBus<C extends string = string, T extends string = string, P = any> {
44
5
  /**
45
6
  * Exact handlers: `channel → type → [handlers]`.
@@ -51,6 +12,24 @@ export declare class LooseEventBus<C extends string = string, T extends string =
51
12
  * @internal
52
13
  */
53
14
  private patternHandlers;
15
+ /**
16
+ * Patterns bucketed by their first segment, so an emit tests only what could match.
17
+ *
18
+ * @remarks
19
+ * Delivery used to walk every pattern registered on the channel and run the full segment
20
+ * matcher against each. That is linear in the number of patterns rather than in the number
21
+ * that match, and it re-split both the pattern and the subject on every test — for a thousand
22
+ * patterns, two thousand string splits to deliver one event.
23
+ *
24
+ * A subject's first segment can only be matched by a pattern whose first segment is that same
25
+ * literal, or is `*` or `**`. Bucketing on that turns the common shape — distinct event
26
+ * families like `panel.*` and `order.**` — from a scan of everything into a map lookup plus
27
+ * the handful that begin with a wildcard.
28
+ *
29
+ * It buys nothing for a channel where every pattern starts with `**`, since all of those must
30
+ * still be tested. That is the honest worst case, and it is unchanged rather than worsened.
31
+ */
32
+ private patternIndex;
54
33
  /**
55
34
  * Subscribes a handler to either an **exact** type or a **pattern**.
56
35
  *
@@ -142,6 +121,23 @@ export declare class LooseEventBus<C extends string = string, T extends string =
142
121
  * @public
143
122
  */
144
123
  emit(channel: C, type: T, payload: P): void;
124
+ /**
125
+ * Emits a payload that is only built if somebody is listening.
126
+ *
127
+ * @param channel - Channel to emit on.
128
+ * @param type - Concrete event type.
129
+ * @param make - Builds the payload. Called at most once, and only when a handler matched.
130
+ *
131
+ * @remarks
132
+ * Same matching as {@link LooseEventBus.emit}; the difference is *when* the payload exists.
133
+ * The store's change notification carries the old and new value at a path, and reading those
134
+ * means walking the state tree twice per path. Doing that eagerly meant a slice nobody had
135
+ * subscribed to paid the full cost of describing changes to an audience of nobody — the
136
+ * matching work was already being done to discover there were no handlers.
137
+ *
138
+ * @public
139
+ */
140
+ emitWith(channel: C, type: T, make: () => P): void;
145
141
  /**
146
142
  * Determines if a string is a **pattern** (contains `*`).
147
143
  * @param s - Event type or pattern string.
@@ -169,29 +165,56 @@ export declare class LooseEventBus<C extends string = string, T extends string =
169
165
  */
170
166
  private splitPath;
171
167
  /**
172
- * Pattern matcher over dot-separated segments.
168
+ * Files a pattern under the first segment that could select it.
169
+ * @internal
170
+ */
171
+ private indexPattern;
172
+ /**
173
+ * Removes a pattern from the index. Paired with {@link LooseEventBus.offPattern}.
174
+ * @internal
175
+ */
176
+ private unindexPattern;
177
+ /**
178
+ * The handler lists of every pattern matching this subject.
179
+ *
180
+ * @remarks
181
+ * Shared by `emit` and `emitWith` so the two cannot drift on what "matching" means — which
182
+ * they could, being two copies of the same walk before.
183
+ *
184
+ * The subject is split once here rather than once per pattern tested.
185
+ *
186
+ * @internal
187
+ */
188
+ private matchingPatternHandlers;
189
+ /**
190
+ * Pattern matcher over dot-separated segments, which arrive already split.
173
191
  *
174
192
  * Rules:
175
193
  * - **literal**: exact match.
176
194
  * - `*` : matches exactly **one** segment.
177
195
  * - `**` : matches **zero or more** remaining segments (including empty).
178
196
  *
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`.
197
+ * @remarks
198
+ * Takes segments rather than strings so delivery can split each pattern once at registration
199
+ * and the subject once per emit, instead of both once per test. Re-splitting per test was most
200
+ * of what made wildcard delivery expensive: a thousand patterns meant two thousand string
201
+ * splits to deliver one event.
202
+ *
203
+ * @param pSegs - Pattern segments (may include `*`/`**`).
204
+ * @param sSegs - Subject segments to test.
205
+ * @returns `true` if the pattern matches; otherwise `false`.
182
206
  *
183
207
  * @example
184
208
  * ```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
209
+ * matchSegments(['a', '*'], ['a', 'b']) // true
210
+ * matchSegments(['a', '*'], ['a', 'b', 'c']) // false
211
+ * matchSegments(['a', '**'], ['a']) // true
212
+ * matchSegments(['**', 'end'], ['x', 'y', 'end']) // true
190
213
  * ```
191
214
  *
192
215
  * @internal
193
216
  */
194
- private matchPattern;
217
+ private matchSegments;
195
218
  /**
196
219
  * Removes **all** listeners (exact and pattern). Useful for tests/HMR teardown.
197
220
  *
@@ -1,2 +1,2 @@
1
- export * from './EventBus';
2
- export * from './LooseEventBus';
1
+ export * from './EventBus.js';
2
+ export * from './LooseEventBus.js';
@@ -5,11 +5,19 @@
5
5
  *
6
6
  * @packageDocumentation
7
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, EmitOptions, InstrumentedEvent, InstrumentationObserver, Unsubscribe, StoreSpec, StoreInstance, ReducerSpec, ReducerFunction, ReducersMapAny, StateFromReducers, EMFromReducersStrict, EffectSpec, EffectFunction, MiddlewareFunction, MiddlewareSpec, MiddlewareInput, DeepReadonly, DeepRO, Primitive, Path, PathValue, WithGlob, Dotted, EventPhase, EventSubscriptionHandler, NarrowedEventHandler, When, EventFromWhen, EventConsumerType, EventConsumerMeta, } from './types';
8
+ export { EventBus } from './eventBus/EventBus.js';
9
+ export { LooseEventBus } from './eventBus/LooseEventBus.js';
10
+ export { Reducer } from './reducer/Reducer.js';
11
+ export { Store, createStore, typedEvents } from './store/Store.js';
12
+ export { detectChangedProps } from './utils/detectChangedProps.js';
13
+ export { freezeState } from './utils/immutability.js';
14
+ export { eventKeys } from './types.js';
15
+ export type { EventMapBase, EventKey, Event, EventUnion, Change, Emit, EmitOptions, EventMeta, InstrumentedEvent, InstrumentationObserver, Unsubscribe, StoreSpec, StoreInstance, ReducerSpec, ReducerFunction, ReducersMapAny, StateFromReducers, EMFromReducersStrict, EffectSpec, EffectFunction, MiddlewareFunction, MiddlewareSpec, MiddlewareInput, DeepReadonly, DeepRO, Primitive, RootValue, Path, PathValue, WithGlob, Dotted, EventPhase, EventSubscriptionHandler, NarrowedEventHandler, When, EventFromWhen, EventConsumerType, EventConsumerMeta, } from './types.js';
16
+ export { createEntityAdapter } from './entity/entityAdapter.js';
17
+ export type { EntityAdapter, EntityAdapterOptions, EntityId, EntityState, EntityUpdate, } from './entity/entityAdapter.js';
18
+ export { decodeState, encodeState, encodeStateBounded } from './serialize/codec.js';
19
+ export type { BoundedEncodeResult, EncodeOptions, EncodeReport, EncodeResult, } from './serialize/codec.js';
20
+ export { dehydrate, hydrate, persist, withHydration } from './persistence/persist.js';
21
+ export type { Hydration, PersistableStore, PersistenceAdapter, PersistencePhase, PersistOptions, } from './persistence/persist.js';
22
+ export { createMemoryAdapter, createWebStorageAdapter } from './persistence/adapters.js';
23
+ export type { WebStorageLike } from './persistence/adapters.js';
@@ -0,0 +1,33 @@
1
+ import { PersistenceAdapter } from './persist.js';
2
+ /** The slice of the Web Storage API used here. */
3
+ export interface WebStorageLike {
4
+ getItem(key: string): string | null;
5
+ setItem(key: string, value: string): void;
6
+ removeItem(key: string): void;
7
+ }
8
+ /**
9
+ * Wraps a Web Storage object.
10
+ *
11
+ * @remarks
12
+ * Pass `localStorage` or `sessionStorage` explicitly. Reading the global here would make this
13
+ * module unusable anywhere one does not exist, which includes a server render — exactly where
14
+ * hydration payloads are produced.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * const adapter = createWebStorageAdapter(localStorage);
19
+ * ```
20
+ *
21
+ * @public
22
+ */
23
+ export declare function createWebStorageAdapter(storage: WebStorageLike): PersistenceAdapter;
24
+ /**
25
+ * Keeps state in memory.
26
+ *
27
+ * @remarks
28
+ * For tests, and for a server render that wants the persistence path exercised without a
29
+ * store behind it. It forgets on restart, which is the whole of what it claims.
30
+ *
31
+ * @public
32
+ */
33
+ export declare function createMemoryAdapter(initial?: Record<string, string>): PersistenceAdapter;