@yoltra/core 0.4.0 → 0.6.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 +214 -7
- package/README.md +315 -13
- package/dist/types/eventBus/EventBus.d.ts +1 -1
- package/dist/types/eventBus/index.d.ts +2 -2
- package/dist/types/index.d.ts +21 -16
- package/dist/types/persistence/adapters.d.ts +1 -1
- package/dist/types/persistence/persist.d.ts +3 -6
- package/dist/types/reducer/Reducer.d.ts +4 -3
- package/dist/types/store/Store.d.ts +252 -17
- package/dist/types/store/call.d.ts +149 -0
- package/dist/types/store/callQueue.d.ts +79 -0
- package/dist/types/store/rejection.d.ts +58 -0
- package/dist/types/types.d.ts +279 -14
- package/dist/types/utils/detectChangedProps.d.ts +6 -0
- package/dist/types/utils/immutability.d.ts +1 -1
- package/dist/types/utils/index.d.ts +2 -2
- package/dist/yoltra.cjs +11 -0
- package/dist/yoltra.cjs.map +1 -0
- package/dist/yoltra.mjs +2902 -0
- package/dist/yoltra.mjs.map +1 -0
- package/dist/yoltra.umd.js +2 -2
- package/dist/yoltra.umd.js.map +1 -0
- package/package.json +21 -21
- package/dist/yoltra.cjs.js +0 -11
- package/dist/yoltra.esm.js +0 -2374
package/dist/yoltra.mjs
ADDED
|
@@ -0,0 +1,2902 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* @yoltra/core v0.6.0
|
|
3
|
+
* (c) 2026 Manu Ramirez <@pixerael>
|
|
4
|
+
* License: MIT
|
|
5
|
+
* Homepage: https://yoltra.dev
|
|
6
|
+
*
|
|
7
|
+
* This source code is licensed under the MIT license found in the
|
|
8
|
+
* LICENSE file in the root directory of this source tree
|
|
9
|
+
*/
|
|
10
|
+
var K = Object.defineProperty;
|
|
11
|
+
var F = (c, e, t) => e in c ? K(c, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : c[e] = t;
|
|
12
|
+
var p = (c, e, t) => F(c, typeof e != "symbol" ? e + "" : e, t);
|
|
13
|
+
class U {
|
|
14
|
+
constructor() {
|
|
15
|
+
/**
|
|
16
|
+
* Internal registry: `channel → type → Set<handler>`.
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
p(this, "handlers", /* @__PURE__ */ new Map());
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Subscribes a handler to an exact `(channel, type)`.
|
|
23
|
+
*
|
|
24
|
+
* @typeParam C - Channel key (must be a string key of `EM`).
|
|
25
|
+
* @typeParam T - Type key within channel `C` (must be a string key of `EM[C]`).
|
|
26
|
+
* @param channel - Channel name to subscribe to.
|
|
27
|
+
* @param type - Event type within the channel.
|
|
28
|
+
* @param handler - Function invoked with the payload type `EM[C][T]`. It optionally
|
|
29
|
+
* receives the **source event** as a second argument when the emitter supplies one, so
|
|
30
|
+
* subscribers can read the true `id` (and any `meta`) instead of reconstructing an event
|
|
31
|
+
* from the payload alone. Handlers that declare only `payload` remain valid.
|
|
32
|
+
* @returns An **unsubscribe** function that removes this handler.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* const off = bus.on('data', 'loaded', ({ items }) => {
|
|
37
|
+
* console.log('Loaded', items.length, 'items');
|
|
38
|
+
* });
|
|
39
|
+
*
|
|
40
|
+
* // Later, stop listening:
|
|
41
|
+
* off();
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* @example Reading the source event
|
|
45
|
+
* ```ts
|
|
46
|
+
* bus.on('data', 'loaded', (payload, event) => {
|
|
47
|
+
* console.log('event id:', event?.id);
|
|
48
|
+
* });
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @public
|
|
52
|
+
*/
|
|
53
|
+
on(e, t, s) {
|
|
54
|
+
let n = this.handlers.get(e);
|
|
55
|
+
n || (n = /* @__PURE__ */ new Map(), this.handlers.set(e, n));
|
|
56
|
+
let r = n.get(t);
|
|
57
|
+
return r || (r = /* @__PURE__ */ new Set(), n.set(t, r)), r.add(s), () => this.off(e, t, s);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Removes a specific handler previously added with {@link EventBus.on | `on`}.
|
|
61
|
+
*
|
|
62
|
+
* @typeParam C - Channel key (string key of `EM`).
|
|
63
|
+
* @typeParam T - Type key within channel `C` (string key of `EM[C]`).
|
|
64
|
+
* @param channel - Channel name of the subscription to remove.
|
|
65
|
+
* @param type - Event type of the subscription to remove.
|
|
66
|
+
* @param handler - The same handler reference that was passed to `on`.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* const h = (n: number) => console.log('inc', n);
|
|
71
|
+
* bus.on('math', 'inc', h);
|
|
72
|
+
*
|
|
73
|
+
* // Explicitly remove this handler:
|
|
74
|
+
* bus.off('math', 'inc', h);
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* @public
|
|
78
|
+
*/
|
|
79
|
+
off(e, t, s) {
|
|
80
|
+
const n = this.handlers.get(e);
|
|
81
|
+
if (!n) return;
|
|
82
|
+
const r = n.get(t);
|
|
83
|
+
r && (r.delete(s), r.size === 0 && n.delete(t), n.size === 0 && this.handlers.delete(e));
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Emits an event to all subscribers of the exact `(channel, type)`.
|
|
87
|
+
*
|
|
88
|
+
* Handlers are invoked **synchronously**. Any exception thrown by a handler is
|
|
89
|
+
* caught and logged, and other handlers still run.
|
|
90
|
+
*
|
|
91
|
+
* @typeParam C - Channel key (string key of `EM`).
|
|
92
|
+
* @typeParam T - Type key within channel `C` (string key of `EM[C]`).
|
|
93
|
+
* @param channel - Channel name to emit on.
|
|
94
|
+
* @param type - Event type to emit.
|
|
95
|
+
* @param payload - Payload matching `EM[C][T]`.
|
|
96
|
+
* @param event - Optional **source event**, forwarded to handlers as a second argument.
|
|
97
|
+
* Supply it whenever the caller already holds the real event so subscribers observe its
|
|
98
|
+
* true `id` rather than reconstructing one; omitting it keeps the original behaviour.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* bus.emit('ui', 'toggle', false);
|
|
103
|
+
* ```
|
|
104
|
+
*
|
|
105
|
+
* @public
|
|
106
|
+
*/
|
|
107
|
+
emit(e, t, s, n) {
|
|
108
|
+
const r = this.handlers.get(e);
|
|
109
|
+
if (!r) return;
|
|
110
|
+
const o = r.get(t);
|
|
111
|
+
if (!(!o || o.size === 0))
|
|
112
|
+
for (const l of [...o])
|
|
113
|
+
try {
|
|
114
|
+
l(s, n);
|
|
115
|
+
} catch (i) {
|
|
116
|
+
console.error("EventBus handler error:", i);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Clears **all** listeners across all channels/types.
|
|
121
|
+
*
|
|
122
|
+
* Useful for tests or during HMR teardown to avoid duplicate handlers.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```ts
|
|
126
|
+
* // In a test teardown:
|
|
127
|
+
* afterEach(() => bus.clear());
|
|
128
|
+
* ```
|
|
129
|
+
*
|
|
130
|
+
* @public
|
|
131
|
+
*/
|
|
132
|
+
clear() {
|
|
133
|
+
this.handlers.clear();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
class L {
|
|
137
|
+
constructor() {
|
|
138
|
+
/**
|
|
139
|
+
* Exact handlers: `channel → type → [handlers]`.
|
|
140
|
+
* @internal
|
|
141
|
+
*/
|
|
142
|
+
p(this, "handlers", /* @__PURE__ */ new Map());
|
|
143
|
+
/**
|
|
144
|
+
* Pattern handlers with `*` and `**`: `channel → pattern(string) → [handlers]`.
|
|
145
|
+
* @internal
|
|
146
|
+
*/
|
|
147
|
+
p(this, "patternHandlers", /* @__PURE__ */ new Map());
|
|
148
|
+
/**
|
|
149
|
+
* Patterns bucketed by their first segment, so an emit tests only what could match.
|
|
150
|
+
*
|
|
151
|
+
* @remarks
|
|
152
|
+
* Delivery used to walk every pattern registered on the channel and run the full segment
|
|
153
|
+
* matcher against each. That is linear in the number of patterns rather than in the number
|
|
154
|
+
* that match, and it re-split both the pattern and the subject on every test — for a thousand
|
|
155
|
+
* patterns, two thousand string splits to deliver one event.
|
|
156
|
+
*
|
|
157
|
+
* A subject's first segment can only be matched by a pattern whose first segment is that same
|
|
158
|
+
* literal, or is `*` or `**`. Bucketing on that turns the common shape — distinct event
|
|
159
|
+
* families like `panel.*` and `order.**` — from a scan of everything into a map lookup plus
|
|
160
|
+
* the handful that begin with a wildcard.
|
|
161
|
+
*
|
|
162
|
+
* It buys nothing for a channel where every pattern starts with `**`, since all of those must
|
|
163
|
+
* still be tested. That is the honest worst case, and it is unchanged rather than worsened.
|
|
164
|
+
*/
|
|
165
|
+
p(this, "patternIndex", /* @__PURE__ */ new Map());
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Subscribes a handler to either an **exact** type or a **pattern**.
|
|
169
|
+
*
|
|
170
|
+
* @param channel - Channel to subscribe on.
|
|
171
|
+
* @param type - Exact event type (e.g. `"a.b"`) or pattern (contains `*`/`**`).
|
|
172
|
+
* @param handler - Function invoked with the emitted payload.
|
|
173
|
+
* @returns An **unsubscribe** function that removes this handler.
|
|
174
|
+
*
|
|
175
|
+
* @remarks
|
|
176
|
+
* - Exact subscriptions are stored under a **normalized** key (leading `.` removed).
|
|
177
|
+
* - Pattern subscriptions are stored **as provided**; matching normalizes the subject.
|
|
178
|
+
*
|
|
179
|
+
* @example Exact subscription
|
|
180
|
+
* ```ts
|
|
181
|
+
* const off = bus.on('data', 'items.loaded', ({ count }) => {
|
|
182
|
+
* console.log('Loaded', count);
|
|
183
|
+
* });
|
|
184
|
+
* // Later
|
|
185
|
+
* off();
|
|
186
|
+
* ```
|
|
187
|
+
*
|
|
188
|
+
* @example Pattern subscription
|
|
189
|
+
* ```ts
|
|
190
|
+
* // Match any single sub-event: 'panel.open', 'panel.close', etc.
|
|
191
|
+
* const offStar = bus.on('ui', 'panel.*', () => {});
|
|
192
|
+
*
|
|
193
|
+
* // Match any depth: 'panel.open', 'panel.items.add', 'panel', etc.
|
|
194
|
+
* const offGlob = bus.on('ui', 'panel.**', () => {});
|
|
195
|
+
* ```
|
|
196
|
+
*
|
|
197
|
+
* @public
|
|
198
|
+
*/
|
|
199
|
+
on(e, t, s) {
|
|
200
|
+
const n = String(t);
|
|
201
|
+
if (this.isPattern(n)) {
|
|
202
|
+
const r = n;
|
|
203
|
+
this.patternHandlers.has(e) || this.patternHandlers.set(e, /* @__PURE__ */ new Map());
|
|
204
|
+
const o = this.patternHandlers.get(e);
|
|
205
|
+
return o.has(r) || (o.set(r, []), this.indexPattern(e, r)), o.get(r).push(s), () => this.offPattern(e, r, s);
|
|
206
|
+
} else {
|
|
207
|
+
const r = this.normalizeTypeKey(n);
|
|
208
|
+
this.handlers.has(e) || this.handlers.set(e, /* @__PURE__ */ new Map());
|
|
209
|
+
const o = this.handlers.get(e);
|
|
210
|
+
return o.has(r) || o.set(r, []), o.get(r).push(s), () => this.offExactNormalized(e, r, s);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Unsubscribes an **exact** handler. The `type` key is normalized internally,
|
|
215
|
+
* so callers can pass `"foo"` or `".foo"` interchangeably.
|
|
216
|
+
*
|
|
217
|
+
* @param channel - Channel name.
|
|
218
|
+
* @param type - Exact event type key to remove (normalization applied).
|
|
219
|
+
* @param handler - The same handler reference previously passed to {@link LooseEventBus.on | `on`}.
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```ts
|
|
223
|
+
* const h = () => {};
|
|
224
|
+
* bus.on('ui', 'panel.open', h);
|
|
225
|
+
* // Remove it (with or without leading dot)
|
|
226
|
+
* bus.off('ui', '.panel.open', h);
|
|
227
|
+
* ```
|
|
228
|
+
*
|
|
229
|
+
* @public
|
|
230
|
+
*/
|
|
231
|
+
off(e, t, s) {
|
|
232
|
+
const n = this.normalizeTypeKey(String(t));
|
|
233
|
+
this.offExactNormalized(e, n, s);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Internal exact unsubscription using an already **normalized** type key.
|
|
237
|
+
*
|
|
238
|
+
* @param channel - Channel name.
|
|
239
|
+
* @param normalizedType - Event type key with leading dot removed.
|
|
240
|
+
* @param handler - Handler to remove.
|
|
241
|
+
* @internal
|
|
242
|
+
*/
|
|
243
|
+
offExactNormalized(e, t, s) {
|
|
244
|
+
const n = this.handlers.get(e);
|
|
245
|
+
if (!n) return;
|
|
246
|
+
const r = n.get(t);
|
|
247
|
+
if (!r) return;
|
|
248
|
+
const o = r.indexOf(s);
|
|
249
|
+
o !== -1 && r.splice(o, 1), r.length === 0 && n.delete(t), n.size === 0 && this.handlers.delete(e);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Internal removal for a **pattern** subscription. No-ops if missing.
|
|
253
|
+
*
|
|
254
|
+
* @param channel - Channel name.
|
|
255
|
+
* @param pattern - Pattern string as originally subscribed.
|
|
256
|
+
* @param handler - Handler to remove.
|
|
257
|
+
* @internal
|
|
258
|
+
*/
|
|
259
|
+
offPattern(e, t, s) {
|
|
260
|
+
const n = this.patternHandlers.get(e);
|
|
261
|
+
if (!n) return;
|
|
262
|
+
const r = n.get(t);
|
|
263
|
+
if (!r) return;
|
|
264
|
+
const o = r.indexOf(s);
|
|
265
|
+
o !== -1 && r.splice(o, 1), r.length === 0 && (n.delete(t), this.unindexPattern(e, t)), n.size === 0 && (this.patternHandlers.delete(e), this.patternIndex.delete(e));
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Emits an event to all exact subscribers first, then to **matching pattern** subscribers.
|
|
269
|
+
* Duplicate handler references are called **once** (de-duped).
|
|
270
|
+
*
|
|
271
|
+
* @param channel - Channel to emit on.
|
|
272
|
+
* @param type - Event type (subject). A leading dot is ignored for matching.
|
|
273
|
+
* @param payload - Payload delivered to handlers.
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* ```ts
|
|
277
|
+
* // Suppose:
|
|
278
|
+
* // - on('ui', 'panel.open', h)
|
|
279
|
+
* // - on('ui', 'panel.*', h) // same handler ref!
|
|
280
|
+
* // - on('ui', 'panel.**', other)
|
|
281
|
+
* bus.emit('ui', 'panel.open', { id: 1 });
|
|
282
|
+
* // => 'h' runs once (de-duped), then 'other'
|
|
283
|
+
* ```
|
|
284
|
+
*
|
|
285
|
+
* @public
|
|
286
|
+
*/
|
|
287
|
+
emit(e, t, s) {
|
|
288
|
+
const n = String(t), r = this.normalizeTypeKey(n), o = this.handlers.get(e)?.get(r) ?? [], l = this.matchingPatternHandlers(e, n), i = /* @__PURE__ */ new Set(), a = (d) => {
|
|
289
|
+
for (const u of [...d])
|
|
290
|
+
if (!i.has(u)) {
|
|
291
|
+
i.add(u);
|
|
292
|
+
try {
|
|
293
|
+
u(s);
|
|
294
|
+
} catch (f) {
|
|
295
|
+
console.error(f);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
a(o);
|
|
301
|
+
for (const d of l) a(d);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Emits a payload that is only built if somebody is listening.
|
|
305
|
+
*
|
|
306
|
+
* @param channel - Channel to emit on.
|
|
307
|
+
* @param type - Concrete event type.
|
|
308
|
+
* @param make - Builds the payload. Called at most once, and only when a handler matched.
|
|
309
|
+
*
|
|
310
|
+
* @remarks
|
|
311
|
+
* Same matching as {@link LooseEventBus.emit}; the difference is *when* the payload exists.
|
|
312
|
+
* The store's change notification carries the old and new value at a path, and reading those
|
|
313
|
+
* means walking the state tree twice per path. Doing that eagerly meant a slice nobody had
|
|
314
|
+
* subscribed to paid the full cost of describing changes to an audience of nobody — the
|
|
315
|
+
* matching work was already being done to discover there were no handlers.
|
|
316
|
+
*
|
|
317
|
+
* @public
|
|
318
|
+
*/
|
|
319
|
+
emitWith(e, t, s) {
|
|
320
|
+
const n = String(t), r = this.normalizeTypeKey(n), o = this.handlers.get(e)?.get(r) ?? [], l = this.matchingPatternHandlers(e, n);
|
|
321
|
+
if (o.length === 0 && l.length === 0) return;
|
|
322
|
+
const i = s(), a = /* @__PURE__ */ new Set(), d = (u) => {
|
|
323
|
+
for (const f of [...u])
|
|
324
|
+
if (!a.has(f)) {
|
|
325
|
+
a.add(f);
|
|
326
|
+
try {
|
|
327
|
+
f(i);
|
|
328
|
+
} catch (m) {
|
|
329
|
+
console.error(m);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
d(o);
|
|
335
|
+
for (const u of l) d(u);
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Determines if a string is a **pattern** (contains `*`).
|
|
339
|
+
* @param s - Event type or pattern string.
|
|
340
|
+
* @returns `true` if it contains at least one `*`, else `false`.
|
|
341
|
+
* @internal
|
|
342
|
+
*/
|
|
343
|
+
isPattern(e) {
|
|
344
|
+
return e.includes("*");
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Normalizes event type keys for exact matching by stripping a **single** leading dot.
|
|
348
|
+
*
|
|
349
|
+
* @param s - Event type key.
|
|
350
|
+
* @returns Normalized key without a leading dot.
|
|
351
|
+
* @example
|
|
352
|
+
* ```ts
|
|
353
|
+
* normalizeTypeKey('.a.b') // 'a.b'
|
|
354
|
+
* normalizeTypeKey('a.b') // 'a.b'
|
|
355
|
+
* ```
|
|
356
|
+
* @internal
|
|
357
|
+
*/
|
|
358
|
+
normalizeTypeKey(e) {
|
|
359
|
+
return e.replace(/^\./, "");
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Splits a path into dot-separated segments after normalization and removes empties.
|
|
363
|
+
* @param p - Event type or pattern string.
|
|
364
|
+
* @internal
|
|
365
|
+
*/
|
|
366
|
+
splitPath(e) {
|
|
367
|
+
return this.normalizeTypeKey(e).split(".").filter(Boolean);
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Files a pattern under the first segment that could select it.
|
|
371
|
+
* @internal
|
|
372
|
+
*/
|
|
373
|
+
indexPattern(e, t) {
|
|
374
|
+
let s = this.patternIndex.get(e);
|
|
375
|
+
s === void 0 && (s = { byHead: /* @__PURE__ */ new Map(), anyHead: [] }, this.patternIndex.set(e, s));
|
|
376
|
+
const n = this.splitPath(t), r = { pattern: t, segments: n }, o = n[0];
|
|
377
|
+
if (o === void 0 || o === "*" || o === "**") {
|
|
378
|
+
s.anyHead.push(r);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const l = s.byHead.get(o);
|
|
382
|
+
l === void 0 ? s.byHead.set(o, [r]) : l.push(r);
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Removes a pattern from the index. Paired with {@link LooseEventBus.offPattern}.
|
|
386
|
+
* @internal
|
|
387
|
+
*/
|
|
388
|
+
unindexPattern(e, t) {
|
|
389
|
+
const s = this.patternIndex.get(e);
|
|
390
|
+
if (s === void 0) return;
|
|
391
|
+
const n = this.splitPath(t)[0], r = n === void 0 || n === "*" || n === "**" ? s.anyHead : s.byHead.get(n);
|
|
392
|
+
if (r === void 0) return;
|
|
393
|
+
const o = r.findIndex((l) => l.pattern === t);
|
|
394
|
+
o !== -1 && r.splice(o, 1), r.length === 0 && r !== s.anyHead && n !== void 0 && s.byHead.delete(n);
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* The handler lists of every pattern matching this subject.
|
|
398
|
+
*
|
|
399
|
+
* @remarks
|
|
400
|
+
* Shared by `emit` and `emitWith` so the two cannot drift on what "matching" means — which
|
|
401
|
+
* they could, being two copies of the same walk before.
|
|
402
|
+
*
|
|
403
|
+
* The subject is split once here rather than once per pattern tested.
|
|
404
|
+
*
|
|
405
|
+
* @internal
|
|
406
|
+
*/
|
|
407
|
+
matchingPatternHandlers(e, t) {
|
|
408
|
+
const s = this.patternHandlers.get(e), n = this.patternIndex.get(e);
|
|
409
|
+
if (s === void 0 || s.size === 0 || n === void 0) return [];
|
|
410
|
+
const r = this.splitPath(t), o = [], l = (a) => {
|
|
411
|
+
for (const d of a) {
|
|
412
|
+
if (!this.matchSegments(d.segments, r)) continue;
|
|
413
|
+
const u = s.get(d.pattern);
|
|
414
|
+
u !== void 0 && o.push(u);
|
|
415
|
+
}
|
|
416
|
+
}, i = r[0];
|
|
417
|
+
if (i !== void 0) {
|
|
418
|
+
const a = n.byHead.get(i);
|
|
419
|
+
a !== void 0 && l(a);
|
|
420
|
+
}
|
|
421
|
+
return l(n.anyHead), o;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Pattern matcher over dot-separated segments, which arrive already split.
|
|
425
|
+
*
|
|
426
|
+
* Rules:
|
|
427
|
+
* - **literal**: exact match.
|
|
428
|
+
* - `*` : matches exactly **one** segment.
|
|
429
|
+
* - `**` : matches **zero or more** remaining segments (including empty).
|
|
430
|
+
*
|
|
431
|
+
* @remarks
|
|
432
|
+
* Takes segments rather than strings so delivery can split each pattern once at registration
|
|
433
|
+
* and the subject once per emit, instead of both once per test. Re-splitting per test was most
|
|
434
|
+
* of what made wildcard delivery expensive: a thousand patterns meant two thousand string
|
|
435
|
+
* splits to deliver one event.
|
|
436
|
+
*
|
|
437
|
+
* @param pSegs - Pattern segments (may include `*`/`**`).
|
|
438
|
+
* @param sSegs - Subject segments to test.
|
|
439
|
+
* @returns `true` if the pattern matches; otherwise `false`.
|
|
440
|
+
*
|
|
441
|
+
* @example
|
|
442
|
+
* ```ts
|
|
443
|
+
* matchSegments(['a', '*'], ['a', 'b']) // true
|
|
444
|
+
* matchSegments(['a', '*'], ['a', 'b', 'c']) // false
|
|
445
|
+
* matchSegments(['a', '**'], ['a']) // true
|
|
446
|
+
* matchSegments(['**', 'end'], ['x', 'y', 'end']) // true
|
|
447
|
+
* ```
|
|
448
|
+
*
|
|
449
|
+
* @internal
|
|
450
|
+
*/
|
|
451
|
+
matchSegments(e, t) {
|
|
452
|
+
let s = 0, n = 0, r = -1, o = 0;
|
|
453
|
+
for (; n < t.length; )
|
|
454
|
+
if (s < e.length && (e[s] === "*" || e[s] === t[n]))
|
|
455
|
+
s++, n++;
|
|
456
|
+
else if (s < e.length && e[s] === "**")
|
|
457
|
+
r = s, o = n, s++;
|
|
458
|
+
else if (r !== -1)
|
|
459
|
+
s = r + 1, n = ++o;
|
|
460
|
+
else
|
|
461
|
+
return !1;
|
|
462
|
+
for (; s < e.length && e[s] === "**"; ) s++;
|
|
463
|
+
return s === e.length;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Removes **all** listeners (exact and pattern). Useful for tests/HMR teardown.
|
|
467
|
+
*
|
|
468
|
+
* @example
|
|
469
|
+
* ```ts
|
|
470
|
+
* afterEach(() => bus.clear());
|
|
471
|
+
* ```
|
|
472
|
+
*
|
|
473
|
+
* @public
|
|
474
|
+
*/
|
|
475
|
+
clear() {
|
|
476
|
+
this.handlers.clear(), this.patternHandlers.clear(), this.patternIndex.clear();
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Returns a snapshot of all registered subscriptions for DevTools introspection.
|
|
480
|
+
*
|
|
481
|
+
* @returns An array of `{ channel, type, count }` entries for each distinct
|
|
482
|
+
* (channel, type/pattern) pair with at least one handler.
|
|
483
|
+
*
|
|
484
|
+
* @internal
|
|
485
|
+
*/
|
|
486
|
+
__introspect() {
|
|
487
|
+
const e = [];
|
|
488
|
+
for (const [t, s] of this.handlers)
|
|
489
|
+
for (const [n, r] of s)
|
|
490
|
+
r.length > 0 && e.push({ channel: t, type: n, count: r.length });
|
|
491
|
+
for (const [t, s] of this.patternHandlers)
|
|
492
|
+
for (const [n, r] of s)
|
|
493
|
+
r.length > 0 && e.push({ channel: t, type: n, count: r.length });
|
|
494
|
+
return e;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
class V {
|
|
498
|
+
/**
|
|
499
|
+
* Creates a new {@link Reducer} from a pure reducer function.
|
|
500
|
+
*
|
|
501
|
+
* @param reduce - A function `(state, event) => nextState` that implements your update logic.
|
|
502
|
+
*
|
|
503
|
+
* @example
|
|
504
|
+
* ```ts
|
|
505
|
+
* const reducer = new Reducer<MyState, MyEM>((state, event) => {
|
|
506
|
+
* // implement your transitions here
|
|
507
|
+
* return state;
|
|
508
|
+
* });
|
|
509
|
+
* ```
|
|
510
|
+
*
|
|
511
|
+
* @public
|
|
512
|
+
*/
|
|
513
|
+
constructor(e) {
|
|
514
|
+
/**
|
|
515
|
+
* The underlying pure reducer function.
|
|
516
|
+
* @internal
|
|
517
|
+
*/
|
|
518
|
+
p(this, "_reduce");
|
|
519
|
+
this._reduce = e;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Applies the reducer to produce the next state.
|
|
523
|
+
*
|
|
524
|
+
* @param state - Current state.
|
|
525
|
+
* @param event - An event drawn from {@link EventUnion | `EventUnion<EM>`}.
|
|
526
|
+
* @returns The next state, or a {@link Rejection} if the reducer refused the write.
|
|
527
|
+
*
|
|
528
|
+
* @example
|
|
529
|
+
* ```ts
|
|
530
|
+
* const next = reducer.reduce(curr, someEvent as EventUnion<MyEM>);
|
|
531
|
+
* ```
|
|
532
|
+
*
|
|
533
|
+
* @public
|
|
534
|
+
*/
|
|
535
|
+
reduce(e, t) {
|
|
536
|
+
return this._reduce(e, t);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
const T = /* @__PURE__ */ new Set();
|
|
540
|
+
function O(c, e) {
|
|
541
|
+
const t = c ? `${c}.${e}` : e;
|
|
542
|
+
T.has(t) || (T.add(t), console.warn(
|
|
543
|
+
`[yoltra] State key "${e}"${c ? ` under "${c}"` : ""} contains a dot. Paths are dotted, so this key is indistinguishable from nested objects of the same name: a subscription to "${t}" may match the wrong value, and DevTools patches for it will address the wrong node. Rename the key, or nest it.`
|
|
544
|
+
));
|
|
545
|
+
}
|
|
546
|
+
function I(c, e, t = "", s = /* @__PURE__ */ new Map()) {
|
|
547
|
+
const n = [];
|
|
548
|
+
return P(c, e, t, s, n), n;
|
|
549
|
+
}
|
|
550
|
+
function P(c, e, t, s, n) {
|
|
551
|
+
if (c === e) return;
|
|
552
|
+
if (typeof c != "object" || typeof e != "object" || c === null || e === null) {
|
|
553
|
+
if (typeof c == "number" && Number.isNaN(c) && Number.isNaN(e))
|
|
554
|
+
return;
|
|
555
|
+
n.push(t);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (c instanceof Date && e instanceof Date) {
|
|
559
|
+
c.getTime() !== e.getTime() && n.push(t);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (c instanceof RegExp && e instanceof RegExp) {
|
|
563
|
+
(c.source !== e.source || e.flags !== c.flags) && n.push(t);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (c instanceof Map || e instanceof Map) {
|
|
567
|
+
n.push(t);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (c instanceof Set || e instanceof Set) {
|
|
571
|
+
n.push(t);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
const r = c, o = e, l = s.get(r);
|
|
575
|
+
if (l?.has(o)) return;
|
|
576
|
+
const i = l ?? /* @__PURE__ */ new Set();
|
|
577
|
+
i.add(o), l || s.set(r, i);
|
|
578
|
+
try {
|
|
579
|
+
const a = Array.isArray(c), d = Array.isArray(e);
|
|
580
|
+
if (a !== d) {
|
|
581
|
+
n.push(t);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (a) {
|
|
585
|
+
const h = c, y = e;
|
|
586
|
+
h.length !== y.length && t && n.push(t);
|
|
587
|
+
const g = Math.min(h.length, y.length);
|
|
588
|
+
for (let w = 0; w < g; w++)
|
|
589
|
+
h[w] !== y[w] && P(h[w], y[w], t ? `${t}.${w}` : `${w}`, s, n);
|
|
590
|
+
for (let w = g; w < Math.max(h.length, y.length); w++)
|
|
591
|
+
n.push(t ? `${t}.${w}` : `${w}`);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
const u = Object.keys(c), f = Object.keys(e);
|
|
595
|
+
if (u.length === 0 && f.length === 0) {
|
|
596
|
+
n.push(t);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
let m = u.length === f.length;
|
|
600
|
+
if (m) {
|
|
601
|
+
for (let h = 0; h < f.length; h++)
|
|
602
|
+
if (!Object.prototype.hasOwnProperty.call(c, f[h])) {
|
|
603
|
+
m = !1;
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (m) {
|
|
608
|
+
for (const h of f)
|
|
609
|
+
c[h] !== e[h] && (process.env.NODE_ENV !== "production" && h.includes(".") && O(t, h), P(c[h], e[h], t ? `${t}.${h}` : h, s, n));
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
for (const h of f) {
|
|
613
|
+
const y = Object.prototype.hasOwnProperty.call(c, h);
|
|
614
|
+
if (y && c[h] === e[h]) continue;
|
|
615
|
+
process.env.NODE_ENV !== "production" && h.includes(".") && O(t, h);
|
|
616
|
+
const g = t ? `${t}.${h}` : h;
|
|
617
|
+
if (!y) {
|
|
618
|
+
n.push(g);
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
P(c[h], e[h], g, s, n);
|
|
622
|
+
}
|
|
623
|
+
for (const h of u)
|
|
624
|
+
Object.prototype.hasOwnProperty.call(e, h) || (process.env.NODE_ENV !== "production" && h.includes(".") && O(t, h), n.push(t ? `${t}.${h}` : h));
|
|
625
|
+
} finally {
|
|
626
|
+
i.delete(o), i.size === 0 && s.delete(r);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
function M(c, e = /* @__PURE__ */ new WeakSet(), t) {
|
|
630
|
+
if (c === null || typeof c != "object" || e.has(c) || (t !== void 0 && c === t.watch && t.onFound(), Object.isFrozen(c))) return c;
|
|
631
|
+
if (e.add(c), Array.isArray(c)) {
|
|
632
|
+
const s = c;
|
|
633
|
+
for (let n = 0; n < s.length; n++)
|
|
634
|
+
s[n] = M(s[n], e, t);
|
|
635
|
+
return Object.freeze(s);
|
|
636
|
+
}
|
|
637
|
+
for (const s of Object.getOwnPropertyNames(c)) {
|
|
638
|
+
const n = Object.getOwnPropertyDescriptor(c, s);
|
|
639
|
+
!n || !("value" in n) || (c[s] = M(c[s], e, t));
|
|
640
|
+
}
|
|
641
|
+
for (const s of Object.getOwnPropertySymbols(c)) {
|
|
642
|
+
const n = Object.getOwnPropertyDescriptor(c, s);
|
|
643
|
+
!n || !("value" in n) || (c[s] = M(c[s], e, t));
|
|
644
|
+
}
|
|
645
|
+
return Object.freeze(c);
|
|
646
|
+
}
|
|
647
|
+
const N = /* @__PURE__ */ Symbol.for("yoltra.rejected");
|
|
648
|
+
function le(c) {
|
|
649
|
+
return { [N]: !0, reason: c };
|
|
650
|
+
}
|
|
651
|
+
function Q(c) {
|
|
652
|
+
return typeof c == "object" && c !== null && c[N] === !0;
|
|
653
|
+
}
|
|
654
|
+
class G {
|
|
655
|
+
constructor(e) {
|
|
656
|
+
p(this, "highWaterMark");
|
|
657
|
+
p(this, "buffer", []);
|
|
658
|
+
/** Consumers parked in `take`, oldest first. */
|
|
659
|
+
p(this, "takers", []);
|
|
660
|
+
/** Producers parked in `put`, each with the item they are waiting to hand over. */
|
|
661
|
+
p(this, "putters", []);
|
|
662
|
+
p(this, "consuming", !1);
|
|
663
|
+
/** No more items will be accepted, but what is already here is still owed to the consumer. */
|
|
664
|
+
p(this, "ended", !1);
|
|
665
|
+
/** Abandoned: nothing further is owed to anybody. */
|
|
666
|
+
p(this, "closed", !1);
|
|
667
|
+
/** Items discarded because nobody was iterating and the buffer was full. */
|
|
668
|
+
p(this, "dropped", 0);
|
|
669
|
+
this.highWaterMark = e;
|
|
670
|
+
}
|
|
671
|
+
/** How many items were discarded for want of a consumer. */
|
|
672
|
+
get droppedCount() {
|
|
673
|
+
return this.dropped;
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Marks that a consumer has started pulling. From here on, a full buffer parks the producer
|
|
677
|
+
* rather than dropping.
|
|
678
|
+
*/
|
|
679
|
+
beginConsuming() {
|
|
680
|
+
this.consuming = !0;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Offers an item. The returned promise settles when the item has been taken — or immediately,
|
|
684
|
+
* if it fit in the buffer or was dropped.
|
|
685
|
+
*/
|
|
686
|
+
put(e) {
|
|
687
|
+
if (this.closed || this.ended) return Promise.resolve();
|
|
688
|
+
const t = this.takers.shift();
|
|
689
|
+
return t !== void 0 ? (t({ value: e, done: !1 }), Promise.resolve()) : this.buffer.length < this.highWaterMark ? (this.buffer.push(e), Promise.resolve()) : this.consuming ? new Promise((s) => {
|
|
690
|
+
this.putters.push({ item: e, release: s });
|
|
691
|
+
}) : (this.dropped++, Promise.resolve());
|
|
692
|
+
}
|
|
693
|
+
/** Takes the next item, waiting if none is available. Resolves `done` once closed and drained. */
|
|
694
|
+
take() {
|
|
695
|
+
this.consuming = !0;
|
|
696
|
+
const e = this.buffer.shift();
|
|
697
|
+
if (e !== void 0) {
|
|
698
|
+
const s = this.putters.shift();
|
|
699
|
+
return s !== void 0 && (this.buffer.push(s.item), s.release()), Promise.resolve({ value: e, done: !1 });
|
|
700
|
+
}
|
|
701
|
+
const t = this.putters.shift();
|
|
702
|
+
return t !== void 0 ? (t.release(), Promise.resolve({ value: t.item, done: !1 })) : this.closed || this.ended ? Promise.resolve({ value: void 0, done: !0 }) : new Promise((s) => {
|
|
703
|
+
this.takers.push(s);
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Stops accepting items, but keeps owing the consumer everything already queued.
|
|
708
|
+
*
|
|
709
|
+
* @remarks
|
|
710
|
+
* What the terminal reply does. Closing outright at that moment would throw away progress the
|
|
711
|
+
* responder had already handed over and the consumer had not yet read — which is exactly what
|
|
712
|
+
* happened before this existed: a six-step job delivered five steps, because the sixth was in
|
|
713
|
+
* the buffer when `done` arrived and the buffer was cleared. The terminal event says "no more
|
|
714
|
+
* is coming", not "forget what you were given".
|
|
715
|
+
*/
|
|
716
|
+
end() {
|
|
717
|
+
if (this.ended || this.closed) return;
|
|
718
|
+
this.ended = !0;
|
|
719
|
+
let e = this.putters.shift();
|
|
720
|
+
for (; e !== void 0; )
|
|
721
|
+
this.buffer.push(e.item), e.release(), e = this.putters.shift();
|
|
722
|
+
let t = this.takers.shift();
|
|
723
|
+
for (; t !== void 0; ) {
|
|
724
|
+
const s = this.buffer.shift();
|
|
725
|
+
t(
|
|
726
|
+
s !== void 0 ? { value: s, done: !1 } : { value: void 0, done: !0 }
|
|
727
|
+
), t = this.takers.shift();
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Closes the queue: waiting consumers are told `done`, and **every parked producer is
|
|
732
|
+
* released**.
|
|
733
|
+
*
|
|
734
|
+
* @remarks
|
|
735
|
+
* Releasing producers is not tidying up. A producer parked on `put` is a pending `await emit`
|
|
736
|
+
* somewhere; leaving it parked when the call has already settled would hang the responder for
|
|
737
|
+
* good — turning a timed-out call into a wedged process, which is worse than the problem
|
|
738
|
+
* backpressure was added to solve.
|
|
739
|
+
*/
|
|
740
|
+
close() {
|
|
741
|
+
if (this.closed) return;
|
|
742
|
+
this.closed = !0, this.buffer.length = 0;
|
|
743
|
+
let e = this.takers.shift();
|
|
744
|
+
for (; e !== void 0; )
|
|
745
|
+
e({ value: void 0, done: !0 }), e = this.takers.shift();
|
|
746
|
+
let t = this.putters.shift();
|
|
747
|
+
for (; t !== void 0; )
|
|
748
|
+
t.release(), t = this.putters.shift();
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
class J extends Error {
|
|
752
|
+
constructor(t, s, n) {
|
|
753
|
+
super(
|
|
754
|
+
`[yoltra] call to "${t}/${s}" saw no correlated reply for ${n}ms. The timeout is idle rather than total, so this means the responder went quiet, not that it was slow. Check that something handles "${t}/${s}" and that its reply is emitted through the \`emit\` it was handed — a reply emitted from an unrelated context carries no causal link, and needs an explicit correlationId instead.`
|
|
755
|
+
);
|
|
756
|
+
p(this, "channel");
|
|
757
|
+
p(this, "type");
|
|
758
|
+
p(this, "idleMs");
|
|
759
|
+
this.name = "CallTimeoutError", this.channel = t, this.type = s, this.idleMs = n;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
class C extends Error {
|
|
763
|
+
constructor(e) {
|
|
764
|
+
super(`[yoltra] call aborted: ${e}`), this.name = "CallAbortedError";
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function q(c) {
|
|
768
|
+
const [e, t] = c;
|
|
769
|
+
if (t === void 0) return { channel: e, isTerminal: () => !0 };
|
|
770
|
+
if (typeof t == "string") return { channel: e, isTerminal: (n) => n === t };
|
|
771
|
+
const s = new Set(t);
|
|
772
|
+
return { channel: e, isTerminal: (n) => s.has(n) };
|
|
773
|
+
}
|
|
774
|
+
function Y(c, e, t) {
|
|
775
|
+
return c.parentId === e ? !0 : t === void 0 ? !1 : c.meta?.correlationId === t;
|
|
776
|
+
}
|
|
777
|
+
function X(c, e) {
|
|
778
|
+
try {
|
|
779
|
+
return structuredClone(e);
|
|
780
|
+
} catch (t) {
|
|
781
|
+
throw new Error(
|
|
782
|
+
`[yoltra] Initial state for slice "${String(c)}" could not be copied: ${t instanceof Error ? t.message : String(t)}. State must be structured-cloneable — functions, class instances and DOM nodes are not. Keep behaviour out of state and store plain data.`
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
function R(c, e) {
|
|
787
|
+
return process.env.NODE_ENV === "production" ? c : M(c, /* @__PURE__ */ new WeakSet(), e);
|
|
788
|
+
}
|
|
789
|
+
const D = 100, Z = 64, z = 16, ee = 3e4, te = 16, $ = Object.freeze({ committed: !1, written: !1 }), ne = Object.freeze({ committed: !0, written: !1 }), se = Object.freeze({ committed: !0, written: !0 }), A = () => typeof performance < "u" && typeof performance.now == "function" ? performance.now() : Date.now();
|
|
790
|
+
class x {
|
|
791
|
+
/**
|
|
792
|
+
* Creates a store from a {@link StoreSpec}.
|
|
793
|
+
*
|
|
794
|
+
* @param spec - Store configuration (name, reducers, middleware, optional effects).
|
|
795
|
+
*
|
|
796
|
+
* @public
|
|
797
|
+
*/
|
|
798
|
+
constructor(e) {
|
|
799
|
+
/**
|
|
800
|
+
* Store name (used by DevTools & diagnostics).
|
|
801
|
+
*
|
|
802
|
+
* @public
|
|
803
|
+
*/
|
|
804
|
+
p(this, "name");
|
|
805
|
+
/**
|
|
806
|
+
* Registered middleware pipeline (run **before** reducers).
|
|
807
|
+
* Stores either raw functions (legacy) or MiddlewareSpec objects.
|
|
808
|
+
* Return `false` from the middleware function to stop propagation.
|
|
809
|
+
*
|
|
810
|
+
* @internal
|
|
811
|
+
*/
|
|
812
|
+
p(this, "middleware");
|
|
813
|
+
/**
|
|
814
|
+
* Installed slice reducers keyed by slice name.
|
|
815
|
+
*
|
|
816
|
+
* @internal
|
|
817
|
+
*/
|
|
818
|
+
p(this, "reducers");
|
|
819
|
+
/**
|
|
820
|
+
* Current immutable snapshot of the store state.
|
|
821
|
+
* This reference changes whenever any slice changes (shallow immutability).
|
|
822
|
+
*
|
|
823
|
+
* @internal
|
|
824
|
+
*/
|
|
825
|
+
p(this, "state");
|
|
826
|
+
/**
|
|
827
|
+
* Bus for reducer wiring (emit by `(channel, type)`).
|
|
828
|
+
*
|
|
829
|
+
* @internal
|
|
830
|
+
*/
|
|
831
|
+
p(this, "reducerBus");
|
|
832
|
+
/**
|
|
833
|
+
* Bus for **granular** connector events (emit by **dotted path** inside a slice).
|
|
834
|
+
*
|
|
835
|
+
* @internal
|
|
836
|
+
*/
|
|
837
|
+
p(this, "connectorBus");
|
|
838
|
+
/**
|
|
839
|
+
* Coarse-grained listeners (called once per committed event, only if state changed).
|
|
840
|
+
*
|
|
841
|
+
* @internal
|
|
842
|
+
*/
|
|
843
|
+
p(this, "listeners", /* @__PURE__ */ new Set());
|
|
844
|
+
/**
|
|
845
|
+
* Registered effect handlers keyed by `"channel::type"` for O(1) lookup.
|
|
846
|
+
* Used for effects with explicit `keys` targeting.
|
|
847
|
+
*
|
|
848
|
+
* @internal
|
|
849
|
+
*/
|
|
850
|
+
p(this, "effects", /* @__PURE__ */ new Map());
|
|
851
|
+
/**
|
|
852
|
+
* Pattern-based effects that need runtime matching.
|
|
853
|
+
* Used for effects with `when: { any }`, `{ channel }`, or `{ channels }`.
|
|
854
|
+
* Stores tuples of [effect function, when matcher].
|
|
855
|
+
*
|
|
856
|
+
* @internal
|
|
857
|
+
*/
|
|
858
|
+
p(this, "patternEffects", /* @__PURE__ */ new Set());
|
|
859
|
+
/**
|
|
860
|
+
* Committed event subscribers keyed by `"channel::type"` for O(1) lookup.
|
|
861
|
+
* Notified after reducers, before effects, for events that passed middleware.
|
|
862
|
+
*
|
|
863
|
+
* @internal
|
|
864
|
+
*/
|
|
865
|
+
p(this, "committedEventSubscribers", /* @__PURE__ */ new Map());
|
|
866
|
+
/**
|
|
867
|
+
* Uncommitted event subscribers keyed by `"channel::type"` for O(1) lookup.
|
|
868
|
+
* Notified when middleware rejects an event.
|
|
869
|
+
*
|
|
870
|
+
* @internal
|
|
871
|
+
*/
|
|
872
|
+
p(this, "uncommittedEventSubscribers", /* @__PURE__ */ new Map());
|
|
873
|
+
/**
|
|
874
|
+
* All-events subscribers keyed by `"channel::type"` for O(1) lookup.
|
|
875
|
+
* Notified for both committed and uncommitted events with phase parameter.
|
|
876
|
+
*
|
|
877
|
+
* @internal
|
|
878
|
+
*/
|
|
879
|
+
/**
|
|
880
|
+
* Subscribers to events that actually changed state, notified after the commit.
|
|
881
|
+
*
|
|
882
|
+
* @remarks
|
|
883
|
+
* Separate from `committedEventSubscribers` rather than a filter over it, because the two
|
|
884
|
+
* answer different questions and one of them is load bearing: `committed` means "not vetoed"
|
|
885
|
+
* and fires for every event a store accepts, including every event in a store with no
|
|
886
|
+
* reducers. Narrowing it would have silently stopped toasts and analytics firing.
|
|
887
|
+
*
|
|
888
|
+
* @internal
|
|
889
|
+
*/
|
|
890
|
+
p(this, "writtenEventSubscribers", /* @__PURE__ */ new Map());
|
|
891
|
+
p(this, "allEventSubscribers", /* @__PURE__ */ new Map());
|
|
892
|
+
/**
|
|
893
|
+
* Track reducerBus unsubs per slice for HMR/register/unregister.
|
|
894
|
+
*
|
|
895
|
+
* @internal
|
|
896
|
+
*/
|
|
897
|
+
p(this, "sliceUnsubs", /* @__PURE__ */ new Map());
|
|
898
|
+
/**
|
|
899
|
+
* Pattern-based reducers that need runtime matching.
|
|
900
|
+
* Used for reducers with `when: { any }`, `{ channel }`, or `{ channels }`.
|
|
901
|
+
* Maps slice name to the `when` matcher.
|
|
902
|
+
*
|
|
903
|
+
* @internal
|
|
904
|
+
*/
|
|
905
|
+
p(this, "patternReducers", /* @__PURE__ */ new Map());
|
|
906
|
+
/**
|
|
907
|
+
* Whether `__replayEvents()` is allowed.
|
|
908
|
+
* Set from `spec.devtools.allowReplay`.
|
|
909
|
+
*
|
|
910
|
+
* @internal
|
|
911
|
+
*/
|
|
912
|
+
p(this, "replayEnabled");
|
|
913
|
+
/**
|
|
914
|
+
* Produces the `id` for each emitted event. Defaults to `crypto.randomUUID()`; overridable
|
|
915
|
+
* via {@link StoreSpec.idFactory} for runtimes lacking it or for deterministic tests.
|
|
916
|
+
*
|
|
917
|
+
* @internal
|
|
918
|
+
*/
|
|
919
|
+
p(this, "idFactory");
|
|
920
|
+
/**
|
|
921
|
+
* Optional hook invoked when an effect throws/rejects. See
|
|
922
|
+
* {@link StoreSpec.onEffectError}. `await emit()` never rejects on effect
|
|
923
|
+
* failure — this is how callers observe effect errors.
|
|
924
|
+
*/
|
|
925
|
+
p(this, "onEffectError");
|
|
926
|
+
/**
|
|
927
|
+
* Optional hook invoked when a reducer throws. See {@link StoreSpec.onReducerError}. The
|
|
928
|
+
* failing slice is isolated rather than the event being rolled back, so this is the only
|
|
929
|
+
* signal that a reducer misbehaved.
|
|
930
|
+
*/
|
|
931
|
+
p(this, "onReducerError");
|
|
932
|
+
/**
|
|
933
|
+
* `slice:channel:type` combinations already warned about for payload aliasing.
|
|
934
|
+
*
|
|
935
|
+
* @remarks
|
|
936
|
+
* Development-only diagnostics have to stay quiet enough to be read. One warning names the
|
|
937
|
+
* pattern; repeating it once per event would bury it.
|
|
938
|
+
*/
|
|
939
|
+
p(this, "warnedPayloadAliases", /* @__PURE__ */ new Set());
|
|
940
|
+
/**
|
|
941
|
+
* Pending events awaiting the **synchronous** reduce phase (middleware +
|
|
942
|
+
* reducers + subscribers + coarse listeners). Drained by {@link drainReduce}.
|
|
943
|
+
*
|
|
944
|
+
* @internal
|
|
945
|
+
*/
|
|
946
|
+
p(this, "reduceQueue", []);
|
|
947
|
+
/**
|
|
948
|
+
* Re-entrancy guard for the synchronous reduce phase.
|
|
949
|
+
*
|
|
950
|
+
* @internal
|
|
951
|
+
*/
|
|
952
|
+
p(this, "isReducing", !1);
|
|
953
|
+
/**
|
|
954
|
+
* The event currently being reduced, or `null` outside the drain.
|
|
955
|
+
*
|
|
956
|
+
* @remarks
|
|
957
|
+
* This is what makes causality exact rather than best-effort. The drain is synchronous — no
|
|
958
|
+
* `await` can interleave — so any `emit` that arrives while it is set is, without ambiguity, a
|
|
959
|
+
* consequence of this event. That catches the case a scoped `emit` closure cannot: a
|
|
960
|
+
* middleware or subscriber that captured the store and calls `store.emit` directly instead of
|
|
961
|
+
* using the injected one. Attribution should not depend on which reference a consumer reached
|
|
962
|
+
* for.
|
|
963
|
+
*
|
|
964
|
+
* @internal
|
|
965
|
+
*/
|
|
966
|
+
p(this, "currentEvent", null);
|
|
967
|
+
/**
|
|
968
|
+
* Events processed by the drain currently in progress. Compared against
|
|
969
|
+
* `maxTransitionsPerDrain`, which is off unless configured.
|
|
970
|
+
*
|
|
971
|
+
* @internal
|
|
972
|
+
*/
|
|
973
|
+
p(this, "transitionsThisDrain", 0);
|
|
974
|
+
/**
|
|
975
|
+
* Ceilings that stop a cascade from becoming a hung process. See {@link StoreSpec.maxReduceDepth}.
|
|
976
|
+
*
|
|
977
|
+
* @internal
|
|
978
|
+
*/
|
|
979
|
+
p(this, "maxReduceDepth");
|
|
980
|
+
p(this, "maxTransitionsPerDrain");
|
|
981
|
+
p(this, "onCascade");
|
|
982
|
+
p(this, "onRejected");
|
|
983
|
+
/**
|
|
984
|
+
* Registered instrumentation observers (DevTools seam). See {@link instrument}.
|
|
985
|
+
*
|
|
986
|
+
* @internal
|
|
987
|
+
*/
|
|
988
|
+
p(this, "instrumentObservers", /* @__PURE__ */ new Set());
|
|
989
|
+
/**
|
|
990
|
+
* Scratch array collecting slice-prefixed changed leaf paths during an
|
|
991
|
+
* instrumented reduce. Set by {@link drainReduce} while observers are active;
|
|
992
|
+
* appended to by {@link commitStaged}. `null` when not instrumenting.
|
|
993
|
+
*
|
|
994
|
+
* @internal
|
|
995
|
+
*/
|
|
996
|
+
p(this, "changedPathSink", null);
|
|
997
|
+
/**
|
|
998
|
+
* Where keyed reducers put their pending writes during a reduce, and the refusal one of them
|
|
999
|
+
* returned.
|
|
1000
|
+
*
|
|
1001
|
+
* @remarks
|
|
1002
|
+
* Keyed reducers are invoked through `reducerBus`, which delivers to handlers and has no way
|
|
1003
|
+
* to hand a value back — the same reason `changedPathSink` exists. `null` outside a reduce.
|
|
1004
|
+
*
|
|
1005
|
+
* @internal
|
|
1006
|
+
*/
|
|
1007
|
+
p(this, "stagingSink", null);
|
|
1008
|
+
p(this, "stagedRejection", null);
|
|
1009
|
+
p(this, "stagedRejectedBy", "");
|
|
1010
|
+
/**
|
|
1011
|
+
* Count of effect tasks currently in flight; surfaced as queue depth by
|
|
1012
|
+
* {@link __devtoolsIntrospect}.
|
|
1013
|
+
*
|
|
1014
|
+
* @internal
|
|
1015
|
+
*/
|
|
1016
|
+
p(this, "inFlightEffects", 0);
|
|
1017
|
+
/**
|
|
1018
|
+
* Tracks processed events by fingerprint with timestamps for TTL-based deduplication.
|
|
1019
|
+
*
|
|
1020
|
+
* **Deduplication Behavior:**
|
|
1021
|
+
* - Events are fingerprinted using `channel::type::JSON(payload)`
|
|
1022
|
+
* - If an identical fingerprint is seen within the dedup window, it's skipped
|
|
1023
|
+
* - The window is 50ms in development, 100ms in production
|
|
1024
|
+
*
|
|
1025
|
+
* **Limitations:**
|
|
1026
|
+
* - Non-serializable payloads (functions, symbols, circular refs) get unique
|
|
1027
|
+
* fingerprints and won't be deduplicated
|
|
1028
|
+
* - Legitimate rapid-fire identical events may be incorrectly deduplicated
|
|
1029
|
+
* - The cache is bounded to 1000 entries with lazy pruning
|
|
1030
|
+
*
|
|
1031
|
+
* @internal
|
|
1032
|
+
*/
|
|
1033
|
+
p(this, "processedEvents", /* @__PURE__ */ new Map());
|
|
1034
|
+
/**
|
|
1035
|
+
* Lifetime count of events suppressed by the deduplication cache.
|
|
1036
|
+
* Exposed via {@link __devtoolsIntrospect} so the DevTools agent can
|
|
1037
|
+
* surface it in the STORE_METRICS response without further core changes.
|
|
1038
|
+
*
|
|
1039
|
+
* @internal
|
|
1040
|
+
*/
|
|
1041
|
+
p(this, "dedupCount", 0);
|
|
1042
|
+
/**
|
|
1043
|
+
* Store-owned metadata for registered effects, keyed by the effect function.
|
|
1044
|
+
* Kept **off** the caller's function object: mutating a user-owned function
|
|
1045
|
+
* (the old `fn.__quoMeta`) bled metadata across stores that share a handler
|
|
1046
|
+
* and left it attached after unregister. Cleared on {@link dispose}.
|
|
1047
|
+
*
|
|
1048
|
+
* @internal
|
|
1049
|
+
*/
|
|
1050
|
+
p(this, "effectMeta", /* @__PURE__ */ new WeakMap());
|
|
1051
|
+
/**
|
|
1052
|
+
* Configuration for event deduplication.
|
|
1053
|
+
* @internal
|
|
1054
|
+
*/
|
|
1055
|
+
p(this, "dedupConfig");
|
|
1056
|
+
/**
|
|
1057
|
+
* Timer for periodic cleanup of processed events.
|
|
1058
|
+
*
|
|
1059
|
+
* @internal
|
|
1060
|
+
*/
|
|
1061
|
+
p(this, "eventCleanupTimer", null);
|
|
1062
|
+
if (this.name = e.name ?? "yoltra Store", this.reducerBus = new U(), this.connectorBus = new L(), this.middleware = [...e.middleware ?? []], this.reducers = {}, this.state = {}, this.replayEnabled = e.devtools?.allowReplay ?? !1, this.idFactory = e.idFactory ?? (() => crypto.randomUUID()), this.onEffectError = e.onEffectError, this.onReducerError = e.onReducerError, this.maxReduceDepth = e.maxReduceDepth ?? Z, this.maxTransitionsPerDrain = e.maxTransitionsPerDrain ?? 1 / 0, this.onCascade = e.onCascade, this.onRejected = e.onRejected, this.dedupConfig = {
|
|
1063
|
+
windowMs: e.dedupWindowMs ?? 0,
|
|
1064
|
+
maxCacheSize: 1e3
|
|
1065
|
+
}, Object.entries(e.reducer).forEach(([t, s]) => {
|
|
1066
|
+
this.mountSlice(t, s, { preserveState: !1 });
|
|
1067
|
+
}), e.effects?.length)
|
|
1068
|
+
for (const t of e.effects)
|
|
1069
|
+
this.registerEffect(t);
|
|
1070
|
+
this.dispose = this.dispose.bind(this), this.notifyEffects = this.notifyEffects.bind(this), this.__applyExternalState = this.__applyExternalState.bind(this), this.__replayEvents = this.__replayEvents.bind(this), this.__devtoolsIntrospect = this.__devtoolsIntrospect.bind(this), this.mountSlice = this.mountSlice.bind(this), this.unmountSlice = this.unmountSlice.bind(this), this.getAtPath = this.getAtPath.bind(this), this.emit = this.emit.bind(this), this.subscribe = this.subscribe.bind(this), this.connect = this.connect.bind(this), this.onEffect = this.onEffect.bind(this), this.onEvent = this.onEvent.bind(this), this.getState = this.getState.bind(this), this.registerEffect = this.registerEffect.bind(this), this.registerMiddleware = this.registerMiddleware.bind(this), this.registerReducer = this.registerReducer.bind(this), this.replaceMiddleware = this.replaceMiddleware.bind(this), this.replaceEffects = this.replaceEffects.bind(this), this.replaceReducers = this.replaceReducers.bind(this), this.hotReplace = this.hotReplace.bind(this);
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Cleanup resources (timers, etc.) when disposing the store.
|
|
1074
|
+
* Call this if you're dynamically creating/destroying stores.
|
|
1075
|
+
*
|
|
1076
|
+
* @example
|
|
1077
|
+
* ```ts
|
|
1078
|
+
* const store = createStore({ ... });
|
|
1079
|
+
* // later
|
|
1080
|
+
* store.dispose();
|
|
1081
|
+
* ```
|
|
1082
|
+
*
|
|
1083
|
+
* @public
|
|
1084
|
+
*/
|
|
1085
|
+
dispose() {
|
|
1086
|
+
this.eventCleanupTimer && (clearInterval(this.eventCleanupTimer), this.eventCleanupTimer = null), this.processedEvents.clear(), this.effects.clear(), this.patternEffects.clear(), this.effectMeta = /* @__PURE__ */ new WeakMap(), this.warnedPayloadAliases.clear(), this.listeners.clear(), this.committedEventSubscribers.clear(), this.uncommittedEventSubscribers.clear(), this.writtenEventSubscribers.clear(), this.allEventSubscribers.clear(), this.instrumentObservers.clear(), this.connectorBus.clear(), this.reducerBus.clear(), this.patternReducers.clear(), this.sliceUnsubs.clear(), this.changedPathSink = null;
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Generates a fingerprint for an event for deduplication purposes.
|
|
1090
|
+
* Falls back gracefully for non-serializable payloads.
|
|
1091
|
+
*
|
|
1092
|
+
* @param channel - Event channel.
|
|
1093
|
+
* @param type - Event type.
|
|
1094
|
+
* @param payload - Event payload.
|
|
1095
|
+
* @returns A string fingerprint for the event.
|
|
1096
|
+
*
|
|
1097
|
+
* @internal
|
|
1098
|
+
*/
|
|
1099
|
+
fingerprint(e, t, s) {
|
|
1100
|
+
const n = `${e}::${t}`;
|
|
1101
|
+
try {
|
|
1102
|
+
if (s == null)
|
|
1103
|
+
return `${n}::null`;
|
|
1104
|
+
if (typeof s != "object")
|
|
1105
|
+
return `${n}::${String(s)}`;
|
|
1106
|
+
const r = JSON.stringify(s);
|
|
1107
|
+
return `${n}::${r}`;
|
|
1108
|
+
} catch {
|
|
1109
|
+
return `${n}::${Date.now()}::${Math.random()}`;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Checks if an event should be deduplicated.
|
|
1114
|
+
* Returns true if this is a duplicate that should be skipped.
|
|
1115
|
+
*
|
|
1116
|
+
* @param fp - Event fingerprint.
|
|
1117
|
+
* @returns `true` if duplicate (should skip), `false` otherwise.
|
|
1118
|
+
*
|
|
1119
|
+
* @internal
|
|
1120
|
+
*/
|
|
1121
|
+
shouldDedupe(e, t) {
|
|
1122
|
+
const s = Date.now(), n = this.processedEvents.get(e);
|
|
1123
|
+
return n !== void 0 && s - n < t ? (this.dedupCount++, !0) : (this.processedEvents.set(e, s), this.ensureCleanupTimer(), this.processedEvents.size > this.dedupConfig.maxCacheSize && this.pruneProcessedEvents(s), !1);
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Starts the periodic prune interval if it isn't already running. Called when
|
|
1127
|
+
* the first entry is cached so the timer's lifetime tracks actual dedup use
|
|
1128
|
+
* (content window or identity `dedupKey`), independent of `dedupWindowMs`.
|
|
1129
|
+
*
|
|
1130
|
+
* @internal
|
|
1131
|
+
*/
|
|
1132
|
+
ensureCleanupTimer() {
|
|
1133
|
+
this.eventCleanupTimer === null && (this.eventCleanupTimer = setInterval(() => {
|
|
1134
|
+
this.pruneProcessedEvents(Date.now());
|
|
1135
|
+
}, 5e3), this.eventCleanupTimer.unref?.());
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Removes expired entries from the processed events cache.
|
|
1139
|
+
*
|
|
1140
|
+
* @param now - Current timestamp.
|
|
1141
|
+
*
|
|
1142
|
+
* @internal
|
|
1143
|
+
*/
|
|
1144
|
+
pruneProcessedEvents(e) {
|
|
1145
|
+
const t = Math.max(this.dedupConfig.windowMs, D), s = e - t * 2;
|
|
1146
|
+
for (const [n, r] of this.processedEvents)
|
|
1147
|
+
r < s && this.processedEvents.delete(n);
|
|
1148
|
+
this.processedEvents.size === 0 && this.eventCleanupTimer !== null && (clearInterval(this.eventCleanupTimer), this.eventCleanupTimer = null);
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Reports a breached ceiling and refuses the emit.
|
|
1152
|
+
*
|
|
1153
|
+
* @remarks
|
|
1154
|
+
* Console *and* hook, matching how reducer and effect errors are reported: a cascade is a
|
|
1155
|
+
* wiring bug, and the console line is what a developer who has not registered a hook will
|
|
1156
|
+
* actually see. Without one, refusing the emit would look exactly like the event never having
|
|
1157
|
+
* been emitted at all — which is the invisibility this whole guard exists to end.
|
|
1158
|
+
*
|
|
1159
|
+
* @internal
|
|
1160
|
+
*/
|
|
1161
|
+
reportCascade(e, t, s, n, r) {
|
|
1162
|
+
console.error(
|
|
1163
|
+
`[yoltra] Cascade stopped: "${s.channel}/${s.type}" would exceed ${e} (${t}). This event was refused and the chain ends here. A chain this long is almost always two consumers emitting into each other — check what reacts to "${s.channel}/${s.type}" and what that emits in turn.` + (r.length > 0 ? ` Recent causal chain: ${r.join(" → ")} → (refused).` : "")
|
|
1164
|
+
);
|
|
1165
|
+
try {
|
|
1166
|
+
this.onCascade?.({ limit: e, limitValue: t, event: s, depth: n, chain: r });
|
|
1167
|
+
} catch (o) {
|
|
1168
|
+
console.error("onCascade handler error:", o);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Checks if an event matches a `When` matcher.
|
|
1173
|
+
*
|
|
1174
|
+
* @param when - The When matcher (or undefined for "all events").
|
|
1175
|
+
* @param event - The event to check.
|
|
1176
|
+
* @returns `true` if the event matches, `false` otherwise.
|
|
1177
|
+
*
|
|
1178
|
+
* @remarks
|
|
1179
|
+
* - `undefined` or missing `when` matches ALL events.
|
|
1180
|
+
* - `{ any: true }` matches ALL events.
|
|
1181
|
+
* - `{ keys: [...] }` matches if event's `[channel, type]` is in the array.
|
|
1182
|
+
* - `{ channel: 'x' }` matches if event's channel equals 'x'.
|
|
1183
|
+
* - `{ channels: ['x', 'y'] }` matches if event's channel is in the array.
|
|
1184
|
+
*
|
|
1185
|
+
* @internal
|
|
1186
|
+
*/
|
|
1187
|
+
matchesWhen(e, t) {
|
|
1188
|
+
return !e || "any" in e && e.any === !0 ? !0 : "keys" in e ? e.keys.some(
|
|
1189
|
+
([s, n]) => t.channel === s && t.type === n
|
|
1190
|
+
) : "channel" in e ? t.channel === e.channel : "channels" in e ? e.channels.includes(t.channel) : !1;
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Extracts the middleware function from a MiddlewareInput.
|
|
1194
|
+
* Handles both raw functions (legacy) and MiddlewareSpec objects.
|
|
1195
|
+
*
|
|
1196
|
+
* @param input - MiddlewareInput (function or spec).
|
|
1197
|
+
* @returns The middleware function.
|
|
1198
|
+
*
|
|
1199
|
+
* @internal
|
|
1200
|
+
*/
|
|
1201
|
+
getMiddlewareFunction(e) {
|
|
1202
|
+
return typeof e == "function" ? e : e.middleware;
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* Gets the `when` matcher from a MiddlewareInput.
|
|
1206
|
+
*
|
|
1207
|
+
* @param input - MiddlewareInput (function or spec).
|
|
1208
|
+
* @returns The `when` matcher, or `undefined` for raw functions (match all).
|
|
1209
|
+
*
|
|
1210
|
+
* @internal
|
|
1211
|
+
*/
|
|
1212
|
+
getMiddlewareWhen(e) {
|
|
1213
|
+
if (typeof e != "function")
|
|
1214
|
+
return e.when;
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Invokes all registered **effects** for a given event.
|
|
1218
|
+
* Handles both key-based effects (O(1) lookup) and pattern-based effects (runtime matching).
|
|
1219
|
+
* Errors are caught and logged.
|
|
1220
|
+
*
|
|
1221
|
+
* @param event - The event that was reduced.
|
|
1222
|
+
* @internal
|
|
1223
|
+
*/
|
|
1224
|
+
async notifyEffects(e) {
|
|
1225
|
+
const t = this.scopedEmit(e), s = `${String(e.channel)}::${String(e.type)}`, n = this.effects.get(s);
|
|
1226
|
+
if (n && n.size > 0)
|
|
1227
|
+
for (const r of [...n])
|
|
1228
|
+
try {
|
|
1229
|
+
await r(e, this.getState, t);
|
|
1230
|
+
} catch (o) {
|
|
1231
|
+
console.error("Effect error:", o), this.onEffectError?.(o, e);
|
|
1232
|
+
}
|
|
1233
|
+
for (const { effect: r, when: o } of this.patternEffects)
|
|
1234
|
+
if (this.matchesWhen(o, e))
|
|
1235
|
+
try {
|
|
1236
|
+
await r(e, this.getState, t);
|
|
1237
|
+
} catch (l) {
|
|
1238
|
+
console.error("Effect error:", l), this.onEffectError?.(l, e);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* An `emit` that attributes whatever it sends to `cause`.
|
|
1243
|
+
*
|
|
1244
|
+
* @remarks
|
|
1245
|
+
* Built per event rather than per effect: every effect reacting to one event shares a cause,
|
|
1246
|
+
* and one closure is cheaper than one per handler on a path that runs for every committed
|
|
1247
|
+
* event.
|
|
1248
|
+
*
|
|
1249
|
+
* @internal
|
|
1250
|
+
*/
|
|
1251
|
+
scopedEmit(e) {
|
|
1252
|
+
const t = {
|
|
1253
|
+
id: e.id,
|
|
1254
|
+
depth: e.depth ?? 0,
|
|
1255
|
+
chain: [...this.currentEvent?.chain ?? [], e.id].slice(-z)
|
|
1256
|
+
};
|
|
1257
|
+
return ((s, n, r, o) => this.emitCaused(t, s, n, r, o));
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Notifies event subscribers for a specific phase.
|
|
1261
|
+
*
|
|
1262
|
+
* Calls both phase-specific subscribers and 'all' subscribers.
|
|
1263
|
+
* Errors are caught and logged, allowing other subscribers to continue.
|
|
1264
|
+
*
|
|
1265
|
+
* @param event - The event to notify about.
|
|
1266
|
+
* @param phase - The phase ('committed' or 'uncommitted').
|
|
1267
|
+
* @internal
|
|
1268
|
+
*/
|
|
1269
|
+
notifyEventSubscribers(e, t) {
|
|
1270
|
+
const s = `${String(e.channel)}::${String(e.type)}`, r = (t === "committed" ? this.committedEventSubscribers : t === "written" ? this.writtenEventSubscribers : this.uncommittedEventSubscribers).get(s);
|
|
1271
|
+
if (r?.size)
|
|
1272
|
+
for (const l of [...r]) this.invokeEventSubscriber(l, e, t);
|
|
1273
|
+
if (t === "written") return;
|
|
1274
|
+
const o = this.allEventSubscribers.get(s);
|
|
1275
|
+
if (o?.size)
|
|
1276
|
+
for (const l of [...o]) this.invokeEventSubscriber(l, e, t);
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Invokes a single event-subscription handler **fire-and-forget**: synchronous
|
|
1280
|
+
* throws and async rejections are logged but never block the emit pipeline.
|
|
1281
|
+
* Event subscribers are notifications, not part of the committed reduce result.
|
|
1282
|
+
*
|
|
1283
|
+
* @internal
|
|
1284
|
+
*/
|
|
1285
|
+
invokeEventSubscriber(e, t, s) {
|
|
1286
|
+
try {
|
|
1287
|
+
const n = e(t, this.getState, this.emit, s);
|
|
1288
|
+
n && typeof n.then == "function" && n.catch((r) => console.error("Event subscription error:", r));
|
|
1289
|
+
} catch (n) {
|
|
1290
|
+
console.error("Event subscription error:", n);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Applies a reduced event to a slice and emits **precise** connector events.
|
|
1295
|
+
*
|
|
1296
|
+
* For each changed **leaf path** (via {@link detectChangedProps}), emits that leaf and
|
|
1297
|
+
* all of its **ancestors** once (e.g., `"data"`, `"data.123"`, `"data.123.title"`).
|
|
1298
|
+
*
|
|
1299
|
+
* A slice whose state **is** a single value — a primitive, a `Map`/`Set`, a `Date` — has no
|
|
1300
|
+
* leaf below its root, and `detectChangedProps` reports its change as the empty path `""`.
|
|
1301
|
+
* That path is emitted as-is, so `connect({ reducer, property: "" })` (and any `**` pattern)
|
|
1302
|
+
* hears it. It has no ancestors to walk.
|
|
1303
|
+
*
|
|
1304
|
+
* **State Immutability**: When a slice changes, a new state object is created via
|
|
1305
|
+
* shallow spread: `{ ...this.state, [sliceName]: newSlice }`. This ensures that
|
|
1306
|
+
* `this.state` reference changes, enabling efficient change detection via `===`.
|
|
1307
|
+
*
|
|
1308
|
+
* @param rName - Slice name being updated.
|
|
1309
|
+
* @param event - Reduced event with typed payload.
|
|
1310
|
+
* @returns `true` if the slice actually changed, `false` otherwise.
|
|
1311
|
+
*
|
|
1312
|
+
* @internal
|
|
1313
|
+
*/
|
|
1314
|
+
/**
|
|
1315
|
+
* Reduces one slice and contains any error it raises.
|
|
1316
|
+
*
|
|
1317
|
+
* @returns `true` when the slice changed.
|
|
1318
|
+
*
|
|
1319
|
+
* @remarks
|
|
1320
|
+
* The single funnel both dispatch paths go through, which is the point. Keyed reducers run
|
|
1321
|
+
* through `reducerBus`, whose handler loop caught and logged; pattern reducers were called
|
|
1322
|
+
* straight from the drain, so their errors escaped to the caller instead. The same bug in the
|
|
1323
|
+
* same reducer therefore produced two different outcomes depending on how the slice happened
|
|
1324
|
+
* to be targeted — a keyed reducer's throw let the event commit and its effects run, while a
|
|
1325
|
+
* pattern reducer's throw aborted the commit and notified nobody, not even the uncommitted
|
|
1326
|
+
* subscribers a veto would have reached.
|
|
1327
|
+
*
|
|
1328
|
+
* The semantics are the same either way: **the failing slice is isolated.** Its state is
|
|
1329
|
+
* unchanged, every other slice still reduces, and the event still commits if anything else
|
|
1330
|
+
* changed.
|
|
1331
|
+
*
|
|
1332
|
+
* That is deliberately *not* what a {@link Rejected} refusal does, which discards the whole
|
|
1333
|
+
* event. A crash and a refusal are different acts: a reducer that throws has a bug and should
|
|
1334
|
+
* not be able to veto its neighbours' work, while a reducer that refuses has made a decision
|
|
1335
|
+
* and must be able to.
|
|
1336
|
+
*
|
|
1337
|
+
* This once argued that rolling back was untenable, because subscribers were notified as each
|
|
1338
|
+
* slice committed and a later revert would have told them about a value that no longer
|
|
1339
|
+
* existed. Staging removed that obstacle — nothing is notified until every slice is written —
|
|
1340
|
+
* which is what made refusal possible at all.
|
|
1341
|
+
*
|
|
1342
|
+
* @internal
|
|
1343
|
+
*/
|
|
1344
|
+
stageSliceGuarded(e, t, s) {
|
|
1345
|
+
try {
|
|
1346
|
+
return this.stageSlice(e, t, s);
|
|
1347
|
+
} catch (n) {
|
|
1348
|
+
return console.error(`Reducer error in slice "${e}":`, n), this.onReducerError?.(n, t, e), null;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Runs one slice's reducer and records what it *would* write. Writes nothing.
|
|
1353
|
+
*
|
|
1354
|
+
* @returns The reducer's {@link Rejection} if it refused, otherwise `null`.
|
|
1355
|
+
*
|
|
1356
|
+
* @remarks
|
|
1357
|
+
* The staging half of the write path. Nothing here touches `this.state` or notifies anybody,
|
|
1358
|
+
* which is what lets the event be refused after every reducer has had its say — a decision
|
|
1359
|
+
* that has to see the whole diff cannot be made one slice at a time.
|
|
1360
|
+
*
|
|
1361
|
+
* Freezing happens here rather than at commit because it is where the new value is built, and
|
|
1362
|
+
* the freeze is a no-op on anything already frozen; a staged slice that never commits is
|
|
1363
|
+
* discarded frozen, which costs nothing and keeps the committed path free of a second walk.
|
|
1364
|
+
*
|
|
1365
|
+
* @internal
|
|
1366
|
+
*/
|
|
1367
|
+
stageSlice(e, t, s) {
|
|
1368
|
+
const n = this.state[e], r = this.reducers[e].reduce(n, t);
|
|
1369
|
+
if (Q(r)) return r;
|
|
1370
|
+
if (n === r) return null;
|
|
1371
|
+
const o = I(n, r);
|
|
1372
|
+
if (o.length === 0) return null;
|
|
1373
|
+
const l = t.payload, i = process.env.NODE_ENV !== "production" && l !== null && typeof l == "object" ? {
|
|
1374
|
+
watch: l,
|
|
1375
|
+
onFound: () => {
|
|
1376
|
+
const a = `${e}:${t.channel}:${t.type}`;
|
|
1377
|
+
this.warnedPayloadAliases.has(a) || (this.warnedPayloadAliases.add(a), console.warn(
|
|
1378
|
+
`[yoltra] Slice "${e}" stored the payload of "${t.channel}/${t.type}" by reference. It is now frozen along with the rest of the state, so the emitter mutating it later will throw in development and silently corrupt state in production. Copy the payload in the reducer instead.`
|
|
1379
|
+
));
|
|
1380
|
+
}
|
|
1381
|
+
} : void 0;
|
|
1382
|
+
return s.push({
|
|
1383
|
+
name: e,
|
|
1384
|
+
prev: n,
|
|
1385
|
+
frozen: R(r, i),
|
|
1386
|
+
leafPaths: o
|
|
1387
|
+
}), null;
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Writes every staged slice, then tells the world — in that order.
|
|
1391
|
+
*
|
|
1392
|
+
* @remarks
|
|
1393
|
+
* The commit half. Assigning all slices under a single new root before any notification goes
|
|
1394
|
+
* out is what closes the window this used to leave open: notifications fired per slice as each
|
|
1395
|
+
* committed, so a subscriber to slice A that read `getState()` could observe slice B of the
|
|
1396
|
+
* *same event* not yet applied. In React that window is real, because the atomic hooks use a
|
|
1397
|
+
* change as a bare signal and then re-read the whole store.
|
|
1398
|
+
*
|
|
1399
|
+
* It is also what makes refusal possible at all. The previous code documented rollback as
|
|
1400
|
+
* untenable precisely because "an event that reverted afterwards would have already told
|
|
1401
|
+
* components about a value that no longer exists" — true when notification and commit were the
|
|
1402
|
+
* same step, and no longer true now that they are not.
|
|
1403
|
+
*
|
|
1404
|
+
* @returns `true` if anything was written.
|
|
1405
|
+
*
|
|
1406
|
+
* @internal
|
|
1407
|
+
*/
|
|
1408
|
+
commitStaged(e, t) {
|
|
1409
|
+
if (e.length === 0) return !1;
|
|
1410
|
+
const s = { ...this.state };
|
|
1411
|
+
for (const n of e) s[n.name] = n.frozen;
|
|
1412
|
+
if (this.state = s, this.changedPathSink)
|
|
1413
|
+
for (const n of e)
|
|
1414
|
+
for (const r of n.leafPaths)
|
|
1415
|
+
this.changedPathSink.push(r ? `${n.name}.${r}` : n.name);
|
|
1416
|
+
for (const n of e) {
|
|
1417
|
+
const r = /* @__PURE__ */ new Set();
|
|
1418
|
+
for (const o of n.leafPaths) {
|
|
1419
|
+
if (o === "") {
|
|
1420
|
+
r.add("");
|
|
1421
|
+
continue;
|
|
1422
|
+
}
|
|
1423
|
+
for (const l of x.buildAncestorPaths(o)) r.add(l);
|
|
1424
|
+
}
|
|
1425
|
+
for (const o of r)
|
|
1426
|
+
this.connectorBus.emitWith(n.name, o, () => ({
|
|
1427
|
+
oldValue: this.getAtPath(n.prev, o),
|
|
1428
|
+
newValue: this.getAtPath(n.frozen, o),
|
|
1429
|
+
path: o,
|
|
1430
|
+
// Provenance, built inside the same lazy factory as the values: a subscriber that
|
|
1431
|
+
// needs to know why a value moved no longer has to mirror the cause into state and
|
|
1432
|
+
// keep it there twice.
|
|
1433
|
+
eventId: t.id,
|
|
1434
|
+
channel: t.channel,
|
|
1435
|
+
type: t.type
|
|
1436
|
+
}));
|
|
1437
|
+
}
|
|
1438
|
+
return !0;
|
|
1439
|
+
}
|
|
1440
|
+
/**
|
|
1441
|
+
* Returns a structured introspection snapshot for DevTools UIs.
|
|
1442
|
+
*
|
|
1443
|
+
* @remarks
|
|
1444
|
+
* Reads the internal middleware, effects, reducers, and subscriber
|
|
1445
|
+
* registries and returns a plain-object summary matching the
|
|
1446
|
+
* `STORE_SUBSCRIPTIONS` protocol message shape.
|
|
1447
|
+
*
|
|
1448
|
+
* @public
|
|
1449
|
+
*/
|
|
1450
|
+
__devtoolsIntrospect() {
|
|
1451
|
+
const e = Object.keys(this.reducers).map((l) => {
|
|
1452
|
+
const i = this.patternReducers.get(l);
|
|
1453
|
+
return { name: l, when: i };
|
|
1454
|
+
}), t = [];
|
|
1455
|
+
for (const [l, i] of this.effects) {
|
|
1456
|
+
if (i.size === 0) continue;
|
|
1457
|
+
const [a, d] = l.split("::");
|
|
1458
|
+
for (const u of i) {
|
|
1459
|
+
const f = this.effectMeta.get(u);
|
|
1460
|
+
t.push({ channel: a, type: d, name: f?.name, description: f?.description });
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
for (const l of this.patternEffects) {
|
|
1464
|
+
const i = this.effectMeta.get(l.effect);
|
|
1465
|
+
t.push({
|
|
1466
|
+
channel: "*",
|
|
1467
|
+
type: "*",
|
|
1468
|
+
name: i?.name,
|
|
1469
|
+
description: i?.description
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
const s = [];
|
|
1473
|
+
for (const l of this.middleware)
|
|
1474
|
+
typeof l == "function" ? s.push({ name: l.name || void 0 }) : s.push({
|
|
1475
|
+
name: l.meta?.name,
|
|
1476
|
+
description: l.meta?.description,
|
|
1477
|
+
when: l.when
|
|
1478
|
+
});
|
|
1479
|
+
const n = [];
|
|
1480
|
+
for (const l of this.connectorBus.__introspect())
|
|
1481
|
+
for (let i = 0; i < l.count; i++)
|
|
1482
|
+
n.push({ reducer: l.channel, property: l.type });
|
|
1483
|
+
const r = [];
|
|
1484
|
+
for (const [l, i] of this.committedEventSubscribers) {
|
|
1485
|
+
if (i.size === 0) continue;
|
|
1486
|
+
const [a, d] = l.split("::");
|
|
1487
|
+
for (let u = 0; u < i.size; u++)
|
|
1488
|
+
r.push({ channel: a, type: d, phase: "committed" });
|
|
1489
|
+
}
|
|
1490
|
+
for (const [l, i] of this.uncommittedEventSubscribers) {
|
|
1491
|
+
if (i.size === 0) continue;
|
|
1492
|
+
const [a, d] = l.split("::");
|
|
1493
|
+
for (let u = 0; u < i.size; u++)
|
|
1494
|
+
r.push({ channel: a, type: d, phase: "uncommitted" });
|
|
1495
|
+
}
|
|
1496
|
+
for (const [l, i] of this.allEventSubscribers) {
|
|
1497
|
+
if (i.size === 0) continue;
|
|
1498
|
+
const [a, d] = l.split("::");
|
|
1499
|
+
for (let u = 0; u < i.size; u++)
|
|
1500
|
+
r.push({ channel: a, type: d, phase: "all" });
|
|
1501
|
+
}
|
|
1502
|
+
const o = this.listeners.size;
|
|
1503
|
+
return {
|
|
1504
|
+
reducers: e,
|
|
1505
|
+
effects: t,
|
|
1506
|
+
middleware: s,
|
|
1507
|
+
atomic: n,
|
|
1508
|
+
event: r,
|
|
1509
|
+
coarse: o,
|
|
1510
|
+
dedupHits: this.dedupCount,
|
|
1511
|
+
queueDepth: this.reduceQueue.length + this.inFlightEffects
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* Applies an externally provided **whole-state** (e.g., DevTools time travel) and emits
|
|
1516
|
+
* fine-grained path changes for each slice.
|
|
1517
|
+
*
|
|
1518
|
+
* **State Immutability**: If any slices change, a new state object is created via
|
|
1519
|
+
* shallow spread. This ensures consistent immutability with {@link commitStaged}.
|
|
1520
|
+
*
|
|
1521
|
+
* **Missing slices**: the snapshot should contain every slice. A slice absent
|
|
1522
|
+
* from `nextPlain` is **retained at its current value** (not blanked to
|
|
1523
|
+
* `undefined`, which would make `getState().<slice>` throw on next access).
|
|
1524
|
+
*
|
|
1525
|
+
* @param nextPlain - Plain JS object to become the new state.
|
|
1526
|
+
*
|
|
1527
|
+
* @internal
|
|
1528
|
+
*/
|
|
1529
|
+
__applyExternalState(e) {
|
|
1530
|
+
if (!this.replayEnabled)
|
|
1531
|
+
throw new Error(
|
|
1532
|
+
"[yoltra] External state apply (time-travel) is disabled. Enable it with createStore({ devtools: { allowReplay: true } })"
|
|
1533
|
+
);
|
|
1534
|
+
const t = this.state, s = e, n = { ...this.state };
|
|
1535
|
+
let r = !1;
|
|
1536
|
+
Object.keys(this.reducers).forEach((o) => {
|
|
1537
|
+
const l = t?.[o], i = s?.[o];
|
|
1538
|
+
if (i === void 0) {
|
|
1539
|
+
process.env.NODE_ENV !== "production" && console.warn(
|
|
1540
|
+
`[yoltra] External state is missing slice "${String(
|
|
1541
|
+
o
|
|
1542
|
+
)}"; retaining its current value. Time-travel snapshots should contain all slices.`
|
|
1543
|
+
);
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
if (l === i) return;
|
|
1547
|
+
const a = R(i);
|
|
1548
|
+
n[o] = a, r = !0;
|
|
1549
|
+
const d = I(l, i);
|
|
1550
|
+
if (d.length === 0) return;
|
|
1551
|
+
const u = /* @__PURE__ */ new Set();
|
|
1552
|
+
for (const f of d) {
|
|
1553
|
+
if (f === "") {
|
|
1554
|
+
u.add("");
|
|
1555
|
+
continue;
|
|
1556
|
+
}
|
|
1557
|
+
for (const m of x.buildAncestorPaths(f)) u.add(m);
|
|
1558
|
+
}
|
|
1559
|
+
for (const f of u) {
|
|
1560
|
+
const m = this.getAtPath(l, f), h = this.getAtPath(a, f);
|
|
1561
|
+
this.connectorBus.emit(o, f, { oldValue: m, newValue: h, path: f });
|
|
1562
|
+
}
|
|
1563
|
+
}), r && (this.state = n), r && this.listeners.forEach((o) => o());
|
|
1564
|
+
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Replays a sequence of events from a snapshot through reducers and event
|
|
1567
|
+
* subscribers ONLY. Skips dedup, middleware, and effects.
|
|
1568
|
+
*
|
|
1569
|
+
* This method is gated by the `devtools.allowReplay` runtime config.
|
|
1570
|
+
* If replay is not enabled, this method throws.
|
|
1571
|
+
*
|
|
1572
|
+
* @param snapshot - The state snapshot to restore before replaying.
|
|
1573
|
+
* @param events - Array of events to replay (in order).
|
|
1574
|
+
*
|
|
1575
|
+
* @internal
|
|
1576
|
+
*/
|
|
1577
|
+
__replayEvents(e, t) {
|
|
1578
|
+
if (!this.replayEnabled)
|
|
1579
|
+
throw new Error(
|
|
1580
|
+
"[yoltra] Event replay is disabled. Enable it with createStore({ devtools: { allowReplay: true } })"
|
|
1581
|
+
);
|
|
1582
|
+
this.__applyExternalState(e);
|
|
1583
|
+
for (const s of t) {
|
|
1584
|
+
const n = s, r = [];
|
|
1585
|
+
this.stagingSink = r;
|
|
1586
|
+
let o = null;
|
|
1587
|
+
try {
|
|
1588
|
+
this.reducerBus.emit(n.channel, n.type, n.payload, n), o = this.stagedRejection;
|
|
1589
|
+
for (const [i, a] of this.patternReducers) {
|
|
1590
|
+
if (o !== null) break;
|
|
1591
|
+
if (this.matchesWhen(a, n)) {
|
|
1592
|
+
const d = this.stageSliceGuarded(i, n, r);
|
|
1593
|
+
d !== null && (o = d);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
} finally {
|
|
1597
|
+
this.stagingSink = null, this.stagedRejection = null, this.stagedRejectedBy = "";
|
|
1598
|
+
}
|
|
1599
|
+
const l = o === null && this.commitStaged(r, n);
|
|
1600
|
+
this.notifyEventSubscribers(n, "committed"), l && (this.notifyEventSubscribers(n, "written"), this.listeners.forEach((i) => i()));
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
/**
|
|
1604
|
+
* Emits a typed event `(channel, type, payload)`.
|
|
1605
|
+
* Events are queued and processed **sequentially** (FIFO).
|
|
1606
|
+
*
|
|
1607
|
+
* **Pipeline per event:** the *reduce phase* (steps 1-4) runs **synchronously**,
|
|
1608
|
+
* so `getState()` reflects the change as soon as `emit()` returns; the *effect
|
|
1609
|
+
* phase* (step 5) runs afterwards, asynchronously.
|
|
1610
|
+
* 1. **Deduplication** (opt-in) - Skip when content-dedup is enabled (`dedupWindowMs > 0`) or a matching `dedupKey` recurs; off by default
|
|
1611
|
+
* 2. **Middleware** (sync) - Pre-reducer hooks; may cancel by returning `false`
|
|
1612
|
+
* 3. **Reducers** (sync) - every matching slice is *staged*; nothing is written yet, so a refusal from the last reducer still stops the first one's write
|
|
1613
|
+
* 4. **Commit + subscribers** (sync) - all staged slices are assigned under one new root, then event subscribers (`committed`, then `written` when state actually changed), then coarse listeners
|
|
1614
|
+
* 5. **Effects** (async) - side-effects keyed by `(channel, type)`; the returned promise resolves once they complete
|
|
1615
|
+
*
|
|
1616
|
+
* **Change Detection**: Uses reference equality (`===`) on `this.state` to determine
|
|
1617
|
+
* if any slice changed. Works because the commit builds a new state reference via
|
|
1618
|
+
* shallow spread when any slice changes.
|
|
1619
|
+
*
|
|
1620
|
+
* @typeParam C - Channel key in `EM`.
|
|
1621
|
+
* @typeParam T - Type key within channel `C`.
|
|
1622
|
+
* @param channel - Channel name.
|
|
1623
|
+
* @param type - Event type name.
|
|
1624
|
+
* @param payload - Payload typed as `EM[C][T]`.
|
|
1625
|
+
* @param opts - Optional per-emit options (e.g. `dedupKey` for identity-based dedup).
|
|
1626
|
+
* @returns A promise that resolves once this event's effects have finished.
|
|
1627
|
+
* State is already updated synchronously before `emit()` returns.
|
|
1628
|
+
*
|
|
1629
|
+
* @example Basic usage
|
|
1630
|
+
* ```ts
|
|
1631
|
+
* await store.emit('ui', 'increment', 1);
|
|
1632
|
+
* ```
|
|
1633
|
+
*
|
|
1634
|
+
* @example With middleware cancellation
|
|
1635
|
+
* ```ts
|
|
1636
|
+
* store.registerMiddleware((state, event) => {
|
|
1637
|
+
* if (event.type === 'dangerous') return false; // cancel
|
|
1638
|
+
* return true; // allow
|
|
1639
|
+
* });
|
|
1640
|
+
*
|
|
1641
|
+
* await store.emit('ui', 'dangerous', null); // cancelled, no state change
|
|
1642
|
+
* ```
|
|
1643
|
+
*
|
|
1644
|
+
* @public
|
|
1645
|
+
*/
|
|
1646
|
+
async emit(e, t, s, n) {
|
|
1647
|
+
return this.emitCaused(null, e, t, s, n);
|
|
1648
|
+
}
|
|
1649
|
+
/**
|
|
1650
|
+
* The real emit, with an explicitly supplied cause.
|
|
1651
|
+
*
|
|
1652
|
+
* @remarks
|
|
1653
|
+
* Exists so the parent can be passed without a pseudo-private field on the public
|
|
1654
|
+
* {@link EmitOptions}. Two callers supply one: the public {@link emit} passes `null` and lets
|
|
1655
|
+
* `currentEvent` speak for the synchronous case, and the scoped `emit` handed to effects passes
|
|
1656
|
+
* the event that triggered them — effects resume after the drain has ended, so nothing else
|
|
1657
|
+
* could still know what caused them.
|
|
1658
|
+
*
|
|
1659
|
+
* @internal
|
|
1660
|
+
*/
|
|
1661
|
+
async emitCaused(e, t, s, n, r) {
|
|
1662
|
+
const o = r?.dedupKey, l = this.dedupConfig.windowMs;
|
|
1663
|
+
if (r?.skipDedup !== !0 && (l > 0 || o !== void 0)) {
|
|
1664
|
+
const m = o !== void 0 && l <= 0 ? D : l, h = o !== void 0 ? `${t}::${s}::#${o}` : this.fingerprint(t, s, n);
|
|
1665
|
+
if (this.shouldDedupe(h, m))
|
|
1666
|
+
return $;
|
|
1667
|
+
}
|
|
1668
|
+
const i = r?.id ?? this.idFactory(), a = this.currentEvent ?? e, d = a === null ? 0 : a.depth + 1;
|
|
1669
|
+
if (a !== null && d > this.maxReduceDepth)
|
|
1670
|
+
return this.reportCascade(
|
|
1671
|
+
"maxReduceDepth",
|
|
1672
|
+
this.maxReduceDepth,
|
|
1673
|
+
{
|
|
1674
|
+
channel: t,
|
|
1675
|
+
type: s,
|
|
1676
|
+
payload: n,
|
|
1677
|
+
id: i,
|
|
1678
|
+
...r?.meta !== void 0 ? { meta: r.meta } : {},
|
|
1679
|
+
parentId: a.id,
|
|
1680
|
+
depth: d
|
|
1681
|
+
},
|
|
1682
|
+
d,
|
|
1683
|
+
a.chain
|
|
1684
|
+
), $;
|
|
1685
|
+
let u;
|
|
1686
|
+
const f = new Promise((m) => {
|
|
1687
|
+
u = m;
|
|
1688
|
+
});
|
|
1689
|
+
return this.reduceQueue.push({
|
|
1690
|
+
channel: t,
|
|
1691
|
+
type: s,
|
|
1692
|
+
payload: n,
|
|
1693
|
+
id: i,
|
|
1694
|
+
meta: r?.meta,
|
|
1695
|
+
resolve: u,
|
|
1696
|
+
// Only carried for caused events, so a root event's object stays byte-identical to one
|
|
1697
|
+
// built before causality existed — the same rule `meta` follows.
|
|
1698
|
+
...a !== null ? { parentId: a.id, depth: d, chain: a.chain } : {}
|
|
1699
|
+
}), this.drainReduce(), f;
|
|
1700
|
+
}
|
|
1701
|
+
/**
|
|
1702
|
+
* Drains the reduce queue **synchronously**. For each event it runs middleware,
|
|
1703
|
+
* reducers, event subscribers, and coarse listeners in the same tick, so
|
|
1704
|
+
* `getState()` reflects the change the moment {@link emit} returns. Re-entrant
|
|
1705
|
+
* emits (from middleware or subscribers) are appended and drained in the same
|
|
1706
|
+
* pass — preserving FIFO order without interleaving reducers. Each committed
|
|
1707
|
+
* event's effects then run in an independent task (see {@link runEventEffects}).
|
|
1708
|
+
*
|
|
1709
|
+
* @internal
|
|
1710
|
+
*/
|
|
1711
|
+
drainReduce() {
|
|
1712
|
+
if (!this.isReducing) {
|
|
1713
|
+
this.isReducing = !0, this.transitionsThisDrain = 0;
|
|
1714
|
+
try {
|
|
1715
|
+
for (; this.reduceQueue.length > 0; ) {
|
|
1716
|
+
const e = this.reduceQueue.shift(), { channel: t, type: s, payload: n, id: r, meta: o, resolve: l, parentId: i, depth: a, chain: d } = e, u = {
|
|
1717
|
+
channel: t,
|
|
1718
|
+
type: s,
|
|
1719
|
+
payload: n,
|
|
1720
|
+
id: r,
|
|
1721
|
+
...o !== void 0 ? { meta: o } : {},
|
|
1722
|
+
...i !== void 0 ? { parentId: i, depth: a } : {}
|
|
1723
|
+
};
|
|
1724
|
+
if (i !== void 0 && ++this.transitionsThisDrain > this.maxTransitionsPerDrain) {
|
|
1725
|
+
this.reportCascade(
|
|
1726
|
+
"maxTransitionsPerDrain",
|
|
1727
|
+
this.maxTransitionsPerDrain,
|
|
1728
|
+
u,
|
|
1729
|
+
a,
|
|
1730
|
+
d
|
|
1731
|
+
), l($);
|
|
1732
|
+
continue;
|
|
1733
|
+
}
|
|
1734
|
+
this.currentEvent = {
|
|
1735
|
+
id: r,
|
|
1736
|
+
depth: a ?? 0,
|
|
1737
|
+
chain: [...d ?? [], r].slice(-z)
|
|
1738
|
+
};
|
|
1739
|
+
const f = this.instrumentObservers.size > 0, m = f ? this.state : void 0, h = f ? [] : void 0;
|
|
1740
|
+
h !== void 0 && (this.changedPathSink = h);
|
|
1741
|
+
const y = f ? A() : 0;
|
|
1742
|
+
let g = $;
|
|
1743
|
+
try {
|
|
1744
|
+
g = this.applyEventSync(u);
|
|
1745
|
+
} catch (w) {
|
|
1746
|
+
console.error("Emit reduce error:", w);
|
|
1747
|
+
} finally {
|
|
1748
|
+
f && (this.changedPathSink = null), this.currentEvent = null;
|
|
1749
|
+
}
|
|
1750
|
+
f && this.emitInstrumentation(
|
|
1751
|
+
u,
|
|
1752
|
+
g,
|
|
1753
|
+
h ?? [],
|
|
1754
|
+
m,
|
|
1755
|
+
A() - y
|
|
1756
|
+
), this.runEventEffects(u, g, l);
|
|
1757
|
+
}
|
|
1758
|
+
} finally {
|
|
1759
|
+
this.isReducing = !1;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
/**
|
|
1764
|
+
* Runs the **synchronous** part of the pipeline for a single event: middleware
|
|
1765
|
+
* (may veto), key- and pattern-based reducers, committed/uncommitted event
|
|
1766
|
+
* subscribers (fire-and-forget), and coarse listeners.
|
|
1767
|
+
*
|
|
1768
|
+
* @returns `true` if the event was committed (passed middleware), `false` if a
|
|
1769
|
+
* middleware vetoed it.
|
|
1770
|
+
*
|
|
1771
|
+
* @internal
|
|
1772
|
+
*/
|
|
1773
|
+
applyEventSync(e) {
|
|
1774
|
+
for (const o of this.middleware) {
|
|
1775
|
+
const l = this.getMiddlewareWhen(o);
|
|
1776
|
+
if (!this.matchesWhen(l, e)) continue;
|
|
1777
|
+
const i = this.getMiddlewareFunction(o);
|
|
1778
|
+
let a;
|
|
1779
|
+
try {
|
|
1780
|
+
a = i(this.state, e, this.emit), process.env.NODE_ENV !== "production" && typeof a?.then == "function" && console.error(
|
|
1781
|
+
`[yoltra] Middleware for "${e.channel}/${e.type}" returned a Promise. Middleware is synchronous: a Promise is truthy, so this event was allowed without waiting and a "return false" inside it can never veto. Do the check synchronously, and put anything that must await in an effect.`
|
|
1782
|
+
);
|
|
1783
|
+
} catch (d) {
|
|
1784
|
+
console.error("Middleware error:", d), a = !1;
|
|
1785
|
+
}
|
|
1786
|
+
if (!a)
|
|
1787
|
+
return this.notifyEventSubscribers(e, "uncommitted"), $;
|
|
1788
|
+
}
|
|
1789
|
+
const t = [];
|
|
1790
|
+
this.stagingSink = t;
|
|
1791
|
+
let s = null, n = "";
|
|
1792
|
+
try {
|
|
1793
|
+
this.reducerBus.emit(
|
|
1794
|
+
e.channel,
|
|
1795
|
+
e.type,
|
|
1796
|
+
e.payload,
|
|
1797
|
+
e
|
|
1798
|
+
), s = this.stagedRejection, n = this.stagedRejectedBy;
|
|
1799
|
+
for (const [o, l] of this.patternReducers) {
|
|
1800
|
+
if (s !== null) break;
|
|
1801
|
+
if (this.matchesWhen(l, e)) {
|
|
1802
|
+
const i = this.stageSliceGuarded(o, e, t);
|
|
1803
|
+
i !== null && (s = i, n = o);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
} finally {
|
|
1807
|
+
this.stagingSink = null, this.stagedRejection = null, this.stagedRejectedBy = "";
|
|
1808
|
+
}
|
|
1809
|
+
if (s !== null)
|
|
1810
|
+
return this.onRejected?.(s, e, n), this.notifyEventSubscribers(e, "committed"), { committed: !0, written: !1, rejected: s };
|
|
1811
|
+
const r = this.commitStaged(t, e);
|
|
1812
|
+
return this.notifyEventSubscribers(e, "committed"), r && (this.notifyEventSubscribers(e, "written"), this.listeners.forEach((o) => o())), r ? se : ne;
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* Runs a single committed event's effects as an **independent async task**,
|
|
1816
|
+
* then resolves that event's completion deferred so `await emit(...)` settles
|
|
1817
|
+
* once its effects finish. Per-event tasks (rather than one shared serialized
|
|
1818
|
+
* loop) let an effect `await` a re-entrant emit without deadlocking.
|
|
1819
|
+
*
|
|
1820
|
+
* @internal
|
|
1821
|
+
*/
|
|
1822
|
+
async runEventEffects(e, t, s) {
|
|
1823
|
+
this.inFlightEffects++;
|
|
1824
|
+
try {
|
|
1825
|
+
t.committed && await this.notifyEffects(e);
|
|
1826
|
+
} catch (n) {
|
|
1827
|
+
console.error("Effect error:", n);
|
|
1828
|
+
} finally {
|
|
1829
|
+
this.inFlightEffects--, s(t);
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* Registers an instrumentation observer. See {@link StoreInstance.instrument}.
|
|
1834
|
+
*
|
|
1835
|
+
* @public
|
|
1836
|
+
*/
|
|
1837
|
+
instrument(e) {
|
|
1838
|
+
return this.instrumentObservers.add(e), () => {
|
|
1839
|
+
this.instrumentObservers.delete(e);
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
/**
|
|
1843
|
+
* Builds an {@link InstrumentedEvent} from the reduce result and notifies
|
|
1844
|
+
* observers. `changedPaths` are the exact slice-prefixed leaf paths recorded
|
|
1845
|
+
* by {@link commitStaged} during this reduce, so DevTools patches need no
|
|
1846
|
+
* re-diff.
|
|
1847
|
+
*
|
|
1848
|
+
* @internal
|
|
1849
|
+
*/
|
|
1850
|
+
emitInstrumentation(e, t, s, n, r) {
|
|
1851
|
+
const o = {}, l = {};
|
|
1852
|
+
for (const a of s)
|
|
1853
|
+
o[a] = this.getAtPath(n, a), l[a] = this.getAtPath(this.state, a);
|
|
1854
|
+
const i = {
|
|
1855
|
+
event: {
|
|
1856
|
+
id: e.id,
|
|
1857
|
+
channel: e.channel,
|
|
1858
|
+
type: e.type,
|
|
1859
|
+
payload: e.payload,
|
|
1860
|
+
// Conditional, so an event without metadata produces an observer payload
|
|
1861
|
+
// byte-identical to the pre-`meta` shape.
|
|
1862
|
+
...e.meta !== void 0 ? { meta: e.meta } : {}
|
|
1863
|
+
},
|
|
1864
|
+
committed: t.committed,
|
|
1865
|
+
changedPaths: s,
|
|
1866
|
+
prevValues: o,
|
|
1867
|
+
nextValues: l,
|
|
1868
|
+
reduceTimeMs: r,
|
|
1869
|
+
// Present only when a reducer refused, so an observer can tell a refusal from a veto —
|
|
1870
|
+
// identical in state, entirely different in cause.
|
|
1871
|
+
...t.rejected !== void 0 ? { rejected: t.rejected } : {}
|
|
1872
|
+
};
|
|
1873
|
+
for (const a of [...this.instrumentObservers])
|
|
1874
|
+
try {
|
|
1875
|
+
a(i);
|
|
1876
|
+
} catch (d) {
|
|
1877
|
+
console.error("Instrumentation observer error:", d);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Connects a **fine-grained** listener to a dotted path under a slice.
|
|
1882
|
+
*
|
|
1883
|
+
* @param spec - `{ reducer, property }` where `property` is a dotted path (e.g., `"items.0.title"`).
|
|
1884
|
+
* Supports wildcards: `*` (one segment) and `**` (zero or more segments).
|
|
1885
|
+
* @param h - Handler receiving a {@link Change} with `{ oldValue, newValue, path }`.
|
|
1886
|
+
* @returns Unsubscribe function.
|
|
1887
|
+
*
|
|
1888
|
+
* @example Exact path
|
|
1889
|
+
* ```ts
|
|
1890
|
+
* const off = store.connect(
|
|
1891
|
+
* { reducer: 'todos', property: 'items.0.title' },
|
|
1892
|
+
* (chg) => console.log('title changed:', chg.newValue)
|
|
1893
|
+
* );
|
|
1894
|
+
* off();
|
|
1895
|
+
* ```
|
|
1896
|
+
*
|
|
1897
|
+
* @example Wildcard pattern
|
|
1898
|
+
* ```ts
|
|
1899
|
+
* // Listen to any item title change
|
|
1900
|
+
* const off = store.connect(
|
|
1901
|
+
* { reducer: 'todos', property: 'items.*.title' },
|
|
1902
|
+
* (chg) => console.log('some title changed')
|
|
1903
|
+
* );
|
|
1904
|
+
* ```
|
|
1905
|
+
*
|
|
1906
|
+
* @public
|
|
1907
|
+
*/
|
|
1908
|
+
connect(e, t, s) {
|
|
1909
|
+
const n = this.connectorBus.on(e.reducer, e.property, t);
|
|
1910
|
+
if (s?.immediate === !0) {
|
|
1911
|
+
const r = this.state[e.reducer], o = e.property.includes("*") ? "" : e.property;
|
|
1912
|
+
t({ oldValue: void 0, newValue: this.getAtPath(r, o), path: o });
|
|
1913
|
+
}
|
|
1914
|
+
return n;
|
|
1915
|
+
}
|
|
1916
|
+
/**
|
|
1917
|
+
* Subscribe to events by channel and type.
|
|
1918
|
+
*
|
|
1919
|
+
* Event subscriptions are intended for the View layer (e.g., React components)
|
|
1920
|
+
* to react to events without affecting the event flow. They are fire-and-forget
|
|
1921
|
+
* and cannot cancel event propagation.
|
|
1922
|
+
*
|
|
1923
|
+
* **Phases:**
|
|
1924
|
+
* - `'committed'` (default): Events that passed middleware and reached reducers.
|
|
1925
|
+
* Notified after reducers, before effects.
|
|
1926
|
+
* - `'uncommitted'`: Events rejected by middleware. Notified immediately after rejection.
|
|
1927
|
+
* - `'all'`: Both committed and uncommitted events. Handler receives the phase parameter
|
|
1928
|
+
* to distinguish between the two.
|
|
1929
|
+
*
|
|
1930
|
+
* @typeParam C - Channel key within `EM`.
|
|
1931
|
+
* @typeParam T - Event type key within channel `C`.
|
|
1932
|
+
* @param channel - Channel to subscribe to.
|
|
1933
|
+
* @param type - Event type to subscribe to.
|
|
1934
|
+
* @param handler - Handler function `(event, getState, emit, phase)`.
|
|
1935
|
+
* @param phase - Event phase to subscribe to (default: `'committed'`).
|
|
1936
|
+
* @returns Unsubscribe function.
|
|
1937
|
+
*
|
|
1938
|
+
* @example Committed events (default)
|
|
1939
|
+
* ```ts
|
|
1940
|
+
* const off = store.onEvent('ui', 'save', (event, getState, emit, phase) => {
|
|
1941
|
+
* console.log('Save committed:', event.payload);
|
|
1942
|
+
* });
|
|
1943
|
+
* off();
|
|
1944
|
+
* ```
|
|
1945
|
+
*
|
|
1946
|
+
* @example Uncommitted (rejected) events
|
|
1947
|
+
* ```ts
|
|
1948
|
+
* store.onEvent('ui', 'delete', (event, getState, emit, phase) => {
|
|
1949
|
+
* console.log('Delete was rejected by middleware');
|
|
1950
|
+
* }, 'uncommitted');
|
|
1951
|
+
* ```
|
|
1952
|
+
*
|
|
1953
|
+
* @example All events
|
|
1954
|
+
* ```ts
|
|
1955
|
+
* store.onEvent('ui', 'action', (event, getState, emit, phase) => {
|
|
1956
|
+
* console.log('Action:', phase); // 'committed' or 'uncommitted'
|
|
1957
|
+
* }, 'all');
|
|
1958
|
+
* ```
|
|
1959
|
+
*
|
|
1960
|
+
* @public
|
|
1961
|
+
*/
|
|
1962
|
+
onEvent(e, t, s, n = "committed") {
|
|
1963
|
+
const r = `${e}::${String(t)}`, o = n === "committed" ? this.committedEventSubscribers : n === "uncommitted" ? this.uncommittedEventSubscribers : n === "written" ? this.writtenEventSubscribers : this.allEventSubscribers;
|
|
1964
|
+
return o.has(r) || o.set(r, /* @__PURE__ */ new Set()), o.get(r).add(s), () => {
|
|
1965
|
+
const l = o.get(r);
|
|
1966
|
+
l && (l.delete(s), l.size === 0 && o.delete(r));
|
|
1967
|
+
};
|
|
1968
|
+
}
|
|
1969
|
+
/**
|
|
1970
|
+
* Subscribes to **coarse-grained** commits (called once per successful event, only if state changed).
|
|
1971
|
+
*
|
|
1972
|
+
* **Use Case**: React's `useSyncExternalStore` or similar external store integrations.
|
|
1973
|
+
*
|
|
1974
|
+
* @param fn - Listener invoked after reducers/effects have run and state has changed.
|
|
1975
|
+
* @returns Unsubscribe function.
|
|
1976
|
+
*
|
|
1977
|
+
* @example
|
|
1978
|
+
* ```ts
|
|
1979
|
+
* const off = store.subscribe(() => console.log('state committed'));
|
|
1980
|
+
* // Later:
|
|
1981
|
+
* off();
|
|
1982
|
+
* ```
|
|
1983
|
+
*
|
|
1984
|
+
* @public
|
|
1985
|
+
*/
|
|
1986
|
+
subscribe(e) {
|
|
1987
|
+
return this.listeners.add(e), () => this.listeners.delete(e);
|
|
1988
|
+
}
|
|
1989
|
+
/**
|
|
1990
|
+
* Returns the current immutable state snapshot.
|
|
1991
|
+
*
|
|
1992
|
+
* @returns Deep-readonly state object.
|
|
1993
|
+
*
|
|
1994
|
+
* @example
|
|
1995
|
+
* ```ts
|
|
1996
|
+
* const state = store.getState();
|
|
1997
|
+
* console.log(state.counter.value);
|
|
1998
|
+
* ```
|
|
1999
|
+
*
|
|
2000
|
+
* @public
|
|
2001
|
+
*/
|
|
2002
|
+
getState() {
|
|
2003
|
+
return this.state;
|
|
2004
|
+
}
|
|
2005
|
+
/**
|
|
2006
|
+
* Registers a middleware (runs **before** reducers).
|
|
2007
|
+
*
|
|
2008
|
+
* @param mw - Middleware `(state, event, emit) => boolean`. Return `false` to cancel event
|
|
2009
|
+
* propagation.
|
|
2010
|
+
* @returns Unsubscribe function that removes this middleware.
|
|
2011
|
+
*
|
|
2012
|
+
* @remarks
|
|
2013
|
+
* **Synchronous, and that is the contract.** The reduce phase completes before `emit()`
|
|
2014
|
+
* returns, so the commit decision has to be available in the same tick. An `async` middleware
|
|
2015
|
+
* returns a Promise, every Promise is truthy, and the veto would therefore never fire — the
|
|
2016
|
+
* event would commit while the middleware was still deciding. The type rejects it; this note
|
|
2017
|
+
* exists because the examples here used to teach it. Do authorization and validation here, and
|
|
2018
|
+
* anything that needs to await in an effect.
|
|
2019
|
+
*
|
|
2020
|
+
* @example Logging middleware
|
|
2021
|
+
* ```ts
|
|
2022
|
+
* const off = store.registerMiddleware((state, event) => {
|
|
2023
|
+
* console.log('Event:', event.channel, event.type, event.payload);
|
|
2024
|
+
* return true; // allow
|
|
2025
|
+
* });
|
|
2026
|
+
* off();
|
|
2027
|
+
* ```
|
|
2028
|
+
*
|
|
2029
|
+
* @example Cancellation middleware
|
|
2030
|
+
* ```ts
|
|
2031
|
+
* store.registerMiddleware((state, event) => {
|
|
2032
|
+
* if (event.type === 'forbidden') return false; // cancel
|
|
2033
|
+
* return true;
|
|
2034
|
+
* });
|
|
2035
|
+
* ```
|
|
2036
|
+
*
|
|
2037
|
+
* @public
|
|
2038
|
+
*/
|
|
2039
|
+
registerMiddleware(e) {
|
|
2040
|
+
return this.middleware.push(e), () => {
|
|
2041
|
+
const t = this.middleware.indexOf(e);
|
|
2042
|
+
t !== -1 && this.middleware.splice(t, 1);
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
2045
|
+
/**
|
|
2046
|
+
* Dynamically **adds** a named slice reducer at runtime.
|
|
2047
|
+
*
|
|
2048
|
+
* @param name - New slice name (must not already exist).
|
|
2049
|
+
* @param spec - Reducer spec (state, when, reducer).
|
|
2050
|
+
* @returns Disposer function that **removes** the slice (and its state).
|
|
2051
|
+
*
|
|
2052
|
+
* @example
|
|
2053
|
+
* ```ts
|
|
2054
|
+
* const dispose = store.registerReducer('filters', {
|
|
2055
|
+
* state: { q: '' },
|
|
2056
|
+
* events: [['ui', 'setQuery']],
|
|
2057
|
+
* reducer(s, evt) {
|
|
2058
|
+
* return evt.type === 'setQuery' ? { q: evt.payload } : s;
|
|
2059
|
+
* }
|
|
2060
|
+
* });
|
|
2061
|
+
* // Later:
|
|
2062
|
+
* dispose();
|
|
2063
|
+
* ```
|
|
2064
|
+
*
|
|
2065
|
+
* @public
|
|
2066
|
+
*/
|
|
2067
|
+
registerReducer(e, t) {
|
|
2068
|
+
if (Object.prototype.hasOwnProperty.call(this.reducers, e))
|
|
2069
|
+
throw new Error(`Reducer ${e} already exists`);
|
|
2070
|
+
return this.mountSlice(e, t, {
|
|
2071
|
+
preserveState: !1
|
|
2072
|
+
}), this.listeners.forEach((s) => s()), () => {
|
|
2073
|
+
this.unmountSlice(e, { deleteState: !0 }), this.listeners.forEach((s) => s());
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
/**
|
|
2077
|
+
* Registers an **effect** (stateless async event consumer) that runs after reducers.
|
|
2078
|
+
*
|
|
2079
|
+
* Effects are **keyed** by `(channel, type)` for O(1) lookup (no scanning all effects).
|
|
2080
|
+
*
|
|
2081
|
+
* @param spec - Effect specification with `when` targeting and `effect` (handler).
|
|
2082
|
+
* @returns Unsubscribe function.
|
|
2083
|
+
*
|
|
2084
|
+
* @example Logging effect
|
|
2085
|
+
* ```ts
|
|
2086
|
+
* const off = store.registerEffect({
|
|
2087
|
+
* events: [['ui', 'increment']],
|
|
2088
|
+
* effect: async (evt, getState, emit) => {
|
|
2089
|
+
* console.log('increment', evt.payload, getState().counter.value);
|
|
2090
|
+
* }
|
|
2091
|
+
* });
|
|
2092
|
+
* off();
|
|
2093
|
+
* ```
|
|
2094
|
+
*
|
|
2095
|
+
* @example Multi-event effect
|
|
2096
|
+
* ```ts
|
|
2097
|
+
* store.registerEffect({
|
|
2098
|
+
* events: [['ui', 'increment'], ['ui', 'decrement']],
|
|
2099
|
+
* effect: async (evt, getState, emit) => {
|
|
2100
|
+
* // Runs for both increment and decrement
|
|
2101
|
+
* await saveToServer(getState());
|
|
2102
|
+
* }
|
|
2103
|
+
* });
|
|
2104
|
+
* ```
|
|
2105
|
+
*
|
|
2106
|
+
* @public
|
|
2107
|
+
*/
|
|
2108
|
+
/**
|
|
2109
|
+
* Sends a request and waits for the reply, correlating the two automatically.
|
|
2110
|
+
*
|
|
2111
|
+
* @typeParam C - Request channel.
|
|
2112
|
+
* @typeParam T - Request type within `C`.
|
|
2113
|
+
* @param channel - Channel to send on.
|
|
2114
|
+
* @param type - Event type to send.
|
|
2115
|
+
* @param payload - The **request** payload. This is what you are sending; what comes back is
|
|
2116
|
+
* described by {@link CallOptions.reply}, not by this.
|
|
2117
|
+
* @param opts - Which replies end the call, and how long to wait. See {@link CallOptions}.
|
|
2118
|
+
* @returns A {@link CallHandle}: `await` it for the terminal reply, or `for await` it for
|
|
2119
|
+
* progress events as they arrive.
|
|
2120
|
+
*
|
|
2121
|
+
* @remarks
|
|
2122
|
+
* Every consumer of an event bus eventually writes request/reply by hand — mint an id,
|
|
2123
|
+
* subscribe, match, time out, unsubscribe — and every one of them writes the same eighty lines
|
|
2124
|
+
* with the same two bugs: the subscription outlives the call, and a responder that forgets to
|
|
2125
|
+
* echo the id produces a timeout with nothing to point at. This is that, once.
|
|
2126
|
+
*
|
|
2127
|
+
* **Correlation is causal.** The store stamps `parentId` on anything emitted while an event is
|
|
2128
|
+
* being handled, so a responder that replies through the `emit` it was handed is already
|
|
2129
|
+
* correlated. There is no id to mint, echo, or forget:
|
|
2130
|
+
*
|
|
2131
|
+
* ```ts
|
|
2132
|
+
* store.registerEffect({
|
|
2133
|
+
* when: { keys: [["rpc", "ask"]] },
|
|
2134
|
+
* effect: async (event, _get, emit) => {
|
|
2135
|
+
* await emit("rpc", "answer", await lookup(event.payload.q));
|
|
2136
|
+
* },
|
|
2137
|
+
* });
|
|
2138
|
+
* ```
|
|
2139
|
+
*
|
|
2140
|
+
* **The reply carries its own discriminant.** A call resolves to the *event*, not the payload,
|
|
2141
|
+
* because a caller often cannot know which kind of reply it will get:
|
|
2142
|
+
*
|
|
2143
|
+
* ```ts
|
|
2144
|
+
* const res = await store.call("rpc", "ask", { q }, { reply: ["rpc", ["answer", "error"]] });
|
|
2145
|
+
* switch (res.type) {
|
|
2146
|
+
* case "answer": return res.payload;
|
|
2147
|
+
* case "error": throw new Error(res.payload.reason);
|
|
2148
|
+
* }
|
|
2149
|
+
* ```
|
|
2150
|
+
*
|
|
2151
|
+
* **Progress streams, with backpressure.** Any correlated event that is not terminal is
|
|
2152
|
+
* progress, and iterating the call consumes it. The producer genuinely waits: `emit` resolves
|
|
2153
|
+
* only once its effects have run, and the collector is an effect that does not return until the
|
|
2154
|
+
* consumer has taken the item. A responder writing `await emit("rpc", "progress", chunk)` is
|
|
2155
|
+
* therefore paced by the reader, with nothing buffering without bound.
|
|
2156
|
+
*
|
|
2157
|
+
* ```ts
|
|
2158
|
+
* const call = store.call("job", "start", { id }, {
|
|
2159
|
+
* reply: ["job", "done"],
|
|
2160
|
+
* highWaterMark: 4,
|
|
2161
|
+
* });
|
|
2162
|
+
* for await (const step of call) await render(step.payload); // producer waits on this
|
|
2163
|
+
* const { payload } = await call;
|
|
2164
|
+
* ```
|
|
2165
|
+
*
|
|
2166
|
+
* Backpressure engages **once you begin iterating**. A call that is only awaited never pulls,
|
|
2167
|
+
* so blocking its producer would deadlock the call itself — progress nobody reads would stop
|
|
2168
|
+
* the terminal event from ever being sent. Un-iterated progress therefore buffers to
|
|
2169
|
+
* `highWaterMark` and is then counted on {@link CallHandle.dropped} rather than blocking.
|
|
2170
|
+
*
|
|
2171
|
+
* **This is a local primitive.** A reply cannot reach it from a federated peer: the federation
|
|
2172
|
+
* envelope carries neither `meta` nor `parentId`, and ingress namespaces the channel, so
|
|
2173
|
+
* neither correlation nor the reply route survives the hop. That is not an oversight to route
|
|
2174
|
+
* around — federation answers cross-node request/reply with typed peer *queries*, which are
|
|
2175
|
+
* gated by a responder policy that may concede or deny. A call that federated silently would
|
|
2176
|
+
* turn that access decision into an accident of which channel someone named. Ask a peer with a
|
|
2177
|
+
* query; use `call` within a process.
|
|
2178
|
+
*
|
|
2179
|
+
* @example Timeout is idle, not total
|
|
2180
|
+
* ```ts
|
|
2181
|
+
* // Survives a job that streams for minutes; fails a responder that goes quiet for 5s.
|
|
2182
|
+
* await store.call("job", "start", { id }, { reply: ["job", "done"], timeoutMs: 5_000 });
|
|
2183
|
+
* ```
|
|
2184
|
+
*
|
|
2185
|
+
* @example Cancelling
|
|
2186
|
+
* ```ts
|
|
2187
|
+
* const call = store.call("rpc", "ask", { q }, { reply: ["rpc", "answer"] });
|
|
2188
|
+
* useEffect(() => () => call.cancel("unmounted"), [call]);
|
|
2189
|
+
* ```
|
|
2190
|
+
*
|
|
2191
|
+
* @public
|
|
2192
|
+
*/
|
|
2193
|
+
call(e, t, s, n) {
|
|
2194
|
+
const { channel: r, isTerminal: o } = q(n.reply), l = n.timeoutMs ?? ee, i = new G(n.highWaterMark ?? te), a = this.idFactory();
|
|
2195
|
+
let d, u, f = !1;
|
|
2196
|
+
const m = new Promise((b, S) => {
|
|
2197
|
+
d = b, u = S;
|
|
2198
|
+
});
|
|
2199
|
+
m.catch(() => {
|
|
2200
|
+
});
|
|
2201
|
+
let h = null, y = null;
|
|
2202
|
+
const g = (b, S = !1) => {
|
|
2203
|
+
f || (f = !0, h !== null && clearTimeout(h), h = null, y?.(), y = null, S ? i.end() : i.close(), n.signal?.removeEventListener("abort", w), b());
|
|
2204
|
+
};
|
|
2205
|
+
function w() {
|
|
2206
|
+
g(() => u(new C(String(n.signal?.reason ?? "signal aborted"))));
|
|
2207
|
+
}
|
|
2208
|
+
const k = () => {
|
|
2209
|
+
h !== null && clearTimeout(h), h = setTimeout(() => {
|
|
2210
|
+
g(() => u(new J(e, t, l)));
|
|
2211
|
+
}, l), h.unref?.();
|
|
2212
|
+
};
|
|
2213
|
+
return y = this.registerEffect({
|
|
2214
|
+
// A pattern effect on the reply channel: which types are terminal is known, which are
|
|
2215
|
+
// progress is not, so the filter cannot be a key list.
|
|
2216
|
+
when: { channel: r },
|
|
2217
|
+
effect: async (b) => {
|
|
2218
|
+
if (!f && Y(b, a, n.correlationId)) {
|
|
2219
|
+
if (k(), o(String(b.type))) {
|
|
2220
|
+
g(() => d(b), !0);
|
|
2221
|
+
return;
|
|
2222
|
+
}
|
|
2223
|
+
await i.put(b);
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
}), n.signal !== void 0 && (n.signal.aborted ? w() : n.signal.addEventListener("abort", w, { once: !0 })), k(), this.emit(e, t, s, {
|
|
2227
|
+
id: a,
|
|
2228
|
+
...n.correlationId !== void 0 ? { meta: { correlationId: n.correlationId } } : {}
|
|
2229
|
+
}), {
|
|
2230
|
+
then: (b, S) => m.then(b, S),
|
|
2231
|
+
catch: (b) => m.catch(b),
|
|
2232
|
+
finally: (b) => m.finally(b),
|
|
2233
|
+
get dropped() {
|
|
2234
|
+
return i.droppedCount;
|
|
2235
|
+
},
|
|
2236
|
+
cancel: (b = "cancelled") => {
|
|
2237
|
+
g(() => u(new C(b)));
|
|
2238
|
+
},
|
|
2239
|
+
[Symbol.asyncIterator]: () => (i.beginConsuming(), {
|
|
2240
|
+
next: () => i.take(),
|
|
2241
|
+
// Called by `for await` on `break`, `return` or a throw. Without it, abandoning the
|
|
2242
|
+
// loop would leave the effect registered and the producer parked for good.
|
|
2243
|
+
return: async () => (i.close(), { value: void 0, done: !0 })
|
|
2244
|
+
})
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
registerEffect(e) {
|
|
2248
|
+
const { effect: t, meta: s, when: n } = e, r = [];
|
|
2249
|
+
if (s && this.effectMeta.set(t, s), n && ("any" in n && n.any === !0 || "channel" in n || "channels" in n)) {
|
|
2250
|
+
const i = { effect: t, when: n };
|
|
2251
|
+
return this.patternEffects.add(i), () => {
|
|
2252
|
+
this.patternEffects.delete(i);
|
|
2253
|
+
};
|
|
2254
|
+
}
|
|
2255
|
+
const l = this.normalizeEventKeys(e);
|
|
2256
|
+
if (l.length === 0 && !n) {
|
|
2257
|
+
const i = { effect: t, when: { any: !0 } };
|
|
2258
|
+
return this.patternEffects.add(i), () => {
|
|
2259
|
+
this.patternEffects.delete(i);
|
|
2260
|
+
};
|
|
2261
|
+
}
|
|
2262
|
+
for (const [i, a] of l) {
|
|
2263
|
+
const d = `${String(i)}::${String(a)}`;
|
|
2264
|
+
this.effects.has(d) || this.effects.set(d, /* @__PURE__ */ new Set()), this.effects.get(d).add(t), r.push(() => {
|
|
2265
|
+
const u = this.effects.get(d);
|
|
2266
|
+
u && (u.delete(t), u.size === 0 && this.effects.delete(d));
|
|
2267
|
+
});
|
|
2268
|
+
}
|
|
2269
|
+
return () => {
|
|
2270
|
+
for (const i of r) i();
|
|
2271
|
+
};
|
|
2272
|
+
}
|
|
2273
|
+
/**
|
|
2274
|
+
* Convenience helper to register an **effect** filtered by a single `(channel, type)` pair.
|
|
2275
|
+
*
|
|
2276
|
+
* @typeParam C - Channel key within `EM`.
|
|
2277
|
+
* @typeParam T - Event type key within channel `C`.
|
|
2278
|
+
* @param channel - Channel to filter.
|
|
2279
|
+
* @param type - Event type to filter.
|
|
2280
|
+
* @param handler - Effect handler `(payload, getState, emit, event)`.
|
|
2281
|
+
* @returns Unsubscribe/teardown function.
|
|
2282
|
+
*
|
|
2283
|
+
* @example
|
|
2284
|
+
* ```ts
|
|
2285
|
+
* const off = store.onEffect('ui', 'increment', async (n, get, emit) => {
|
|
2286
|
+
* if (n > 10) await emit('ui', 'increment', -10);
|
|
2287
|
+
* });
|
|
2288
|
+
* // later
|
|
2289
|
+
* off();
|
|
2290
|
+
* ```
|
|
2291
|
+
*
|
|
2292
|
+
* @public
|
|
2293
|
+
*/
|
|
2294
|
+
onEffect(e, t, s) {
|
|
2295
|
+
const n = async (r, o, l) => {
|
|
2296
|
+
if (r.channel !== e || r.type !== t) return;
|
|
2297
|
+
const i = r;
|
|
2298
|
+
return s(i.payload, o, l, i);
|
|
2299
|
+
};
|
|
2300
|
+
return this.registerEffect({
|
|
2301
|
+
when: { keys: [[e, t]] },
|
|
2302
|
+
effect: n
|
|
2303
|
+
});
|
|
2304
|
+
}
|
|
2305
|
+
/**
|
|
2306
|
+
* Replaces the **entire** middleware pipeline (HMR-friendly).
|
|
2307
|
+
*
|
|
2308
|
+
* @param next - New middleware array.
|
|
2309
|
+
*
|
|
2310
|
+
* @example Hot module replacement
|
|
2311
|
+
* ```ts
|
|
2312
|
+
* if (import.meta.hot) {
|
|
2313
|
+
* import.meta.hot.accept('./middleware', (newModule) => {
|
|
2314
|
+
* store.replaceMiddleware(newModule.middleware);
|
|
2315
|
+
* });
|
|
2316
|
+
* }
|
|
2317
|
+
* ```
|
|
2318
|
+
*
|
|
2319
|
+
* @public
|
|
2320
|
+
*/
|
|
2321
|
+
replaceMiddleware(e) {
|
|
2322
|
+
this.middleware.length = 0;
|
|
2323
|
+
for (const t of e) this.middleware.push(t);
|
|
2324
|
+
}
|
|
2325
|
+
/**
|
|
2326
|
+
* Replaces all registered **effects** (HMR-friendly).
|
|
2327
|
+
*
|
|
2328
|
+
* @param next - New effects array (as EffectSpecs).
|
|
2329
|
+
*
|
|
2330
|
+
* @example Hot module replacement
|
|
2331
|
+
* ```ts
|
|
2332
|
+
* if (import.meta.hot) {
|
|
2333
|
+
* import.meta.hot.accept('./effects', (newModule) => {
|
|
2334
|
+
* store.replaceEffects(newModule.effects);
|
|
2335
|
+
* });
|
|
2336
|
+
* }
|
|
2337
|
+
* ```
|
|
2338
|
+
*
|
|
2339
|
+
* @public
|
|
2340
|
+
*/
|
|
2341
|
+
replaceEffects(e) {
|
|
2342
|
+
this.effects.clear(), this.patternEffects.clear();
|
|
2343
|
+
for (const t of e)
|
|
2344
|
+
this.registerEffect(t);
|
|
2345
|
+
}
|
|
2346
|
+
/**
|
|
2347
|
+
* Replaces the entire **reducer set** (HMR-friendly).
|
|
2348
|
+
*
|
|
2349
|
+
* @param next - Map of slice specs keyed by slice name.
|
|
2350
|
+
* @param opts - `{ preserveState?: boolean }` (default `true`).
|
|
2351
|
+
*
|
|
2352
|
+
* @example Hot module replacement
|
|
2353
|
+
* ```ts
|
|
2354
|
+
* if (import.meta.hot) {
|
|
2355
|
+
* import.meta.hot.accept('./reducers', (newModule) => {
|
|
2356
|
+
* store.replaceReducers(newModule.reducers, { preserveState: true });
|
|
2357
|
+
* });
|
|
2358
|
+
* }
|
|
2359
|
+
* ```
|
|
2360
|
+
*
|
|
2361
|
+
* @public
|
|
2362
|
+
*/
|
|
2363
|
+
replaceReducers(e, t = {}) {
|
|
2364
|
+
const s = t.preserveState !== !1, n = new Set(Object.keys(this.reducers)), r = Object.entries(e), o = new Set(r.map(([l]) => l));
|
|
2365
|
+
for (const l of n)
|
|
2366
|
+
o.has(l) || this.unmountSlice(l, { deleteState: !0 });
|
|
2367
|
+
for (const [l, i] of r)
|
|
2368
|
+
n.has(l) ? (this.unmountSlice(l, { deleteState: !1 }), this.mountSlice(l, i, { preserveState: s })) : this.mountSlice(l, i, { preserveState: !1 });
|
|
2369
|
+
}
|
|
2370
|
+
/**
|
|
2371
|
+
* Convenience API to replace **any subset** of store parts (HMR patterns).
|
|
2372
|
+
*
|
|
2373
|
+
* @param partial - Partial replacement set.
|
|
2374
|
+
*
|
|
2375
|
+
* @example Replace everything
|
|
2376
|
+
* ```ts
|
|
2377
|
+
* store.hotReplace({
|
|
2378
|
+
* reducer: newReducers,
|
|
2379
|
+
* middleware: newMiddleware,
|
|
2380
|
+
* effects: newEffects,
|
|
2381
|
+
* preserveState: true
|
|
2382
|
+
* });
|
|
2383
|
+
* ```
|
|
2384
|
+
*
|
|
2385
|
+
* @public
|
|
2386
|
+
*/
|
|
2387
|
+
hotReplace(e) {
|
|
2388
|
+
e.middleware && this.replaceMiddleware(e.middleware), e.effects && this.replaceEffects(e.effects), e.reducer && this.replaceReducers(e.reducer, { preserveState: e.preserveState });
|
|
2389
|
+
}
|
|
2390
|
+
/**
|
|
2391
|
+
* Mounts a slice: installs reducer, initializes state (unless preserved),
|
|
2392
|
+
* and wires `(channel, type)` listeners on the reducer bus.
|
|
2393
|
+
*
|
|
2394
|
+
* @param name - Slice name.
|
|
2395
|
+
* @param rSpec - Reducer spec (state, when, reducer).
|
|
2396
|
+
* @param opts - `{ preserveState: boolean }` whether to keep existing state.
|
|
2397
|
+
*
|
|
2398
|
+
* @internal
|
|
2399
|
+
*/
|
|
2400
|
+
mountSlice(e, t, s) {
|
|
2401
|
+
const n = e, { reducer: r, state: o, when: l } = t;
|
|
2402
|
+
if (this.reducers[e] = new V(r), (!s.preserveState || this.state[n] === void 0) && (this.state = {
|
|
2403
|
+
...this.state,
|
|
2404
|
+
[n]: R(X(n, o))
|
|
2405
|
+
}), l && ("any" in l && l.any === !0 || "channel" in l || "channels" in l)) {
|
|
2406
|
+
this.patternReducers.set(e, l), this.sliceUnsubs.set(n, []);
|
|
2407
|
+
return;
|
|
2408
|
+
}
|
|
2409
|
+
const a = this.normalizeEventKeys(t);
|
|
2410
|
+
if (a.length === 0 && !l) {
|
|
2411
|
+
this.patternReducers.set(e, { any: !0 }), this.sliceUnsubs.set(n, []);
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2414
|
+
const d = [];
|
|
2415
|
+
for (const [u, f] of a) {
|
|
2416
|
+
const m = this.reducerBus.on(u, f, (h, y) => {
|
|
2417
|
+
const g = y ?? {
|
|
2418
|
+
channel: u,
|
|
2419
|
+
type: f,
|
|
2420
|
+
payload: h,
|
|
2421
|
+
id: this.idFactory()
|
|
2422
|
+
};
|
|
2423
|
+
if (this.stagingSink === null) return;
|
|
2424
|
+
const w = this.stageSliceGuarded(e, g, this.stagingSink);
|
|
2425
|
+
w !== null && this.stagedRejection === null && (this.stagedRejection = w, this.stagedRejectedBy = e);
|
|
2426
|
+
});
|
|
2427
|
+
d.push(m);
|
|
2428
|
+
}
|
|
2429
|
+
this.sliceUnsubs.set(n, d);
|
|
2430
|
+
}
|
|
2431
|
+
/**
|
|
2432
|
+
* Unmounts a slice: disposes reducer-bus listeners, removes reducer,
|
|
2433
|
+
* and optionally deletes the slice state.
|
|
2434
|
+
*
|
|
2435
|
+
* @param name - Slice name.
|
|
2436
|
+
* @param opts - `{ deleteState: boolean }`.
|
|
2437
|
+
*
|
|
2438
|
+
* @internal
|
|
2439
|
+
*/
|
|
2440
|
+
unmountSlice(e, t) {
|
|
2441
|
+
const s = e;
|
|
2442
|
+
this.patternReducers.delete(e);
|
|
2443
|
+
const n = this.sliceUnsubs.get(s);
|
|
2444
|
+
if (n) {
|
|
2445
|
+
for (const r of n)
|
|
2446
|
+
try {
|
|
2447
|
+
r();
|
|
2448
|
+
} catch (o) {
|
|
2449
|
+
console.error(`[Store error]: ${o}`);
|
|
2450
|
+
}
|
|
2451
|
+
this.sliceUnsubs.delete(s);
|
|
2452
|
+
}
|
|
2453
|
+
if (delete this.reducers[e], t.deleteState) {
|
|
2454
|
+
const { [s]: r, ...o } = this.state;
|
|
2455
|
+
this.state = o;
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
/**
|
|
2459
|
+
* Normalizes event targeting from `when` to an array of EventKeys.
|
|
2460
|
+
*
|
|
2461
|
+
* @param spec - Object with an optional `when` matcher.
|
|
2462
|
+
* @returns Array of `[channel, type]` pairs.
|
|
2463
|
+
*
|
|
2464
|
+
* @internal
|
|
2465
|
+
*/
|
|
2466
|
+
normalizeEventKeys(e) {
|
|
2467
|
+
if (e.when) {
|
|
2468
|
+
const t = e.when;
|
|
2469
|
+
if ("keys" in t)
|
|
2470
|
+
return t.keys;
|
|
2471
|
+
}
|
|
2472
|
+
return [];
|
|
2473
|
+
}
|
|
2474
|
+
/**
|
|
2475
|
+
* Reads a dotted path from an object (supports numeric array indices via string keys).
|
|
2476
|
+
*
|
|
2477
|
+
* @param obj - Root object (slice or value).
|
|
2478
|
+
* @param path - Dotted path; leading dot is ignored.
|
|
2479
|
+
* @returns The value at the path, or `undefined`.
|
|
2480
|
+
*
|
|
2481
|
+
* @internal
|
|
2482
|
+
*/
|
|
2483
|
+
getAtPath(e, t) {
|
|
2484
|
+
if (!t) return e;
|
|
2485
|
+
const n = (t[0] === "." ? t.slice(1) : t).split(".");
|
|
2486
|
+
let r = e;
|
|
2487
|
+
for (const o of n) {
|
|
2488
|
+
if (r == null) return;
|
|
2489
|
+
r = r[o];
|
|
2490
|
+
}
|
|
2491
|
+
return r;
|
|
2492
|
+
}
|
|
2493
|
+
/**
|
|
2494
|
+
* Builds ancestor paths for a dotted path.
|
|
2495
|
+
*
|
|
2496
|
+
* For `"a.b.c"`, returns `["a", "a.b", "a.b.c"]`. Leading dots are trimmed.
|
|
2497
|
+
*
|
|
2498
|
+
* @param path - Dotted path string.
|
|
2499
|
+
* @returns Array of ancestor paths.
|
|
2500
|
+
*
|
|
2501
|
+
* @example
|
|
2502
|
+
* ```ts
|
|
2503
|
+
* Store.buildAncestorPaths('x.y.z'); // ['x','x.y','x.y.z']
|
|
2504
|
+
* ```
|
|
2505
|
+
*
|
|
2506
|
+
* @public
|
|
2507
|
+
*/
|
|
2508
|
+
static buildAncestorPaths(e) {
|
|
2509
|
+
if (!e) return [];
|
|
2510
|
+
const s = (e[0] === "." ? e.slice(1) : e).split("."), n = [];
|
|
2511
|
+
for (let r = 0; r < s.length; r++)
|
|
2512
|
+
n.push(s.slice(0, r + 1).join("."));
|
|
2513
|
+
return n;
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
function de(c) {
|
|
2517
|
+
return new x({
|
|
2518
|
+
...c,
|
|
2519
|
+
reducer: c.reducer ?? {},
|
|
2520
|
+
middleware: c.middleware ?? [],
|
|
2521
|
+
effects: c.effects ?? []
|
|
2522
|
+
});
|
|
2523
|
+
}
|
|
2524
|
+
const ue = (c) => (e, t) => t.map((s) => [e, s]), fe = () => (c) => c, _ = /* @__PURE__ */ new Set();
|
|
2525
|
+
function re(c) {
|
|
2526
|
+
const e = String(c);
|
|
2527
|
+
_.has(e) || (_.add(e), console.warn(
|
|
2528
|
+
`[yoltra] Entity id "${e}" contains a dot. Paths are dotted, so a subscription to "entities.${e}" is indistinguishable from one to a nested object of the same name. Use ids without dots.`
|
|
2529
|
+
));
|
|
2530
|
+
}
|
|
2531
|
+
function ie(c, e) {
|
|
2532
|
+
if (c.length !== e.length) return e;
|
|
2533
|
+
for (let t = 0; t < c.length; t++)
|
|
2534
|
+
if (c[t] !== e[t]) return e;
|
|
2535
|
+
return c;
|
|
2536
|
+
}
|
|
2537
|
+
function he(c = {}) {
|
|
2538
|
+
const e = c.selectId ?? ((i) => i.id), { sortComparer: t } = c, s = (i, a) => {
|
|
2539
|
+
if (t === void 0) return a;
|
|
2540
|
+
const d = [...a].sort((u, f) => {
|
|
2541
|
+
const m = i.entities[u], h = i.entities[f];
|
|
2542
|
+
return m === void 0 || h === void 0 ? 0 : t(m, h);
|
|
2543
|
+
});
|
|
2544
|
+
return ie(a, d);
|
|
2545
|
+
}, n = (i, a, d) => {
|
|
2546
|
+
const u = { ...i, entities: a, ids: d };
|
|
2547
|
+
return { ...u, ids: s(u, d) };
|
|
2548
|
+
}, r = (i, a, d) => {
|
|
2549
|
+
let u = null, f = null;
|
|
2550
|
+
for (const m of a) {
|
|
2551
|
+
const h = e(m);
|
|
2552
|
+
process.env.NODE_ENV !== "production" && String(h).includes(".") && re(h);
|
|
2553
|
+
const y = (u ?? i.entities)[h];
|
|
2554
|
+
if (y !== void 0 && d === "add") continue;
|
|
2555
|
+
const g = y !== void 0 && d === "upsert" ? { ...y, ...m } : m;
|
|
2556
|
+
u ?? (u = { ...i.entities }), u[h] = g, y === void 0 && (f ?? (f = [...i.ids]), f.push(h));
|
|
2557
|
+
}
|
|
2558
|
+
return u === null ? i : n(i, u, f ?? i.ids);
|
|
2559
|
+
}, o = (i, a) => {
|
|
2560
|
+
let d = null;
|
|
2561
|
+
for (const { id: u, changes: f } of a) {
|
|
2562
|
+
const m = (d ?? i.entities)[u];
|
|
2563
|
+
m !== void 0 && (d ?? (d = { ...i.entities }), d[u] = { ...m, ...f });
|
|
2564
|
+
}
|
|
2565
|
+
return d === null ? i : n(i, d, i.ids);
|
|
2566
|
+
}, l = (i, a) => {
|
|
2567
|
+
const d = new Set(a.filter((f) => i.entities[f] !== void 0));
|
|
2568
|
+
if (d.size === 0) return i;
|
|
2569
|
+
const u = { ...i.entities };
|
|
2570
|
+
for (const f of d) delete u[f];
|
|
2571
|
+
return n(
|
|
2572
|
+
i,
|
|
2573
|
+
u,
|
|
2574
|
+
i.ids.filter((f) => !d.has(f))
|
|
2575
|
+
);
|
|
2576
|
+
};
|
|
2577
|
+
return {
|
|
2578
|
+
getInitialState(i) {
|
|
2579
|
+
const a = { ids: [], entities: {} };
|
|
2580
|
+
return i === void 0 ? a : { ...a, ...i };
|
|
2581
|
+
},
|
|
2582
|
+
addOne: (i, a) => r(i, [a], "add"),
|
|
2583
|
+
addMany: (i, a) => r(i, a, "add"),
|
|
2584
|
+
setOne: (i, a) => r(i, [a], "set"),
|
|
2585
|
+
setMany: (i, a) => r(i, a, "set"),
|
|
2586
|
+
setAll: (i, a) => {
|
|
2587
|
+
const d = {}, u = [];
|
|
2588
|
+
for (const f of a) {
|
|
2589
|
+
const m = e(f);
|
|
2590
|
+
d[m] === void 0 && u.push(m), d[m] = f;
|
|
2591
|
+
}
|
|
2592
|
+
return n(i, d, u);
|
|
2593
|
+
},
|
|
2594
|
+
updateOne: (i, a) => o(i, [a]),
|
|
2595
|
+
updateMany: (i, a) => o(i, a),
|
|
2596
|
+
upsertOne: (i, a) => r(i, [a], "upsert"),
|
|
2597
|
+
upsertMany: (i, a) => r(i, a, "upsert"),
|
|
2598
|
+
removeOne: (i, a) => l(i, [a]),
|
|
2599
|
+
removeMany: (i, a) => l(i, a),
|
|
2600
|
+
removeAll: (i) => i.ids.length === 0 ? i : n(i, {}, []),
|
|
2601
|
+
selectIds: (i) => i.ids,
|
|
2602
|
+
selectEntities: (i) => i.entities,
|
|
2603
|
+
selectAll: (i) => i.ids.map((a) => i.entities[a]),
|
|
2604
|
+
selectById: (i, a) => i.entities[a],
|
|
2605
|
+
selectTotal: (i) => i.ids.length,
|
|
2606
|
+
idsPath: "ids",
|
|
2607
|
+
pathTo: (i, a) => a === void 0 ? `entities.${i}` : `entities.${i}.${a}`,
|
|
2608
|
+
anyField: (i) => `entities.*.${i}`
|
|
2609
|
+
};
|
|
2610
|
+
}
|
|
2611
|
+
const E = "$yoltra";
|
|
2612
|
+
function B(c, e = {}) {
|
|
2613
|
+
const t = e.maxNodes ?? 1e5, s = e.sanitize, n = [], r = /* @__PURE__ */ new Map();
|
|
2614
|
+
let o = 0, l = !1;
|
|
2615
|
+
function i(d, u) {
|
|
2616
|
+
if (s !== void 0 && (d = s(u, d)), o += 1, o > t)
|
|
2617
|
+
return l = !0, { [E]: "unsupported", kind: "truncated" };
|
|
2618
|
+
switch (typeof d) {
|
|
2619
|
+
case "undefined":
|
|
2620
|
+
return { [E]: "undefined" };
|
|
2621
|
+
case "bigint":
|
|
2622
|
+
return { [E]: "bigint", value: d.toString() };
|
|
2623
|
+
case "number":
|
|
2624
|
+
return Number.isNaN(d) ? { [E]: "nan" } : d === 1 / 0 ? { [E]: "infinity", sign: 1 } : d === -1 / 0 ? { [E]: "infinity", sign: -1 } : d;
|
|
2625
|
+
case "function":
|
|
2626
|
+
case "symbol":
|
|
2627
|
+
return n.push(u), { [E]: "unsupported", kind: typeof d };
|
|
2628
|
+
case "string":
|
|
2629
|
+
case "boolean":
|
|
2630
|
+
return d;
|
|
2631
|
+
}
|
|
2632
|
+
if (d === null) return null;
|
|
2633
|
+
const f = d, m = r.get(f);
|
|
2634
|
+
if (m !== void 0) return { [E]: "ref", path: m };
|
|
2635
|
+
if (r.set(f, u), d instanceof Date)
|
|
2636
|
+
return { [E]: "date", iso: d.toISOString() };
|
|
2637
|
+
if (d instanceof RegExp)
|
|
2638
|
+
return { [E]: "regexp", source: d.source, flags: d.flags };
|
|
2639
|
+
if (d instanceof Error)
|
|
2640
|
+
return { [E]: "error", name: d.name, message: d.message };
|
|
2641
|
+
if (d instanceof Map) {
|
|
2642
|
+
const y = [];
|
|
2643
|
+
let g = 0;
|
|
2644
|
+
for (const [w, k] of d)
|
|
2645
|
+
y.push([i(w, `${u}/@k${g}`), i(k, `${u}/${g}`)]), g += 1;
|
|
2646
|
+
return { [E]: "map", entries: y };
|
|
2647
|
+
}
|
|
2648
|
+
if (d instanceof Set) {
|
|
2649
|
+
const y = [];
|
|
2650
|
+
let g = 0;
|
|
2651
|
+
for (const w of d)
|
|
2652
|
+
y.push(i(w, `${u}/${g}`)), g += 1;
|
|
2653
|
+
return { [E]: "set", values: y };
|
|
2654
|
+
}
|
|
2655
|
+
if (Array.isArray(d))
|
|
2656
|
+
return d.map((y, g) => i(y, `${u}/${g}`));
|
|
2657
|
+
const h = {};
|
|
2658
|
+
for (const [y, g] of Object.entries(d))
|
|
2659
|
+
h[y] = i(g, `${u}/${H(y)}`);
|
|
2660
|
+
return E in h ? { [E]: "escaped", value: h } : h;
|
|
2661
|
+
}
|
|
2662
|
+
return { value: i(c, ""), report: { truncated: l, unsupported: n } };
|
|
2663
|
+
}
|
|
2664
|
+
function oe(c) {
|
|
2665
|
+
const e = /* @__PURE__ */ new Map(), t = [];
|
|
2666
|
+
function s(o, l) {
|
|
2667
|
+
if (o === null || typeof o != "object") return o;
|
|
2668
|
+
if (Array.isArray(o)) {
|
|
2669
|
+
const a = [];
|
|
2670
|
+
return e.set(l, a), o.forEach((d, u) => {
|
|
2671
|
+
if (j(d)) {
|
|
2672
|
+
t.push({ target: a, key: u, path: d.path }), a[u] = void 0;
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
2675
|
+
a[u] = s(d, `${l}/${u}`);
|
|
2676
|
+
}), a;
|
|
2677
|
+
}
|
|
2678
|
+
if (typeof o[E] == "string") {
|
|
2679
|
+
const a = o;
|
|
2680
|
+
switch (a[E]) {
|
|
2681
|
+
case "undefined":
|
|
2682
|
+
return;
|
|
2683
|
+
case "nan":
|
|
2684
|
+
return Number.NaN;
|
|
2685
|
+
case "infinity":
|
|
2686
|
+
return a.sign === 1 ? 1 / 0 : -1 / 0;
|
|
2687
|
+
case "bigint":
|
|
2688
|
+
return BigInt(a.value);
|
|
2689
|
+
case "date":
|
|
2690
|
+
return new Date(a.iso);
|
|
2691
|
+
case "regexp":
|
|
2692
|
+
return new RegExp(a.source, a.flags);
|
|
2693
|
+
case "error": {
|
|
2694
|
+
const d = new Error(a.message);
|
|
2695
|
+
return d.name = a.name, d;
|
|
2696
|
+
}
|
|
2697
|
+
case "unsupported":
|
|
2698
|
+
return;
|
|
2699
|
+
case "ref":
|
|
2700
|
+
return;
|
|
2701
|
+
case "map": {
|
|
2702
|
+
const d = /* @__PURE__ */ new Map();
|
|
2703
|
+
return e.set(l, d), a.entries.forEach(([u, f], m) => {
|
|
2704
|
+
d.set(s(u, `${l}/@k${m}`), s(f, `${l}/${m}`));
|
|
2705
|
+
}), d;
|
|
2706
|
+
}
|
|
2707
|
+
case "set": {
|
|
2708
|
+
const d = /* @__PURE__ */ new Set();
|
|
2709
|
+
return e.set(l, d), a.values.forEach((u, f) => d.add(s(u, `${l}/${f}`))), d;
|
|
2710
|
+
}
|
|
2711
|
+
case "escaped":
|
|
2712
|
+
return n(a.value, l);
|
|
2713
|
+
default:
|
|
2714
|
+
return;
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
return n(o, l);
|
|
2718
|
+
}
|
|
2719
|
+
function n(o, l) {
|
|
2720
|
+
const i = {};
|
|
2721
|
+
e.set(l, i);
|
|
2722
|
+
for (const [a, d] of Object.entries(o)) {
|
|
2723
|
+
const u = `${l}/${H(a)}`;
|
|
2724
|
+
if (j(d)) {
|
|
2725
|
+
t.push({ target: i, key: a, path: d.path }), i[a] = void 0;
|
|
2726
|
+
continue;
|
|
2727
|
+
}
|
|
2728
|
+
i[a] = s(d, u);
|
|
2729
|
+
}
|
|
2730
|
+
return i;
|
|
2731
|
+
}
|
|
2732
|
+
const r = s(c, "");
|
|
2733
|
+
e.set("", r);
|
|
2734
|
+
for (const { target: o, key: l, path: i } of t)
|
|
2735
|
+
o[l] = e.get(i);
|
|
2736
|
+
return r;
|
|
2737
|
+
}
|
|
2738
|
+
function j(c) {
|
|
2739
|
+
return c !== null && typeof c == "object" && c[E] === "ref" && typeof c.path == "string";
|
|
2740
|
+
}
|
|
2741
|
+
function H(c) {
|
|
2742
|
+
return c.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
2743
|
+
}
|
|
2744
|
+
function pe(c, e, t = {}) {
|
|
2745
|
+
let s = t.maxNodes ?? 1e5;
|
|
2746
|
+
for (let n = 0; n < 8; n += 1) {
|
|
2747
|
+
const { value: r, report: o } = B(c, { ...t, maxNodes: s });
|
|
2748
|
+
let l;
|
|
2749
|
+
try {
|
|
2750
|
+
l = JSON.stringify(r)?.length ?? 0;
|
|
2751
|
+
} catch {
|
|
2752
|
+
l = Number.POSITIVE_INFINITY;
|
|
2753
|
+
}
|
|
2754
|
+
if (l <= e)
|
|
2755
|
+
return o.truncated ? {
|
|
2756
|
+
value: r,
|
|
2757
|
+
truncated: !0,
|
|
2758
|
+
note: `State was too large to send in full; parts beyond ${s} nodes are omitted.`
|
|
2759
|
+
} : { value: r, truncated: !1 };
|
|
2760
|
+
const i = Math.floor(s * e * 0.8 / l);
|
|
2761
|
+
if (s = Math.max(1, Math.min(i, s - 1)), s <= 1 && n > 0)
|
|
2762
|
+
break;
|
|
2763
|
+
}
|
|
2764
|
+
return {
|
|
2765
|
+
value: { [E]: "unsupported", kind: "truncated" },
|
|
2766
|
+
truncated: !0,
|
|
2767
|
+
note: `State exceeds the ${e}-byte transport limit and could not be reduced to fit.`
|
|
2768
|
+
};
|
|
2769
|
+
}
|
|
2770
|
+
function v(c, e, t) {
|
|
2771
|
+
c.onError?.(e, t);
|
|
2772
|
+
}
|
|
2773
|
+
async function me(c) {
|
|
2774
|
+
const e = { slices: {}, restored: !1 };
|
|
2775
|
+
let t;
|
|
2776
|
+
try {
|
|
2777
|
+
t = c.source ?? await c.adapter.read(c.key);
|
|
2778
|
+
} catch (n) {
|
|
2779
|
+
return v(c, n, "read"), e;
|
|
2780
|
+
}
|
|
2781
|
+
if (t == null || t === "") return e;
|
|
2782
|
+
let s;
|
|
2783
|
+
try {
|
|
2784
|
+
s = oe(JSON.parse(t));
|
|
2785
|
+
} catch (n) {
|
|
2786
|
+
return v(c, n, "decode"), e;
|
|
2787
|
+
}
|
|
2788
|
+
if (s === null || typeof s != "object" || typeof s.version != "number")
|
|
2789
|
+
return v(c, new Error("persisted payload is not a recognisable envelope"), "decode"), e;
|
|
2790
|
+
if (s.version !== c.version) {
|
|
2791
|
+
if (c.migrate === void 0)
|
|
2792
|
+
return v(
|
|
2793
|
+
c,
|
|
2794
|
+
new Error(
|
|
2795
|
+
`persisted state is version ${s.version}, this build expects ${c.version}, and no migrate was supplied`
|
|
2796
|
+
),
|
|
2797
|
+
"migrate"
|
|
2798
|
+
), e;
|
|
2799
|
+
try {
|
|
2800
|
+
const n = c.migrate(s.slices, s.version);
|
|
2801
|
+
return n === null ? e : { slices: n, restored: !0 };
|
|
2802
|
+
} catch (n) {
|
|
2803
|
+
return v(c, n, "migrate"), e;
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
return { slices: s.slices ?? {}, restored: !0 };
|
|
2807
|
+
}
|
|
2808
|
+
function ye(c, e) {
|
|
2809
|
+
if (!e.restored) return c;
|
|
2810
|
+
const t = {};
|
|
2811
|
+
for (const [s, n] of Object.entries(c)) {
|
|
2812
|
+
const r = e.slices[s];
|
|
2813
|
+
t[s] = r === void 0 ? n : { ...n, state: r };
|
|
2814
|
+
}
|
|
2815
|
+
return t;
|
|
2816
|
+
}
|
|
2817
|
+
function W(c, e) {
|
|
2818
|
+
const t = c ?? {}, s = e.slices === void 0 ? t : Object.fromEntries(e.slices.filter((n) => n in t).map((n) => [n, t[n]]));
|
|
2819
|
+
return JSON.stringify(B({ version: e.version, slices: s }).value);
|
|
2820
|
+
}
|
|
2821
|
+
function ge(c, e) {
|
|
2822
|
+
const t = e.throttleMs ?? 250, s = e.slices;
|
|
2823
|
+
let n = null, r = !1;
|
|
2824
|
+
const o = () => {
|
|
2825
|
+
if (r) {
|
|
2826
|
+
r = !1;
|
|
2827
|
+
try {
|
|
2828
|
+
const a = e.adapter.write(e.key, W(c.getState(), e));
|
|
2829
|
+
a instanceof Promise && a.catch((d) => v(e, d, "write"));
|
|
2830
|
+
} catch (a) {
|
|
2831
|
+
v(e, a, "write");
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
}, l = () => {
|
|
2835
|
+
if (r = !0, t <= 0) {
|
|
2836
|
+
o();
|
|
2837
|
+
return;
|
|
2838
|
+
}
|
|
2839
|
+
n === null && (n = setTimeout(() => {
|
|
2840
|
+
n = null, o();
|
|
2841
|
+
}, t), n.unref?.());
|
|
2842
|
+
}, i = c.instrument((a) => {
|
|
2843
|
+
if (s === void 0) {
|
|
2844
|
+
l();
|
|
2845
|
+
return;
|
|
2846
|
+
}
|
|
2847
|
+
(a.changedPaths ?? []).some(
|
|
2848
|
+
(u) => s.some((f) => u === f || u.startsWith(`${f}.`))
|
|
2849
|
+
) && l();
|
|
2850
|
+
});
|
|
2851
|
+
return () => {
|
|
2852
|
+
i(), n !== null && (clearTimeout(n), n = null), o();
|
|
2853
|
+
};
|
|
2854
|
+
}
|
|
2855
|
+
function we(c, e) {
|
|
2856
|
+
return W(c.getState(), e);
|
|
2857
|
+
}
|
|
2858
|
+
function Ee(c) {
|
|
2859
|
+
return {
|
|
2860
|
+
read: (e) => c.getItem(e),
|
|
2861
|
+
write: (e, t) => c.setItem(e, t),
|
|
2862
|
+
remove: (e) => c.removeItem(e)
|
|
2863
|
+
};
|
|
2864
|
+
}
|
|
2865
|
+
function be(c) {
|
|
2866
|
+
const e = new Map(Object.entries(c ?? {}));
|
|
2867
|
+
return {
|
|
2868
|
+
read: (t) => e.get(t) ?? null,
|
|
2869
|
+
write: (t, s) => {
|
|
2870
|
+
e.set(t, s);
|
|
2871
|
+
},
|
|
2872
|
+
remove: (t) => {
|
|
2873
|
+
e.delete(t);
|
|
2874
|
+
}
|
|
2875
|
+
};
|
|
2876
|
+
}
|
|
2877
|
+
export {
|
|
2878
|
+
C as CallAbortedError,
|
|
2879
|
+
J as CallTimeoutError,
|
|
2880
|
+
U as EventBus,
|
|
2881
|
+
L as LooseEventBus,
|
|
2882
|
+
V as Reducer,
|
|
2883
|
+
le as Rejected,
|
|
2884
|
+
x as Store,
|
|
2885
|
+
he as createEntityAdapter,
|
|
2886
|
+
be as createMemoryAdapter,
|
|
2887
|
+
de as createStore,
|
|
2888
|
+
Ee as createWebStorageAdapter,
|
|
2889
|
+
oe as decodeState,
|
|
2890
|
+
we as dehydrate,
|
|
2891
|
+
I as detectChangedProps,
|
|
2892
|
+
B as encodeState,
|
|
2893
|
+
pe as encodeStateBounded,
|
|
2894
|
+
fe as eventKeys,
|
|
2895
|
+
M as freezeState,
|
|
2896
|
+
me as hydrate,
|
|
2897
|
+
Q as isRejected,
|
|
2898
|
+
ge as persist,
|
|
2899
|
+
ue as typedEvents,
|
|
2900
|
+
ye as withHydration
|
|
2901
|
+
};
|
|
2902
|
+
//# sourceMappingURL=yoltra.mjs.map
|