@yoltra/core 0.6.0 → 0.7.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 +322 -143
- package/README.md +85 -76
- package/dist/types/store/Store.d.ts +5 -52
- package/dist/types/store/matching.d.ts +49 -0
- package/dist/types/store/paths.d.ts +39 -0
- package/dist/types/store/performCall.d.ts +15 -0
- package/dist/types/store/rejection.d.ts +2 -2
- package/dist/types/types.d.ts +4 -4
- package/dist/yoltra.cjs +3 -8
- package/dist/yoltra.cjs.map +1 -1
- package/dist/yoltra.mjs +898 -938
- package/dist/yoltra.mjs.map +1 -1
- package/dist/yoltra.umd.js +3 -8
- package/dist/yoltra.umd.js.map +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ emit(channel, type, payload)
|
|
|
34
34
|
│
|
|
35
35
|
├─ 0. Dedup (opt-in) ─── Skip a duplicate only when dedupWindowMs > 0 or a dedupKey is given
|
|
36
36
|
│
|
|
37
|
-
│ ══ SYNCHRONOUS reduce phase
|
|
37
|
+
│ ══ SYNCHRONOUS reduce phase: runs before emit() returns ══
|
|
38
38
|
├─ 1. Middleware ─── Synchronous pre-reducer hooks (return false to reject → "uncommitted" event)
|
|
39
39
|
├─ 2. Reducers ─── Every matching slice staged, then all committed under one root
|
|
40
40
|
├─ 3. Event subscribers ─── Committed/uncommitted event notifications
|
|
@@ -43,11 +43,11 @@ emit(channel, type, payload)
|
|
|
43
43
|
└─ 5. Effects ─── ASYNC side-effects, one independent task per event (keyed for O(1) lookup)
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
The reduce phase (1–4) is **synchronous**, so `getState()` is correct the instant `emit()` returns
|
|
47
|
-
|
|
46
|
+
The reduce phase (1–4) is **synchronous**, so `getState()` is correct the instant `emit()` returns,
|
|
47
|
+
even with middleware. Effects (5) run afterward as an independent async task; the promise from
|
|
48
48
|
`emit()` resolves when that event's effects finish. Every stage is hook-able, and
|
|
49
|
-
`store.instrument()` exposes the whole flow
|
|
50
|
-
phase
|
|
49
|
+
`store.instrument()` exposes the whole flow (changed leaf paths, reduce timing, committed/rejected
|
|
50
|
+
phase) to the DevTools with no `as any`. See the
|
|
51
51
|
[Event Pipeline Architecture](../../docs/en/design/event-queue-architecture.md) for the full model.
|
|
52
52
|
|
|
53
53
|
---
|
|
@@ -71,17 +71,17 @@ Subscribe to exact state paths using dotted notation. Supports `*` (one segment)
|
|
|
71
71
|
or more segments) wildcards:
|
|
72
72
|
|
|
73
73
|
```typescript
|
|
74
|
-
// Exact path
|
|
74
|
+
// Exact path: fires when items[0].title changes
|
|
75
75
|
store.connect({ reducer: "todos", property: "items.0.title" }, (change) =>
|
|
76
76
|
console.log("title:", change.oldValue, "→", change.newValue),
|
|
77
77
|
);
|
|
78
78
|
|
|
79
|
-
// Single-segment wildcard
|
|
79
|
+
// Single-segment wildcard: fires when ANY item's title changes
|
|
80
80
|
store.connect({ reducer: "todos", property: "items.*.title" }, (change) =>
|
|
81
81
|
console.log("some title changed at", change.path),
|
|
82
82
|
);
|
|
83
83
|
|
|
84
|
-
// Deep wildcard
|
|
84
|
+
// Deep wildcard: fires when anything under items changes
|
|
85
85
|
store.connect({ reducer: "todos", property: "items.**" }, (change) =>
|
|
86
86
|
console.log("items tree changed at", change.path),
|
|
87
87
|
);
|
|
@@ -108,7 +108,7 @@ await store.emit("auth", "login", { token: "abc123" });
|
|
|
108
108
|
store.getState().token; // "abc123"
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
-
Such a slice has no property beneath it, so its changes are reported at the **slice root
|
|
111
|
+
Such a slice has no property beneath it, so its changes are reported at the **slice root**,
|
|
112
112
|
the empty path. Subscribe to it with `property: ""`:
|
|
113
113
|
|
|
114
114
|
```typescript
|
|
@@ -117,20 +117,20 @@ store.connect({ reducer: "token", property: "" }, (change) =>
|
|
|
117
117
|
);
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
-
The types know the difference. `property` on a root-value slice accepts `""` and nothing else
|
|
121
|
-
there is no key to address
|
|
120
|
+
The types know the difference. `property` on a root-value slice accepts `""` and nothing else,
|
|
121
|
+
because there is no key to address, and the value comes back correctly typed:
|
|
122
122
|
|
|
123
123
|
```typescript
|
|
124
124
|
const token = useAtomicProp({ reducer: "token", property: "" }); // string | null
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
-
### `""` versus `"**"
|
|
127
|
+
### `""` versus `"**"`: watching a whole slice
|
|
128
128
|
|
|
129
129
|
Two subscriptions sound alike and are not:
|
|
130
130
|
|
|
131
131
|
| Pattern | Fires when |
|
|
132
132
|
|---|---|
|
|
133
|
-
| `""` | the slice's **whole value** is replaced
|
|
133
|
+
| `""` | the slice's **whole value** is replaced: a primitive changes, a `Map` is rebuilt, an object slice becomes `null` |
|
|
134
134
|
| `"**"` | **anything** in the slice changes, at any depth. Matches the root too, since `**` matches zero segments |
|
|
135
135
|
| `"*"` | one level down, exactly. Never matches the root |
|
|
136
136
|
|
|
@@ -140,7 +140,7 @@ because such a slice reports its changes at their leaves.
|
|
|
140
140
|
|
|
141
141
|
`Map` and `Set` are compared by reference, not by entry: a reducer returning a new `Map` is a
|
|
142
142
|
change, mutating one in place is not. That follows from the immutability contract rather than
|
|
143
|
-
being a special case
|
|
143
|
+
being a special case. Build a new collection instead of mutating the stored one. It is also why
|
|
144
144
|
they have no paths beneath them: `"byId"` is subscribable, `"byId.get"` is not, and the types
|
|
145
145
|
say so.
|
|
146
146
|
|
|
@@ -169,7 +169,7 @@ type AppEM = {
|
|
|
169
169
|
system: { init: void; shutdown: void };
|
|
170
170
|
};
|
|
171
171
|
|
|
172
|
-
// Match specific event keys (recommended
|
|
172
|
+
// Match specific event keys (recommended: preserves type correlation)
|
|
173
173
|
const counterReducer = {
|
|
174
174
|
state: { value: 0 },
|
|
175
175
|
when: {
|
|
@@ -218,7 +218,7 @@ both raw functions (legacy) and `MiddlewareSpec` objects with targeting:
|
|
|
218
218
|
```typescript
|
|
219
219
|
import type { MiddlewareSpec } from "@yoltra/core";
|
|
220
220
|
|
|
221
|
-
// Targeted middleware
|
|
221
|
+
// Targeted middleware: only runs for admin channel events
|
|
222
222
|
const adminGuard: MiddlewareSpec<AppState, AppEM> = {
|
|
223
223
|
when: { channel: "admin" },
|
|
224
224
|
middleware: (state, event) => {
|
|
@@ -228,7 +228,7 @@ const adminGuard: MiddlewareSpec<AppState, AppEM> = {
|
|
|
228
228
|
meta: { type: "middleware", name: "adminGuard" },
|
|
229
229
|
};
|
|
230
230
|
|
|
231
|
-
// Global middleware
|
|
231
|
+
// Global middleware: runs for all events (synchronous: return a boolean, never a Promise)
|
|
232
232
|
const logger = (state, event) => {
|
|
233
233
|
console.log("Event:", event.channel, event.type);
|
|
234
234
|
return true;
|
|
@@ -301,12 +301,12 @@ Subscribe to events (not state) from the view layer. Useful for notifications, a
|
|
|
301
301
|
responding to rejected events:
|
|
302
302
|
|
|
303
303
|
```typescript
|
|
304
|
-
// Committed events (default)
|
|
304
|
+
// Committed events (default): events that passed middleware
|
|
305
305
|
const off = store.onEvent("ui", "save", (event, getState, emit, phase) => {
|
|
306
306
|
console.log("Save committed:", event.payload);
|
|
307
307
|
});
|
|
308
308
|
|
|
309
|
-
// Uncommitted events
|
|
309
|
+
// Uncommitted events: events rejected by middleware
|
|
310
310
|
store.onEvent(
|
|
311
311
|
"ui",
|
|
312
312
|
"delete",
|
|
@@ -316,7 +316,7 @@ store.onEvent(
|
|
|
316
316
|
"uncommitted",
|
|
317
317
|
);
|
|
318
318
|
|
|
319
|
-
// Written events
|
|
319
|
+
// Written events: state actually changed. Fires after the commit, so getState() is current.
|
|
320
320
|
store.onEvent(
|
|
321
321
|
"plan",
|
|
322
322
|
"patch",
|
|
@@ -326,7 +326,7 @@ store.onEvent(
|
|
|
326
326
|
"written",
|
|
327
327
|
);
|
|
328
328
|
|
|
329
|
-
// All events
|
|
329
|
+
// All events: both committed and uncommitted (not written; see below)
|
|
330
330
|
store.onEvent(
|
|
331
331
|
"ui",
|
|
332
332
|
"action",
|
|
@@ -338,7 +338,7 @@ store.onEvent(
|
|
|
338
338
|
```
|
|
339
339
|
|
|
340
340
|
`committed` means **not vetoed**, and always has: it fires for every event middleware let through,
|
|
341
|
-
whether or not a reducer wrote anything
|
|
341
|
+
whether or not a reducer wrote anything, including every event in a store with no reducers at
|
|
342
342
|
all. `written` is the stricter fact, added rather than substituted, so toasts and analytics keep
|
|
343
343
|
working unchanged. `all` stays `committed | uncommitted`; folding `written` in would hand existing
|
|
344
344
|
subscribers a second notification per event.
|
|
@@ -348,7 +348,7 @@ subscribers a second notification per event.
|
|
|
348
348
|
## Commits are atomic across slices
|
|
349
349
|
|
|
350
350
|
An event that touches several slices writes all of them, then notifies. Nothing observes a
|
|
351
|
-
half-applied event
|
|
351
|
+
half-applied event. A subscriber to one slice reading `getState()` sees every other slice of the
|
|
352
352
|
same event already applied.
|
|
353
353
|
|
|
354
354
|
That matters most where a change is used as a signal to re-read, which is what the React hooks do.
|
|
@@ -380,8 +380,8 @@ const store = createStore({
|
|
|
380
380
|
|
|
381
381
|
const result = await store.emit("plan", "patch", { steps, expectedVersion: 1 });
|
|
382
382
|
|
|
383
|
-
result.committed; // true
|
|
384
|
-
result.written; // false
|
|
383
|
+
result.committed; // true: middleware allowed it
|
|
384
|
+
result.written; // false: nothing was written
|
|
385
385
|
result.rejected?.reason; // "stale write: expected v1, have v3"
|
|
386
386
|
```
|
|
387
387
|
|
|
@@ -400,10 +400,10 @@ has made a decision and the whole event yields to it.
|
|
|
400
400
|
|
|
401
401
|
---
|
|
402
402
|
|
|
403
|
-
## Request and reply
|
|
403
|
+
## Request and reply: `store.call()`
|
|
404
404
|
|
|
405
405
|
Every event-bus consumer eventually writes request/reply by hand: mint an id, subscribe, match,
|
|
406
|
-
time out, unsubscribe. It is about eighty lines and it has the same two bugs every time
|
|
406
|
+
time out, unsubscribe. It is about eighty lines and it has the same two bugs every time: the
|
|
407
407
|
subscription outlives the call, and a responder that forgets to echo the id produces a timeout
|
|
408
408
|
with nothing to point at.
|
|
409
409
|
|
|
@@ -413,7 +413,7 @@ res.payload.text;
|
|
|
413
413
|
```
|
|
414
414
|
|
|
415
415
|
The responder does nothing special. It replies through the `emit` it was handed, and the store's
|
|
416
|
-
causal stamp correlates the two
|
|
416
|
+
causal stamp correlates the two, and **there is no id to mint, echo, or forget**:
|
|
417
417
|
|
|
418
418
|
```typescript
|
|
419
419
|
store.registerEffect({
|
|
@@ -453,7 +453,7 @@ const { payload } = await call;
|
|
|
453
453
|
```
|
|
454
454
|
|
|
455
455
|
The backpressure is real, not a buffer with a limit. `emit` resolves only once its effects have
|
|
456
|
-
run, and the collector is an effect that does not return until the consumer has taken the item
|
|
456
|
+
run, and the collector is an effect that does not return until the consumer has taken the item,
|
|
457
457
|
so a responder writing `await emit("job", "tick", chunk)` is **paced by the reader**:
|
|
458
458
|
|
|
459
459
|
```typescript
|
|
@@ -466,7 +466,7 @@ effect: async (_event, _get, emit) => {
|
|
|
466
466
|
```
|
|
467
467
|
|
|
468
468
|
Backpressure engages **once you begin iterating**. A call that is only awaited never pulls, so
|
|
469
|
-
blocking its producer would deadlock the call itself
|
|
469
|
+
blocking its producer would deadlock the call itself: progress nobody reads would stop the
|
|
470
470
|
terminal event from ever being sent. Un-iterated progress therefore buffers to `highWaterMark`
|
|
471
471
|
and is then counted on `call.dropped` rather than blocking.
|
|
472
472
|
|
|
@@ -474,17 +474,17 @@ and is then counted on `call.dropped` rather than blocking.
|
|
|
474
474
|
|
|
475
475
|
| | |
|
|
476
476
|
|---|---|
|
|
477
|
-
| `timeoutMs` | **Idle**, not total
|
|
477
|
+
| `timeoutMs` | **Idle**, not total: every correlated event resets it, progress included. A job that streams for two minutes will not fail a thirty-second call. Default 30s. |
|
|
478
478
|
| `signal` | An `AbortSignal`, for a real deadline or a cancelled action. |
|
|
479
479
|
| `call.cancel(reason)` | Stops listening and settles. Safe to call twice. |
|
|
480
480
|
|
|
481
|
-
However a call ends
|
|
481
|
+
However a call ends, whether resolved, timed out or aborted, the subscription is removed and any producer
|
|
482
482
|
parked on backpressure is released. A wedged responder is worse than the unbounded buffer this
|
|
483
483
|
replaced.
|
|
484
484
|
|
|
485
485
|
## Reading a value as you subscribe
|
|
486
486
|
|
|
487
|
-
`connect` starts at "from now on", so a subscriber's first read had to repeat the path elsewhere
|
|
487
|
+
`connect` starts at "from now on", so a subscriber's first read had to repeat the path elsewhere:
|
|
488
488
|
the same path in two places, free to drift:
|
|
489
489
|
|
|
490
490
|
```typescript
|
|
@@ -513,14 +513,14 @@ store.connect({ reducer: "orders", property: "status" }, (change) => {
|
|
|
513
513
|
});
|
|
514
514
|
```
|
|
515
515
|
|
|
516
|
-
Provenance is **absent** when no event caused the change
|
|
516
|
+
Provenance is **absent** when no event caused the change: a DevTools time-travel jump, or the
|
|
517
517
|
`immediate` delivery above. Absence is the signal, rather than a fabricated id.
|
|
518
518
|
|
|
519
519
|
---
|
|
520
520
|
|
|
521
521
|
## Event Deduplication (opt-in)
|
|
522
522
|
|
|
523
|
-
Deduplication is **off by default
|
|
523
|
+
Deduplication is **off by default**. Yoltra never silently drops legitimate rapid-fire identical
|
|
524
524
|
events (double-clicks, repeated `+1`). Opt in only when you actually want coalescing:
|
|
525
525
|
|
|
526
526
|
```typescript
|
|
@@ -533,7 +533,7 @@ const store = createStore({
|
|
|
533
533
|
dedupWindowMs: 100, // default: 0 (disabled)
|
|
534
534
|
});
|
|
535
535
|
|
|
536
|
-
// Identity-based: dedupe by an explicit key
|
|
536
|
+
// Identity-based: dedupe by an explicit key, e.g. a React Strict Mode double-invoke in an effect.
|
|
537
537
|
await store.emit("analytics", "pageView", { page }, { dedupKey: `pageView:${page}` });
|
|
538
538
|
```
|
|
539
539
|
|
|
@@ -541,8 +541,8 @@ await store.emit("analytics", "pageView", { page }, { dedupKey: `pageView:${page
|
|
|
541
541
|
|
|
542
542
|
## Cascade protection (on by default)
|
|
543
543
|
|
|
544
|
-
Two consumers wired into each other
|
|
545
|
-
two slices that answer each other's events
|
|
544
|
+
Two consumers wired into each other, whether a subscriber that emits what its own reducer answers or
|
|
545
|
+
two slices that answer each other's events, produce an event chain with no end. The reduce queue
|
|
546
546
|
drains **synchronously**, so that is not a slow program: it is a frozen tab, or a pinned core,
|
|
547
547
|
with no error and no stack to point at.
|
|
548
548
|
|
|
@@ -554,7 +554,7 @@ const store = createStore({
|
|
|
554
554
|
name: "app",
|
|
555
555
|
reducer: { ... },
|
|
556
556
|
|
|
557
|
-
// Defaults to 64. Bounded whether or not you configure it
|
|
557
|
+
// Defaults to 64. Bounded whether or not you configure it. A failure mode this bad
|
|
558
558
|
// should not require configuration to avoid. Set Infinity to opt out and own it.
|
|
559
559
|
maxReduceDepth: 64,
|
|
560
560
|
|
|
@@ -578,13 +578,13 @@ Both fields are **absent** on a root event rather than present as `0`/`undefined
|
|
|
578
578
|
application emits stay byte-identical to before this existed.
|
|
579
579
|
|
|
580
580
|
Breaching does not throw. The offending emit is refused, everything already committed stands, and
|
|
581
|
-
`onCascade` (plus a console error) names it
|
|
581
|
+
`onCascade` (plus a console error) names it. A throw would surface in whichever subscriber or
|
|
582
582
|
effect happened to be emitting, which is the same unattributable failure the ceiling exists to
|
|
583
583
|
prevent.
|
|
584
584
|
|
|
585
585
|
**A wide burst is not a cascade.** One event whose subscriber fans out to five hundred siblings
|
|
586
586
|
is a legitimate shape; depth is what separates it from a cycle, and a plain loop of `store.emit`
|
|
587
|
-
never accumulates depth at all
|
|
587
|
+
never accumulates depth at all, because each call drains to completion before the next, so every one is
|
|
588
588
|
a root. `maxTransitionsPerDrain` bounds burst *width* and is off by default for that reason; the
|
|
589
589
|
event that starts a drain is never refused by it.
|
|
590
590
|
|
|
@@ -639,13 +639,13 @@ if (import.meta.hot) {
|
|
|
639
639
|
|
|
640
640
|
### State is synchronous; `await` only for effects
|
|
641
641
|
|
|
642
|
-
The reduce phase is synchronous, so state reflects your event the moment `emit()` returns
|
|
642
|
+
The reduce phase is synchronous, so state reflects your event the moment `emit()` returns, with no
|
|
643
643
|
`await` needed to read it back. Await `emit()` when you also want _this event's_ effects to have
|
|
644
644
|
finished:
|
|
645
645
|
|
|
646
646
|
```typescript
|
|
647
647
|
emit("todo", "add", todo);
|
|
648
|
-
store.getState(); // Already reflects the new todo
|
|
648
|
+
store.getState(); // Already reflects the new todo. No await required
|
|
649
649
|
|
|
650
650
|
await emit("todo", "save", todo); // resolves once save's effects complete
|
|
651
651
|
```
|
|
@@ -749,7 +749,7 @@ that never happened.
|
|
|
749
749
|
|
|
750
750
|
**Nothing throws on boot.** A missing, unparseable or unmigratable payload falls back to your
|
|
751
751
|
declared defaults and reports through `onError`. A store that will not start because storage
|
|
752
|
-
holds stale JSON is worse than one that starts fresh
|
|
752
|
+
holds stale JSON is worse than one that starts fresh, and a full disk should not take down a
|
|
753
753
|
page, so write failures are reported the same way rather than raised.
|
|
754
754
|
|
|
755
755
|
**Version mismatches are refused, not trusted.** Reducers change, and a snapshot written
|
|
@@ -767,7 +767,7 @@ For a server render, `dehydrate(store, { version })` produces the payload and
|
|
|
767
767
|
## Lists that reorder
|
|
768
768
|
|
|
769
769
|
Path notification is positional for arrays. `items.0.title` names a *slot*, not a thing, so
|
|
770
|
-
`unshift`, `splice(0, 1)` and `sort` move nearly every element into a different slot
|
|
770
|
+
`unshift`, `splice(0, 1)` and `sort` move nearly every element into a different slot, and the
|
|
771
771
|
diff correctly reports that nearly every leaf changed. Inserting one row at the front of a
|
|
772
772
|
thousand wakes a thousand subscribers.
|
|
773
773
|
|
|
@@ -790,7 +790,7 @@ todos.idsPath; // "ids"
|
|
|
790
790
|
`entities.abc.title` survives insert, remove and reorder. A list container subscribes to `ids`
|
|
791
791
|
and reorders its children; rows subscribe to their own entity and stay asleep through a sort.
|
|
792
792
|
|
|
793
|
-
`ids` is still an array, so a reorder still reports `ids.0`, `ids.1` and so on
|
|
793
|
+
`ids` is still an array, so a reorder still reports `ids.0`, `ids.1` and so on. That cost is
|
|
794
794
|
confined, not removed. What you get is cost proportional to what actually changed.
|
|
795
795
|
|
|
796
796
|
For a small list that only ever grows at the end, `items.0.title` is fine and simpler. The
|
|
@@ -803,8 +803,8 @@ normalised, and the array reports roughly a thousand changed paths against two.
|
|
|
803
803
|
case the adapter is for.
|
|
804
804
|
|
|
805
805
|
A single-field update runs the other way: 20 µs for the array against 470 µs normalised.
|
|
806
|
-
`detectChangedProps` indexes an array but enumerates an object's keys
|
|
807
|
-
arrays and a `Set` per comparison
|
|
806
|
+
`detectChangedProps` indexes an array but enumerates an object's keys, building two key
|
|
807
|
+
arrays and a `Set` per comparison, so a wide entity map is more expensive to walk even when
|
|
808
808
|
almost nothing in it moved. The numbers are in `benchmarks/`, and closing that gap is tracked
|
|
809
809
|
work rather than a property of normalising as such.
|
|
810
810
|
|
|
@@ -813,24 +813,33 @@ individual fields edited is better off as an array today.
|
|
|
813
813
|
|
|
814
814
|
## Performance
|
|
815
815
|
|
|
816
|
-
| Metric | Value
|
|
817
|
-
| ------------------ |
|
|
818
|
-
| **Bundle size** |
|
|
819
|
-
| **Tree-shakeable** | Yes (ES modules)
|
|
820
|
-
| **Dependencies** | Zero
|
|
821
|
-
| **TypeScript** | Full type definitions included
|
|
816
|
+
| Metric | Value |
|
|
817
|
+
| ------------------ | -------------------------------------- |
|
|
818
|
+
| **Bundle size** | Measured every build, see table below |
|
|
819
|
+
| **Tree-shakeable** | Yes (ES modules) |
|
|
820
|
+
| **Dependencies** | Zero |
|
|
821
|
+
| **TypeScript** | Full type definitions included |
|
|
822
822
|
|
|
823
823
|
Bundle size is checked, not asserted: `rush size` bundles the package the way a consumer
|
|
824
|
-
would
|
|
825
|
-
`package.json`.
|
|
824
|
+
would (tree-shaken, minified, gzipped) and fails when it exceeds the budget declared in
|
|
825
|
+
`package.json`. The table below is written by that same check, so it cannot drift from what
|
|
826
|
+
was measured; editing it by hand fails CI.
|
|
826
827
|
|
|
827
828
|
The number that matters is what you import, not what the package exports:
|
|
828
829
|
|
|
829
|
-
|
|
830
|
-
|
|
|
831
|
-
|
|
|
832
|
-
| `{ createStore
|
|
833
|
-
|
|
|
830
|
+
<!-- size-table:start -->
|
|
831
|
+
| Import | Size | Budget |
|
|
832
|
+
| --- | --- | --- |
|
|
833
|
+
| `{ createStore }` | 8.3 KB | 14 KB |
|
|
834
|
+
| `{ createStore, hydrate, persist }` | 9.7 KB | 16 KB |
|
|
835
|
+
| everything | 11.2 KB | 18 KB |
|
|
836
|
+
<!-- size-table:end -->
|
|
837
|
+
|
|
838
|
+
These are **production** figures: what you ship once your bundler defines
|
|
839
|
+
`NODE_ENV=production` and the development-only guards drop out. The budget column is the
|
|
840
|
+
ceiling `rush size` enforces, and it is checked against a development build instead, which is
|
|
841
|
+
the larger of the two: dev-only code cannot grow unnoticed just because it never reaches a
|
|
842
|
+
user. So the headroom implied here is deliberately conservative.
|
|
834
843
|
|
|
835
844
|
The **gap between rows** is the tree-shaking claim, and it is what to watch: persistence adds
|
|
836
845
|
1.5 KB to the people who import it and nothing to anyone else, and the whole barrel is 2.9 KB
|
|
@@ -846,27 +855,27 @@ stops a runaway from hanging the tab.
|
|
|
846
855
|
|
|
847
856
|
## Documentation
|
|
848
857
|
|
|
849
|
-
- **[yoltra Root README](../../README.md)
|
|
858
|
+
- **[yoltra Root README](../../README.md)**: Overview and
|
|
850
859
|
quick start
|
|
851
|
-
- **[@yoltra/react](../react/README.md)
|
|
860
|
+
- **[@yoltra/react](../react/README.md)**:
|
|
852
861
|
React hooks and Suspense
|
|
853
|
-
- **[Quick Start Guide](https://github.com/yoltra/yoltra/blob/main/docs/en/QUICK_START_GUIDE.md)
|
|
854
|
-
|
|
855
|
-
- **[Event Queue Architecture](https://github.com/yoltra/yoltra/blob/main/docs/en/design/event-queue-architecture.md)
|
|
856
|
-
|
|
857
|
-
- **[Library Comparison](https://github.com/yoltra/yoltra/blob/main/docs/en/design/state-management-library-comparison.md)
|
|
858
|
-
|
|
862
|
+
- **[Quick Start Guide](https://github.com/yoltra/yoltra/blob/main/docs/en/QUICK_START_GUIDE.md)**:
|
|
863
|
+
Five steps to a working app
|
|
864
|
+
- **[Event Queue Architecture](https://github.com/yoltra/yoltra/blob/main/docs/en/design/event-queue-architecture.md)**:
|
|
865
|
+
Technical deep-dive
|
|
866
|
+
- **[Library Comparison](https://github.com/yoltra/yoltra/blob/main/docs/en/design/state-management-library-comparison.md)**:
|
|
867
|
+
Architectural comparison
|
|
859
868
|
|
|
860
869
|
---
|
|
861
870
|
|
|
862
871
|
## Examples
|
|
863
872
|
|
|
864
|
-
- **[Todo App](https://github.com/yoltra/yoltra/blob/main/examples/v0/yoltra-in-react)
|
|
873
|
+
- **[Todo App](https://github.com/yoltra/yoltra/blob/main/examples/v0/yoltra-in-react)**: Full
|
|
865
874
|
CRUD with performance profiling · [▶ Open the live demo](https://yoltra.dev/en/demos/in-react)
|
|
866
|
-
- **[Kinetic Logo](https://github.com/yoltra/yoltra/blob/main/examples/v0/yoltra-kinetic-logo)
|
|
867
|
-
|
|
868
|
-
- **[Next.js Integration](https://github.com/yoltra/yoltra/blob/main/examples/v0/yoltra-in-nextjs)
|
|
869
|
-
|
|
875
|
+
- **[Kinetic Logo](https://github.com/yoltra/yoltra/blob/main/examples/v0/yoltra-kinetic-logo)**:
|
|
876
|
+
3000 circles with physics simulation · [▶ Open the live demo](https://yoltra.dev/en/demos/kinetic-logo)
|
|
877
|
+
- **[Next.js Integration](https://github.com/yoltra/yoltra/blob/main/examples/v0/yoltra-in-nextjs)**:
|
|
878
|
+
Pages Router, client-side state + theme switcher · [▶ Open the live demo](https://yoltra.dev/en/demos/in-nextjs)
|
|
870
879
|
|
|
871
880
|
---
|
|
872
881
|
|
|
@@ -879,10 +888,10 @@ stops a runaway from hanging the tab.
|
|
|
879
888
|
|
|
880
889
|
## Status
|
|
881
890
|
|
|
882
|
-
**Release Candidate
|
|
891
|
+
**Release Candidate**. APIs are stable, used in production, minor changes possible before v1.0.
|
|
883
892
|
|
|
884
893
|
---
|
|
885
894
|
|
|
886
895
|
## License
|
|
887
896
|
|
|
888
|
-
**MIT
|
|
897
|
+
**MIT**. Free to use in commercial and open-source projects.
|
|
@@ -337,42 +337,6 @@ export declare class Store<EM extends EventMapBase, R extends string, S extends
|
|
|
337
337
|
* @internal
|
|
338
338
|
*/
|
|
339
339
|
private reportCascade;
|
|
340
|
-
/**
|
|
341
|
-
* Checks if an event matches a `When` matcher.
|
|
342
|
-
*
|
|
343
|
-
* @param when - The When matcher (or undefined for "all events").
|
|
344
|
-
* @param event - The event to check.
|
|
345
|
-
* @returns `true` if the event matches, `false` otherwise.
|
|
346
|
-
*
|
|
347
|
-
* @remarks
|
|
348
|
-
* - `undefined` or missing `when` matches ALL events.
|
|
349
|
-
* - `{ any: true }` matches ALL events.
|
|
350
|
-
* - `{ keys: [...] }` matches if event's `[channel, type]` is in the array.
|
|
351
|
-
* - `{ channel: 'x' }` matches if event's channel equals 'x'.
|
|
352
|
-
* - `{ channels: ['x', 'y'] }` matches if event's channel is in the array.
|
|
353
|
-
*
|
|
354
|
-
* @internal
|
|
355
|
-
*/
|
|
356
|
-
private matchesWhen;
|
|
357
|
-
/**
|
|
358
|
-
* Extracts the middleware function from a MiddlewareInput.
|
|
359
|
-
* Handles both raw functions (legacy) and MiddlewareSpec objects.
|
|
360
|
-
*
|
|
361
|
-
* @param input - MiddlewareInput (function or spec).
|
|
362
|
-
* @returns The middleware function.
|
|
363
|
-
*
|
|
364
|
-
* @internal
|
|
365
|
-
*/
|
|
366
|
-
private getMiddlewareFunction;
|
|
367
|
-
/**
|
|
368
|
-
* Gets the `when` matcher from a MiddlewareInput.
|
|
369
|
-
*
|
|
370
|
-
* @param input - MiddlewareInput (function or spec).
|
|
371
|
-
* @returns The `when` matcher, or `undefined` for raw functions (match all).
|
|
372
|
-
*
|
|
373
|
-
* @internal
|
|
374
|
-
*/
|
|
375
|
-
private getMiddlewareWhen;
|
|
376
340
|
/**
|
|
377
341
|
* Invokes all registered **effects** for a given event.
|
|
378
342
|
* Handles both key-based effects (O(1) lookup) and pattern-based effects (runtime matching).
|
|
@@ -942,13 +906,7 @@ export declare class Store<EM extends EventMapBase, R extends string, S extends
|
|
|
942
906
|
* the terminal event from ever being sent. Un-iterated progress therefore buffers to
|
|
943
907
|
* `highWaterMark` and is then counted on {@link CallHandle.dropped} rather than blocking.
|
|
944
908
|
*
|
|
945
|
-
* **This is a local primitive.**
|
|
946
|
-
* envelope carries neither `meta` nor `parentId`, and ingress namespaces the channel, so
|
|
947
|
-
* neither correlation nor the reply route survives the hop. That is not an oversight to route
|
|
948
|
-
* around — federation answers cross-node request/reply with typed peer *queries*, which are
|
|
949
|
-
* gated by a responder policy that may concede or deny. A call that federated silently would
|
|
950
|
-
* turn that access decision into an accident of which channel someone named. Ask a peer with a
|
|
951
|
-
* query; use `call` within a process.
|
|
909
|
+
* **This is a local primitive.**
|
|
952
910
|
*
|
|
953
911
|
* @example Timeout is idle, not total
|
|
954
912
|
* ```ts
|
|
@@ -1086,15 +1044,6 @@ export declare class Store<EM extends EventMapBase, R extends string, S extends
|
|
|
1086
1044
|
* @internal
|
|
1087
1045
|
*/
|
|
1088
1046
|
private unmountSlice;
|
|
1089
|
-
/**
|
|
1090
|
-
* Normalizes event targeting from `when` to an array of EventKeys.
|
|
1091
|
-
*
|
|
1092
|
-
* @param spec - Object with an optional `when` matcher.
|
|
1093
|
-
* @returns Array of `[channel, type]` pairs.
|
|
1094
|
-
*
|
|
1095
|
-
* @internal
|
|
1096
|
-
*/
|
|
1097
|
-
private normalizeEventKeys;
|
|
1098
1047
|
/**
|
|
1099
1048
|
* Reads a dotted path from an object (supports numeric array indices via string keys).
|
|
1100
1049
|
*
|
|
@@ -1102,6 +1051,10 @@ export declare class Store<EM extends EventMapBase, R extends string, S extends
|
|
|
1102
1051
|
* @param path - Dotted path; leading dot is ignored.
|
|
1103
1052
|
* @returns The value at the path, or `undefined`.
|
|
1104
1053
|
*
|
|
1054
|
+
* @remarks
|
|
1055
|
+
* A member rather than a bare import: a test replaces this on the instance to count how many
|
|
1056
|
+
* walks describing a change costs, which only works while the callers go through `this`.
|
|
1057
|
+
*
|
|
1105
1058
|
* @internal
|
|
1106
1059
|
*/
|
|
1107
1060
|
private getAtPath;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { EventKey, EventMapBase, EventUnion, MiddlewareFunction, MiddlewareInput, When } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Checks if an event matches a `When` matcher.
|
|
4
|
+
*
|
|
5
|
+
* @param when - The When matcher (or undefined for "all events").
|
|
6
|
+
* @param event - The event to check.
|
|
7
|
+
* @returns `true` if the event matches, `false` otherwise.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* - `undefined` or missing `when` matches ALL events.
|
|
11
|
+
* - `{ any: true }` matches ALL events.
|
|
12
|
+
* - `{ keys: [...] }` matches if event's `[channel, type]` is in the array.
|
|
13
|
+
* - `{ channel: 'x' }` matches if event's channel equals 'x'.
|
|
14
|
+
* - `{ channels: ['x', 'y'] }` matches if event's channel is in the array.
|
|
15
|
+
*
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export declare function matchesWhen<EM extends EventMapBase>(when: When<EM> | undefined, event: EventUnion<EM>): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Extracts the middleware function from a MiddlewareInput.
|
|
21
|
+
* Handles both raw functions (legacy) and MiddlewareSpec objects.
|
|
22
|
+
*
|
|
23
|
+
* @param input - MiddlewareInput (function or spec).
|
|
24
|
+
* @returns The middleware function.
|
|
25
|
+
*
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
export declare function getMiddlewareFunction<St, EM extends EventMapBase>(input: MiddlewareInput<St, EM>): MiddlewareFunction<St, EM>;
|
|
29
|
+
/**
|
|
30
|
+
* Gets the `when` matcher from a MiddlewareInput.
|
|
31
|
+
*
|
|
32
|
+
* @param input - MiddlewareInput (function or spec).
|
|
33
|
+
* @returns The `when` matcher, or `undefined` for raw functions (match all).
|
|
34
|
+
*
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
export declare function getMiddlewareWhen<St, EM extends EventMapBase>(input: MiddlewareInput<St, EM>): When<EM> | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Normalizes event targeting from `when` to an array of EventKeys.
|
|
40
|
+
*
|
|
41
|
+
* @param spec - Object with an optional `when` matcher.
|
|
42
|
+
* @returns Array of `[channel, type]` pairs.
|
|
43
|
+
*
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
46
|
+
export declare function normalizeEventKeys<EM extends EventMapBase>(spec: {
|
|
47
|
+
when?: When<EM>;
|
|
48
|
+
events?: ReadonlyArray<EventKey<EM>>;
|
|
49
|
+
}): ReadonlyArray<EventKey<EM>>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading and expanding dotted state paths.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Moved out of `Store.ts` unchanged. Neither function touched an instance field.
|
|
6
|
+
*
|
|
7
|
+
* `Store` still exposes both as members, and deliberately so. `Store.buildAncestorPaths` is
|
|
8
|
+
* public API that appears in the committed reference, and `getAtPath` is replaced on the
|
|
9
|
+
* instance by a test that counts the walks a change description costs, so the internal callers
|
|
10
|
+
* have to keep reaching it through `this`.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Reads a dotted path from an object (supports numeric array indices via string keys).
|
|
16
|
+
*
|
|
17
|
+
* @param obj - Root object (slice or value).
|
|
18
|
+
* @param path - Dotted path; leading dot is ignored.
|
|
19
|
+
* @returns The value at the path, or `undefined`.
|
|
20
|
+
*
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export declare function getAtPath(obj: any, path: string): any;
|
|
24
|
+
/**
|
|
25
|
+
* Builds ancestor paths for a dotted path.
|
|
26
|
+
*
|
|
27
|
+
* For `"a.b.c"`, returns `["a", "a.b", "a.b.c"]`. Leading dots are trimmed.
|
|
28
|
+
*
|
|
29
|
+
* @param path - Dotted path string.
|
|
30
|
+
* @returns Array of ancestor paths.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* buildAncestorPaths('x.y.z'); // ['x','x.y','x.y.z']
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @public
|
|
38
|
+
*/
|
|
39
|
+
export declare function buildAncestorPaths(path: string): string[];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { DeepReadonly, EffectSpec, EmitOptions, EmitResult, EventMapBase, EventUnion } from '../types.js';
|
|
2
|
+
import { CallHandle, CallOptions } from './call.js';
|
|
3
|
+
/**
|
|
4
|
+
* What `performCall` needs from the store.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* Three members, named rather than structural over the whole class, because three is few enough
|
|
8
|
+
* that naming them documents the coupling instead of hiding it.
|
|
9
|
+
*/
|
|
10
|
+
export interface CallDeps<St, EM extends EventMapBase> {
|
|
11
|
+
readonly idFactory: () => string;
|
|
12
|
+
readonly registerEffect: (spec: EffectSpec<DeepReadonly<St>, EM>) => () => void;
|
|
13
|
+
readonly emit: <C extends keyof EM & string, T extends keyof EM[C] & string>(channel: C, type: T, payload: EM[C][T], opts?: EmitOptions) => Promise<EmitResult>;
|
|
14
|
+
}
|
|
15
|
+
export declare function performCall<St, EM extends EventMapBase, C extends keyof EM & string, T extends keyof EM[C] & string>(deps: CallDeps<St, EM>, channel: C, type: T, payload: EM[C][T], opts: CallOptions<EM>): CallHandle<EventUnion<EM>, EventUnion<EM>>;
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @remarks
|
|
8
8
|
* `Symbol.for` rather than `Symbol()`, so the brand survives two copies of this package meeting
|
|
9
|
-
* at runtime — a duplicated dependency, a
|
|
10
|
-
* minor. With a unique symbol the check would silently answer `false` across that boundary and a
|
|
9
|
+
* at runtime — a duplicated dependency, a bundle that inlined a second copy, a consumer that
|
|
10
|
+
* pinned an older minor. With a unique symbol the check would silently answer `false` across that boundary and a
|
|
11
11
|
* refusal would read as ordinary state, which is the failure this whole feature exists to end.
|
|
12
12
|
*
|
|
13
13
|
* @internal
|