@rotorsoft/act 0.32.4 → 0.32.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/act.d.ts +65 -47
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/adapters/{ConsoleLogger.d.ts → console-logger.d.ts} +2 -2
- package/dist/@types/adapters/console-logger.d.ts.map +1 -0
- package/dist/@types/adapters/{InMemoryCache.d.ts → in-memory-cache.d.ts} +2 -3
- package/dist/@types/adapters/in-memory-cache.d.ts.map +1 -0
- package/dist/@types/adapters/{InMemoryStore.d.ts → in-memory-store.d.ts} +5 -1
- package/dist/@types/adapters/in-memory-store.d.ts.map +1 -0
- package/dist/@types/adapters/index.d.ts +3 -3
- package/dist/@types/adapters/index.d.ts.map +1 -1
- package/dist/@types/{act-builder.d.ts → builders/act-builder.d.ts} +5 -5
- package/dist/@types/builders/act-builder.d.ts.map +1 -0
- package/dist/@types/builders/index.d.ts +13 -0
- package/dist/@types/builders/index.d.ts.map +1 -0
- package/dist/@types/{projection-builder.d.ts → builders/projection-builder.d.ts} +3 -3
- package/dist/@types/builders/projection-builder.d.ts.map +1 -0
- package/dist/@types/{slice-builder.d.ts → builders/slice-builder.d.ts} +2 -2
- package/dist/@types/builders/slice-builder.d.ts.map +1 -0
- package/dist/@types/{state-builder.d.ts → builders/state-builder.d.ts} +1 -1
- package/dist/@types/builders/state-builder.d.ts.map +1 -0
- package/dist/@types/config.d.ts.map +1 -1
- package/dist/@types/index.d.ts +1 -4
- package/dist/@types/index.d.ts.map +1 -1
- package/dist/@types/internal/close-cycle.d.ts +38 -0
- package/dist/@types/internal/close-cycle.d.ts.map +1 -0
- package/dist/@types/internal/drain-cycle.d.ts +61 -0
- package/dist/@types/internal/drain-cycle.d.ts.map +1 -0
- package/dist/@types/internal/drain-ratio.d.ts +26 -0
- package/dist/@types/internal/drain-ratio.d.ts.map +1 -0
- package/dist/@types/internal/event-sourcing.d.ts +14 -0
- package/dist/@types/internal/event-sourcing.d.ts.map +1 -1
- package/dist/@types/internal/index.d.ts +5 -1
- package/dist/@types/internal/index.d.ts.map +1 -1
- package/dist/@types/internal/lru-map.d.ts +50 -0
- package/dist/@types/internal/lru-map.d.ts.map +1 -0
- package/dist/@types/internal/merge.d.ts +13 -1
- package/dist/@types/internal/merge.d.ts.map +1 -1
- package/dist/@types/internal/tracing.d.ts.map +1 -1
- package/dist/@types/ports.d.ts +1 -1
- package/dist/@types/ports.d.ts.map +1 -1
- package/dist/@types/types/errors.d.ts.map +1 -1
- package/dist/@types/types/reaction.d.ts +7 -1
- package/dist/@types/types/reaction.d.ts.map +1 -1
- package/dist/@types/utils.d.ts +27 -296
- package/dist/@types/utils.d.ts.map +1 -1
- package/dist/{chunk-JBKZJXQZ.js → chunk-IDEYGKT4.js} +2 -2
- package/dist/{chunk-JBKZJXQZ.js.map → chunk-IDEYGKT4.js.map} +1 -1
- package/dist/index.cjs +628 -422
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +627 -422
- package/dist/index.js.map +1 -1
- package/dist/types/index.cjs +1 -1
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.js +1 -1
- package/package.json +1 -1
- package/dist/@types/act-builder.d.ts.map +0 -1
- package/dist/@types/adapters/ConsoleLogger.d.ts.map +0 -1
- package/dist/@types/adapters/InMemoryCache.d.ts.map +0 -1
- package/dist/@types/adapters/InMemoryStore.d.ts.map +0 -1
- package/dist/@types/projection-builder.d.ts.map +0 -1
- package/dist/@types/slice-builder.d.ts.map +0 -1
- package/dist/@types/state-builder.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -13,9 +13,9 @@ import {
|
|
|
13
13
|
TargetSchema,
|
|
14
14
|
ValidationError,
|
|
15
15
|
ZodEmpty
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-IDEYGKT4.js";
|
|
17
17
|
|
|
18
|
-
// src/adapters/
|
|
18
|
+
// src/adapters/console-logger.ts
|
|
19
19
|
var LEVEL_VALUES = {
|
|
20
20
|
fatal: 60,
|
|
21
21
|
error: 50,
|
|
@@ -84,14 +84,25 @@ var ConsoleLogger = class _ConsoleLogger {
|
|
|
84
84
|
obj = {};
|
|
85
85
|
} else if (objOrMsg !== null && typeof objOrMsg === "object") {
|
|
86
86
|
message = msg;
|
|
87
|
-
obj =
|
|
87
|
+
obj = { ...objOrMsg };
|
|
88
88
|
} else {
|
|
89
89
|
message = msg;
|
|
90
90
|
obj = { value: objOrMsg };
|
|
91
91
|
}
|
|
92
92
|
const entry = Object.assign({ level, time: Date.now() }, bindings, obj);
|
|
93
93
|
if (message) entry.msg = message;
|
|
94
|
-
|
|
94
|
+
let line;
|
|
95
|
+
try {
|
|
96
|
+
line = JSON.stringify(entry);
|
|
97
|
+
} catch {
|
|
98
|
+
line = JSON.stringify({
|
|
99
|
+
level,
|
|
100
|
+
time: entry.time,
|
|
101
|
+
msg: message ?? "[unserializable]",
|
|
102
|
+
unserializable: true
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
process.stdout.write(line + "\n");
|
|
95
106
|
}
|
|
96
107
|
_prettyWrite(bindings, level, _num, objOrMsg, msg) {
|
|
97
108
|
const color = LEVEL_COLORS[level];
|
|
@@ -117,26 +128,75 @@ var ConsoleLogger = class _ConsoleLogger {
|
|
|
117
128
|
}
|
|
118
129
|
};
|
|
119
130
|
|
|
120
|
-
// src/
|
|
121
|
-
var
|
|
131
|
+
// src/internal/lru-map.ts
|
|
132
|
+
var LruMap = class {
|
|
133
|
+
constructor(_maxSize) {
|
|
134
|
+
this._maxSize = _maxSize;
|
|
135
|
+
}
|
|
122
136
|
_entries = /* @__PURE__ */ new Map();
|
|
123
|
-
|
|
137
|
+
get(key) {
|
|
138
|
+
const v = this._entries.get(key);
|
|
139
|
+
if (v === void 0) return void 0;
|
|
140
|
+
this._entries.delete(key);
|
|
141
|
+
this._entries.set(key, v);
|
|
142
|
+
return v;
|
|
143
|
+
}
|
|
144
|
+
has(key) {
|
|
145
|
+
return this._entries.has(key);
|
|
146
|
+
}
|
|
147
|
+
set(key, value) {
|
|
148
|
+
this._entries.delete(key);
|
|
149
|
+
if (this._entries.size >= this._maxSize) {
|
|
150
|
+
const oldest = this._entries.keys().next().value;
|
|
151
|
+
this._entries.delete(oldest);
|
|
152
|
+
}
|
|
153
|
+
this._entries.set(key, value);
|
|
154
|
+
}
|
|
155
|
+
delete(key) {
|
|
156
|
+
return this._entries.delete(key);
|
|
157
|
+
}
|
|
158
|
+
clear() {
|
|
159
|
+
this._entries.clear();
|
|
160
|
+
}
|
|
161
|
+
get size() {
|
|
162
|
+
return this._entries.size;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
var LruSet = class {
|
|
166
|
+
_map;
|
|
167
|
+
constructor(maxSize) {
|
|
168
|
+
this._map = new LruMap(maxSize);
|
|
169
|
+
}
|
|
170
|
+
has(value) {
|
|
171
|
+
return this._map.has(value);
|
|
172
|
+
}
|
|
173
|
+
add(value) {
|
|
174
|
+
this._map.set(value, true);
|
|
175
|
+
}
|
|
176
|
+
delete(value) {
|
|
177
|
+
return this._map.delete(value);
|
|
178
|
+
}
|
|
179
|
+
clear() {
|
|
180
|
+
this._map.clear();
|
|
181
|
+
}
|
|
182
|
+
get size() {
|
|
183
|
+
return this._map.size;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// src/adapters/in-memory-cache.ts
|
|
188
|
+
var InMemoryCache = class {
|
|
189
|
+
// CacheEntry<any> lets `get<TState>` and `set<TState>` flow without casts:
|
|
190
|
+
// any is bidirectionally compatible with the per-call TState binding, while
|
|
191
|
+
// the public Cache interface still presents a typed surface to callers.
|
|
192
|
+
_entries;
|
|
124
193
|
constructor(options) {
|
|
125
|
-
this.
|
|
194
|
+
this._entries = new LruMap(options?.maxSize ?? 1e3);
|
|
126
195
|
}
|
|
127
196
|
async get(stream) {
|
|
128
|
-
|
|
129
|
-
if (!entry) return void 0;
|
|
130
|
-
this._entries.delete(stream);
|
|
131
|
-
this._entries.set(stream, entry);
|
|
132
|
-
return entry;
|
|
197
|
+
return this._entries.get(stream);
|
|
133
198
|
}
|
|
134
199
|
async set(stream, entry) {
|
|
135
|
-
this._entries.delete(stream);
|
|
136
|
-
if (this._entries.size >= this._maxSize) {
|
|
137
|
-
const first = this._entries.keys().next().value;
|
|
138
|
-
this._entries.delete(first);
|
|
139
|
-
}
|
|
140
200
|
this._entries.set(stream, entry);
|
|
141
201
|
}
|
|
142
202
|
async invalidate(stream) {
|
|
@@ -167,10 +227,21 @@ var PackageSchema = z.object({
|
|
|
167
227
|
license: z.string().min(1).optional(),
|
|
168
228
|
dependencies: z.record(z.string(), z.string()).optional()
|
|
169
229
|
});
|
|
230
|
+
var FALLBACK_PACKAGE = {
|
|
231
|
+
name: "act-fallback",
|
|
232
|
+
version: "0.0.0-fallback",
|
|
233
|
+
description: "Synthetic fallback \u2014 package.json could not be loaded"
|
|
234
|
+
};
|
|
170
235
|
var getPackage = () => {
|
|
171
|
-
|
|
172
|
-
|
|
236
|
+
try {
|
|
237
|
+
const raw = fs.readFileSync("package.json");
|
|
238
|
+
return JSON.parse(raw.toString());
|
|
239
|
+
} catch (err) {
|
|
240
|
+
pkgLoadError = err;
|
|
241
|
+
return FALLBACK_PACKAGE;
|
|
242
|
+
}
|
|
173
243
|
};
|
|
244
|
+
var pkgLoadError;
|
|
174
245
|
var BaseSchema = PackageSchema.extend({
|
|
175
246
|
env: z.enum(Environments),
|
|
176
247
|
logLevel: z.enum(LogLevels),
|
|
@@ -190,6 +261,13 @@ var config = () => {
|
|
|
190
261
|
{ ...pkg, env, logLevel, logSingleLine, sleepMs },
|
|
191
262
|
BaseSchema
|
|
192
263
|
);
|
|
264
|
+
if (pkgLoadError) {
|
|
265
|
+
const msg = pkgLoadError instanceof Error ? pkgLoadError.message : typeof pkgLoadError === "string" ? pkgLoadError : "unknown error";
|
|
266
|
+
log().warn(
|
|
267
|
+
`[act] Could not read package.json (${msg}); using synthetic name="${FALLBACK_PACKAGE.name}" version="${FALLBACK_PACKAGE.version}".`
|
|
268
|
+
);
|
|
269
|
+
pkgLoadError = void 0;
|
|
270
|
+
}
|
|
193
271
|
}
|
|
194
272
|
return _validated;
|
|
195
273
|
};
|
|
@@ -213,7 +291,7 @@ async function sleep(ms) {
|
|
|
213
291
|
return new Promise((resolve) => setTimeout(resolve, ms ?? config().sleepMs));
|
|
214
292
|
}
|
|
215
293
|
|
|
216
|
-
// src/adapters/
|
|
294
|
+
// src/adapters/in-memory-store.ts
|
|
217
295
|
var InMemoryStream = class {
|
|
218
296
|
constructor(stream, source) {
|
|
219
297
|
this.stream = stream;
|
|
@@ -308,11 +386,13 @@ var InMemoryStream = class {
|
|
|
308
386
|
}
|
|
309
387
|
}
|
|
310
388
|
/**
|
|
311
|
-
* Reset this stream's watermark and state for replay.
|
|
389
|
+
* Reset this stream's watermark and state for replay. The retry counter
|
|
390
|
+
* resets to -1 to match the constructor + ack() invariant ("released
|
|
391
|
+
* stream"); the next claim() bumps it to 0 (first attempt).
|
|
312
392
|
*/
|
|
313
393
|
reset() {
|
|
314
394
|
this._at = -1;
|
|
315
|
-
this._retry =
|
|
395
|
+
this._retry = -1;
|
|
316
396
|
this._blocked = false;
|
|
317
397
|
this._error = "";
|
|
318
398
|
this._leased_by = void 0;
|
|
@@ -324,13 +404,26 @@ var InMemoryStore = class {
|
|
|
324
404
|
_events = [];
|
|
325
405
|
// stored stream positions and other metadata
|
|
326
406
|
_streams = /* @__PURE__ */ new Map();
|
|
407
|
+
// last committed version per stream — O(1) replacement for filter-on-commit
|
|
408
|
+
_streamVersions = /* @__PURE__ */ new Map();
|
|
409
|
+
// max non-snapshot event id per stream — drives the source-pattern probe in claim()
|
|
410
|
+
// without scanning the full event log.
|
|
411
|
+
_maxEventIdByStream = /* @__PURE__ */ new Map();
|
|
412
|
+
// global max non-snapshot event id — fast pre-check for source-less streams in claim()
|
|
413
|
+
_maxNonSnapEventId = -1;
|
|
414
|
+
_resetIndexes() {
|
|
415
|
+
this._events.length = 0;
|
|
416
|
+
this._streamVersions.clear();
|
|
417
|
+
this._maxEventIdByStream.clear();
|
|
418
|
+
this._maxNonSnapEventId = -1;
|
|
419
|
+
}
|
|
327
420
|
/**
|
|
328
421
|
* Dispose of the store and clear all events.
|
|
329
422
|
* @returns Promise that resolves when disposal is complete.
|
|
330
423
|
*/
|
|
331
424
|
async dispose() {
|
|
332
425
|
await sleep();
|
|
333
|
-
this.
|
|
426
|
+
this._resetIndexes();
|
|
334
427
|
}
|
|
335
428
|
/**
|
|
336
429
|
* Seed the store with initial data (no-op for in-memory).
|
|
@@ -345,7 +438,7 @@ var InMemoryStore = class {
|
|
|
345
438
|
*/
|
|
346
439
|
async drop() {
|
|
347
440
|
await sleep();
|
|
348
|
-
this.
|
|
441
|
+
this._resetIndexes();
|
|
349
442
|
this._streams = /* @__PURE__ */ new Map();
|
|
350
443
|
}
|
|
351
444
|
in_query(query, e) {
|
|
@@ -408,18 +501,19 @@ var InMemoryStore = class {
|
|
|
408
501
|
*/
|
|
409
502
|
async commit(stream, msgs, meta, expectedVersion) {
|
|
410
503
|
await sleep();
|
|
411
|
-
const
|
|
412
|
-
if (typeof expectedVersion === "number" &&
|
|
504
|
+
const currentVersion = this._streamVersions.get(stream) ?? -1;
|
|
505
|
+
if (typeof expectedVersion === "number" && currentVersion !== expectedVersion) {
|
|
413
506
|
throw new ConcurrencyError(
|
|
414
507
|
stream,
|
|
415
|
-
|
|
508
|
+
currentVersion,
|
|
416
509
|
msgs,
|
|
417
510
|
expectedVersion
|
|
418
511
|
);
|
|
419
512
|
}
|
|
420
|
-
let version =
|
|
421
|
-
|
|
422
|
-
|
|
513
|
+
let version = currentVersion + 1;
|
|
514
|
+
let lastNonSnapId = -1;
|
|
515
|
+
const committed = msgs.map(({ name, data }) => {
|
|
516
|
+
const c = {
|
|
423
517
|
id: this._events.length,
|
|
424
518
|
stream,
|
|
425
519
|
version,
|
|
@@ -428,10 +522,17 @@ var InMemoryStore = class {
|
|
|
428
522
|
data,
|
|
429
523
|
meta
|
|
430
524
|
};
|
|
431
|
-
this._events.push(
|
|
525
|
+
this._events.push(c);
|
|
526
|
+
if (name !== SNAP_EVENT) lastNonSnapId = c.id;
|
|
432
527
|
version++;
|
|
433
|
-
return
|
|
528
|
+
return c;
|
|
434
529
|
});
|
|
530
|
+
this._streamVersions.set(stream, version - 1);
|
|
531
|
+
if (lastNonSnapId >= 0) {
|
|
532
|
+
this._maxEventIdByStream.set(stream, lastNonSnapId);
|
|
533
|
+
this._maxNonSnapEventId = lastNonSnapId;
|
|
534
|
+
}
|
|
535
|
+
return committed;
|
|
435
536
|
}
|
|
436
537
|
/**
|
|
437
538
|
* Atomically discovers and leases streams for processing.
|
|
@@ -444,10 +545,26 @@ var InMemoryStore = class {
|
|
|
444
545
|
*/
|
|
445
546
|
async claim(lagging, leading, by, millis) {
|
|
446
547
|
await sleep();
|
|
548
|
+
const sourceRegex = /* @__PURE__ */ new Map();
|
|
549
|
+
const getRegex = (source) => {
|
|
550
|
+
let re = sourceRegex.get(source);
|
|
551
|
+
if (!re) {
|
|
552
|
+
re = new RegExp(source);
|
|
553
|
+
sourceRegex.set(source, re);
|
|
554
|
+
}
|
|
555
|
+
return re;
|
|
556
|
+
};
|
|
557
|
+
const hasWork = (s) => {
|
|
558
|
+
if (s.at < 0) return true;
|
|
559
|
+
if (!s.source) return s.at < this._maxNonSnapEventId;
|
|
560
|
+
const re = getRegex(s.source);
|
|
561
|
+
for (const [streamName, maxId] of this._maxEventIdByStream) {
|
|
562
|
+
if (maxId > s.at && re.test(streamName)) return true;
|
|
563
|
+
}
|
|
564
|
+
return false;
|
|
565
|
+
};
|
|
447
566
|
const available = [...this._streams.values()].filter(
|
|
448
|
-
(s) => s.is_available && (s
|
|
449
|
-
(e) => e.id > s.at && e.name !== SNAP_EVENT && (!s.source || RegExp(s.source).test(e.stream))
|
|
450
|
-
))
|
|
567
|
+
(s) => s.is_available && hasWork(s)
|
|
451
568
|
);
|
|
452
569
|
const lag = available.sort((a, b) => a.at - b.at).slice(0, lagging).map((s) => ({
|
|
453
570
|
stream: s.stream,
|
|
@@ -584,9 +701,13 @@ var InMemoryStore = class {
|
|
|
584
701
|
}
|
|
585
702
|
}
|
|
586
703
|
this._events = this._events.filter((e) => !streamSet.has(e.stream));
|
|
704
|
+
for (const stream of streamSet) {
|
|
705
|
+
this._streams.delete(stream);
|
|
706
|
+
this._streamVersions.delete(stream);
|
|
707
|
+
this._maxEventIdByStream.delete(stream);
|
|
708
|
+
}
|
|
587
709
|
const result = /* @__PURE__ */ new Map();
|
|
588
710
|
for (const { stream, snapshot, meta } of targets) {
|
|
589
|
-
this._streams.delete(stream);
|
|
590
711
|
const event = {
|
|
591
712
|
id: this._events.length,
|
|
592
713
|
stream,
|
|
@@ -597,11 +718,18 @@ var InMemoryStore = class {
|
|
|
597
718
|
meta: meta ?? { correlation: "", causation: {} }
|
|
598
719
|
};
|
|
599
720
|
this._events.push(event);
|
|
721
|
+
this._streamVersions.set(stream, 0);
|
|
722
|
+
if (event.name !== SNAP_EVENT) {
|
|
723
|
+
this._maxEventIdByStream.set(stream, event.id);
|
|
724
|
+
}
|
|
600
725
|
result.set(stream, {
|
|
601
726
|
deleted: deletedCounts.get(stream) ?? 0,
|
|
602
727
|
committed: event
|
|
603
728
|
});
|
|
604
729
|
}
|
|
730
|
+
let max = -1;
|
|
731
|
+
for (const id of this._maxEventIdByStream.values()) if (id > max) max = id;
|
|
732
|
+
this._maxNonSnapEventId = max;
|
|
605
733
|
return result;
|
|
606
734
|
}
|
|
607
735
|
};
|
|
@@ -634,7 +762,12 @@ var cache = port(function cache2(adapter) {
|
|
|
634
762
|
});
|
|
635
763
|
var disposers = [];
|
|
636
764
|
async function disposeAndExit(code = "EXIT") {
|
|
637
|
-
if (code === "ERROR" && config().env === "production")
|
|
765
|
+
if (code === "ERROR" && config().env === "production") {
|
|
766
|
+
log().warn(
|
|
767
|
+
"disposeAndExit('ERROR') ignored in production \u2014 process kept alive"
|
|
768
|
+
);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
638
771
|
for (const disposer of [...disposers].reverse()) {
|
|
639
772
|
await disposer();
|
|
640
773
|
}
|
|
@@ -671,9 +804,225 @@ process.once("unhandledRejection", async (arg) => {
|
|
|
671
804
|
});
|
|
672
805
|
|
|
673
806
|
// src/act.ts
|
|
674
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
675
807
|
import EventEmitter from "events";
|
|
676
808
|
|
|
809
|
+
// src/internal/close-cycle.ts
|
|
810
|
+
import { randomUUID } from "crypto";
|
|
811
|
+
async function runCloseCycle(targets, deps) {
|
|
812
|
+
const targetMap = new Map(targets.map((t) => [t.stream, t]));
|
|
813
|
+
const streams = [...targetMap.keys()];
|
|
814
|
+
const skipped = [];
|
|
815
|
+
const streamInfo = await scanStreamHeads(streams);
|
|
816
|
+
const safe = await partitionBySafety(
|
|
817
|
+
streamInfo,
|
|
818
|
+
deps.reactiveEventsSize,
|
|
819
|
+
skipped
|
|
820
|
+
);
|
|
821
|
+
if (!safe.length) return { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
822
|
+
const correlation = randomUUID();
|
|
823
|
+
const { guarded, guardEvents } = await guardWithTombstones(
|
|
824
|
+
safe,
|
|
825
|
+
streamInfo,
|
|
826
|
+
correlation,
|
|
827
|
+
deps.tombstone,
|
|
828
|
+
skipped
|
|
829
|
+
);
|
|
830
|
+
if (!guarded.length) return { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
831
|
+
const seedStates = await loadRestartSeeds(
|
|
832
|
+
guarded,
|
|
833
|
+
targetMap,
|
|
834
|
+
streamInfo,
|
|
835
|
+
deps.eventToState,
|
|
836
|
+
deps.load,
|
|
837
|
+
deps.logger
|
|
838
|
+
);
|
|
839
|
+
await runArchiveCallbacks(guarded, targetMap);
|
|
840
|
+
const truncated = await truncateAndWarmCache(
|
|
841
|
+
guarded,
|
|
842
|
+
seedStates,
|
|
843
|
+
guardEvents,
|
|
844
|
+
correlation
|
|
845
|
+
);
|
|
846
|
+
return { truncated, skipped };
|
|
847
|
+
}
|
|
848
|
+
async function scanStreamHeads(streams) {
|
|
849
|
+
const out = /* @__PURE__ */ new Map();
|
|
850
|
+
await Promise.all(
|
|
851
|
+
streams.map(async (s) => {
|
|
852
|
+
let maxId = -1;
|
|
853
|
+
let version = -1;
|
|
854
|
+
let lastEventName = "";
|
|
855
|
+
await store().query(
|
|
856
|
+
(e) => {
|
|
857
|
+
if (e.name === TOMBSTONE_EVENT || maxId !== -1) return;
|
|
858
|
+
maxId = e.id;
|
|
859
|
+
version = e.version;
|
|
860
|
+
lastEventName = e.name;
|
|
861
|
+
},
|
|
862
|
+
{ stream: s, stream_exact: true, backward: true, limit: 1 }
|
|
863
|
+
);
|
|
864
|
+
if (maxId >= 0) out.set(s, { maxId, version, lastEventName });
|
|
865
|
+
})
|
|
866
|
+
);
|
|
867
|
+
return out;
|
|
868
|
+
}
|
|
869
|
+
async function partitionBySafety(streamInfo, reactiveEventsSize, skipped) {
|
|
870
|
+
if (reactiveEventsSize === 0) return [...streamInfo.keys()];
|
|
871
|
+
const pendingSet = /* @__PURE__ */ new Set();
|
|
872
|
+
await store().query_streams((position) => {
|
|
873
|
+
const sourceRe = position.source ? RegExp(position.source) : void 0;
|
|
874
|
+
for (const [stream, info] of streamInfo) {
|
|
875
|
+
if ((!sourceRe || sourceRe.test(stream)) && position.at < info.maxId) {
|
|
876
|
+
pendingSet.add(stream);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
const safe = [];
|
|
881
|
+
for (const [stream] of streamInfo) {
|
|
882
|
+
if (pendingSet.has(stream)) skipped.push(stream);
|
|
883
|
+
else safe.push(stream);
|
|
884
|
+
}
|
|
885
|
+
return safe;
|
|
886
|
+
}
|
|
887
|
+
async function guardWithTombstones(safe, streamInfo, correlation, tombstone2, skipped) {
|
|
888
|
+
const guarded = [];
|
|
889
|
+
const guardEvents = /* @__PURE__ */ new Map();
|
|
890
|
+
await Promise.all(
|
|
891
|
+
safe.map(async (stream) => {
|
|
892
|
+
const info = streamInfo.get(stream);
|
|
893
|
+
const committed = await tombstone2(stream, info.version, correlation);
|
|
894
|
+
if (committed) {
|
|
895
|
+
guarded.push(stream);
|
|
896
|
+
guardEvents.set(stream, { id: committed.id, stream });
|
|
897
|
+
} else {
|
|
898
|
+
skipped.push(stream);
|
|
899
|
+
}
|
|
900
|
+
})
|
|
901
|
+
);
|
|
902
|
+
return { guarded, guardEvents };
|
|
903
|
+
}
|
|
904
|
+
async function loadRestartSeeds(guarded, targetMap, streamInfo, eventToState, load2, logger) {
|
|
905
|
+
const seedStates = /* @__PURE__ */ new Map();
|
|
906
|
+
await Promise.all(
|
|
907
|
+
guarded.filter((s) => targetMap.get(s)?.restart).map(async (stream) => {
|
|
908
|
+
const lastEventName = streamInfo.get(stream).lastEventName;
|
|
909
|
+
const ownerState = eventToState.get(lastEventName);
|
|
910
|
+
if (!ownerState) {
|
|
911
|
+
logger.error(
|
|
912
|
+
`Cannot seed restart for "${stream}": no registered state owns event "${lastEventName}". Stream will be tombstoned instead.`
|
|
913
|
+
);
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
const snap2 = await load2(ownerState, stream);
|
|
917
|
+
seedStates.set(stream, snap2.state);
|
|
918
|
+
})
|
|
919
|
+
);
|
|
920
|
+
return seedStates;
|
|
921
|
+
}
|
|
922
|
+
async function runArchiveCallbacks(guarded, targetMap) {
|
|
923
|
+
for (const stream of guarded) {
|
|
924
|
+
const archiveFn = targetMap.get(stream)?.archive;
|
|
925
|
+
if (archiveFn) await archiveFn();
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
async function truncateAndWarmCache(guarded, seedStates, guardEvents, correlation) {
|
|
929
|
+
const truncTargets = guarded.map((stream) => {
|
|
930
|
+
const snapshot = seedStates.get(stream);
|
|
931
|
+
const guard = guardEvents.get(stream);
|
|
932
|
+
return {
|
|
933
|
+
stream,
|
|
934
|
+
snapshot,
|
|
935
|
+
meta: {
|
|
936
|
+
correlation,
|
|
937
|
+
causation: {
|
|
938
|
+
event: { id: guard.id, name: TOMBSTONE_EVENT, stream: guard.stream }
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
});
|
|
943
|
+
const truncated = await store().truncate(truncTargets);
|
|
944
|
+
await Promise.all(
|
|
945
|
+
guarded.map(async (stream) => {
|
|
946
|
+
const entry = truncated.get(stream);
|
|
947
|
+
const state2 = seedStates.get(stream);
|
|
948
|
+
if (state2 && entry) {
|
|
949
|
+
await cache().set(stream, {
|
|
950
|
+
state: state2,
|
|
951
|
+
version: entry.committed.version,
|
|
952
|
+
event_id: entry.committed.id,
|
|
953
|
+
patches: 0,
|
|
954
|
+
snaps: 1
|
|
955
|
+
});
|
|
956
|
+
} else {
|
|
957
|
+
await cache().invalidate(stream);
|
|
958
|
+
}
|
|
959
|
+
})
|
|
960
|
+
);
|
|
961
|
+
return truncated;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// src/internal/drain-cycle.ts
|
|
965
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
966
|
+
async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch, lagging, leading, eventLimit, leaseMillis) {
|
|
967
|
+
const leased = await ops.claim(lagging, leading, randomUUID2(), leaseMillis);
|
|
968
|
+
if (!leased.length) return void 0;
|
|
969
|
+
const fetched = await ops.fetch(leased, eventLimit);
|
|
970
|
+
const fetchMap = /* @__PURE__ */ new Map();
|
|
971
|
+
const fetch_window_at = fetched.reduce(
|
|
972
|
+
(max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
|
|
973
|
+
0
|
|
974
|
+
);
|
|
975
|
+
for (const f of fetched) {
|
|
976
|
+
const { stream, events } = f;
|
|
977
|
+
const payloads = events.flatMap((event) => {
|
|
978
|
+
const register = registry.events[event.name];
|
|
979
|
+
if (!register) return [];
|
|
980
|
+
return [...register.reactions.values()].filter((reaction) => {
|
|
981
|
+
const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
|
|
982
|
+
return resolved && resolved.target === stream;
|
|
983
|
+
}).map((reaction) => ({ ...reaction, event }));
|
|
984
|
+
});
|
|
985
|
+
fetchMap.set(stream, { fetch: f, payloads });
|
|
986
|
+
}
|
|
987
|
+
const handled = await Promise.all(
|
|
988
|
+
leased.map((lease) => {
|
|
989
|
+
const entry = fetchMap.get(lease.stream);
|
|
990
|
+
const at = entry.fetch.events.at(-1)?.id || fetch_window_at;
|
|
991
|
+
const { payloads } = entry;
|
|
992
|
+
const batchHandler = batchHandlers.get(lease.stream);
|
|
993
|
+
if (batchHandler && payloads.length > 0) {
|
|
994
|
+
return handleBatch({ ...lease, at }, payloads, batchHandler);
|
|
995
|
+
}
|
|
996
|
+
return handle({ ...lease, at }, payloads);
|
|
997
|
+
})
|
|
998
|
+
);
|
|
999
|
+
const acked = await ops.ack(
|
|
1000
|
+
handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
|
|
1001
|
+
);
|
|
1002
|
+
const blocked = await ops.block(
|
|
1003
|
+
handled.filter(({ block: block2 }) => block2).map(({ lease, error }) => ({ ...lease, error }))
|
|
1004
|
+
);
|
|
1005
|
+
return { leased, fetched, handled, acked, blocked };
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// src/internal/drain-ratio.ts
|
|
1009
|
+
var RATIO_MIN = 0.2;
|
|
1010
|
+
var RATIO_MAX = 0.8;
|
|
1011
|
+
var RATIO_DEFAULT = 0.5;
|
|
1012
|
+
function computeLagLeadRatio(handled, lagging, leading) {
|
|
1013
|
+
let lagging_handled = 0;
|
|
1014
|
+
let leading_handled = 0;
|
|
1015
|
+
for (const { lease, handled: count } of handled) {
|
|
1016
|
+
if (lease.lagging) lagging_handled += count;
|
|
1017
|
+
else leading_handled += count;
|
|
1018
|
+
}
|
|
1019
|
+
const lagging_avg = lagging > 0 ? lagging_handled / lagging : 0;
|
|
1020
|
+
const leading_avg = leading > 0 ? leading_handled / leading : 0;
|
|
1021
|
+
const total = lagging_avg + leading_avg;
|
|
1022
|
+
if (total === 0) return RATIO_DEFAULT;
|
|
1023
|
+
return Math.max(RATIO_MIN, Math.min(RATIO_MAX, lagging_avg / total));
|
|
1024
|
+
}
|
|
1025
|
+
|
|
677
1026
|
// src/internal/merge.ts
|
|
678
1027
|
import { ZodObject } from "zod";
|
|
679
1028
|
function baseTypeName(zodType) {
|
|
@@ -781,6 +1130,15 @@ function mergePatches(existing, incoming, stateName) {
|
|
|
781
1130
|
}
|
|
782
1131
|
return merged;
|
|
783
1132
|
}
|
|
1133
|
+
function mergeEventRegister(target, source) {
|
|
1134
|
+
for (const [eventName, sourceReg] of Object.entries(source)) {
|
|
1135
|
+
const targetReg = target[eventName];
|
|
1136
|
+
if (!targetReg) continue;
|
|
1137
|
+
for (const [name, reaction] of sourceReg.reactions) {
|
|
1138
|
+
targetReg.reactions.set(name, reaction);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
784
1142
|
function mergeProjection(proj, events) {
|
|
785
1143
|
for (const eventName of Object.keys(proj.events)) {
|
|
786
1144
|
const projRegister = proj.events[eventName];
|
|
@@ -825,7 +1183,7 @@ var subscribe = (streams) => store().subscribe(streams);
|
|
|
825
1183
|
|
|
826
1184
|
// src/internal/event-sourcing.ts
|
|
827
1185
|
import { patch } from "@rotorsoft/act-patch";
|
|
828
|
-
import { randomUUID } from "crypto";
|
|
1186
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
829
1187
|
async function snap(snapshot) {
|
|
830
1188
|
try {
|
|
831
1189
|
const { id, stream, name, meta, version } = snapshot.event;
|
|
@@ -843,6 +1201,20 @@ async function snap(snapshot) {
|
|
|
843
1201
|
log().error(error);
|
|
844
1202
|
}
|
|
845
1203
|
}
|
|
1204
|
+
async function tombstone(stream, expectedVersion, correlation) {
|
|
1205
|
+
try {
|
|
1206
|
+
const [committed] = await store().commit(
|
|
1207
|
+
stream,
|
|
1208
|
+
[{ name: TOMBSTONE_EVENT, data: {} }],
|
|
1209
|
+
{ correlation, causation: {} },
|
|
1210
|
+
expectedVersion
|
|
1211
|
+
);
|
|
1212
|
+
return committed;
|
|
1213
|
+
} catch (error) {
|
|
1214
|
+
if (error instanceof ConcurrencyError) return void 0;
|
|
1215
|
+
throw error;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
846
1218
|
async function load(me, stream, callback, asOf) {
|
|
847
1219
|
const timeTravel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
|
|
848
1220
|
const cached = timeTravel ? void 0 : await cache().get(stream);
|
|
@@ -907,13 +1279,13 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
907
1279
|
data: skipValidation ? data : validate(name, data, me.events[name])
|
|
908
1280
|
}));
|
|
909
1281
|
const meta = {
|
|
910
|
-
correlation: reactingTo?.meta.correlation ||
|
|
1282
|
+
correlation: reactingTo?.meta.correlation || randomUUID3(),
|
|
911
1283
|
causation: {
|
|
912
1284
|
action: {
|
|
913
1285
|
name: action2,
|
|
914
1286
|
...target
|
|
915
|
-
// payload
|
|
916
|
-
//
|
|
1287
|
+
// payload intentionally omitted: it can be large or contain PII,
|
|
1288
|
+
// and callers correlate via the correlation id when they need it.
|
|
917
1289
|
},
|
|
918
1290
|
event: reactingTo ? {
|
|
919
1291
|
id: reactingTo.id,
|
|
@@ -928,7 +1300,10 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
928
1300
|
stream,
|
|
929
1301
|
emitted,
|
|
930
1302
|
meta,
|
|
931
|
-
//
|
|
1303
|
+
// Reactions skip optimistic concurrency: they always append against the
|
|
1304
|
+
// current head. Stream leasing already serializes concurrent reactions,
|
|
1305
|
+
// and forcing version checks here would turn ordinary catch-up into
|
|
1306
|
+
// spurious retries.
|
|
932
1307
|
reactingTo ? void 0 : expected
|
|
933
1308
|
);
|
|
934
1309
|
} catch (error) {
|
|
@@ -978,7 +1353,12 @@ var traced = (inner, exit, entry) => (async (...args) => {
|
|
|
978
1353
|
});
|
|
979
1354
|
function buildEs(logger) {
|
|
980
1355
|
if (logger.level !== "trace") {
|
|
981
|
-
return {
|
|
1356
|
+
return {
|
|
1357
|
+
snap,
|
|
1358
|
+
load,
|
|
1359
|
+
action,
|
|
1360
|
+
tombstone
|
|
1361
|
+
};
|
|
982
1362
|
}
|
|
983
1363
|
return {
|
|
984
1364
|
snap: traced(snap, void 0, (snapshot) => {
|
|
@@ -1016,7 +1396,13 @@ function buildEs(logger) {
|
|
|
1016
1396
|
es_caption("action", C_BLUE, `${target.stream}.${action2}`)
|
|
1017
1397
|
);
|
|
1018
1398
|
}
|
|
1019
|
-
)
|
|
1399
|
+
),
|
|
1400
|
+
tombstone: traced(tombstone, (committed, stream) => {
|
|
1401
|
+
if (committed)
|
|
1402
|
+
logger.trace(
|
|
1403
|
+
es_caption("tombstoned", C_ORANGE, `${stream}@${committed.version}`)
|
|
1404
|
+
);
|
|
1405
|
+
})
|
|
1020
1406
|
};
|
|
1021
1407
|
}
|
|
1022
1408
|
function buildDrain(logger) {
|
|
@@ -1079,11 +1465,15 @@ function buildDrain(logger) {
|
|
|
1079
1465
|
}
|
|
1080
1466
|
|
|
1081
1467
|
// src/act.ts
|
|
1468
|
+
var DEFAULT_MAX_SUBSCRIBED_STREAMS = 1e3;
|
|
1082
1469
|
var Act = class {
|
|
1083
|
-
constructor(registry, _states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map()) {
|
|
1470
|
+
constructor(registry, _states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map(), options = {}) {
|
|
1084
1471
|
this.registry = registry;
|
|
1085
1472
|
this._states = _states;
|
|
1086
1473
|
this._batch_handlers = batchHandlers;
|
|
1474
|
+
this._subscribed_streams = new LruSet(
|
|
1475
|
+
options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS
|
|
1476
|
+
);
|
|
1087
1477
|
this._es = buildEs(this._logger);
|
|
1088
1478
|
this._cd = buildDrain(this._logger);
|
|
1089
1479
|
const statics = /* @__PURE__ */ new Map();
|
|
@@ -1121,20 +1511,42 @@ var Act = class {
|
|
|
1121
1511
|
_settle_timer = void 0;
|
|
1122
1512
|
_settling = false;
|
|
1123
1513
|
_correlation_checkpoint = -1;
|
|
1124
|
-
|
|
1514
|
+
/**
|
|
1515
|
+
* Streams already subscribed via store.subscribe() — both the static
|
|
1516
|
+
* targets registered at init and dynamic targets discovered by
|
|
1517
|
+
* correlate(). correlate() consults this set to avoid re-subscribing
|
|
1518
|
+
* known streams.
|
|
1519
|
+
*
|
|
1520
|
+
* Bounded LRU so apps that mint millions of dynamic targets (one per
|
|
1521
|
+
* aggregate) don't grow this unbounded. Eviction costs at most one
|
|
1522
|
+
* redundant store.subscribe() call per evicted-but-still-active stream
|
|
1523
|
+
* (subscribe is idempotent). Cap configurable via {@link ActOptions}.
|
|
1524
|
+
*/
|
|
1525
|
+
_subscribed_streams;
|
|
1125
1526
|
_has_dynamic_resolvers = false;
|
|
1126
1527
|
_correlation_initialized = false;
|
|
1127
1528
|
/** Event names with at least one registered reaction (computed at build time) */
|
|
1128
1529
|
_reactive_events = /* @__PURE__ */ new Set();
|
|
1129
1530
|
/** Set in do() when a committed event has reactions — cleared by drain() */
|
|
1130
1531
|
_needs_drain = false;
|
|
1532
|
+
/**
|
|
1533
|
+
* Emit a lifecycle event. The payload type is inferred from the event name
|
|
1534
|
+
* via {@link ActLifecycleEvents}.
|
|
1535
|
+
*/
|
|
1131
1536
|
emit(event, args) {
|
|
1132
1537
|
return this._emitter.emit(event, args);
|
|
1133
1538
|
}
|
|
1539
|
+
/**
|
|
1540
|
+
* Register a listener for a lifecycle event. The listener receives the
|
|
1541
|
+
* event-specific payload.
|
|
1542
|
+
*/
|
|
1134
1543
|
on(event, listener) {
|
|
1135
1544
|
this._emitter.on(event, listener);
|
|
1136
1545
|
return this;
|
|
1137
1546
|
}
|
|
1547
|
+
/**
|
|
1548
|
+
* Remove a previously registered lifecycle listener.
|
|
1549
|
+
*/
|
|
1138
1550
|
off(event, listener) {
|
|
1139
1551
|
this._emitter.off(event, listener);
|
|
1140
1552
|
return this;
|
|
@@ -1168,6 +1580,9 @@ var Act = class {
|
|
|
1168
1580
|
_bound_load = this.load.bind(this);
|
|
1169
1581
|
_bound_query = this.query.bind(this);
|
|
1170
1582
|
_bound_query_array = this.query_array.bind(this);
|
|
1583
|
+
/** Pre-bound dispatchers handed to runDrainCycle each cycle. */
|
|
1584
|
+
_bound_handle = this.handle.bind(this);
|
|
1585
|
+
_bound_handle_batch = this.handleBatch.bind(this);
|
|
1171
1586
|
/**
|
|
1172
1587
|
* Executes an action on a state instance, committing resulting events.
|
|
1173
1588
|
*
|
|
@@ -1370,26 +1785,46 @@ var Act = class {
|
|
|
1370
1785
|
return events;
|
|
1371
1786
|
}
|
|
1372
1787
|
/**
|
|
1373
|
-
*
|
|
1374
|
-
*
|
|
1375
|
-
*
|
|
1376
|
-
*
|
|
1788
|
+
* Shared finalization for the two reaction-runner shapes (per-event
|
|
1789
|
+
* `handle` and bulk `handleBatch`). Centralizes the error log, retry-vs-
|
|
1790
|
+
* block decision, and the "error reported only when nothing was handled"
|
|
1791
|
+
* rule that's true in both shapes (in batch mode, `handled` is always 0
|
|
1792
|
+
* on failure, so the rule degenerates to "always reported").
|
|
1793
|
+
*/
|
|
1794
|
+
_finalize(lease, handled, at, error, options) {
|
|
1795
|
+
if (!error) return { lease, handled, at };
|
|
1796
|
+
this._logger.error(error);
|
|
1797
|
+
const block2 = lease.retry >= options.maxRetries && options.blockOnError;
|
|
1798
|
+
if (block2)
|
|
1799
|
+
this._logger.error(
|
|
1800
|
+
`Blocking ${lease.stream} after ${lease.retry} retries.`
|
|
1801
|
+
);
|
|
1802
|
+
return {
|
|
1803
|
+
lease,
|
|
1804
|
+
handled,
|
|
1805
|
+
at,
|
|
1806
|
+
error: handled === 0 ? error.message : void 0,
|
|
1807
|
+
block: block2
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* Handles leased reactions one event at a time.
|
|
1377
1812
|
*
|
|
1378
|
-
*
|
|
1379
|
-
*
|
|
1380
|
-
*
|
|
1381
|
-
*
|
|
1813
|
+
* Called by the main `drain` loop after fetching new events. Each handler
|
|
1814
|
+
* receives a scoped `IAct` proxy that auto-injects the triggering event
|
|
1815
|
+
* as `reactingTo` when `do()` is called without it, maintaining
|
|
1816
|
+
* correlation chains by default (#587). Handlers can still pass an
|
|
1817
|
+
* explicit `reactingTo` to override.
|
|
1382
1818
|
*
|
|
1383
1819
|
* @internal
|
|
1384
|
-
* @param lease The lease to handle
|
|
1385
|
-
* @param payloads The reactions to handle
|
|
1386
|
-
* @returns The lease with results
|
|
1387
1820
|
*/
|
|
1388
1821
|
async handle(lease, payloads) {
|
|
1389
1822
|
if (payloads.length === 0) return { lease, handled: 0, at: lease.at };
|
|
1390
1823
|
const stream = lease.stream;
|
|
1391
|
-
let at = payloads.at(0).event.id
|
|
1392
|
-
|
|
1824
|
+
let at = payloads.at(0).event.id;
|
|
1825
|
+
let handled = 0;
|
|
1826
|
+
if (lease.retry > 0)
|
|
1827
|
+
this._logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
|
|
1393
1828
|
const doAction = this._bound_do;
|
|
1394
1829
|
const scopedApp = {
|
|
1395
1830
|
do: doAction,
|
|
@@ -1398,7 +1833,7 @@ var Act = class {
|
|
|
1398
1833
|
query_array: this._bound_query_array
|
|
1399
1834
|
};
|
|
1400
1835
|
for (const payload of payloads) {
|
|
1401
|
-
const { event, handler
|
|
1836
|
+
const { event, handler } = payload;
|
|
1402
1837
|
scopedApp.do = (action2, target, payload2, reactingTo, skipValidation) => doAction(
|
|
1403
1838
|
action2,
|
|
1404
1839
|
target,
|
|
@@ -1411,22 +1846,16 @@ var Act = class {
|
|
|
1411
1846
|
at = event.id;
|
|
1412
1847
|
handled++;
|
|
1413
1848
|
} catch (error) {
|
|
1414
|
-
this.
|
|
1415
|
-
const block2 = lease.retry >= options.maxRetries && options.blockOnError;
|
|
1416
|
-
block2 && this._logger.error(
|
|
1417
|
-
`Blocking ${stream} after ${lease.retry} retries.`
|
|
1418
|
-
);
|
|
1419
|
-
return {
|
|
1849
|
+
return this._finalize(
|
|
1420
1850
|
lease,
|
|
1421
1851
|
handled,
|
|
1422
1852
|
at,
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
};
|
|
1853
|
+
error,
|
|
1854
|
+
payload.options
|
|
1855
|
+
);
|
|
1427
1856
|
}
|
|
1428
1857
|
}
|
|
1429
|
-
return
|
|
1858
|
+
return this._finalize(lease, handled, at, void 0, payloads[0].options);
|
|
1430
1859
|
}
|
|
1431
1860
|
/**
|
|
1432
1861
|
* Handles a batch of events for a projection with a batch handler.
|
|
@@ -1436,33 +1865,26 @@ var Act = class {
|
|
|
1436
1865
|
* in a single call, enabling bulk DB operations.
|
|
1437
1866
|
*
|
|
1438
1867
|
* @internal
|
|
1439
|
-
* @param lease The lease to handle
|
|
1440
|
-
* @param payloads The reactions to handle
|
|
1441
|
-
* @param batchHandler The batch handler for this projection
|
|
1442
|
-
* @returns The lease with results
|
|
1443
1868
|
*/
|
|
1444
1869
|
async handleBatch(lease, payloads, batchHandler) {
|
|
1445
1870
|
const stream = lease.stream;
|
|
1446
1871
|
const events = payloads.map((p) => p.event);
|
|
1447
|
-
const
|
|
1448
|
-
lease.retry > 0
|
|
1449
|
-
|
|
1450
|
-
|
|
1872
|
+
const options = payloads[0].options;
|
|
1873
|
+
if (lease.retry > 0)
|
|
1874
|
+
this._logger.warn(
|
|
1875
|
+
`Retrying batch ${stream}@${events[0].id} (${lease.retry}).`
|
|
1876
|
+
);
|
|
1451
1877
|
try {
|
|
1452
1878
|
await batchHandler(events, stream);
|
|
1453
|
-
return
|
|
1454
|
-
} catch (error) {
|
|
1455
|
-
this._logger.error(error);
|
|
1456
|
-
const { options } = payloads[0];
|
|
1457
|
-
const block2 = lease.retry >= options.maxRetries && options.blockOnError;
|
|
1458
|
-
block2 && this._logger.error(`Blocking ${stream} after ${lease.retry} retries.`);
|
|
1459
|
-
return {
|
|
1879
|
+
return this._finalize(
|
|
1460
1880
|
lease,
|
|
1461
|
-
|
|
1462
|
-
at
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1881
|
+
events.length,
|
|
1882
|
+
events.at(-1).id,
|
|
1883
|
+
void 0,
|
|
1884
|
+
options
|
|
1885
|
+
);
|
|
1886
|
+
} catch (error) {
|
|
1887
|
+
return this._finalize(lease, 0, lease.at, error, options);
|
|
1466
1888
|
}
|
|
1467
1889
|
}
|
|
1468
1890
|
/**
|
|
@@ -1512,82 +1934,46 @@ var Act = class {
|
|
|
1512
1934
|
if (!this._needs_drain) {
|
|
1513
1935
|
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
1514
1936
|
}
|
|
1515
|
-
if (
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
for (const f of fetched) {
|
|
1537
|
-
const { stream, events } = f;
|
|
1538
|
-
const payloads = events.flatMap((event) => {
|
|
1539
|
-
const register = this.registry.events[event.name];
|
|
1540
|
-
if (!register) return [];
|
|
1541
|
-
return [...register.reactions.values()].filter((reaction) => {
|
|
1542
|
-
const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
|
|
1543
|
-
return resolved && resolved.target === stream;
|
|
1544
|
-
}).map((reaction) => ({ ...reaction, event }));
|
|
1545
|
-
});
|
|
1546
|
-
fetchMap.set(stream, { fetch: f, payloads });
|
|
1547
|
-
}
|
|
1548
|
-
const handled = await Promise.all(
|
|
1549
|
-
leased.map((lease) => {
|
|
1550
|
-
const entry = fetchMap.get(lease.stream);
|
|
1551
|
-
const at = entry?.fetch.events.at(-1)?.id || fetch_window_at;
|
|
1552
|
-
const payloads = entry?.payloads ?? [];
|
|
1553
|
-
const batchHandler = this._batch_handlers.get(lease.stream);
|
|
1554
|
-
if (batchHandler && payloads.length > 0) {
|
|
1555
|
-
return this.handleBatch({ ...lease, at }, payloads, batchHandler);
|
|
1556
|
-
}
|
|
1557
|
-
return this.handle({ ...lease, at }, payloads);
|
|
1558
|
-
})
|
|
1559
|
-
);
|
|
1560
|
-
const [lagging_handled, leading_handled] = handled.reduce(
|
|
1561
|
-
([lagging_handled2, leading_handled2], { lease, handled: handled2 }) => [
|
|
1562
|
-
lagging_handled2 + (lease.lagging ? handled2 : 0),
|
|
1563
|
-
leading_handled2 + (lease.lagging ? 0 : handled2)
|
|
1564
|
-
],
|
|
1565
|
-
[0, 0]
|
|
1566
|
-
);
|
|
1567
|
-
const lagging_avg = lagging > 0 ? lagging_handled / lagging : 0;
|
|
1568
|
-
const leading_avg = leading > 0 ? leading_handled / leading : 0;
|
|
1569
|
-
const total = lagging_avg + leading_avg;
|
|
1570
|
-
this._drain_lag2lead_ratio = total > 0 ? Math.max(0.2, Math.min(0.8, lagging_avg / total)) : 0.5;
|
|
1571
|
-
const acked = await this._cd.ack(
|
|
1572
|
-
handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
|
|
1573
|
-
);
|
|
1574
|
-
if (acked.length) this.emit("acked", acked);
|
|
1575
|
-
const blocked = await this._cd.block(
|
|
1576
|
-
handled.filter(({ block: block2 }) => block2).map(({ lease, error }) => ({ ...lease, error }))
|
|
1577
|
-
);
|
|
1578
|
-
if (blocked.length) this.emit("blocked", blocked);
|
|
1579
|
-
const result = { fetched, leased, acked, blocked };
|
|
1580
|
-
const hasErrors = handled.some(({ error }) => error);
|
|
1581
|
-
if (!acked.length && !blocked.length && !hasErrors)
|
|
1582
|
-
this._needs_drain = false;
|
|
1583
|
-
return result;
|
|
1584
|
-
} catch (error) {
|
|
1585
|
-
this._logger.error(error);
|
|
1586
|
-
} finally {
|
|
1587
|
-
this._drain_locked = false;
|
|
1937
|
+
if (this._drain_locked) {
|
|
1938
|
+
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
1939
|
+
}
|
|
1940
|
+
try {
|
|
1941
|
+
this._drain_locked = true;
|
|
1942
|
+
const lagging = Math.ceil(streamLimit * this._drain_lag2lead_ratio);
|
|
1943
|
+
const leading = streamLimit - lagging;
|
|
1944
|
+
const cycle = await runDrainCycle(
|
|
1945
|
+
this._cd,
|
|
1946
|
+
this.registry,
|
|
1947
|
+
this._batch_handlers,
|
|
1948
|
+
this._bound_handle,
|
|
1949
|
+
this._bound_handle_batch,
|
|
1950
|
+
lagging,
|
|
1951
|
+
leading,
|
|
1952
|
+
eventLimit,
|
|
1953
|
+
leaseMillis
|
|
1954
|
+
);
|
|
1955
|
+
if (!cycle) {
|
|
1956
|
+
this._needs_drain = false;
|
|
1957
|
+
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
1588
1958
|
}
|
|
1959
|
+
const { leased, fetched, handled, acked, blocked } = cycle;
|
|
1960
|
+
this._drain_lag2lead_ratio = computeLagLeadRatio(
|
|
1961
|
+
handled,
|
|
1962
|
+
lagging,
|
|
1963
|
+
leading
|
|
1964
|
+
);
|
|
1965
|
+
if (acked.length) this.emit("acked", acked);
|
|
1966
|
+
if (blocked.length) this.emit("blocked", blocked);
|
|
1967
|
+
const hasErrors = handled.some(({ error }) => error);
|
|
1968
|
+
if (!acked.length && !blocked.length && !hasErrors)
|
|
1969
|
+
this._needs_drain = false;
|
|
1970
|
+
return { fetched, leased, acked, blocked };
|
|
1971
|
+
} catch (error) {
|
|
1972
|
+
this._logger.error(error);
|
|
1973
|
+
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
1974
|
+
} finally {
|
|
1975
|
+
this._drain_locked = false;
|
|
1589
1976
|
}
|
|
1590
|
-
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
1591
1977
|
}
|
|
1592
1978
|
/**
|
|
1593
1979
|
* Discovers and registers new streams dynamically based on reaction resolvers.
|
|
@@ -1648,7 +2034,7 @@ var Act = class {
|
|
|
1648
2034
|
this._correlation_checkpoint = watermark;
|
|
1649
2035
|
if (this._reactive_events.size > 0) this._needs_drain = true;
|
|
1650
2036
|
for (const { stream } of this._static_targets) {
|
|
1651
|
-
this.
|
|
2037
|
+
this._subscribed_streams.add(stream);
|
|
1652
2038
|
}
|
|
1653
2039
|
}
|
|
1654
2040
|
async correlate(query = { after: -1, limit: 10 }) {
|
|
@@ -1666,7 +2052,7 @@ var Act = class {
|
|
|
1666
2052
|
for (const reaction of register.reactions.values()) {
|
|
1667
2053
|
if (typeof reaction.resolver !== "function") continue;
|
|
1668
2054
|
const resolved = reaction.resolver(event);
|
|
1669
|
-
if (resolved && !this.
|
|
2055
|
+
if (resolved && !this._subscribed_streams.has(resolved.target)) {
|
|
1670
2056
|
const entry = correlated.get(resolved.target) || {
|
|
1671
2057
|
source: resolved.source,
|
|
1672
2058
|
payloads: []
|
|
@@ -1692,7 +2078,7 @@ var Act = class {
|
|
|
1692
2078
|
this._correlation_checkpoint = last_id;
|
|
1693
2079
|
if (subscribed) {
|
|
1694
2080
|
for (const { stream } of streams) {
|
|
1695
|
-
this.
|
|
2081
|
+
this._subscribed_streams.add(stream);
|
|
1696
2082
|
}
|
|
1697
2083
|
}
|
|
1698
2084
|
return { subscribed, last_id };
|
|
@@ -1875,143 +2261,14 @@ var Act = class {
|
|
|
1875
2261
|
*/
|
|
1876
2262
|
async close(targets) {
|
|
1877
2263
|
if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
|
|
1878
|
-
const targetMap = new Map(targets.map((t) => [t.stream, t]));
|
|
1879
|
-
const streams = [...targetMap.keys()];
|
|
1880
2264
|
await this.correlate({ limit: 1e3 });
|
|
1881
|
-
const
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
await store().query(
|
|
1888
|
-
(e) => {
|
|
1889
|
-
if (e.name === TOMBSTONE_EVENT) return;
|
|
1890
|
-
if (maxId === -1) {
|
|
1891
|
-
maxId = e.id;
|
|
1892
|
-
version = e.version;
|
|
1893
|
-
}
|
|
1894
|
-
if (e.name !== SNAP_EVENT && lastEventName === void 0) {
|
|
1895
|
-
lastEventName = e.name;
|
|
1896
|
-
}
|
|
1897
|
-
},
|
|
1898
|
-
// limit: 2 covers the typical snapshot-at-head case (snapshot is
|
|
1899
|
-
// always preceded by the domain event it captured). Streams with
|
|
1900
|
-
// unusual layouts fall back to no-seed via the lookup miss path.
|
|
1901
|
-
{ stream: s, stream_exact: true, backward: true, limit: 2 }
|
|
1902
|
-
);
|
|
1903
|
-
if (maxId >= 0) streamInfo.set(s, { maxId, version, lastEventName });
|
|
1904
|
-
})
|
|
1905
|
-
);
|
|
1906
|
-
const skipped = [];
|
|
1907
|
-
let safe;
|
|
1908
|
-
if (this._reactive_events.size === 0) {
|
|
1909
|
-
safe = [...streamInfo.keys()];
|
|
1910
|
-
} else {
|
|
1911
|
-
const pendingSet = /* @__PURE__ */ new Set();
|
|
1912
|
-
await store().query_streams((position) => {
|
|
1913
|
-
const sourceRe = position.source ? RegExp(position.source) : void 0;
|
|
1914
|
-
for (const [stream, info] of streamInfo) {
|
|
1915
|
-
if ((!sourceRe || sourceRe.test(stream)) && position.at < info.maxId) {
|
|
1916
|
-
pendingSet.add(stream);
|
|
1917
|
-
}
|
|
1918
|
-
}
|
|
1919
|
-
});
|
|
1920
|
-
safe = [];
|
|
1921
|
-
for (const [stream] of streamInfo) {
|
|
1922
|
-
if (pendingSet.has(stream)) {
|
|
1923
|
-
skipped.push(stream);
|
|
1924
|
-
} else {
|
|
1925
|
-
safe.push(stream);
|
|
1926
|
-
}
|
|
1927
|
-
}
|
|
1928
|
-
}
|
|
1929
|
-
if (!safe.length) {
|
|
1930
|
-
const result2 = { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
1931
|
-
this.emit("closed", result2);
|
|
1932
|
-
return result2;
|
|
1933
|
-
}
|
|
1934
|
-
const correlation = randomUUID2();
|
|
1935
|
-
const guarded = [];
|
|
1936
|
-
const guardEvents = /* @__PURE__ */ new Map();
|
|
1937
|
-
await Promise.all(
|
|
1938
|
-
safe.map(async (stream) => {
|
|
1939
|
-
try {
|
|
1940
|
-
const info = streamInfo.get(stream);
|
|
1941
|
-
const [committed] = await store().commit(
|
|
1942
|
-
stream,
|
|
1943
|
-
[{ name: TOMBSTONE_EVENT, data: {} }],
|
|
1944
|
-
{ correlation, causation: {} },
|
|
1945
|
-
info.version
|
|
1946
|
-
);
|
|
1947
|
-
guarded.push(stream);
|
|
1948
|
-
guardEvents.set(stream, { id: committed.id, stream });
|
|
1949
|
-
} catch {
|
|
1950
|
-
skipped.push(stream);
|
|
1951
|
-
}
|
|
1952
|
-
})
|
|
1953
|
-
);
|
|
1954
|
-
if (!guarded.length) {
|
|
1955
|
-
const result2 = { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
1956
|
-
this.emit("closed", result2);
|
|
1957
|
-
return result2;
|
|
1958
|
-
}
|
|
1959
|
-
const seedStates = /* @__PURE__ */ new Map();
|
|
1960
|
-
await Promise.all(
|
|
1961
|
-
guarded.filter((s) => targetMap.get(s)?.restart).map(async (stream) => {
|
|
1962
|
-
const lastEventName = streamInfo.get(stream)?.lastEventName;
|
|
1963
|
-
const ownerState = lastEventName ? this._event_to_state.get(lastEventName) : void 0;
|
|
1964
|
-
if (!ownerState) {
|
|
1965
|
-
this._logger.error(
|
|
1966
|
-
`Cannot seed restart for "${stream}": no registered state owns event "${lastEventName ?? "<none>"}". Stream will be tombstoned instead.`
|
|
1967
|
-
);
|
|
1968
|
-
return;
|
|
1969
|
-
}
|
|
1970
|
-
const snap2 = await this._es.load(ownerState, stream);
|
|
1971
|
-
seedStates.set(stream, snap2.state);
|
|
1972
|
-
})
|
|
1973
|
-
);
|
|
1974
|
-
for (const stream of guarded) {
|
|
1975
|
-
const archiveFn = targetMap.get(stream)?.archive;
|
|
1976
|
-
if (archiveFn) await archiveFn();
|
|
1977
|
-
}
|
|
1978
|
-
const truncTargets = guarded.map((stream) => {
|
|
1979
|
-
const snapshot = seedStates.get(stream);
|
|
1980
|
-
const guard = guardEvents.get(stream);
|
|
1981
|
-
return {
|
|
1982
|
-
stream,
|
|
1983
|
-
snapshot,
|
|
1984
|
-
meta: {
|
|
1985
|
-
correlation,
|
|
1986
|
-
causation: {
|
|
1987
|
-
event: {
|
|
1988
|
-
id: guard.id,
|
|
1989
|
-
name: TOMBSTONE_EVENT,
|
|
1990
|
-
stream: guard.stream
|
|
1991
|
-
}
|
|
1992
|
-
}
|
|
1993
|
-
}
|
|
1994
|
-
};
|
|
2265
|
+
const result = await runCloseCycle(targets, {
|
|
2266
|
+
reactiveEventsSize: this._reactive_events.size,
|
|
2267
|
+
eventToState: this._event_to_state,
|
|
2268
|
+
load: this._es.load,
|
|
2269
|
+
tombstone: this._es.tombstone,
|
|
2270
|
+
logger: this._logger
|
|
1995
2271
|
});
|
|
1996
|
-
const truncated = await store().truncate(truncTargets);
|
|
1997
|
-
await Promise.all(
|
|
1998
|
-
guarded.map(async (stream) => {
|
|
1999
|
-
const entry = truncated.get(stream);
|
|
2000
|
-
const state2 = seedStates.get(stream);
|
|
2001
|
-
if (state2 && entry) {
|
|
2002
|
-
await cache().set(stream, {
|
|
2003
|
-
state: state2,
|
|
2004
|
-
version: entry.committed.version,
|
|
2005
|
-
event_id: entry.committed.id,
|
|
2006
|
-
patches: 0,
|
|
2007
|
-
snaps: 1
|
|
2008
|
-
});
|
|
2009
|
-
} else {
|
|
2010
|
-
await cache().invalidate(stream);
|
|
2011
|
-
}
|
|
2012
|
-
})
|
|
2013
|
-
);
|
|
2014
|
-
const result = { truncated, skipped };
|
|
2015
2272
|
this.emit("closed", result);
|
|
2016
2273
|
return result;
|
|
2017
2274
|
}
|
|
@@ -2080,7 +2337,7 @@ var Act = class {
|
|
|
2080
2337
|
}
|
|
2081
2338
|
};
|
|
2082
2339
|
|
|
2083
|
-
// src/act-builder.ts
|
|
2340
|
+
// src/builders/act-builder.ts
|
|
2084
2341
|
function registerBatchHandler(proj, batchHandlers) {
|
|
2085
2342
|
if (!proj.batchHandler || !proj.target) return;
|
|
2086
2343
|
const existing = batchHandlers.get(proj.target);
|
|
@@ -2089,56 +2346,33 @@ function registerBatchHandler(proj, batchHandlers) {
|
|
|
2089
2346
|
}
|
|
2090
2347
|
batchHandlers.set(proj.target, proj.batchHandler);
|
|
2091
2348
|
}
|
|
2092
|
-
function act(
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
},
|
|
2349
|
+
function act() {
|
|
2350
|
+
const states = /* @__PURE__ */ new Map();
|
|
2351
|
+
const registry = {
|
|
2352
|
+
actions: {},
|
|
2353
|
+
events: {}
|
|
2354
|
+
};
|
|
2355
|
+
const pendingProjections = [];
|
|
2356
|
+
const batchHandlers = /* @__PURE__ */ new Map();
|
|
2096
2357
|
const builder = {
|
|
2097
2358
|
withState: (state2) => {
|
|
2098
2359
|
registerState(state2, states, registry.actions, registry.events);
|
|
2099
|
-
return
|
|
2100
|
-
states,
|
|
2101
|
-
registry,
|
|
2102
|
-
pendingProjections,
|
|
2103
|
-
batchHandlers
|
|
2104
|
-
);
|
|
2360
|
+
return builder;
|
|
2105
2361
|
},
|
|
2106
2362
|
withSlice: (input) => {
|
|
2107
2363
|
for (const s of input.states.values()) {
|
|
2108
2364
|
registerState(s, states, registry.actions, registry.events);
|
|
2109
2365
|
}
|
|
2110
|
-
|
|
2111
|
-
const sliceRegister = input.events[eventName];
|
|
2112
|
-
for (const [name, reaction] of sliceRegister.reactions) {
|
|
2113
|
-
registry.events[eventName].reactions.set(name, reaction);
|
|
2114
|
-
}
|
|
2115
|
-
}
|
|
2366
|
+
mergeEventRegister(registry.events, input.events);
|
|
2116
2367
|
pendingProjections.push(...input.projections);
|
|
2117
|
-
return
|
|
2118
|
-
states,
|
|
2119
|
-
registry,
|
|
2120
|
-
pendingProjections,
|
|
2121
|
-
batchHandlers
|
|
2122
|
-
);
|
|
2368
|
+
return builder;
|
|
2123
2369
|
},
|
|
2124
2370
|
withProjection: (proj) => {
|
|
2125
2371
|
mergeProjection(proj, registry.events);
|
|
2126
2372
|
registerBatchHandler(proj, batchHandlers);
|
|
2127
|
-
return
|
|
2128
|
-
states,
|
|
2129
|
-
registry,
|
|
2130
|
-
pendingProjections,
|
|
2131
|
-
batchHandlers
|
|
2132
|
-
);
|
|
2133
|
-
},
|
|
2134
|
-
withActor: () => {
|
|
2135
|
-
return act(
|
|
2136
|
-
states,
|
|
2137
|
-
registry,
|
|
2138
|
-
pendingProjections,
|
|
2139
|
-
batchHandlers
|
|
2140
|
-
);
|
|
2373
|
+
return builder;
|
|
2141
2374
|
},
|
|
2375
|
+
withActor: () => builder,
|
|
2142
2376
|
on: (event) => ({
|
|
2143
2377
|
do: (handler, options) => {
|
|
2144
2378
|
const reaction = {
|
|
@@ -2154,19 +2388,15 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
|
|
|
2154
2388
|
`Reaction handler for "${String(event)}" must be a named function`
|
|
2155
2389
|
);
|
|
2156
2390
|
registry.events[event].reactions.set(handler.name, reaction);
|
|
2157
|
-
return {
|
|
2158
|
-
...builder,
|
|
2391
|
+
return Object.assign(builder, {
|
|
2159
2392
|
to(resolver) {
|
|
2160
|
-
|
|
2161
|
-
...reaction,
|
|
2162
|
-
resolver: typeof resolver === "string" ? { target: resolver } : resolver
|
|
2163
|
-
});
|
|
2393
|
+
reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
|
|
2164
2394
|
return builder;
|
|
2165
2395
|
}
|
|
2166
|
-
};
|
|
2396
|
+
});
|
|
2167
2397
|
}
|
|
2168
2398
|
}),
|
|
2169
|
-
build: () => {
|
|
2399
|
+
build: (options) => {
|
|
2170
2400
|
for (const proj of pendingProjections) {
|
|
2171
2401
|
mergeProjection(proj, registry.events);
|
|
2172
2402
|
registerBatchHandler(proj, batchHandlers);
|
|
@@ -2174,7 +2404,8 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
|
|
|
2174
2404
|
return new Act(
|
|
2175
2405
|
registry,
|
|
2176
2406
|
states,
|
|
2177
|
-
batchHandlers
|
|
2407
|
+
batchHandlers,
|
|
2408
|
+
options
|
|
2178
2409
|
);
|
|
2179
2410
|
},
|
|
2180
2411
|
events: registry.events
|
|
@@ -2182,8 +2413,9 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
|
|
|
2182
2413
|
return builder;
|
|
2183
2414
|
}
|
|
2184
2415
|
|
|
2185
|
-
// src/projection-builder.ts
|
|
2186
|
-
function _projection(target
|
|
2416
|
+
// src/builders/projection-builder.ts
|
|
2417
|
+
function _projection(target) {
|
|
2418
|
+
const events = {};
|
|
2187
2419
|
const defaultResolver = typeof target === "string" ? { target } : void 0;
|
|
2188
2420
|
const base = {
|
|
2189
2421
|
on: (entry) => {
|
|
@@ -2213,17 +2445,13 @@ function _projection(target, events) {
|
|
|
2213
2445
|
`Projection handler for "${event}" must be a named function`
|
|
2214
2446
|
);
|
|
2215
2447
|
register.reactions.set(handler.name, reaction);
|
|
2216
|
-
const
|
|
2217
|
-
return {
|
|
2218
|
-
...nextBuilder,
|
|
2448
|
+
const widened = base;
|
|
2449
|
+
return Object.assign(widened, {
|
|
2219
2450
|
to(resolver) {
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
resolver: typeof resolver === "string" ? { target: resolver } : resolver
|
|
2223
|
-
});
|
|
2224
|
-
return nextBuilder;
|
|
2451
|
+
reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
|
|
2452
|
+
return widened;
|
|
2225
2453
|
}
|
|
2226
|
-
};
|
|
2454
|
+
});
|
|
2227
2455
|
}
|
|
2228
2456
|
};
|
|
2229
2457
|
},
|
|
@@ -2235,8 +2463,7 @@ function _projection(target, events) {
|
|
|
2235
2463
|
events
|
|
2236
2464
|
};
|
|
2237
2465
|
if (typeof target === "string") {
|
|
2238
|
-
return {
|
|
2239
|
-
...base,
|
|
2466
|
+
return Object.assign(base, {
|
|
2240
2467
|
batch: (handler) => ({
|
|
2241
2468
|
build: () => ({
|
|
2242
2469
|
_tag: "Projection",
|
|
@@ -2245,34 +2472,28 @@ function _projection(target, events) {
|
|
|
2245
2472
|
batchHandler: handler
|
|
2246
2473
|
})
|
|
2247
2474
|
})
|
|
2248
|
-
};
|
|
2475
|
+
});
|
|
2249
2476
|
}
|
|
2250
2477
|
return base;
|
|
2251
2478
|
}
|
|
2252
|
-
function projection(target
|
|
2253
|
-
return _projection(target
|
|
2479
|
+
function projection(target) {
|
|
2480
|
+
return _projection(target);
|
|
2254
2481
|
}
|
|
2255
2482
|
|
|
2256
|
-
// src/slice-builder.ts
|
|
2257
|
-
function slice(
|
|
2483
|
+
// src/builders/slice-builder.ts
|
|
2484
|
+
function slice() {
|
|
2485
|
+
const states = /* @__PURE__ */ new Map();
|
|
2486
|
+
const actions = {};
|
|
2487
|
+
const events = {};
|
|
2488
|
+
const projections = [];
|
|
2258
2489
|
const builder = {
|
|
2259
2490
|
withState: (state2) => {
|
|
2260
2491
|
registerState(state2, states, actions, events);
|
|
2261
|
-
return
|
|
2262
|
-
states,
|
|
2263
|
-
actions,
|
|
2264
|
-
events,
|
|
2265
|
-
projections
|
|
2266
|
-
);
|
|
2492
|
+
return builder;
|
|
2267
2493
|
},
|
|
2268
2494
|
withProjection: (proj) => {
|
|
2269
2495
|
projections.push(proj);
|
|
2270
|
-
return
|
|
2271
|
-
states,
|
|
2272
|
-
actions,
|
|
2273
|
-
events,
|
|
2274
|
-
projections
|
|
2275
|
-
);
|
|
2496
|
+
return builder;
|
|
2276
2497
|
},
|
|
2277
2498
|
on: (event) => ({
|
|
2278
2499
|
do: (handler, options) => {
|
|
@@ -2289,16 +2510,12 @@ function slice(states = /* @__PURE__ */ new Map(), actions = {}, events = {}, pr
|
|
|
2289
2510
|
`Reaction handler for "${String(event)}" must be a named function`
|
|
2290
2511
|
);
|
|
2291
2512
|
events[event].reactions.set(handler.name, reaction);
|
|
2292
|
-
return {
|
|
2293
|
-
...builder,
|
|
2513
|
+
return Object.assign(builder, {
|
|
2294
2514
|
to(resolver) {
|
|
2295
|
-
|
|
2296
|
-
...reaction,
|
|
2297
|
-
resolver: typeof resolver === "string" ? { target: resolver } : resolver
|
|
2298
|
-
});
|
|
2515
|
+
reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
|
|
2299
2516
|
return builder;
|
|
2300
2517
|
}
|
|
2301
|
-
};
|
|
2518
|
+
});
|
|
2302
2519
|
}
|
|
2303
2520
|
}),
|
|
2304
2521
|
build: () => ({
|
|
@@ -2312,7 +2529,7 @@ function slice(states = /* @__PURE__ */ new Map(), actions = {}, events = {}, pr
|
|
|
2312
2529
|
return builder;
|
|
2313
2530
|
}
|
|
2314
2531
|
|
|
2315
|
-
// src/state-builder.ts
|
|
2532
|
+
// src/builders/state-builder.ts
|
|
2316
2533
|
function state(entry) {
|
|
2317
2534
|
const keys = Object.keys(entry);
|
|
2318
2535
|
if (keys.length !== 1) throw new Error("state() requires exactly one key");
|
|
@@ -2330,7 +2547,7 @@ function state(entry) {
|
|
|
2330
2547
|
return [k, fn];
|
|
2331
2548
|
})
|
|
2332
2549
|
);
|
|
2333
|
-
const
|
|
2550
|
+
const internal = {
|
|
2334
2551
|
events,
|
|
2335
2552
|
actions: {},
|
|
2336
2553
|
state: stateSchema,
|
|
@@ -2338,18 +2555,12 @@ function state(entry) {
|
|
|
2338
2555
|
init,
|
|
2339
2556
|
patch: defaultPatch,
|
|
2340
2557
|
on: {}
|
|
2341
|
-
}
|
|
2558
|
+
};
|
|
2559
|
+
const builder = action_builder(internal);
|
|
2342
2560
|
return Object.assign(builder, {
|
|
2343
2561
|
patch(customPatch) {
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
actions: {},
|
|
2347
|
-
state: stateSchema,
|
|
2348
|
-
name,
|
|
2349
|
-
init,
|
|
2350
|
-
patch: { ...defaultPatch, ...customPatch },
|
|
2351
|
-
on: {}
|
|
2352
|
-
});
|
|
2562
|
+
Object.assign(internal.patch, customPatch);
|
|
2563
|
+
return builder;
|
|
2353
2564
|
}
|
|
2354
2565
|
});
|
|
2355
2566
|
}
|
|
@@ -2358,50 +2569,43 @@ function state(entry) {
|
|
|
2358
2569
|
};
|
|
2359
2570
|
}
|
|
2360
2571
|
function action_builder(state2) {
|
|
2361
|
-
|
|
2572
|
+
const internal = state2;
|
|
2573
|
+
const builder = {
|
|
2362
2574
|
on(entry) {
|
|
2363
2575
|
const keys = Object.keys(entry);
|
|
2364
2576
|
if (keys.length !== 1) throw new Error(".on() requires exactly one key");
|
|
2365
2577
|
const action2 = keys[0];
|
|
2366
2578
|
const schema = entry[action2];
|
|
2367
|
-
if (action2 in
|
|
2579
|
+
if (action2 in internal.actions)
|
|
2368
2580
|
throw new Error(`Duplicate action "${action2}"`);
|
|
2369
|
-
|
|
2370
|
-
...state2.actions,
|
|
2371
|
-
[action2]: schema
|
|
2372
|
-
};
|
|
2373
|
-
const on = { ...state2.on };
|
|
2374
|
-
const _given = { ...state2.given };
|
|
2581
|
+
internal.actions[action2] = schema;
|
|
2375
2582
|
function given(rules) {
|
|
2376
|
-
|
|
2583
|
+
(internal.given ??= {})[action2] = rules;
|
|
2377
2584
|
return { emit };
|
|
2378
2585
|
}
|
|
2379
2586
|
function emit(handler) {
|
|
2380
2587
|
if (typeof handler === "string") {
|
|
2381
2588
|
const eventName = handler;
|
|
2382
|
-
on[action2] = (
|
|
2589
|
+
internal.on[action2] = (payload) => [
|
|
2590
|
+
eventName,
|
|
2591
|
+
payload
|
|
2592
|
+
];
|
|
2383
2593
|
} else {
|
|
2384
|
-
on[action2] = handler;
|
|
2594
|
+
internal.on[action2] = handler;
|
|
2385
2595
|
}
|
|
2386
|
-
return
|
|
2387
|
-
...state2,
|
|
2388
|
-
actions,
|
|
2389
|
-
on,
|
|
2390
|
-
given: _given
|
|
2391
|
-
});
|
|
2596
|
+
return builder;
|
|
2392
2597
|
}
|
|
2393
2598
|
return { given, emit };
|
|
2394
2599
|
},
|
|
2395
2600
|
snap(snap2) {
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
snap: snap2
|
|
2399
|
-
});
|
|
2601
|
+
internal.snap = snap2;
|
|
2602
|
+
return builder;
|
|
2400
2603
|
},
|
|
2401
2604
|
build() {
|
|
2402
|
-
return
|
|
2605
|
+
return internal;
|
|
2403
2606
|
}
|
|
2404
2607
|
};
|
|
2608
|
+
return builder;
|
|
2405
2609
|
}
|
|
2406
2610
|
export {
|
|
2407
2611
|
Act,
|
|
@@ -2410,6 +2614,7 @@ export {
|
|
|
2410
2614
|
CommittedMetaSchema,
|
|
2411
2615
|
ConcurrencyError,
|
|
2412
2616
|
ConsoleLogger,
|
|
2617
|
+
DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
2413
2618
|
Environments,
|
|
2414
2619
|
Errors,
|
|
2415
2620
|
EventMetaSchema,
|