@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.cjs
CHANGED
|
@@ -36,6 +36,7 @@ __export(index_exports, {
|
|
|
36
36
|
CommittedMetaSchema: () => CommittedMetaSchema,
|
|
37
37
|
ConcurrencyError: () => ConcurrencyError,
|
|
38
38
|
ConsoleLogger: () => ConsoleLogger,
|
|
39
|
+
DEFAULT_MAX_SUBSCRIBED_STREAMS: () => DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
39
40
|
Environments: () => Environments,
|
|
40
41
|
Errors: () => Errors,
|
|
41
42
|
EventMetaSchema: () => EventMetaSchema,
|
|
@@ -69,7 +70,7 @@ __export(index_exports, {
|
|
|
69
70
|
});
|
|
70
71
|
module.exports = __toCommonJS(index_exports);
|
|
71
72
|
|
|
72
|
-
// src/adapters/
|
|
73
|
+
// src/adapters/console-logger.ts
|
|
73
74
|
var LEVEL_VALUES = {
|
|
74
75
|
fatal: 60,
|
|
75
76
|
error: 50,
|
|
@@ -138,14 +139,25 @@ var ConsoleLogger = class _ConsoleLogger {
|
|
|
138
139
|
obj = {};
|
|
139
140
|
} else if (objOrMsg !== null && typeof objOrMsg === "object") {
|
|
140
141
|
message = msg;
|
|
141
|
-
obj =
|
|
142
|
+
obj = { ...objOrMsg };
|
|
142
143
|
} else {
|
|
143
144
|
message = msg;
|
|
144
145
|
obj = { value: objOrMsg };
|
|
145
146
|
}
|
|
146
147
|
const entry = Object.assign({ level, time: Date.now() }, bindings, obj);
|
|
147
148
|
if (message) entry.msg = message;
|
|
148
|
-
|
|
149
|
+
let line;
|
|
150
|
+
try {
|
|
151
|
+
line = JSON.stringify(entry);
|
|
152
|
+
} catch {
|
|
153
|
+
line = JSON.stringify({
|
|
154
|
+
level,
|
|
155
|
+
time: entry.time,
|
|
156
|
+
msg: message ?? "[unserializable]",
|
|
157
|
+
unserializable: true
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
process.stdout.write(line + "\n");
|
|
149
161
|
}
|
|
150
162
|
_prettyWrite(bindings, level, _num, objOrMsg, msg) {
|
|
151
163
|
const color = LEVEL_COLORS[level];
|
|
@@ -171,26 +183,75 @@ var ConsoleLogger = class _ConsoleLogger {
|
|
|
171
183
|
}
|
|
172
184
|
};
|
|
173
185
|
|
|
174
|
-
// src/
|
|
175
|
-
var
|
|
186
|
+
// src/internal/lru-map.ts
|
|
187
|
+
var LruMap = class {
|
|
188
|
+
constructor(_maxSize) {
|
|
189
|
+
this._maxSize = _maxSize;
|
|
190
|
+
}
|
|
176
191
|
_entries = /* @__PURE__ */ new Map();
|
|
177
|
-
|
|
192
|
+
get(key) {
|
|
193
|
+
const v = this._entries.get(key);
|
|
194
|
+
if (v === void 0) return void 0;
|
|
195
|
+
this._entries.delete(key);
|
|
196
|
+
this._entries.set(key, v);
|
|
197
|
+
return v;
|
|
198
|
+
}
|
|
199
|
+
has(key) {
|
|
200
|
+
return this._entries.has(key);
|
|
201
|
+
}
|
|
202
|
+
set(key, value) {
|
|
203
|
+
this._entries.delete(key);
|
|
204
|
+
if (this._entries.size >= this._maxSize) {
|
|
205
|
+
const oldest = this._entries.keys().next().value;
|
|
206
|
+
this._entries.delete(oldest);
|
|
207
|
+
}
|
|
208
|
+
this._entries.set(key, value);
|
|
209
|
+
}
|
|
210
|
+
delete(key) {
|
|
211
|
+
return this._entries.delete(key);
|
|
212
|
+
}
|
|
213
|
+
clear() {
|
|
214
|
+
this._entries.clear();
|
|
215
|
+
}
|
|
216
|
+
get size() {
|
|
217
|
+
return this._entries.size;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
var LruSet = class {
|
|
221
|
+
_map;
|
|
222
|
+
constructor(maxSize) {
|
|
223
|
+
this._map = new LruMap(maxSize);
|
|
224
|
+
}
|
|
225
|
+
has(value) {
|
|
226
|
+
return this._map.has(value);
|
|
227
|
+
}
|
|
228
|
+
add(value) {
|
|
229
|
+
this._map.set(value, true);
|
|
230
|
+
}
|
|
231
|
+
delete(value) {
|
|
232
|
+
return this._map.delete(value);
|
|
233
|
+
}
|
|
234
|
+
clear() {
|
|
235
|
+
this._map.clear();
|
|
236
|
+
}
|
|
237
|
+
get size() {
|
|
238
|
+
return this._map.size;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// src/adapters/in-memory-cache.ts
|
|
243
|
+
var InMemoryCache = class {
|
|
244
|
+
// CacheEntry<any> lets `get<TState>` and `set<TState>` flow without casts:
|
|
245
|
+
// any is bidirectionally compatible with the per-call TState binding, while
|
|
246
|
+
// the public Cache interface still presents a typed surface to callers.
|
|
247
|
+
_entries;
|
|
178
248
|
constructor(options) {
|
|
179
|
-
this.
|
|
249
|
+
this._entries = new LruMap(options?.maxSize ?? 1e3);
|
|
180
250
|
}
|
|
181
251
|
async get(stream) {
|
|
182
|
-
|
|
183
|
-
if (!entry) return void 0;
|
|
184
|
-
this._entries.delete(stream);
|
|
185
|
-
this._entries.set(stream, entry);
|
|
186
|
-
return entry;
|
|
252
|
+
return this._entries.get(stream);
|
|
187
253
|
}
|
|
188
254
|
async set(stream, entry) {
|
|
189
|
-
this._entries.delete(stream);
|
|
190
|
-
if (this._entries.size >= this._maxSize) {
|
|
191
|
-
const first = this._entries.keys().next().value;
|
|
192
|
-
this._entries.delete(first);
|
|
193
|
-
}
|
|
194
255
|
this._entries.set(stream, entry);
|
|
195
256
|
}
|
|
196
257
|
async invalidate(stream) {
|
|
@@ -244,7 +305,7 @@ var StreamClosedError = class extends Error {
|
|
|
244
305
|
var ConcurrencyError = class extends Error {
|
|
245
306
|
constructor(stream, lastVersion, events, expectedVersion) {
|
|
246
307
|
super(
|
|
247
|
-
`Concurrency error committing "${events.map((e) => `${stream}.${e.name}
|
|
308
|
+
`Concurrency error committing "${events.map((e) => `${stream}.${e.name}`).join(
|
|
248
309
|
", "
|
|
249
310
|
)}". Expected version ${expectedVersion} but found version ${lastVersion}.`
|
|
250
311
|
);
|
|
@@ -333,10 +394,21 @@ var PackageSchema = import_zod2.z.object({
|
|
|
333
394
|
license: import_zod2.z.string().min(1).optional(),
|
|
334
395
|
dependencies: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.string()).optional()
|
|
335
396
|
});
|
|
397
|
+
var FALLBACK_PACKAGE = {
|
|
398
|
+
name: "act-fallback",
|
|
399
|
+
version: "0.0.0-fallback",
|
|
400
|
+
description: "Synthetic fallback \u2014 package.json could not be loaded"
|
|
401
|
+
};
|
|
336
402
|
var getPackage = () => {
|
|
337
|
-
|
|
338
|
-
|
|
403
|
+
try {
|
|
404
|
+
const raw = fs.readFileSync("package.json");
|
|
405
|
+
return JSON.parse(raw.toString());
|
|
406
|
+
} catch (err) {
|
|
407
|
+
pkgLoadError = err;
|
|
408
|
+
return FALLBACK_PACKAGE;
|
|
409
|
+
}
|
|
339
410
|
};
|
|
411
|
+
var pkgLoadError;
|
|
340
412
|
var BaseSchema = PackageSchema.extend({
|
|
341
413
|
env: import_zod2.z.enum(Environments),
|
|
342
414
|
logLevel: import_zod2.z.enum(LogLevels),
|
|
@@ -356,6 +428,13 @@ var config = () => {
|
|
|
356
428
|
{ ...pkg, env, logLevel, logSingleLine, sleepMs },
|
|
357
429
|
BaseSchema
|
|
358
430
|
);
|
|
431
|
+
if (pkgLoadError) {
|
|
432
|
+
const msg = pkgLoadError instanceof Error ? pkgLoadError.message : typeof pkgLoadError === "string" ? pkgLoadError : "unknown error";
|
|
433
|
+
log().warn(
|
|
434
|
+
`[act] Could not read package.json (${msg}); using synthetic name="${FALLBACK_PACKAGE.name}" version="${FALLBACK_PACKAGE.version}".`
|
|
435
|
+
);
|
|
436
|
+
pkgLoadError = void 0;
|
|
437
|
+
}
|
|
359
438
|
}
|
|
360
439
|
return _validated;
|
|
361
440
|
};
|
|
@@ -379,7 +458,7 @@ async function sleep(ms) {
|
|
|
379
458
|
return new Promise((resolve) => setTimeout(resolve, ms ?? config().sleepMs));
|
|
380
459
|
}
|
|
381
460
|
|
|
382
|
-
// src/adapters/
|
|
461
|
+
// src/adapters/in-memory-store.ts
|
|
383
462
|
var InMemoryStream = class {
|
|
384
463
|
constructor(stream, source) {
|
|
385
464
|
this.stream = stream;
|
|
@@ -474,11 +553,13 @@ var InMemoryStream = class {
|
|
|
474
553
|
}
|
|
475
554
|
}
|
|
476
555
|
/**
|
|
477
|
-
* Reset this stream's watermark and state for replay.
|
|
556
|
+
* Reset this stream's watermark and state for replay. The retry counter
|
|
557
|
+
* resets to -1 to match the constructor + ack() invariant ("released
|
|
558
|
+
* stream"); the next claim() bumps it to 0 (first attempt).
|
|
478
559
|
*/
|
|
479
560
|
reset() {
|
|
480
561
|
this._at = -1;
|
|
481
|
-
this._retry =
|
|
562
|
+
this._retry = -1;
|
|
482
563
|
this._blocked = false;
|
|
483
564
|
this._error = "";
|
|
484
565
|
this._leased_by = void 0;
|
|
@@ -490,13 +571,26 @@ var InMemoryStore = class {
|
|
|
490
571
|
_events = [];
|
|
491
572
|
// stored stream positions and other metadata
|
|
492
573
|
_streams = /* @__PURE__ */ new Map();
|
|
574
|
+
// last committed version per stream — O(1) replacement for filter-on-commit
|
|
575
|
+
_streamVersions = /* @__PURE__ */ new Map();
|
|
576
|
+
// max non-snapshot event id per stream — drives the source-pattern probe in claim()
|
|
577
|
+
// without scanning the full event log.
|
|
578
|
+
_maxEventIdByStream = /* @__PURE__ */ new Map();
|
|
579
|
+
// global max non-snapshot event id — fast pre-check for source-less streams in claim()
|
|
580
|
+
_maxNonSnapEventId = -1;
|
|
581
|
+
_resetIndexes() {
|
|
582
|
+
this._events.length = 0;
|
|
583
|
+
this._streamVersions.clear();
|
|
584
|
+
this._maxEventIdByStream.clear();
|
|
585
|
+
this._maxNonSnapEventId = -1;
|
|
586
|
+
}
|
|
493
587
|
/**
|
|
494
588
|
* Dispose of the store and clear all events.
|
|
495
589
|
* @returns Promise that resolves when disposal is complete.
|
|
496
590
|
*/
|
|
497
591
|
async dispose() {
|
|
498
592
|
await sleep();
|
|
499
|
-
this.
|
|
593
|
+
this._resetIndexes();
|
|
500
594
|
}
|
|
501
595
|
/**
|
|
502
596
|
* Seed the store with initial data (no-op for in-memory).
|
|
@@ -511,7 +605,7 @@ var InMemoryStore = class {
|
|
|
511
605
|
*/
|
|
512
606
|
async drop() {
|
|
513
607
|
await sleep();
|
|
514
|
-
this.
|
|
608
|
+
this._resetIndexes();
|
|
515
609
|
this._streams = /* @__PURE__ */ new Map();
|
|
516
610
|
}
|
|
517
611
|
in_query(query, e) {
|
|
@@ -574,18 +668,19 @@ var InMemoryStore = class {
|
|
|
574
668
|
*/
|
|
575
669
|
async commit(stream, msgs, meta, expectedVersion) {
|
|
576
670
|
await sleep();
|
|
577
|
-
const
|
|
578
|
-
if (typeof expectedVersion === "number" &&
|
|
671
|
+
const currentVersion = this._streamVersions.get(stream) ?? -1;
|
|
672
|
+
if (typeof expectedVersion === "number" && currentVersion !== expectedVersion) {
|
|
579
673
|
throw new ConcurrencyError(
|
|
580
674
|
stream,
|
|
581
|
-
|
|
675
|
+
currentVersion,
|
|
582
676
|
msgs,
|
|
583
677
|
expectedVersion
|
|
584
678
|
);
|
|
585
679
|
}
|
|
586
|
-
let version =
|
|
587
|
-
|
|
588
|
-
|
|
680
|
+
let version = currentVersion + 1;
|
|
681
|
+
let lastNonSnapId = -1;
|
|
682
|
+
const committed = msgs.map(({ name, data }) => {
|
|
683
|
+
const c = {
|
|
589
684
|
id: this._events.length,
|
|
590
685
|
stream,
|
|
591
686
|
version,
|
|
@@ -594,10 +689,17 @@ var InMemoryStore = class {
|
|
|
594
689
|
data,
|
|
595
690
|
meta
|
|
596
691
|
};
|
|
597
|
-
this._events.push(
|
|
692
|
+
this._events.push(c);
|
|
693
|
+
if (name !== SNAP_EVENT) lastNonSnapId = c.id;
|
|
598
694
|
version++;
|
|
599
|
-
return
|
|
695
|
+
return c;
|
|
600
696
|
});
|
|
697
|
+
this._streamVersions.set(stream, version - 1);
|
|
698
|
+
if (lastNonSnapId >= 0) {
|
|
699
|
+
this._maxEventIdByStream.set(stream, lastNonSnapId);
|
|
700
|
+
this._maxNonSnapEventId = lastNonSnapId;
|
|
701
|
+
}
|
|
702
|
+
return committed;
|
|
601
703
|
}
|
|
602
704
|
/**
|
|
603
705
|
* Atomically discovers and leases streams for processing.
|
|
@@ -610,10 +712,26 @@ var InMemoryStore = class {
|
|
|
610
712
|
*/
|
|
611
713
|
async claim(lagging, leading, by, millis) {
|
|
612
714
|
await sleep();
|
|
715
|
+
const sourceRegex = /* @__PURE__ */ new Map();
|
|
716
|
+
const getRegex = (source) => {
|
|
717
|
+
let re = sourceRegex.get(source);
|
|
718
|
+
if (!re) {
|
|
719
|
+
re = new RegExp(source);
|
|
720
|
+
sourceRegex.set(source, re);
|
|
721
|
+
}
|
|
722
|
+
return re;
|
|
723
|
+
};
|
|
724
|
+
const hasWork = (s) => {
|
|
725
|
+
if (s.at < 0) return true;
|
|
726
|
+
if (!s.source) return s.at < this._maxNonSnapEventId;
|
|
727
|
+
const re = getRegex(s.source);
|
|
728
|
+
for (const [streamName, maxId] of this._maxEventIdByStream) {
|
|
729
|
+
if (maxId > s.at && re.test(streamName)) return true;
|
|
730
|
+
}
|
|
731
|
+
return false;
|
|
732
|
+
};
|
|
613
733
|
const available = [...this._streams.values()].filter(
|
|
614
|
-
(s) => s.is_available && (s
|
|
615
|
-
(e) => e.id > s.at && e.name !== SNAP_EVENT && (!s.source || RegExp(s.source).test(e.stream))
|
|
616
|
-
))
|
|
734
|
+
(s) => s.is_available && hasWork(s)
|
|
617
735
|
);
|
|
618
736
|
const lag = available.sort((a, b) => a.at - b.at).slice(0, lagging).map((s) => ({
|
|
619
737
|
stream: s.stream,
|
|
@@ -750,9 +868,13 @@ var InMemoryStore = class {
|
|
|
750
868
|
}
|
|
751
869
|
}
|
|
752
870
|
this._events = this._events.filter((e) => !streamSet.has(e.stream));
|
|
871
|
+
for (const stream of streamSet) {
|
|
872
|
+
this._streams.delete(stream);
|
|
873
|
+
this._streamVersions.delete(stream);
|
|
874
|
+
this._maxEventIdByStream.delete(stream);
|
|
875
|
+
}
|
|
753
876
|
const result = /* @__PURE__ */ new Map();
|
|
754
877
|
for (const { stream, snapshot, meta } of targets) {
|
|
755
|
-
this._streams.delete(stream);
|
|
756
878
|
const event = {
|
|
757
879
|
id: this._events.length,
|
|
758
880
|
stream,
|
|
@@ -763,11 +885,18 @@ var InMemoryStore = class {
|
|
|
763
885
|
meta: meta ?? { correlation: "", causation: {} }
|
|
764
886
|
};
|
|
765
887
|
this._events.push(event);
|
|
888
|
+
this._streamVersions.set(stream, 0);
|
|
889
|
+
if (event.name !== SNAP_EVENT) {
|
|
890
|
+
this._maxEventIdByStream.set(stream, event.id);
|
|
891
|
+
}
|
|
766
892
|
result.set(stream, {
|
|
767
893
|
deleted: deletedCounts.get(stream) ?? 0,
|
|
768
894
|
committed: event
|
|
769
895
|
});
|
|
770
896
|
}
|
|
897
|
+
let max = -1;
|
|
898
|
+
for (const id of this._maxEventIdByStream.values()) if (id > max) max = id;
|
|
899
|
+
this._maxNonSnapEventId = max;
|
|
771
900
|
return result;
|
|
772
901
|
}
|
|
773
902
|
};
|
|
@@ -800,7 +929,12 @@ var cache = port(function cache2(adapter) {
|
|
|
800
929
|
});
|
|
801
930
|
var disposers = [];
|
|
802
931
|
async function disposeAndExit(code = "EXIT") {
|
|
803
|
-
if (code === "ERROR" && config().env === "production")
|
|
932
|
+
if (code === "ERROR" && config().env === "production") {
|
|
933
|
+
log().warn(
|
|
934
|
+
"disposeAndExit('ERROR') ignored in production \u2014 process kept alive"
|
|
935
|
+
);
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
804
938
|
for (const disposer of [...disposers].reverse()) {
|
|
805
939
|
await disposer();
|
|
806
940
|
}
|
|
@@ -837,9 +971,225 @@ process.once("unhandledRejection", async (arg) => {
|
|
|
837
971
|
});
|
|
838
972
|
|
|
839
973
|
// src/act.ts
|
|
840
|
-
var import_crypto2 = require("crypto");
|
|
841
974
|
var import_events = __toESM(require("events"), 1);
|
|
842
975
|
|
|
976
|
+
// src/internal/close-cycle.ts
|
|
977
|
+
var import_crypto = require("crypto");
|
|
978
|
+
async function runCloseCycle(targets, deps) {
|
|
979
|
+
const targetMap = new Map(targets.map((t) => [t.stream, t]));
|
|
980
|
+
const streams = [...targetMap.keys()];
|
|
981
|
+
const skipped = [];
|
|
982
|
+
const streamInfo = await scanStreamHeads(streams);
|
|
983
|
+
const safe = await partitionBySafety(
|
|
984
|
+
streamInfo,
|
|
985
|
+
deps.reactiveEventsSize,
|
|
986
|
+
skipped
|
|
987
|
+
);
|
|
988
|
+
if (!safe.length) return { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
989
|
+
const correlation = (0, import_crypto.randomUUID)();
|
|
990
|
+
const { guarded, guardEvents } = await guardWithTombstones(
|
|
991
|
+
safe,
|
|
992
|
+
streamInfo,
|
|
993
|
+
correlation,
|
|
994
|
+
deps.tombstone,
|
|
995
|
+
skipped
|
|
996
|
+
);
|
|
997
|
+
if (!guarded.length) return { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
998
|
+
const seedStates = await loadRestartSeeds(
|
|
999
|
+
guarded,
|
|
1000
|
+
targetMap,
|
|
1001
|
+
streamInfo,
|
|
1002
|
+
deps.eventToState,
|
|
1003
|
+
deps.load,
|
|
1004
|
+
deps.logger
|
|
1005
|
+
);
|
|
1006
|
+
await runArchiveCallbacks(guarded, targetMap);
|
|
1007
|
+
const truncated = await truncateAndWarmCache(
|
|
1008
|
+
guarded,
|
|
1009
|
+
seedStates,
|
|
1010
|
+
guardEvents,
|
|
1011
|
+
correlation
|
|
1012
|
+
);
|
|
1013
|
+
return { truncated, skipped };
|
|
1014
|
+
}
|
|
1015
|
+
async function scanStreamHeads(streams) {
|
|
1016
|
+
const out = /* @__PURE__ */ new Map();
|
|
1017
|
+
await Promise.all(
|
|
1018
|
+
streams.map(async (s) => {
|
|
1019
|
+
let maxId = -1;
|
|
1020
|
+
let version = -1;
|
|
1021
|
+
let lastEventName = "";
|
|
1022
|
+
await store().query(
|
|
1023
|
+
(e) => {
|
|
1024
|
+
if (e.name === TOMBSTONE_EVENT || maxId !== -1) return;
|
|
1025
|
+
maxId = e.id;
|
|
1026
|
+
version = e.version;
|
|
1027
|
+
lastEventName = e.name;
|
|
1028
|
+
},
|
|
1029
|
+
{ stream: s, stream_exact: true, backward: true, limit: 1 }
|
|
1030
|
+
);
|
|
1031
|
+
if (maxId >= 0) out.set(s, { maxId, version, lastEventName });
|
|
1032
|
+
})
|
|
1033
|
+
);
|
|
1034
|
+
return out;
|
|
1035
|
+
}
|
|
1036
|
+
async function partitionBySafety(streamInfo, reactiveEventsSize, skipped) {
|
|
1037
|
+
if (reactiveEventsSize === 0) return [...streamInfo.keys()];
|
|
1038
|
+
const pendingSet = /* @__PURE__ */ new Set();
|
|
1039
|
+
await store().query_streams((position) => {
|
|
1040
|
+
const sourceRe = position.source ? RegExp(position.source) : void 0;
|
|
1041
|
+
for (const [stream, info] of streamInfo) {
|
|
1042
|
+
if ((!sourceRe || sourceRe.test(stream)) && position.at < info.maxId) {
|
|
1043
|
+
pendingSet.add(stream);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
});
|
|
1047
|
+
const safe = [];
|
|
1048
|
+
for (const [stream] of streamInfo) {
|
|
1049
|
+
if (pendingSet.has(stream)) skipped.push(stream);
|
|
1050
|
+
else safe.push(stream);
|
|
1051
|
+
}
|
|
1052
|
+
return safe;
|
|
1053
|
+
}
|
|
1054
|
+
async function guardWithTombstones(safe, streamInfo, correlation, tombstone2, skipped) {
|
|
1055
|
+
const guarded = [];
|
|
1056
|
+
const guardEvents = /* @__PURE__ */ new Map();
|
|
1057
|
+
await Promise.all(
|
|
1058
|
+
safe.map(async (stream) => {
|
|
1059
|
+
const info = streamInfo.get(stream);
|
|
1060
|
+
const committed = await tombstone2(stream, info.version, correlation);
|
|
1061
|
+
if (committed) {
|
|
1062
|
+
guarded.push(stream);
|
|
1063
|
+
guardEvents.set(stream, { id: committed.id, stream });
|
|
1064
|
+
} else {
|
|
1065
|
+
skipped.push(stream);
|
|
1066
|
+
}
|
|
1067
|
+
})
|
|
1068
|
+
);
|
|
1069
|
+
return { guarded, guardEvents };
|
|
1070
|
+
}
|
|
1071
|
+
async function loadRestartSeeds(guarded, targetMap, streamInfo, eventToState, load2, logger) {
|
|
1072
|
+
const seedStates = /* @__PURE__ */ new Map();
|
|
1073
|
+
await Promise.all(
|
|
1074
|
+
guarded.filter((s) => targetMap.get(s)?.restart).map(async (stream) => {
|
|
1075
|
+
const lastEventName = streamInfo.get(stream).lastEventName;
|
|
1076
|
+
const ownerState = eventToState.get(lastEventName);
|
|
1077
|
+
if (!ownerState) {
|
|
1078
|
+
logger.error(
|
|
1079
|
+
`Cannot seed restart for "${stream}": no registered state owns event "${lastEventName}". Stream will be tombstoned instead.`
|
|
1080
|
+
);
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const snap2 = await load2(ownerState, stream);
|
|
1084
|
+
seedStates.set(stream, snap2.state);
|
|
1085
|
+
})
|
|
1086
|
+
);
|
|
1087
|
+
return seedStates;
|
|
1088
|
+
}
|
|
1089
|
+
async function runArchiveCallbacks(guarded, targetMap) {
|
|
1090
|
+
for (const stream of guarded) {
|
|
1091
|
+
const archiveFn = targetMap.get(stream)?.archive;
|
|
1092
|
+
if (archiveFn) await archiveFn();
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
async function truncateAndWarmCache(guarded, seedStates, guardEvents, correlation) {
|
|
1096
|
+
const truncTargets = guarded.map((stream) => {
|
|
1097
|
+
const snapshot = seedStates.get(stream);
|
|
1098
|
+
const guard = guardEvents.get(stream);
|
|
1099
|
+
return {
|
|
1100
|
+
stream,
|
|
1101
|
+
snapshot,
|
|
1102
|
+
meta: {
|
|
1103
|
+
correlation,
|
|
1104
|
+
causation: {
|
|
1105
|
+
event: { id: guard.id, name: TOMBSTONE_EVENT, stream: guard.stream }
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
});
|
|
1110
|
+
const truncated = await store().truncate(truncTargets);
|
|
1111
|
+
await Promise.all(
|
|
1112
|
+
guarded.map(async (stream) => {
|
|
1113
|
+
const entry = truncated.get(stream);
|
|
1114
|
+
const state2 = seedStates.get(stream);
|
|
1115
|
+
if (state2 && entry) {
|
|
1116
|
+
await cache().set(stream, {
|
|
1117
|
+
state: state2,
|
|
1118
|
+
version: entry.committed.version,
|
|
1119
|
+
event_id: entry.committed.id,
|
|
1120
|
+
patches: 0,
|
|
1121
|
+
snaps: 1
|
|
1122
|
+
});
|
|
1123
|
+
} else {
|
|
1124
|
+
await cache().invalidate(stream);
|
|
1125
|
+
}
|
|
1126
|
+
})
|
|
1127
|
+
);
|
|
1128
|
+
return truncated;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// src/internal/drain-cycle.ts
|
|
1132
|
+
var import_crypto2 = require("crypto");
|
|
1133
|
+
async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch, lagging, leading, eventLimit, leaseMillis) {
|
|
1134
|
+
const leased = await ops.claim(lagging, leading, (0, import_crypto2.randomUUID)(), leaseMillis);
|
|
1135
|
+
if (!leased.length) return void 0;
|
|
1136
|
+
const fetched = await ops.fetch(leased, eventLimit);
|
|
1137
|
+
const fetchMap = /* @__PURE__ */ new Map();
|
|
1138
|
+
const fetch_window_at = fetched.reduce(
|
|
1139
|
+
(max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
|
|
1140
|
+
0
|
|
1141
|
+
);
|
|
1142
|
+
for (const f of fetched) {
|
|
1143
|
+
const { stream, events } = f;
|
|
1144
|
+
const payloads = events.flatMap((event) => {
|
|
1145
|
+
const register = registry.events[event.name];
|
|
1146
|
+
if (!register) return [];
|
|
1147
|
+
return [...register.reactions.values()].filter((reaction) => {
|
|
1148
|
+
const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
|
|
1149
|
+
return resolved && resolved.target === stream;
|
|
1150
|
+
}).map((reaction) => ({ ...reaction, event }));
|
|
1151
|
+
});
|
|
1152
|
+
fetchMap.set(stream, { fetch: f, payloads });
|
|
1153
|
+
}
|
|
1154
|
+
const handled = await Promise.all(
|
|
1155
|
+
leased.map((lease) => {
|
|
1156
|
+
const entry = fetchMap.get(lease.stream);
|
|
1157
|
+
const at = entry.fetch.events.at(-1)?.id || fetch_window_at;
|
|
1158
|
+
const { payloads } = entry;
|
|
1159
|
+
const batchHandler = batchHandlers.get(lease.stream);
|
|
1160
|
+
if (batchHandler && payloads.length > 0) {
|
|
1161
|
+
return handleBatch({ ...lease, at }, payloads, batchHandler);
|
|
1162
|
+
}
|
|
1163
|
+
return handle({ ...lease, at }, payloads);
|
|
1164
|
+
})
|
|
1165
|
+
);
|
|
1166
|
+
const acked = await ops.ack(
|
|
1167
|
+
handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
|
|
1168
|
+
);
|
|
1169
|
+
const blocked = await ops.block(
|
|
1170
|
+
handled.filter(({ block: block2 }) => block2).map(({ lease, error }) => ({ ...lease, error }))
|
|
1171
|
+
);
|
|
1172
|
+
return { leased, fetched, handled, acked, blocked };
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// src/internal/drain-ratio.ts
|
|
1176
|
+
var RATIO_MIN = 0.2;
|
|
1177
|
+
var RATIO_MAX = 0.8;
|
|
1178
|
+
var RATIO_DEFAULT = 0.5;
|
|
1179
|
+
function computeLagLeadRatio(handled, lagging, leading) {
|
|
1180
|
+
let lagging_handled = 0;
|
|
1181
|
+
let leading_handled = 0;
|
|
1182
|
+
for (const { lease, handled: count } of handled) {
|
|
1183
|
+
if (lease.lagging) lagging_handled += count;
|
|
1184
|
+
else leading_handled += count;
|
|
1185
|
+
}
|
|
1186
|
+
const lagging_avg = lagging > 0 ? lagging_handled / lagging : 0;
|
|
1187
|
+
const leading_avg = leading > 0 ? leading_handled / leading : 0;
|
|
1188
|
+
const total = lagging_avg + leading_avg;
|
|
1189
|
+
if (total === 0) return RATIO_DEFAULT;
|
|
1190
|
+
return Math.max(RATIO_MIN, Math.min(RATIO_MAX, lagging_avg / total));
|
|
1191
|
+
}
|
|
1192
|
+
|
|
843
1193
|
// src/internal/merge.ts
|
|
844
1194
|
var import_zod4 = require("zod");
|
|
845
1195
|
function baseTypeName(zodType) {
|
|
@@ -947,6 +1297,15 @@ function mergePatches(existing, incoming, stateName) {
|
|
|
947
1297
|
}
|
|
948
1298
|
return merged;
|
|
949
1299
|
}
|
|
1300
|
+
function mergeEventRegister(target, source) {
|
|
1301
|
+
for (const [eventName, sourceReg] of Object.entries(source)) {
|
|
1302
|
+
const targetReg = target[eventName];
|
|
1303
|
+
if (!targetReg) continue;
|
|
1304
|
+
for (const [name, reaction] of sourceReg.reactions) {
|
|
1305
|
+
targetReg.reactions.set(name, reaction);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
950
1309
|
function mergeProjection(proj, events) {
|
|
951
1310
|
for (const eventName of Object.keys(proj.events)) {
|
|
952
1311
|
const projRegister = proj.events[eventName];
|
|
@@ -991,7 +1350,7 @@ var subscribe = (streams) => store().subscribe(streams);
|
|
|
991
1350
|
|
|
992
1351
|
// src/internal/event-sourcing.ts
|
|
993
1352
|
var import_act_patch = require("@rotorsoft/act-patch");
|
|
994
|
-
var
|
|
1353
|
+
var import_crypto3 = require("crypto");
|
|
995
1354
|
async function snap(snapshot) {
|
|
996
1355
|
try {
|
|
997
1356
|
const { id, stream, name, meta, version } = snapshot.event;
|
|
@@ -1009,6 +1368,20 @@ async function snap(snapshot) {
|
|
|
1009
1368
|
log().error(error);
|
|
1010
1369
|
}
|
|
1011
1370
|
}
|
|
1371
|
+
async function tombstone(stream, expectedVersion, correlation) {
|
|
1372
|
+
try {
|
|
1373
|
+
const [committed] = await store().commit(
|
|
1374
|
+
stream,
|
|
1375
|
+
[{ name: TOMBSTONE_EVENT, data: {} }],
|
|
1376
|
+
{ correlation, causation: {} },
|
|
1377
|
+
expectedVersion
|
|
1378
|
+
);
|
|
1379
|
+
return committed;
|
|
1380
|
+
} catch (error) {
|
|
1381
|
+
if (error instanceof ConcurrencyError) return void 0;
|
|
1382
|
+
throw error;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1012
1385
|
async function load(me, stream, callback, asOf) {
|
|
1013
1386
|
const timeTravel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
|
|
1014
1387
|
const cached = timeTravel ? void 0 : await cache().get(stream);
|
|
@@ -1073,13 +1446,13 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
1073
1446
|
data: skipValidation ? data : validate(name, data, me.events[name])
|
|
1074
1447
|
}));
|
|
1075
1448
|
const meta = {
|
|
1076
|
-
correlation: reactingTo?.meta.correlation || (0,
|
|
1449
|
+
correlation: reactingTo?.meta.correlation || (0, import_crypto3.randomUUID)(),
|
|
1077
1450
|
causation: {
|
|
1078
1451
|
action: {
|
|
1079
1452
|
name: action2,
|
|
1080
1453
|
...target
|
|
1081
|
-
// payload
|
|
1082
|
-
//
|
|
1454
|
+
// payload intentionally omitted: it can be large or contain PII,
|
|
1455
|
+
// and callers correlate via the correlation id when they need it.
|
|
1083
1456
|
},
|
|
1084
1457
|
event: reactingTo ? {
|
|
1085
1458
|
id: reactingTo.id,
|
|
@@ -1094,7 +1467,10 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
1094
1467
|
stream,
|
|
1095
1468
|
emitted,
|
|
1096
1469
|
meta,
|
|
1097
|
-
//
|
|
1470
|
+
// Reactions skip optimistic concurrency: they always append against the
|
|
1471
|
+
// current head. Stream leasing already serializes concurrent reactions,
|
|
1472
|
+
// and forcing version checks here would turn ordinary catch-up into
|
|
1473
|
+
// spurious retries.
|
|
1098
1474
|
reactingTo ? void 0 : expected
|
|
1099
1475
|
);
|
|
1100
1476
|
} catch (error) {
|
|
@@ -1144,7 +1520,12 @@ var traced = (inner, exit, entry) => (async (...args) => {
|
|
|
1144
1520
|
});
|
|
1145
1521
|
function buildEs(logger) {
|
|
1146
1522
|
if (logger.level !== "trace") {
|
|
1147
|
-
return {
|
|
1523
|
+
return {
|
|
1524
|
+
snap,
|
|
1525
|
+
load,
|
|
1526
|
+
action,
|
|
1527
|
+
tombstone
|
|
1528
|
+
};
|
|
1148
1529
|
}
|
|
1149
1530
|
return {
|
|
1150
1531
|
snap: traced(snap, void 0, (snapshot) => {
|
|
@@ -1182,7 +1563,13 @@ function buildEs(logger) {
|
|
|
1182
1563
|
es_caption("action", C_BLUE, `${target.stream}.${action2}`)
|
|
1183
1564
|
);
|
|
1184
1565
|
}
|
|
1185
|
-
)
|
|
1566
|
+
),
|
|
1567
|
+
tombstone: traced(tombstone, (committed, stream) => {
|
|
1568
|
+
if (committed)
|
|
1569
|
+
logger.trace(
|
|
1570
|
+
es_caption("tombstoned", C_ORANGE, `${stream}@${committed.version}`)
|
|
1571
|
+
);
|
|
1572
|
+
})
|
|
1186
1573
|
};
|
|
1187
1574
|
}
|
|
1188
1575
|
function buildDrain(logger) {
|
|
@@ -1245,11 +1632,15 @@ function buildDrain(logger) {
|
|
|
1245
1632
|
}
|
|
1246
1633
|
|
|
1247
1634
|
// src/act.ts
|
|
1635
|
+
var DEFAULT_MAX_SUBSCRIBED_STREAMS = 1e3;
|
|
1248
1636
|
var Act = class {
|
|
1249
|
-
constructor(registry, _states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map()) {
|
|
1637
|
+
constructor(registry, _states = /* @__PURE__ */ new Map(), batchHandlers = /* @__PURE__ */ new Map(), options = {}) {
|
|
1250
1638
|
this.registry = registry;
|
|
1251
1639
|
this._states = _states;
|
|
1252
1640
|
this._batch_handlers = batchHandlers;
|
|
1641
|
+
this._subscribed_streams = new LruSet(
|
|
1642
|
+
options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS
|
|
1643
|
+
);
|
|
1253
1644
|
this._es = buildEs(this._logger);
|
|
1254
1645
|
this._cd = buildDrain(this._logger);
|
|
1255
1646
|
const statics = /* @__PURE__ */ new Map();
|
|
@@ -1287,20 +1678,42 @@ var Act = class {
|
|
|
1287
1678
|
_settle_timer = void 0;
|
|
1288
1679
|
_settling = false;
|
|
1289
1680
|
_correlation_checkpoint = -1;
|
|
1290
|
-
|
|
1681
|
+
/**
|
|
1682
|
+
* Streams already subscribed via store.subscribe() — both the static
|
|
1683
|
+
* targets registered at init and dynamic targets discovered by
|
|
1684
|
+
* correlate(). correlate() consults this set to avoid re-subscribing
|
|
1685
|
+
* known streams.
|
|
1686
|
+
*
|
|
1687
|
+
* Bounded LRU so apps that mint millions of dynamic targets (one per
|
|
1688
|
+
* aggregate) don't grow this unbounded. Eviction costs at most one
|
|
1689
|
+
* redundant store.subscribe() call per evicted-but-still-active stream
|
|
1690
|
+
* (subscribe is idempotent). Cap configurable via {@link ActOptions}.
|
|
1691
|
+
*/
|
|
1692
|
+
_subscribed_streams;
|
|
1291
1693
|
_has_dynamic_resolvers = false;
|
|
1292
1694
|
_correlation_initialized = false;
|
|
1293
1695
|
/** Event names with at least one registered reaction (computed at build time) */
|
|
1294
1696
|
_reactive_events = /* @__PURE__ */ new Set();
|
|
1295
1697
|
/** Set in do() when a committed event has reactions — cleared by drain() */
|
|
1296
1698
|
_needs_drain = false;
|
|
1699
|
+
/**
|
|
1700
|
+
* Emit a lifecycle event. The payload type is inferred from the event name
|
|
1701
|
+
* via {@link ActLifecycleEvents}.
|
|
1702
|
+
*/
|
|
1297
1703
|
emit(event, args) {
|
|
1298
1704
|
return this._emitter.emit(event, args);
|
|
1299
1705
|
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Register a listener for a lifecycle event. The listener receives the
|
|
1708
|
+
* event-specific payload.
|
|
1709
|
+
*/
|
|
1300
1710
|
on(event, listener) {
|
|
1301
1711
|
this._emitter.on(event, listener);
|
|
1302
1712
|
return this;
|
|
1303
1713
|
}
|
|
1714
|
+
/**
|
|
1715
|
+
* Remove a previously registered lifecycle listener.
|
|
1716
|
+
*/
|
|
1304
1717
|
off(event, listener) {
|
|
1305
1718
|
this._emitter.off(event, listener);
|
|
1306
1719
|
return this;
|
|
@@ -1334,6 +1747,9 @@ var Act = class {
|
|
|
1334
1747
|
_bound_load = this.load.bind(this);
|
|
1335
1748
|
_bound_query = this.query.bind(this);
|
|
1336
1749
|
_bound_query_array = this.query_array.bind(this);
|
|
1750
|
+
/** Pre-bound dispatchers handed to runDrainCycle each cycle. */
|
|
1751
|
+
_bound_handle = this.handle.bind(this);
|
|
1752
|
+
_bound_handle_batch = this.handleBatch.bind(this);
|
|
1337
1753
|
/**
|
|
1338
1754
|
* Executes an action on a state instance, committing resulting events.
|
|
1339
1755
|
*
|
|
@@ -1536,26 +1952,46 @@ var Act = class {
|
|
|
1536
1952
|
return events;
|
|
1537
1953
|
}
|
|
1538
1954
|
/**
|
|
1539
|
-
*
|
|
1540
|
-
*
|
|
1541
|
-
*
|
|
1542
|
-
*
|
|
1955
|
+
* Shared finalization for the two reaction-runner shapes (per-event
|
|
1956
|
+
* `handle` and bulk `handleBatch`). Centralizes the error log, retry-vs-
|
|
1957
|
+
* block decision, and the "error reported only when nothing was handled"
|
|
1958
|
+
* rule that's true in both shapes (in batch mode, `handled` is always 0
|
|
1959
|
+
* on failure, so the rule degenerates to "always reported").
|
|
1960
|
+
*/
|
|
1961
|
+
_finalize(lease, handled, at, error, options) {
|
|
1962
|
+
if (!error) return { lease, handled, at };
|
|
1963
|
+
this._logger.error(error);
|
|
1964
|
+
const block2 = lease.retry >= options.maxRetries && options.blockOnError;
|
|
1965
|
+
if (block2)
|
|
1966
|
+
this._logger.error(
|
|
1967
|
+
`Blocking ${lease.stream} after ${lease.retry} retries.`
|
|
1968
|
+
);
|
|
1969
|
+
return {
|
|
1970
|
+
lease,
|
|
1971
|
+
handled,
|
|
1972
|
+
at,
|
|
1973
|
+
error: handled === 0 ? error.message : void 0,
|
|
1974
|
+
block: block2
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
/**
|
|
1978
|
+
* Handles leased reactions one event at a time.
|
|
1543
1979
|
*
|
|
1544
|
-
*
|
|
1545
|
-
*
|
|
1546
|
-
*
|
|
1547
|
-
*
|
|
1980
|
+
* Called by the main `drain` loop after fetching new events. Each handler
|
|
1981
|
+
* receives a scoped `IAct` proxy that auto-injects the triggering event
|
|
1982
|
+
* as `reactingTo` when `do()` is called without it, maintaining
|
|
1983
|
+
* correlation chains by default (#587). Handlers can still pass an
|
|
1984
|
+
* explicit `reactingTo` to override.
|
|
1548
1985
|
*
|
|
1549
1986
|
* @internal
|
|
1550
|
-
* @param lease The lease to handle
|
|
1551
|
-
* @param payloads The reactions to handle
|
|
1552
|
-
* @returns The lease with results
|
|
1553
1987
|
*/
|
|
1554
1988
|
async handle(lease, payloads) {
|
|
1555
1989
|
if (payloads.length === 0) return { lease, handled: 0, at: lease.at };
|
|
1556
1990
|
const stream = lease.stream;
|
|
1557
|
-
let at = payloads.at(0).event.id
|
|
1558
|
-
|
|
1991
|
+
let at = payloads.at(0).event.id;
|
|
1992
|
+
let handled = 0;
|
|
1993
|
+
if (lease.retry > 0)
|
|
1994
|
+
this._logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
|
|
1559
1995
|
const doAction = this._bound_do;
|
|
1560
1996
|
const scopedApp = {
|
|
1561
1997
|
do: doAction,
|
|
@@ -1564,7 +2000,7 @@ var Act = class {
|
|
|
1564
2000
|
query_array: this._bound_query_array
|
|
1565
2001
|
};
|
|
1566
2002
|
for (const payload of payloads) {
|
|
1567
|
-
const { event, handler
|
|
2003
|
+
const { event, handler } = payload;
|
|
1568
2004
|
scopedApp.do = (action2, target, payload2, reactingTo, skipValidation) => doAction(
|
|
1569
2005
|
action2,
|
|
1570
2006
|
target,
|
|
@@ -1577,22 +2013,16 @@ var Act = class {
|
|
|
1577
2013
|
at = event.id;
|
|
1578
2014
|
handled++;
|
|
1579
2015
|
} catch (error) {
|
|
1580
|
-
this.
|
|
1581
|
-
const block2 = lease.retry >= options.maxRetries && options.blockOnError;
|
|
1582
|
-
block2 && this._logger.error(
|
|
1583
|
-
`Blocking ${stream} after ${lease.retry} retries.`
|
|
1584
|
-
);
|
|
1585
|
-
return {
|
|
2016
|
+
return this._finalize(
|
|
1586
2017
|
lease,
|
|
1587
2018
|
handled,
|
|
1588
2019
|
at,
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
};
|
|
2020
|
+
error,
|
|
2021
|
+
payload.options
|
|
2022
|
+
);
|
|
1593
2023
|
}
|
|
1594
2024
|
}
|
|
1595
|
-
return
|
|
2025
|
+
return this._finalize(lease, handled, at, void 0, payloads[0].options);
|
|
1596
2026
|
}
|
|
1597
2027
|
/**
|
|
1598
2028
|
* Handles a batch of events for a projection with a batch handler.
|
|
@@ -1602,33 +2032,26 @@ var Act = class {
|
|
|
1602
2032
|
* in a single call, enabling bulk DB operations.
|
|
1603
2033
|
*
|
|
1604
2034
|
* @internal
|
|
1605
|
-
* @param lease The lease to handle
|
|
1606
|
-
* @param payloads The reactions to handle
|
|
1607
|
-
* @param batchHandler The batch handler for this projection
|
|
1608
|
-
* @returns The lease with results
|
|
1609
2035
|
*/
|
|
1610
2036
|
async handleBatch(lease, payloads, batchHandler) {
|
|
1611
2037
|
const stream = lease.stream;
|
|
1612
2038
|
const events = payloads.map((p) => p.event);
|
|
1613
|
-
const
|
|
1614
|
-
lease.retry > 0
|
|
1615
|
-
|
|
1616
|
-
|
|
2039
|
+
const options = payloads[0].options;
|
|
2040
|
+
if (lease.retry > 0)
|
|
2041
|
+
this._logger.warn(
|
|
2042
|
+
`Retrying batch ${stream}@${events[0].id} (${lease.retry}).`
|
|
2043
|
+
);
|
|
1617
2044
|
try {
|
|
1618
2045
|
await batchHandler(events, stream);
|
|
1619
|
-
return
|
|
1620
|
-
} catch (error) {
|
|
1621
|
-
this._logger.error(error);
|
|
1622
|
-
const { options } = payloads[0];
|
|
1623
|
-
const block2 = lease.retry >= options.maxRetries && options.blockOnError;
|
|
1624
|
-
block2 && this._logger.error(`Blocking ${stream} after ${lease.retry} retries.`);
|
|
1625
|
-
return {
|
|
2046
|
+
return this._finalize(
|
|
1626
2047
|
lease,
|
|
1627
|
-
|
|
1628
|
-
at
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
2048
|
+
events.length,
|
|
2049
|
+
events.at(-1).id,
|
|
2050
|
+
void 0,
|
|
2051
|
+
options
|
|
2052
|
+
);
|
|
2053
|
+
} catch (error) {
|
|
2054
|
+
return this._finalize(lease, 0, lease.at, error, options);
|
|
1632
2055
|
}
|
|
1633
2056
|
}
|
|
1634
2057
|
/**
|
|
@@ -1678,82 +2101,46 @@ var Act = class {
|
|
|
1678
2101
|
if (!this._needs_drain) {
|
|
1679
2102
|
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
1680
2103
|
}
|
|
1681
|
-
if (
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
for (const f of fetched) {
|
|
1703
|
-
const { stream, events } = f;
|
|
1704
|
-
const payloads = events.flatMap((event) => {
|
|
1705
|
-
const register = this.registry.events[event.name];
|
|
1706
|
-
if (!register) return [];
|
|
1707
|
-
return [...register.reactions.values()].filter((reaction) => {
|
|
1708
|
-
const resolved = typeof reaction.resolver === "function" ? reaction.resolver(event) : reaction.resolver;
|
|
1709
|
-
return resolved && resolved.target === stream;
|
|
1710
|
-
}).map((reaction) => ({ ...reaction, event }));
|
|
1711
|
-
});
|
|
1712
|
-
fetchMap.set(stream, { fetch: f, payloads });
|
|
1713
|
-
}
|
|
1714
|
-
const handled = await Promise.all(
|
|
1715
|
-
leased.map((lease) => {
|
|
1716
|
-
const entry = fetchMap.get(lease.stream);
|
|
1717
|
-
const at = entry?.fetch.events.at(-1)?.id || fetch_window_at;
|
|
1718
|
-
const payloads = entry?.payloads ?? [];
|
|
1719
|
-
const batchHandler = this._batch_handlers.get(lease.stream);
|
|
1720
|
-
if (batchHandler && payloads.length > 0) {
|
|
1721
|
-
return this.handleBatch({ ...lease, at }, payloads, batchHandler);
|
|
1722
|
-
}
|
|
1723
|
-
return this.handle({ ...lease, at }, payloads);
|
|
1724
|
-
})
|
|
1725
|
-
);
|
|
1726
|
-
const [lagging_handled, leading_handled] = handled.reduce(
|
|
1727
|
-
([lagging_handled2, leading_handled2], { lease, handled: handled2 }) => [
|
|
1728
|
-
lagging_handled2 + (lease.lagging ? handled2 : 0),
|
|
1729
|
-
leading_handled2 + (lease.lagging ? 0 : handled2)
|
|
1730
|
-
],
|
|
1731
|
-
[0, 0]
|
|
1732
|
-
);
|
|
1733
|
-
const lagging_avg = lagging > 0 ? lagging_handled / lagging : 0;
|
|
1734
|
-
const leading_avg = leading > 0 ? leading_handled / leading : 0;
|
|
1735
|
-
const total = lagging_avg + leading_avg;
|
|
1736
|
-
this._drain_lag2lead_ratio = total > 0 ? Math.max(0.2, Math.min(0.8, lagging_avg / total)) : 0.5;
|
|
1737
|
-
const acked = await this._cd.ack(
|
|
1738
|
-
handled.filter(({ error }) => !error).map(({ at, lease }) => ({ ...lease, at }))
|
|
1739
|
-
);
|
|
1740
|
-
if (acked.length) this.emit("acked", acked);
|
|
1741
|
-
const blocked = await this._cd.block(
|
|
1742
|
-
handled.filter(({ block: block2 }) => block2).map(({ lease, error }) => ({ ...lease, error }))
|
|
1743
|
-
);
|
|
1744
|
-
if (blocked.length) this.emit("blocked", blocked);
|
|
1745
|
-
const result = { fetched, leased, acked, blocked };
|
|
1746
|
-
const hasErrors = handled.some(({ error }) => error);
|
|
1747
|
-
if (!acked.length && !blocked.length && !hasErrors)
|
|
1748
|
-
this._needs_drain = false;
|
|
1749
|
-
return result;
|
|
1750
|
-
} catch (error) {
|
|
1751
|
-
this._logger.error(error);
|
|
1752
|
-
} finally {
|
|
1753
|
-
this._drain_locked = false;
|
|
2104
|
+
if (this._drain_locked) {
|
|
2105
|
+
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
2106
|
+
}
|
|
2107
|
+
try {
|
|
2108
|
+
this._drain_locked = true;
|
|
2109
|
+
const lagging = Math.ceil(streamLimit * this._drain_lag2lead_ratio);
|
|
2110
|
+
const leading = streamLimit - lagging;
|
|
2111
|
+
const cycle = await runDrainCycle(
|
|
2112
|
+
this._cd,
|
|
2113
|
+
this.registry,
|
|
2114
|
+
this._batch_handlers,
|
|
2115
|
+
this._bound_handle,
|
|
2116
|
+
this._bound_handle_batch,
|
|
2117
|
+
lagging,
|
|
2118
|
+
leading,
|
|
2119
|
+
eventLimit,
|
|
2120
|
+
leaseMillis
|
|
2121
|
+
);
|
|
2122
|
+
if (!cycle) {
|
|
2123
|
+
this._needs_drain = false;
|
|
2124
|
+
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
1754
2125
|
}
|
|
2126
|
+
const { leased, fetched, handled, acked, blocked } = cycle;
|
|
2127
|
+
this._drain_lag2lead_ratio = computeLagLeadRatio(
|
|
2128
|
+
handled,
|
|
2129
|
+
lagging,
|
|
2130
|
+
leading
|
|
2131
|
+
);
|
|
2132
|
+
if (acked.length) this.emit("acked", acked);
|
|
2133
|
+
if (blocked.length) this.emit("blocked", blocked);
|
|
2134
|
+
const hasErrors = handled.some(({ error }) => error);
|
|
2135
|
+
if (!acked.length && !blocked.length && !hasErrors)
|
|
2136
|
+
this._needs_drain = false;
|
|
2137
|
+
return { fetched, leased, acked, blocked };
|
|
2138
|
+
} catch (error) {
|
|
2139
|
+
this._logger.error(error);
|
|
2140
|
+
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
2141
|
+
} finally {
|
|
2142
|
+
this._drain_locked = false;
|
|
1755
2143
|
}
|
|
1756
|
-
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
1757
2144
|
}
|
|
1758
2145
|
/**
|
|
1759
2146
|
* Discovers and registers new streams dynamically based on reaction resolvers.
|
|
@@ -1814,7 +2201,7 @@ var Act = class {
|
|
|
1814
2201
|
this._correlation_checkpoint = watermark;
|
|
1815
2202
|
if (this._reactive_events.size > 0) this._needs_drain = true;
|
|
1816
2203
|
for (const { stream } of this._static_targets) {
|
|
1817
|
-
this.
|
|
2204
|
+
this._subscribed_streams.add(stream);
|
|
1818
2205
|
}
|
|
1819
2206
|
}
|
|
1820
2207
|
async correlate(query = { after: -1, limit: 10 }) {
|
|
@@ -1832,7 +2219,7 @@ var Act = class {
|
|
|
1832
2219
|
for (const reaction of register.reactions.values()) {
|
|
1833
2220
|
if (typeof reaction.resolver !== "function") continue;
|
|
1834
2221
|
const resolved = reaction.resolver(event);
|
|
1835
|
-
if (resolved && !this.
|
|
2222
|
+
if (resolved && !this._subscribed_streams.has(resolved.target)) {
|
|
1836
2223
|
const entry = correlated.get(resolved.target) || {
|
|
1837
2224
|
source: resolved.source,
|
|
1838
2225
|
payloads: []
|
|
@@ -1858,7 +2245,7 @@ var Act = class {
|
|
|
1858
2245
|
this._correlation_checkpoint = last_id;
|
|
1859
2246
|
if (subscribed) {
|
|
1860
2247
|
for (const { stream } of streams) {
|
|
1861
|
-
this.
|
|
2248
|
+
this._subscribed_streams.add(stream);
|
|
1862
2249
|
}
|
|
1863
2250
|
}
|
|
1864
2251
|
return { subscribed, last_id };
|
|
@@ -2041,143 +2428,14 @@ var Act = class {
|
|
|
2041
2428
|
*/
|
|
2042
2429
|
async close(targets) {
|
|
2043
2430
|
if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
|
|
2044
|
-
const targetMap = new Map(targets.map((t) => [t.stream, t]));
|
|
2045
|
-
const streams = [...targetMap.keys()];
|
|
2046
2431
|
await this.correlate({ limit: 1e3 });
|
|
2047
|
-
const
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
await store().query(
|
|
2054
|
-
(e) => {
|
|
2055
|
-
if (e.name === TOMBSTONE_EVENT) return;
|
|
2056
|
-
if (maxId === -1) {
|
|
2057
|
-
maxId = e.id;
|
|
2058
|
-
version = e.version;
|
|
2059
|
-
}
|
|
2060
|
-
if (e.name !== SNAP_EVENT && lastEventName === void 0) {
|
|
2061
|
-
lastEventName = e.name;
|
|
2062
|
-
}
|
|
2063
|
-
},
|
|
2064
|
-
// limit: 2 covers the typical snapshot-at-head case (snapshot is
|
|
2065
|
-
// always preceded by the domain event it captured). Streams with
|
|
2066
|
-
// unusual layouts fall back to no-seed via the lookup miss path.
|
|
2067
|
-
{ stream: s, stream_exact: true, backward: true, limit: 2 }
|
|
2068
|
-
);
|
|
2069
|
-
if (maxId >= 0) streamInfo.set(s, { maxId, version, lastEventName });
|
|
2070
|
-
})
|
|
2071
|
-
);
|
|
2072
|
-
const skipped = [];
|
|
2073
|
-
let safe;
|
|
2074
|
-
if (this._reactive_events.size === 0) {
|
|
2075
|
-
safe = [...streamInfo.keys()];
|
|
2076
|
-
} else {
|
|
2077
|
-
const pendingSet = /* @__PURE__ */ new Set();
|
|
2078
|
-
await store().query_streams((position) => {
|
|
2079
|
-
const sourceRe = position.source ? RegExp(position.source) : void 0;
|
|
2080
|
-
for (const [stream, info] of streamInfo) {
|
|
2081
|
-
if ((!sourceRe || sourceRe.test(stream)) && position.at < info.maxId) {
|
|
2082
|
-
pendingSet.add(stream);
|
|
2083
|
-
}
|
|
2084
|
-
}
|
|
2085
|
-
});
|
|
2086
|
-
safe = [];
|
|
2087
|
-
for (const [stream] of streamInfo) {
|
|
2088
|
-
if (pendingSet.has(stream)) {
|
|
2089
|
-
skipped.push(stream);
|
|
2090
|
-
} else {
|
|
2091
|
-
safe.push(stream);
|
|
2092
|
-
}
|
|
2093
|
-
}
|
|
2094
|
-
}
|
|
2095
|
-
if (!safe.length) {
|
|
2096
|
-
const result2 = { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
2097
|
-
this.emit("closed", result2);
|
|
2098
|
-
return result2;
|
|
2099
|
-
}
|
|
2100
|
-
const correlation = (0, import_crypto2.randomUUID)();
|
|
2101
|
-
const guarded = [];
|
|
2102
|
-
const guardEvents = /* @__PURE__ */ new Map();
|
|
2103
|
-
await Promise.all(
|
|
2104
|
-
safe.map(async (stream) => {
|
|
2105
|
-
try {
|
|
2106
|
-
const info = streamInfo.get(stream);
|
|
2107
|
-
const [committed] = await store().commit(
|
|
2108
|
-
stream,
|
|
2109
|
-
[{ name: TOMBSTONE_EVENT, data: {} }],
|
|
2110
|
-
{ correlation, causation: {} },
|
|
2111
|
-
info.version
|
|
2112
|
-
);
|
|
2113
|
-
guarded.push(stream);
|
|
2114
|
-
guardEvents.set(stream, { id: committed.id, stream });
|
|
2115
|
-
} catch {
|
|
2116
|
-
skipped.push(stream);
|
|
2117
|
-
}
|
|
2118
|
-
})
|
|
2119
|
-
);
|
|
2120
|
-
if (!guarded.length) {
|
|
2121
|
-
const result2 = { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
2122
|
-
this.emit("closed", result2);
|
|
2123
|
-
return result2;
|
|
2124
|
-
}
|
|
2125
|
-
const seedStates = /* @__PURE__ */ new Map();
|
|
2126
|
-
await Promise.all(
|
|
2127
|
-
guarded.filter((s) => targetMap.get(s)?.restart).map(async (stream) => {
|
|
2128
|
-
const lastEventName = streamInfo.get(stream)?.lastEventName;
|
|
2129
|
-
const ownerState = lastEventName ? this._event_to_state.get(lastEventName) : void 0;
|
|
2130
|
-
if (!ownerState) {
|
|
2131
|
-
this._logger.error(
|
|
2132
|
-
`Cannot seed restart for "${stream}": no registered state owns event "${lastEventName ?? "<none>"}". Stream will be tombstoned instead.`
|
|
2133
|
-
);
|
|
2134
|
-
return;
|
|
2135
|
-
}
|
|
2136
|
-
const snap2 = await this._es.load(ownerState, stream);
|
|
2137
|
-
seedStates.set(stream, snap2.state);
|
|
2138
|
-
})
|
|
2139
|
-
);
|
|
2140
|
-
for (const stream of guarded) {
|
|
2141
|
-
const archiveFn = targetMap.get(stream)?.archive;
|
|
2142
|
-
if (archiveFn) await archiveFn();
|
|
2143
|
-
}
|
|
2144
|
-
const truncTargets = guarded.map((stream) => {
|
|
2145
|
-
const snapshot = seedStates.get(stream);
|
|
2146
|
-
const guard = guardEvents.get(stream);
|
|
2147
|
-
return {
|
|
2148
|
-
stream,
|
|
2149
|
-
snapshot,
|
|
2150
|
-
meta: {
|
|
2151
|
-
correlation,
|
|
2152
|
-
causation: {
|
|
2153
|
-
event: {
|
|
2154
|
-
id: guard.id,
|
|
2155
|
-
name: TOMBSTONE_EVENT,
|
|
2156
|
-
stream: guard.stream
|
|
2157
|
-
}
|
|
2158
|
-
}
|
|
2159
|
-
}
|
|
2160
|
-
};
|
|
2432
|
+
const result = await runCloseCycle(targets, {
|
|
2433
|
+
reactiveEventsSize: this._reactive_events.size,
|
|
2434
|
+
eventToState: this._event_to_state,
|
|
2435
|
+
load: this._es.load,
|
|
2436
|
+
tombstone: this._es.tombstone,
|
|
2437
|
+
logger: this._logger
|
|
2161
2438
|
});
|
|
2162
|
-
const truncated = await store().truncate(truncTargets);
|
|
2163
|
-
await Promise.all(
|
|
2164
|
-
guarded.map(async (stream) => {
|
|
2165
|
-
const entry = truncated.get(stream);
|
|
2166
|
-
const state2 = seedStates.get(stream);
|
|
2167
|
-
if (state2 && entry) {
|
|
2168
|
-
await cache().set(stream, {
|
|
2169
|
-
state: state2,
|
|
2170
|
-
version: entry.committed.version,
|
|
2171
|
-
event_id: entry.committed.id,
|
|
2172
|
-
patches: 0,
|
|
2173
|
-
snaps: 1
|
|
2174
|
-
});
|
|
2175
|
-
} else {
|
|
2176
|
-
await cache().invalidate(stream);
|
|
2177
|
-
}
|
|
2178
|
-
})
|
|
2179
|
-
);
|
|
2180
|
-
const result = { truncated, skipped };
|
|
2181
2439
|
this.emit("closed", result);
|
|
2182
2440
|
return result;
|
|
2183
2441
|
}
|
|
@@ -2246,7 +2504,7 @@ var Act = class {
|
|
|
2246
2504
|
}
|
|
2247
2505
|
};
|
|
2248
2506
|
|
|
2249
|
-
// src/act-builder.ts
|
|
2507
|
+
// src/builders/act-builder.ts
|
|
2250
2508
|
function registerBatchHandler(proj, batchHandlers) {
|
|
2251
2509
|
if (!proj.batchHandler || !proj.target) return;
|
|
2252
2510
|
const existing = batchHandlers.get(proj.target);
|
|
@@ -2255,56 +2513,33 @@ function registerBatchHandler(proj, batchHandlers) {
|
|
|
2255
2513
|
}
|
|
2256
2514
|
batchHandlers.set(proj.target, proj.batchHandler);
|
|
2257
2515
|
}
|
|
2258
|
-
function act(
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
},
|
|
2516
|
+
function act() {
|
|
2517
|
+
const states = /* @__PURE__ */ new Map();
|
|
2518
|
+
const registry = {
|
|
2519
|
+
actions: {},
|
|
2520
|
+
events: {}
|
|
2521
|
+
};
|
|
2522
|
+
const pendingProjections = [];
|
|
2523
|
+
const batchHandlers = /* @__PURE__ */ new Map();
|
|
2262
2524
|
const builder = {
|
|
2263
2525
|
withState: (state2) => {
|
|
2264
2526
|
registerState(state2, states, registry.actions, registry.events);
|
|
2265
|
-
return
|
|
2266
|
-
states,
|
|
2267
|
-
registry,
|
|
2268
|
-
pendingProjections,
|
|
2269
|
-
batchHandlers
|
|
2270
|
-
);
|
|
2527
|
+
return builder;
|
|
2271
2528
|
},
|
|
2272
2529
|
withSlice: (input) => {
|
|
2273
2530
|
for (const s of input.states.values()) {
|
|
2274
2531
|
registerState(s, states, registry.actions, registry.events);
|
|
2275
2532
|
}
|
|
2276
|
-
|
|
2277
|
-
const sliceRegister = input.events[eventName];
|
|
2278
|
-
for (const [name, reaction] of sliceRegister.reactions) {
|
|
2279
|
-
registry.events[eventName].reactions.set(name, reaction);
|
|
2280
|
-
}
|
|
2281
|
-
}
|
|
2533
|
+
mergeEventRegister(registry.events, input.events);
|
|
2282
2534
|
pendingProjections.push(...input.projections);
|
|
2283
|
-
return
|
|
2284
|
-
states,
|
|
2285
|
-
registry,
|
|
2286
|
-
pendingProjections,
|
|
2287
|
-
batchHandlers
|
|
2288
|
-
);
|
|
2535
|
+
return builder;
|
|
2289
2536
|
},
|
|
2290
2537
|
withProjection: (proj) => {
|
|
2291
2538
|
mergeProjection(proj, registry.events);
|
|
2292
2539
|
registerBatchHandler(proj, batchHandlers);
|
|
2293
|
-
return
|
|
2294
|
-
states,
|
|
2295
|
-
registry,
|
|
2296
|
-
pendingProjections,
|
|
2297
|
-
batchHandlers
|
|
2298
|
-
);
|
|
2299
|
-
},
|
|
2300
|
-
withActor: () => {
|
|
2301
|
-
return act(
|
|
2302
|
-
states,
|
|
2303
|
-
registry,
|
|
2304
|
-
pendingProjections,
|
|
2305
|
-
batchHandlers
|
|
2306
|
-
);
|
|
2540
|
+
return builder;
|
|
2307
2541
|
},
|
|
2542
|
+
withActor: () => builder,
|
|
2308
2543
|
on: (event) => ({
|
|
2309
2544
|
do: (handler, options) => {
|
|
2310
2545
|
const reaction = {
|
|
@@ -2320,19 +2555,15 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
|
|
|
2320
2555
|
`Reaction handler for "${String(event)}" must be a named function`
|
|
2321
2556
|
);
|
|
2322
2557
|
registry.events[event].reactions.set(handler.name, reaction);
|
|
2323
|
-
return {
|
|
2324
|
-
...builder,
|
|
2558
|
+
return Object.assign(builder, {
|
|
2325
2559
|
to(resolver) {
|
|
2326
|
-
|
|
2327
|
-
...reaction,
|
|
2328
|
-
resolver: typeof resolver === "string" ? { target: resolver } : resolver
|
|
2329
|
-
});
|
|
2560
|
+
reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
|
|
2330
2561
|
return builder;
|
|
2331
2562
|
}
|
|
2332
|
-
};
|
|
2563
|
+
});
|
|
2333
2564
|
}
|
|
2334
2565
|
}),
|
|
2335
|
-
build: () => {
|
|
2566
|
+
build: (options) => {
|
|
2336
2567
|
for (const proj of pendingProjections) {
|
|
2337
2568
|
mergeProjection(proj, registry.events);
|
|
2338
2569
|
registerBatchHandler(proj, batchHandlers);
|
|
@@ -2340,7 +2571,8 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
|
|
|
2340
2571
|
return new Act(
|
|
2341
2572
|
registry,
|
|
2342
2573
|
states,
|
|
2343
|
-
batchHandlers
|
|
2574
|
+
batchHandlers,
|
|
2575
|
+
options
|
|
2344
2576
|
);
|
|
2345
2577
|
},
|
|
2346
2578
|
events: registry.events
|
|
@@ -2348,8 +2580,9 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
|
|
|
2348
2580
|
return builder;
|
|
2349
2581
|
}
|
|
2350
2582
|
|
|
2351
|
-
// src/projection-builder.ts
|
|
2352
|
-
function _projection(target
|
|
2583
|
+
// src/builders/projection-builder.ts
|
|
2584
|
+
function _projection(target) {
|
|
2585
|
+
const events = {};
|
|
2353
2586
|
const defaultResolver = typeof target === "string" ? { target } : void 0;
|
|
2354
2587
|
const base = {
|
|
2355
2588
|
on: (entry) => {
|
|
@@ -2379,17 +2612,13 @@ function _projection(target, events) {
|
|
|
2379
2612
|
`Projection handler for "${event}" must be a named function`
|
|
2380
2613
|
);
|
|
2381
2614
|
register.reactions.set(handler.name, reaction);
|
|
2382
|
-
const
|
|
2383
|
-
return {
|
|
2384
|
-
...nextBuilder,
|
|
2615
|
+
const widened = base;
|
|
2616
|
+
return Object.assign(widened, {
|
|
2385
2617
|
to(resolver) {
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
resolver: typeof resolver === "string" ? { target: resolver } : resolver
|
|
2389
|
-
});
|
|
2390
|
-
return nextBuilder;
|
|
2618
|
+
reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
|
|
2619
|
+
return widened;
|
|
2391
2620
|
}
|
|
2392
|
-
};
|
|
2621
|
+
});
|
|
2393
2622
|
}
|
|
2394
2623
|
};
|
|
2395
2624
|
},
|
|
@@ -2401,8 +2630,7 @@ function _projection(target, events) {
|
|
|
2401
2630
|
events
|
|
2402
2631
|
};
|
|
2403
2632
|
if (typeof target === "string") {
|
|
2404
|
-
return {
|
|
2405
|
-
...base,
|
|
2633
|
+
return Object.assign(base, {
|
|
2406
2634
|
batch: (handler) => ({
|
|
2407
2635
|
build: () => ({
|
|
2408
2636
|
_tag: "Projection",
|
|
@@ -2411,34 +2639,28 @@ function _projection(target, events) {
|
|
|
2411
2639
|
batchHandler: handler
|
|
2412
2640
|
})
|
|
2413
2641
|
})
|
|
2414
|
-
};
|
|
2642
|
+
});
|
|
2415
2643
|
}
|
|
2416
2644
|
return base;
|
|
2417
2645
|
}
|
|
2418
|
-
function projection(target
|
|
2419
|
-
return _projection(target
|
|
2646
|
+
function projection(target) {
|
|
2647
|
+
return _projection(target);
|
|
2420
2648
|
}
|
|
2421
2649
|
|
|
2422
|
-
// src/slice-builder.ts
|
|
2423
|
-
function slice(
|
|
2650
|
+
// src/builders/slice-builder.ts
|
|
2651
|
+
function slice() {
|
|
2652
|
+
const states = /* @__PURE__ */ new Map();
|
|
2653
|
+
const actions = {};
|
|
2654
|
+
const events = {};
|
|
2655
|
+
const projections = [];
|
|
2424
2656
|
const builder = {
|
|
2425
2657
|
withState: (state2) => {
|
|
2426
2658
|
registerState(state2, states, actions, events);
|
|
2427
|
-
return
|
|
2428
|
-
states,
|
|
2429
|
-
actions,
|
|
2430
|
-
events,
|
|
2431
|
-
projections
|
|
2432
|
-
);
|
|
2659
|
+
return builder;
|
|
2433
2660
|
},
|
|
2434
2661
|
withProjection: (proj) => {
|
|
2435
2662
|
projections.push(proj);
|
|
2436
|
-
return
|
|
2437
|
-
states,
|
|
2438
|
-
actions,
|
|
2439
|
-
events,
|
|
2440
|
-
projections
|
|
2441
|
-
);
|
|
2663
|
+
return builder;
|
|
2442
2664
|
},
|
|
2443
2665
|
on: (event) => ({
|
|
2444
2666
|
do: (handler, options) => {
|
|
@@ -2455,16 +2677,12 @@ function slice(states = /* @__PURE__ */ new Map(), actions = {}, events = {}, pr
|
|
|
2455
2677
|
`Reaction handler for "${String(event)}" must be a named function`
|
|
2456
2678
|
);
|
|
2457
2679
|
events[event].reactions.set(handler.name, reaction);
|
|
2458
|
-
return {
|
|
2459
|
-
...builder,
|
|
2680
|
+
return Object.assign(builder, {
|
|
2460
2681
|
to(resolver) {
|
|
2461
|
-
|
|
2462
|
-
...reaction,
|
|
2463
|
-
resolver: typeof resolver === "string" ? { target: resolver } : resolver
|
|
2464
|
-
});
|
|
2682
|
+
reaction.resolver = typeof resolver === "string" ? { target: resolver } : resolver;
|
|
2465
2683
|
return builder;
|
|
2466
2684
|
}
|
|
2467
|
-
};
|
|
2685
|
+
});
|
|
2468
2686
|
}
|
|
2469
2687
|
}),
|
|
2470
2688
|
build: () => ({
|
|
@@ -2478,7 +2696,7 @@ function slice(states = /* @__PURE__ */ new Map(), actions = {}, events = {}, pr
|
|
|
2478
2696
|
return builder;
|
|
2479
2697
|
}
|
|
2480
2698
|
|
|
2481
|
-
// src/state-builder.ts
|
|
2699
|
+
// src/builders/state-builder.ts
|
|
2482
2700
|
function state(entry) {
|
|
2483
2701
|
const keys = Object.keys(entry);
|
|
2484
2702
|
if (keys.length !== 1) throw new Error("state() requires exactly one key");
|
|
@@ -2496,7 +2714,7 @@ function state(entry) {
|
|
|
2496
2714
|
return [k, fn];
|
|
2497
2715
|
})
|
|
2498
2716
|
);
|
|
2499
|
-
const
|
|
2717
|
+
const internal = {
|
|
2500
2718
|
events,
|
|
2501
2719
|
actions: {},
|
|
2502
2720
|
state: stateSchema,
|
|
@@ -2504,18 +2722,12 @@ function state(entry) {
|
|
|
2504
2722
|
init,
|
|
2505
2723
|
patch: defaultPatch,
|
|
2506
2724
|
on: {}
|
|
2507
|
-
}
|
|
2725
|
+
};
|
|
2726
|
+
const builder = action_builder(internal);
|
|
2508
2727
|
return Object.assign(builder, {
|
|
2509
2728
|
patch(customPatch) {
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
actions: {},
|
|
2513
|
-
state: stateSchema,
|
|
2514
|
-
name,
|
|
2515
|
-
init,
|
|
2516
|
-
patch: { ...defaultPatch, ...customPatch },
|
|
2517
|
-
on: {}
|
|
2518
|
-
});
|
|
2729
|
+
Object.assign(internal.patch, customPatch);
|
|
2730
|
+
return builder;
|
|
2519
2731
|
}
|
|
2520
2732
|
});
|
|
2521
2733
|
}
|
|
@@ -2524,50 +2736,43 @@ function state(entry) {
|
|
|
2524
2736
|
};
|
|
2525
2737
|
}
|
|
2526
2738
|
function action_builder(state2) {
|
|
2527
|
-
|
|
2739
|
+
const internal = state2;
|
|
2740
|
+
const builder = {
|
|
2528
2741
|
on(entry) {
|
|
2529
2742
|
const keys = Object.keys(entry);
|
|
2530
2743
|
if (keys.length !== 1) throw new Error(".on() requires exactly one key");
|
|
2531
2744
|
const action2 = keys[0];
|
|
2532
2745
|
const schema = entry[action2];
|
|
2533
|
-
if (action2 in
|
|
2746
|
+
if (action2 in internal.actions)
|
|
2534
2747
|
throw new Error(`Duplicate action "${action2}"`);
|
|
2535
|
-
|
|
2536
|
-
...state2.actions,
|
|
2537
|
-
[action2]: schema
|
|
2538
|
-
};
|
|
2539
|
-
const on = { ...state2.on };
|
|
2540
|
-
const _given = { ...state2.given };
|
|
2748
|
+
internal.actions[action2] = schema;
|
|
2541
2749
|
function given(rules) {
|
|
2542
|
-
|
|
2750
|
+
(internal.given ??= {})[action2] = rules;
|
|
2543
2751
|
return { emit };
|
|
2544
2752
|
}
|
|
2545
2753
|
function emit(handler) {
|
|
2546
2754
|
if (typeof handler === "string") {
|
|
2547
2755
|
const eventName = handler;
|
|
2548
|
-
on[action2] = (
|
|
2756
|
+
internal.on[action2] = (payload) => [
|
|
2757
|
+
eventName,
|
|
2758
|
+
payload
|
|
2759
|
+
];
|
|
2549
2760
|
} else {
|
|
2550
|
-
on[action2] = handler;
|
|
2761
|
+
internal.on[action2] = handler;
|
|
2551
2762
|
}
|
|
2552
|
-
return
|
|
2553
|
-
...state2,
|
|
2554
|
-
actions,
|
|
2555
|
-
on,
|
|
2556
|
-
given: _given
|
|
2557
|
-
});
|
|
2763
|
+
return builder;
|
|
2558
2764
|
}
|
|
2559
2765
|
return { given, emit };
|
|
2560
2766
|
},
|
|
2561
2767
|
snap(snap2) {
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
snap: snap2
|
|
2565
|
-
});
|
|
2768
|
+
internal.snap = snap2;
|
|
2769
|
+
return builder;
|
|
2566
2770
|
},
|
|
2567
2771
|
build() {
|
|
2568
|
-
return
|
|
2772
|
+
return internal;
|
|
2569
2773
|
}
|
|
2570
2774
|
};
|
|
2775
|
+
return builder;
|
|
2571
2776
|
}
|
|
2572
2777
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2573
2778
|
0 && (module.exports = {
|
|
@@ -2577,6 +2782,7 @@ function action_builder(state2) {
|
|
|
2577
2782
|
CommittedMetaSchema,
|
|
2578
2783
|
ConcurrencyError,
|
|
2579
2784
|
ConsoleLogger,
|
|
2785
|
+
DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
2580
2786
|
Environments,
|
|
2581
2787
|
Errors,
|
|
2582
2788
|
EventMetaSchema,
|