@rotorsoft/act 1.10.0 → 1.10.1
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 +12 -12
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/adapters/console-logger.d.ts +2 -2
- package/dist/@types/adapters/console-logger.d.ts.map +1 -1
- package/dist/@types/adapters/in-memory-store.d.ts +6 -6
- package/dist/@types/adapters/in-memory-store.d.ts.map +1 -1
- package/dist/@types/builders/state-builder.d.ts +2 -2
- package/dist/@types/builders/state-builder.d.ts.map +1 -1
- package/dist/@types/internal/audit.d.ts +4 -4
- package/dist/@types/internal/audit.d.ts.map +1 -1
- package/dist/@types/internal/backoff.d.ts +1 -1
- package/dist/@types/internal/backoff.d.ts.map +1 -1
- package/dist/@types/internal/build-classify.d.ts +11 -11
- package/dist/@types/internal/build-classify.d.ts.map +1 -1
- package/dist/@types/internal/close-cycle.d.ts +3 -3
- package/dist/@types/internal/close-cycle.d.ts.map +1 -1
- package/dist/@types/internal/correlate-cycle.d.ts +4 -4
- package/dist/@types/internal/correlate-cycle.d.ts.map +1 -1
- package/dist/@types/internal/correlator.d.ts +2 -2
- package/dist/@types/internal/correlator.d.ts.map +1 -1
- package/dist/@types/internal/drain-cycle.d.ts +20 -20
- package/dist/@types/internal/drain-cycle.d.ts.map +1 -1
- package/dist/@types/internal/drain-ratio.d.ts +1 -1
- package/dist/@types/internal/drain-ratio.d.ts.map +1 -1
- package/dist/@types/internal/event-sourcing.d.ts +2 -2
- package/dist/@types/internal/event-sourcing.d.ts.map +1 -1
- package/dist/@types/internal/event-versions.d.ts +2 -2
- package/dist/@types/internal/event-versions.d.ts.map +1 -1
- package/dist/@types/internal/index.d.ts +7 -7
- package/dist/@types/internal/index.d.ts.map +1 -1
- package/dist/@types/internal/merge.d.ts +3 -3
- package/dist/@types/internal/merge.d.ts.map +1 -1
- package/dist/@types/internal/reactions.d.ts +6 -6
- package/dist/@types/internal/reactions.d.ts.map +1 -1
- package/dist/@types/internal/sensitive.d.ts +2 -2
- package/dist/@types/internal/settle.d.ts +3 -3
- package/dist/@types/internal/settle.d.ts.map +1 -1
- package/dist/@types/internal/tracing.d.ts +5 -5
- package/dist/@types/internal/tracing.d.ts.map +1 -1
- package/dist/{chunk-3ZTFNAY7.js → chunk-3Z2HU726.js} +134 -133
- package/dist/chunk-3Z2HU726.js.map +1 -0
- package/dist/{chunk-XSBT63QX.js → chunk-BY5JPOZR.js} +1 -1
- package/dist/{chunk-XSBT63QX.js.map → chunk-BY5JPOZR.js.map} +1 -1
- package/dist/index.cjs +558 -552
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +428 -423
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs +132 -131
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.js +2 -2
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.js +1 -1
- package/package.json +3 -3
- package/dist/chunk-3ZTFNAY7.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
sleep,
|
|
20
20
|
store,
|
|
21
21
|
validate
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-3Z2HU726.js";
|
|
23
23
|
import {
|
|
24
24
|
ActorSchema,
|
|
25
25
|
CausationEventSchema,
|
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
pii_split,
|
|
45
45
|
pii_strip,
|
|
46
46
|
sensitive
|
|
47
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-BY5JPOZR.js";
|
|
48
48
|
import "./chunk-5WRI5ZAA.js";
|
|
49
49
|
|
|
50
50
|
// src/signals.ts
|
|
@@ -78,7 +78,7 @@ function parse(name) {
|
|
|
78
78
|
}
|
|
79
79
|
return { base: name, version: 1 };
|
|
80
80
|
}
|
|
81
|
-
function
|
|
81
|
+
function deprecated_event_names(names) {
|
|
82
82
|
const groups = /* @__PURE__ */ new Map();
|
|
83
83
|
for (const name of names) {
|
|
84
84
|
const { base, version } = parse(name);
|
|
@@ -94,10 +94,10 @@ function deprecatedEventNames(names) {
|
|
|
94
94
|
}
|
|
95
95
|
return deprecated;
|
|
96
96
|
}
|
|
97
|
-
function
|
|
98
|
-
const target = parse(
|
|
97
|
+
function current_version_of(deprecated_name, all_names) {
|
|
98
|
+
const target = parse(deprecated_name);
|
|
99
99
|
let highest;
|
|
100
|
-
for (const name of
|
|
100
|
+
for (const name of all_names) {
|
|
101
101
|
const { base, version } = parse(name);
|
|
102
102
|
if (base !== target.base) continue;
|
|
103
103
|
if (!highest || version > highest.version) highest = { version, name };
|
|
@@ -127,27 +127,27 @@ var ALL_CATEGORIES = [
|
|
|
127
127
|
];
|
|
128
128
|
async function* audit(deps, categories, options = {}) {
|
|
129
129
|
const requested = new Set(categories ?? [...ALL_CATEGORIES]);
|
|
130
|
-
const
|
|
131
|
-
const passes =
|
|
130
|
+
const ordered_categories = ALL_CATEGORIES.filter((c) => requested.has(c));
|
|
131
|
+
const passes = ordered_categories.map(
|
|
132
132
|
(c) => PASS_FACTORIES[c](deps, options)
|
|
133
133
|
);
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
if (
|
|
134
|
+
const need_stats = passes.some((p) => p.on_stat !== void 0);
|
|
135
|
+
const need_streams = passes.some((p) => p.on_stream !== void 0);
|
|
136
|
+
const need_events = passes.some((p) => p.on_event !== void 0);
|
|
137
|
+
if (need_stats) {
|
|
138
138
|
const stats = await deps.store().query_stats({}, { count: true, names: true });
|
|
139
139
|
for (const [stream, s] of stats) {
|
|
140
|
-
for (const p of passes) p.
|
|
140
|
+
for (const p of passes) p.on_stat?.(stream, s);
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
|
-
if (
|
|
143
|
+
if (need_streams) {
|
|
144
144
|
await deps.store().query_streams((pos) => {
|
|
145
|
-
for (const p of passes) p.
|
|
145
|
+
for (const p of passes) p.on_stream?.(pos);
|
|
146
146
|
});
|
|
147
147
|
}
|
|
148
|
-
if (
|
|
148
|
+
if (need_events) {
|
|
149
149
|
await deps.store().query((event) => {
|
|
150
|
-
for (const p of passes) p.
|
|
150
|
+
for (const p of passes) p.on_event?.(event);
|
|
151
151
|
}, options.query);
|
|
152
152
|
}
|
|
153
153
|
for (const p of passes) await p.finalize?.(deps);
|
|
@@ -155,11 +155,11 @@ async function* audit(deps, categories, options = {}) {
|
|
|
155
155
|
for (const f of p.drain()) yield f;
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
|
-
var
|
|
158
|
+
var make_schema_pass = (deps) => {
|
|
159
159
|
const findings = [];
|
|
160
160
|
return {
|
|
161
161
|
category: "schema",
|
|
162
|
-
|
|
162
|
+
on_event(event) {
|
|
163
163
|
const name = String(event.name);
|
|
164
164
|
const state2 = deps.event_to_state.get(name);
|
|
165
165
|
if (!state2) {
|
|
@@ -189,19 +189,19 @@ var makeSchemaPass = (deps) => {
|
|
|
189
189
|
drain: () => findings
|
|
190
190
|
};
|
|
191
191
|
};
|
|
192
|
-
var
|
|
192
|
+
var make_deprecated_load_pass = (deps, options) => {
|
|
193
193
|
const share_min = options.thresholds?.deprecated_min ?? DEFAULTS.deprecated_min;
|
|
194
194
|
const totals = /* @__PURE__ */ new Map();
|
|
195
|
-
const
|
|
195
|
+
const per_stream = /* @__PURE__ */ new Map();
|
|
196
196
|
return {
|
|
197
197
|
category: "deprecated-load",
|
|
198
|
-
|
|
198
|
+
on_stat(stream, { names }) {
|
|
199
199
|
for (const [name, count] of Object.entries(names)) {
|
|
200
200
|
totals.set(name, (totals.get(name) ?? 0) + count);
|
|
201
|
-
let m =
|
|
201
|
+
let m = per_stream.get(name);
|
|
202
202
|
if (!m) {
|
|
203
203
|
m = /* @__PURE__ */ new Map();
|
|
204
|
-
|
|
204
|
+
per_stream.set(name, m);
|
|
205
205
|
}
|
|
206
206
|
m.set(stream, count);
|
|
207
207
|
}
|
|
@@ -210,33 +210,33 @@ var makeDeprecatedLoadPass = (deps, options) => {
|
|
|
210
210
|
const findings = [];
|
|
211
211
|
const grand = [...totals.values()].reduce((s, n) => s + n, 0);
|
|
212
212
|
if (grand === 0) return findings;
|
|
213
|
-
const deprecated =
|
|
213
|
+
const deprecated = deprecated_event_names(deps.known_events);
|
|
214
214
|
const sorted = [...deprecated].map((name) => ({ name, count: totals.get(name) ?? 0 })).sort((a, b) => b.count - a.count);
|
|
215
215
|
for (const { name, count } of sorted) {
|
|
216
216
|
if (count === 0) continue;
|
|
217
217
|
if (count / grand < share_min) continue;
|
|
218
|
-
const
|
|
219
|
-
const
|
|
218
|
+
const current_version = current_version_of(name, deps.known_events);
|
|
219
|
+
const top_streams = [...per_stream.get(name).entries()].map(([stream, c]) => ({ stream, count: c })).sort((a, b) => b.count - a.count).slice(0, 10);
|
|
220
220
|
findings.push({
|
|
221
221
|
category: "deprecated-load",
|
|
222
222
|
name,
|
|
223
|
-
current_version
|
|
223
|
+
current_version,
|
|
224
224
|
total: count,
|
|
225
|
-
top_streams
|
|
225
|
+
top_streams
|
|
226
226
|
});
|
|
227
227
|
}
|
|
228
228
|
return findings;
|
|
229
229
|
}
|
|
230
230
|
};
|
|
231
231
|
};
|
|
232
|
-
var
|
|
232
|
+
var make_close_candidate_pass = (deps, options) => {
|
|
233
233
|
const idle_days = options.thresholds?.idle_days ?? DEFAULTS.idle_days;
|
|
234
234
|
const terminal_events = new Set(options.thresholds?.terminal_events ?? []);
|
|
235
235
|
const idle_cutoff = Date.now() - idle_days * 24 * 60 * 60 * 1e3;
|
|
236
236
|
const findings = [];
|
|
237
237
|
return {
|
|
238
238
|
category: "close-candidate",
|
|
239
|
-
|
|
239
|
+
on_stat(stream, { head }) {
|
|
240
240
|
const head_name = String(head.name);
|
|
241
241
|
if (head_name.startsWith("__")) return;
|
|
242
242
|
const head_time = head.created.getTime();
|
|
@@ -249,22 +249,22 @@ var makeCloseCandidatePass = (deps, options) => {
|
|
|
249
249
|
last_event_at: head.created.toISOString(),
|
|
250
250
|
reason: is_terminal ? "terminal" : "idle",
|
|
251
251
|
idle_days: is_idle ? Math.floor((Date.now() - head_time) / (24 * 60 * 60 * 1e3)) : void 0,
|
|
252
|
-
restart_supported:
|
|
252
|
+
restart_supported: restart_is_supported(deps, head_name)
|
|
253
253
|
});
|
|
254
254
|
},
|
|
255
255
|
drain: () => findings
|
|
256
256
|
};
|
|
257
257
|
};
|
|
258
|
-
var
|
|
258
|
+
var make_restart_candidate_pass = (deps, options) => {
|
|
259
259
|
const threshold = options.thresholds?.restart_min ?? DEFAULTS.restart_min;
|
|
260
260
|
const findings = [];
|
|
261
261
|
return {
|
|
262
262
|
category: "restart-candidate",
|
|
263
|
-
|
|
263
|
+
on_stat(stream, { head, count, names }) {
|
|
264
264
|
if (count < threshold) return;
|
|
265
265
|
const head_name = String(head.name);
|
|
266
266
|
if (head_name.startsWith("__")) return;
|
|
267
|
-
if (!
|
|
267
|
+
if (!restart_is_supported(deps, head_name)) return;
|
|
268
268
|
findings.push({
|
|
269
269
|
category: "restart-candidate",
|
|
270
270
|
stream,
|
|
@@ -278,14 +278,14 @@ var makeRestartCandidatePass = (deps, options) => {
|
|
|
278
278
|
drain: () => findings
|
|
279
279
|
};
|
|
280
280
|
};
|
|
281
|
-
var
|
|
281
|
+
var make_reaction_health_pass = (_deps, options) => {
|
|
282
282
|
const near_block = options.thresholds?.near_block ?? DEFAULTS.near_block;
|
|
283
283
|
const stuck_minutes = options.thresholds?.stuck_minutes ?? DEFAULTS.stuck_minutes;
|
|
284
284
|
const stuck_cutoff = Date.now() - stuck_minutes * 60 * 1e3;
|
|
285
285
|
const findings = [];
|
|
286
286
|
return {
|
|
287
287
|
category: "reaction-health",
|
|
288
|
-
|
|
288
|
+
on_stream(p) {
|
|
289
289
|
if (p.blocked) {
|
|
290
290
|
findings.push({
|
|
291
291
|
category: "reaction-health",
|
|
@@ -322,14 +322,14 @@ var makeReactionHealthPass = (_deps, options) => {
|
|
|
322
322
|
drain: () => findings
|
|
323
323
|
};
|
|
324
324
|
};
|
|
325
|
-
var
|
|
325
|
+
var make_snapshot_drift_pass = (deps, options) => {
|
|
326
326
|
const drift_min = options.thresholds?.drift_min ?? DEFAULTS.drift_min;
|
|
327
327
|
const candidates = [];
|
|
328
328
|
const findings = [];
|
|
329
329
|
return {
|
|
330
330
|
category: "snapshot-drift",
|
|
331
|
-
|
|
332
|
-
if (!
|
|
331
|
+
on_stat(stream, { head, count, names }) {
|
|
332
|
+
if (!restart_is_supported(deps, String(head.name))) return;
|
|
333
333
|
if (count < drift_min) return;
|
|
334
334
|
candidates.push({
|
|
335
335
|
stream,
|
|
@@ -378,12 +378,12 @@ var makeSnapshotDriftPass = (deps, options) => {
|
|
|
378
378
|
drain: () => findings
|
|
379
379
|
};
|
|
380
380
|
};
|
|
381
|
-
var
|
|
381
|
+
var make_routing_health_pass = (deps) => {
|
|
382
382
|
const findings = [];
|
|
383
|
-
const
|
|
383
|
+
const seen_event_names = /* @__PURE__ */ new Set();
|
|
384
384
|
return {
|
|
385
385
|
category: "routing-health",
|
|
386
|
-
|
|
386
|
+
on_stream(p) {
|
|
387
387
|
if (!p.lane) return;
|
|
388
388
|
if (deps.declared_lanes.has(p.lane)) return;
|
|
389
389
|
findings.push({
|
|
@@ -393,13 +393,13 @@ var makeRoutingHealthPass = (deps) => {
|
|
|
393
393
|
lane: p.lane
|
|
394
394
|
});
|
|
395
395
|
},
|
|
396
|
-
|
|
396
|
+
on_stat(_stream, { names }) {
|
|
397
397
|
for (const name of Object.keys(names)) {
|
|
398
|
-
|
|
398
|
+
seen_event_names.add(name);
|
|
399
399
|
}
|
|
400
400
|
},
|
|
401
401
|
finalize() {
|
|
402
|
-
for (const name of
|
|
402
|
+
for (const name of seen_event_names) {
|
|
403
403
|
if (name.startsWith("__")) continue;
|
|
404
404
|
if (deps.routed_events.has(name)) continue;
|
|
405
405
|
findings.push({
|
|
@@ -413,23 +413,23 @@ var makeRoutingHealthPass = (deps) => {
|
|
|
413
413
|
drain: () => findings
|
|
414
414
|
};
|
|
415
415
|
};
|
|
416
|
-
var
|
|
417
|
-
const
|
|
416
|
+
var make_correlation_gaps_pass = () => {
|
|
417
|
+
const seen_ids = /* @__PURE__ */ new Set();
|
|
418
418
|
const checks = [];
|
|
419
419
|
return {
|
|
420
420
|
category: "correlation-gaps",
|
|
421
|
-
|
|
422
|
-
|
|
421
|
+
on_event(e) {
|
|
422
|
+
seen_ids.add(e.id);
|
|
423
423
|
const causation = e.meta?.causation;
|
|
424
|
-
const
|
|
425
|
-
if (
|
|
426
|
-
checks.push({ stream: e.stream, id: e.id,
|
|
424
|
+
const parent_id = causation?.event?.id;
|
|
425
|
+
if (parent_id !== void 0) {
|
|
426
|
+
checks.push({ stream: e.stream, id: e.id, parent_id });
|
|
427
427
|
}
|
|
428
428
|
},
|
|
429
429
|
drain() {
|
|
430
430
|
const findings = [];
|
|
431
|
-
for (const { stream, id,
|
|
432
|
-
if (!
|
|
431
|
+
for (const { stream, id, parent_id } of checks) {
|
|
432
|
+
if (!seen_ids.has(parent_id)) {
|
|
433
433
|
findings.push({
|
|
434
434
|
category: "correlation-gaps",
|
|
435
435
|
stream,
|
|
@@ -442,12 +442,12 @@ var makeCorrelationGapsPass = () => {
|
|
|
442
442
|
}
|
|
443
443
|
};
|
|
444
444
|
};
|
|
445
|
-
var
|
|
445
|
+
var make_clock_anomalies_pass = () => {
|
|
446
446
|
const findings = [];
|
|
447
|
-
const
|
|
447
|
+
const last_per_stream = /* @__PURE__ */ new Map();
|
|
448
448
|
return {
|
|
449
449
|
category: "clock-anomalies",
|
|
450
|
-
|
|
450
|
+
on_event(e) {
|
|
451
451
|
const created = e.created.getTime();
|
|
452
452
|
if (created > Date.now()) {
|
|
453
453
|
findings.push({
|
|
@@ -457,7 +457,7 @@ var makeClockAnomaliesPass = () => {
|
|
|
457
457
|
reason: "future-created"
|
|
458
458
|
});
|
|
459
459
|
}
|
|
460
|
-
const prev =
|
|
460
|
+
const prev = last_per_stream.get(e.stream);
|
|
461
461
|
if (prev !== void 0 && created < prev) {
|
|
462
462
|
findings.push({
|
|
463
463
|
category: "clock-anomalies",
|
|
@@ -466,48 +466,48 @@ var makeClockAnomaliesPass = () => {
|
|
|
466
466
|
reason: "out-of-order"
|
|
467
467
|
});
|
|
468
468
|
}
|
|
469
|
-
|
|
469
|
+
last_per_stream.set(e.stream, created);
|
|
470
470
|
},
|
|
471
471
|
drain: () => findings
|
|
472
472
|
};
|
|
473
473
|
};
|
|
474
|
-
function
|
|
475
|
-
const state2 = deps.event_to_state.get(
|
|
474
|
+
function restart_is_supported(deps, head_event_name) {
|
|
475
|
+
const state2 = deps.event_to_state.get(head_event_name);
|
|
476
476
|
return state2?.snap !== void 0;
|
|
477
477
|
}
|
|
478
478
|
var PASS_FACTORIES = {
|
|
479
|
-
schema:
|
|
480
|
-
"deprecated-load":
|
|
481
|
-
"close-candidate":
|
|
482
|
-
"restart-candidate":
|
|
483
|
-
"reaction-health":
|
|
484
|
-
"snapshot-drift":
|
|
485
|
-
"routing-health":
|
|
486
|
-
"correlation-gaps":
|
|
487
|
-
"clock-anomalies":
|
|
479
|
+
schema: make_schema_pass,
|
|
480
|
+
"deprecated-load": make_deprecated_load_pass,
|
|
481
|
+
"close-candidate": make_close_candidate_pass,
|
|
482
|
+
"restart-candidate": make_restart_candidate_pass,
|
|
483
|
+
"reaction-health": make_reaction_health_pass,
|
|
484
|
+
"snapshot-drift": make_snapshot_drift_pass,
|
|
485
|
+
"routing-health": make_routing_health_pass,
|
|
486
|
+
"correlation-gaps": make_correlation_gaps_pass,
|
|
487
|
+
"clock-anomalies": make_clock_anomalies_pass
|
|
488
488
|
};
|
|
489
489
|
|
|
490
490
|
// src/internal/build-classify.ts
|
|
491
491
|
var ALL_LANES = /* @__PURE__ */ Symbol("act-1103/all-lanes");
|
|
492
|
-
function
|
|
492
|
+
function classify_registry(registry, states) {
|
|
493
493
|
const statics = /* @__PURE__ */ new Map();
|
|
494
|
-
const
|
|
495
|
-
const
|
|
496
|
-
let
|
|
494
|
+
const reactive_events = /* @__PURE__ */ new Set();
|
|
495
|
+
const event_to_lanes = /* @__PURE__ */ new Map();
|
|
496
|
+
let has_dynamic_resolvers = false;
|
|
497
497
|
for (const [name, register] of Object.entries(registry.events)) {
|
|
498
|
-
if (register.reactions.size > 0)
|
|
498
|
+
if (register.reactions.size > 0) reactive_events.add(name);
|
|
499
499
|
for (const reaction of register.reactions.values()) {
|
|
500
500
|
if (typeof reaction.resolver === "function") {
|
|
501
|
-
|
|
502
|
-
|
|
501
|
+
has_dynamic_resolvers = true;
|
|
502
|
+
event_to_lanes.set(name, ALL_LANES);
|
|
503
503
|
} else {
|
|
504
504
|
const { target, source, priority = 0, lane } = reaction.resolver;
|
|
505
505
|
const lane_name = lane ?? "default";
|
|
506
|
-
const existing_lanes =
|
|
506
|
+
const existing_lanes = event_to_lanes.get(name);
|
|
507
507
|
if (existing_lanes !== ALL_LANES) {
|
|
508
508
|
const set = existing_lanes ?? /* @__PURE__ */ new Set();
|
|
509
509
|
set.add(lane_name);
|
|
510
|
-
|
|
510
|
+
event_to_lanes.set(name, set);
|
|
511
511
|
}
|
|
512
512
|
const key = `${target}|${source ?? ""}`;
|
|
513
513
|
const existing = statics.get(key);
|
|
@@ -525,59 +525,59 @@ function classifyRegistry(registry, states) {
|
|
|
525
525
|
}
|
|
526
526
|
}
|
|
527
527
|
}
|
|
528
|
-
const
|
|
528
|
+
const event_to_state = /* @__PURE__ */ new Map();
|
|
529
529
|
for (const merged of states.values()) {
|
|
530
|
-
for (const
|
|
531
|
-
|
|
530
|
+
for (const event_name of Object.keys(merged.events)) {
|
|
531
|
+
event_to_state.set(event_name, merged);
|
|
532
532
|
}
|
|
533
533
|
}
|
|
534
534
|
return {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
535
|
+
static_targets: [...statics.values()],
|
|
536
|
+
has_dynamic_resolvers,
|
|
537
|
+
reactive_events,
|
|
538
|
+
event_to_state,
|
|
539
|
+
event_to_lanes
|
|
540
540
|
};
|
|
541
541
|
}
|
|
542
542
|
|
|
543
543
|
// src/internal/close-cycle.ts
|
|
544
|
-
async function
|
|
545
|
-
const
|
|
546
|
-
const streams = [...
|
|
544
|
+
async function run_close_cycle(targets, deps) {
|
|
545
|
+
const target_map = new Map(targets.map((t) => [t.stream, t]));
|
|
546
|
+
const streams = [...target_map.keys()];
|
|
547
547
|
const skipped = [];
|
|
548
|
-
const
|
|
549
|
-
const safe = await
|
|
550
|
-
|
|
551
|
-
deps.
|
|
548
|
+
const stream_info = await scan_stream_heads(streams);
|
|
549
|
+
const safe = await partition_by_safety(
|
|
550
|
+
stream_info,
|
|
551
|
+
deps.reactive_events_size,
|
|
552
552
|
skipped
|
|
553
553
|
);
|
|
554
554
|
if (!safe.length) return { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
555
|
-
const { guarded,
|
|
555
|
+
const { guarded, guard_events } = await guard_with_tombstones(
|
|
556
556
|
safe,
|
|
557
|
-
|
|
557
|
+
stream_info,
|
|
558
558
|
deps.correlation,
|
|
559
559
|
deps.tombstone,
|
|
560
560
|
skipped
|
|
561
561
|
);
|
|
562
562
|
if (!guarded.length) return { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
563
|
-
const
|
|
563
|
+
const seed_states = await load_restart_seeds(
|
|
564
564
|
guarded,
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
deps.
|
|
565
|
+
target_map,
|
|
566
|
+
stream_info,
|
|
567
|
+
deps.event_to_state,
|
|
568
568
|
deps.load,
|
|
569
569
|
deps.logger
|
|
570
570
|
);
|
|
571
|
-
await
|
|
572
|
-
const truncated = await
|
|
571
|
+
await run_archive_callbacks(guarded, target_map);
|
|
572
|
+
const truncated = await truncate_and_warm_cache(
|
|
573
573
|
guarded,
|
|
574
|
-
|
|
575
|
-
|
|
574
|
+
seed_states,
|
|
575
|
+
guard_events,
|
|
576
576
|
deps.correlation
|
|
577
577
|
);
|
|
578
578
|
return { truncated, skipped };
|
|
579
579
|
}
|
|
580
|
-
async function
|
|
580
|
+
async function scan_stream_heads(streams) {
|
|
581
581
|
const stats = await store().query_stats(streams, {
|
|
582
582
|
exclude: [SNAP_EVENT]
|
|
583
583
|
});
|
|
@@ -585,76 +585,76 @@ async function scanStreamHeads(streams) {
|
|
|
585
585
|
for (const [stream, { head }] of stats) {
|
|
586
586
|
if (head.name === TOMBSTONE_EVENT) continue;
|
|
587
587
|
out.set(stream, {
|
|
588
|
-
|
|
588
|
+
max_id: head.id,
|
|
589
589
|
version: head.version,
|
|
590
|
-
|
|
590
|
+
last_event_name: head.name
|
|
591
591
|
});
|
|
592
592
|
}
|
|
593
593
|
return out;
|
|
594
594
|
}
|
|
595
|
-
async function
|
|
596
|
-
if (
|
|
597
|
-
const
|
|
595
|
+
async function partition_by_safety(stream_info, reactive_events_size, skipped) {
|
|
596
|
+
if (reactive_events_size === 0) return [...stream_info.keys()];
|
|
597
|
+
const pending_set = /* @__PURE__ */ new Set();
|
|
598
598
|
await store().query_streams((position) => {
|
|
599
|
-
const
|
|
600
|
-
for (const [stream, info] of
|
|
601
|
-
if ((!
|
|
602
|
-
|
|
599
|
+
const source_re = position.source ? RegExp(position.source) : void 0;
|
|
600
|
+
for (const [stream, info] of stream_info) {
|
|
601
|
+
if ((!source_re || source_re.test(stream)) && position.at < info.max_id) {
|
|
602
|
+
pending_set.add(stream);
|
|
603
603
|
}
|
|
604
604
|
}
|
|
605
605
|
});
|
|
606
606
|
const safe = [];
|
|
607
|
-
for (const [stream] of
|
|
608
|
-
if (
|
|
607
|
+
for (const [stream] of stream_info) {
|
|
608
|
+
if (pending_set.has(stream)) skipped.push(stream);
|
|
609
609
|
else safe.push(stream);
|
|
610
610
|
}
|
|
611
611
|
return safe;
|
|
612
612
|
}
|
|
613
|
-
async function
|
|
613
|
+
async function guard_with_tombstones(safe, stream_info, correlation, tombstone2, skipped) {
|
|
614
614
|
const guarded = [];
|
|
615
|
-
const
|
|
615
|
+
const guard_events = /* @__PURE__ */ new Map();
|
|
616
616
|
await Promise.all(
|
|
617
617
|
safe.map(async (stream) => {
|
|
618
|
-
const info =
|
|
618
|
+
const info = stream_info.get(stream);
|
|
619
619
|
const committed = await tombstone2(stream, info.version, correlation);
|
|
620
620
|
if (committed) {
|
|
621
621
|
guarded.push(stream);
|
|
622
|
-
|
|
622
|
+
guard_events.set(stream, { id: committed.id, stream });
|
|
623
623
|
} else {
|
|
624
624
|
skipped.push(stream);
|
|
625
625
|
}
|
|
626
626
|
})
|
|
627
627
|
);
|
|
628
|
-
return { guarded,
|
|
628
|
+
return { guarded, guard_events };
|
|
629
629
|
}
|
|
630
|
-
async function
|
|
631
|
-
const
|
|
630
|
+
async function load_restart_seeds(guarded, target_map, stream_info, event_to_state, load2, logger) {
|
|
631
|
+
const seed_states = /* @__PURE__ */ new Map();
|
|
632
632
|
await Promise.all(
|
|
633
|
-
guarded.filter((s) =>
|
|
634
|
-
const
|
|
635
|
-
const
|
|
636
|
-
if (!
|
|
633
|
+
guarded.filter((s) => target_map.get(s)?.restart).map(async (stream) => {
|
|
634
|
+
const last_event_name = stream_info.get(stream).last_event_name;
|
|
635
|
+
const owner_state = event_to_state.get(last_event_name);
|
|
636
|
+
if (!owner_state) {
|
|
637
637
|
logger.error(
|
|
638
|
-
`Cannot seed restart for "${stream}": no registered state owns event "${
|
|
638
|
+
`Cannot seed restart for "${stream}": no registered state owns event "${last_event_name}". Stream will be tombstoned instead.`
|
|
639
639
|
);
|
|
640
640
|
return;
|
|
641
641
|
}
|
|
642
|
-
const snap2 = await load2(
|
|
643
|
-
|
|
642
|
+
const snap2 = await load2(owner_state, stream);
|
|
643
|
+
seed_states.set(stream, snap2.state);
|
|
644
644
|
})
|
|
645
645
|
);
|
|
646
|
-
return
|
|
646
|
+
return seed_states;
|
|
647
647
|
}
|
|
648
|
-
async function
|
|
648
|
+
async function run_archive_callbacks(guarded, target_map) {
|
|
649
649
|
for (const stream of guarded) {
|
|
650
|
-
const
|
|
651
|
-
if (
|
|
650
|
+
const archive_fn = target_map.get(stream)?.archive;
|
|
651
|
+
if (archive_fn) await archive_fn();
|
|
652
652
|
}
|
|
653
653
|
}
|
|
654
|
-
async function
|
|
655
|
-
const
|
|
656
|
-
const snapshot =
|
|
657
|
-
const guard =
|
|
654
|
+
async function truncate_and_warm_cache(guarded, seed_states, guard_events, correlation) {
|
|
655
|
+
const trunc_targets = guarded.map((stream) => {
|
|
656
|
+
const snapshot = seed_states.get(stream);
|
|
657
|
+
const guard = guard_events.get(stream);
|
|
658
658
|
return {
|
|
659
659
|
stream,
|
|
660
660
|
snapshot,
|
|
@@ -666,11 +666,11 @@ async function truncateAndWarmCache(guarded, seedStates, guardEvents, correlatio
|
|
|
666
666
|
}
|
|
667
667
|
};
|
|
668
668
|
});
|
|
669
|
-
const truncated = await store().truncate(
|
|
669
|
+
const truncated = await store().truncate(trunc_targets);
|
|
670
670
|
await Promise.all(
|
|
671
671
|
guarded.map(async (stream) => {
|
|
672
672
|
const entry = truncated.get(stream);
|
|
673
|
-
const state2 =
|
|
673
|
+
const state2 = seed_states.get(stream);
|
|
674
674
|
if (state2 && entry) {
|
|
675
675
|
await cache().set(stream, {
|
|
676
676
|
state: state2,
|
|
@@ -698,13 +698,13 @@ var CorrelateCycle = class {
|
|
|
698
698
|
_has_dynamic_resolvers;
|
|
699
699
|
_cd;
|
|
700
700
|
_on_init;
|
|
701
|
-
constructor(registry,
|
|
701
|
+
constructor(registry, static_targets, has_dynamic_resolvers, cd, maxSubscribedStreams, on_init) {
|
|
702
702
|
this._subscribed = new LruSet(maxSubscribedStreams);
|
|
703
703
|
this._registry = registry;
|
|
704
|
-
this._static_targets =
|
|
705
|
-
this._has_dynamic_resolvers =
|
|
704
|
+
this._static_targets = static_targets;
|
|
705
|
+
this._has_dynamic_resolvers = has_dynamic_resolvers;
|
|
706
706
|
this._cd = cd;
|
|
707
|
-
this._on_init =
|
|
707
|
+
this._on_init = on_init;
|
|
708
708
|
}
|
|
709
709
|
/** Last correlated event id. */
|
|
710
710
|
get checkpoint() {
|
|
@@ -715,7 +715,7 @@ var CorrelateCycle = class {
|
|
|
715
715
|
* - Reads max(at) from store as cold-start checkpoint
|
|
716
716
|
* - Subscribes static resolver targets (idempotent upsert)
|
|
717
717
|
* - Populates the subscribed-streams LRU
|
|
718
|
-
* - Fires `
|
|
718
|
+
* - Fires `on_init` once (Act uses this to flag a cold-start drain)
|
|
719
719
|
*/
|
|
720
720
|
async init() {
|
|
721
721
|
if (this._initialized) return;
|
|
@@ -748,15 +748,15 @@ var CorrelateCycle = class {
|
|
|
748
748
|
if (typeof reaction.resolver !== "function") continue;
|
|
749
749
|
const resolved = reaction.resolver(event);
|
|
750
750
|
if (resolved && !this._subscribed.has(resolved.target)) {
|
|
751
|
-
const
|
|
751
|
+
const incoming_priority = resolved.priority ?? 0;
|
|
752
752
|
const entry = correlated.get(resolved.target) || {
|
|
753
753
|
source: resolved.source,
|
|
754
|
-
priority:
|
|
754
|
+
priority: incoming_priority,
|
|
755
755
|
lane: resolved.lane,
|
|
756
756
|
payloads: []
|
|
757
757
|
};
|
|
758
|
-
if (
|
|
759
|
-
entry.priority =
|
|
758
|
+
if (incoming_priority > entry.priority)
|
|
759
|
+
entry.priority = incoming_priority;
|
|
760
760
|
entry.payloads.push({
|
|
761
761
|
...reaction,
|
|
762
762
|
source: resolved.source,
|
|
@@ -795,7 +795,7 @@ var CorrelateCycle = class {
|
|
|
795
795
|
* running. Errors from `correlate()` are routed through `log()` so they
|
|
796
796
|
* land in the configured logger (the timer keeps running on failure).
|
|
797
797
|
*/
|
|
798
|
-
|
|
798
|
+
start_polling(query = {}, frequency = 1e4, callback) {
|
|
799
799
|
if (this._timer) return false;
|
|
800
800
|
const limit = query.limit || 100;
|
|
801
801
|
this._timer = setInterval(
|
|
@@ -807,7 +807,7 @@ var CorrelateCycle = class {
|
|
|
807
807
|
return true;
|
|
808
808
|
}
|
|
809
809
|
/** Stop the periodic correlation worker. Idempotent. */
|
|
810
|
-
|
|
810
|
+
stop_polling() {
|
|
811
811
|
if (this._timer) {
|
|
812
812
|
clearInterval(this._timer);
|
|
813
813
|
this._timer = void 0;
|
|
@@ -823,14 +823,14 @@ var SEG_SPACE = BASE ** SEG_WIDTH;
|
|
|
823
823
|
function seg(n) {
|
|
824
824
|
return n.toString(BASE).padStart(SEG_WIDTH, "0");
|
|
825
825
|
}
|
|
826
|
-
var
|
|
826
|
+
var default_correlator = ({ state: state2, action: action2 }) => {
|
|
827
827
|
const s = state2.slice(0, SEG_WIDTH).toLowerCase();
|
|
828
828
|
const a = action2.slice(0, SEG_WIDTH).toLowerCase();
|
|
829
829
|
const ts = seg(Date.now() % SEG_SPACE);
|
|
830
830
|
const rnd = seg(randomInt(SEG_SPACE));
|
|
831
831
|
return `${s}-${a}-${ts}${rnd}`;
|
|
832
832
|
};
|
|
833
|
-
function
|
|
833
|
+
function close_correlation(correlator, actor) {
|
|
834
834
|
return correlator({
|
|
835
835
|
state: "$close",
|
|
836
836
|
action: "close",
|
|
@@ -846,7 +846,7 @@ import { randomUUID } from "crypto";
|
|
|
846
846
|
var RATIO_MIN = 0.2;
|
|
847
847
|
var RATIO_MAX = 0.8;
|
|
848
848
|
var RATIO_DEFAULT = 0.5;
|
|
849
|
-
function
|
|
849
|
+
function compute_lag_lead_ratio(handled, lagging, leading) {
|
|
850
850
|
let lagging_handled = 0;
|
|
851
851
|
let leading_handled = 0;
|
|
852
852
|
for (const { lease, handled: count } of handled) {
|
|
@@ -883,7 +883,7 @@ var subscribe = (streams) => store().subscribe(streams);
|
|
|
883
883
|
import { patch } from "@rotorsoft/act-patch";
|
|
884
884
|
|
|
885
885
|
// src/internal/backoff.ts
|
|
886
|
-
function
|
|
886
|
+
function compute_backoff_delay(retry, opts) {
|
|
887
887
|
if (!opts || opts.baseMs <= 0) return 0;
|
|
888
888
|
const r = Math.max(0, retry);
|
|
889
889
|
let delay;
|
|
@@ -1053,8 +1053,8 @@ async function scan(source, opts = {}, callback) {
|
|
|
1053
1053
|
};
|
|
1054
1054
|
}
|
|
1055
1055
|
async function load(me, stream, callback, asOf, actor) {
|
|
1056
|
-
const
|
|
1057
|
-
const cached =
|
|
1056
|
+
const time_travel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
|
|
1057
|
+
const cached = time_travel ? void 0 : await cache().get(stream);
|
|
1058
1058
|
const cache_hit = !!cached;
|
|
1059
1059
|
let state2 = cached?.state ?? (me.init ? me.init() : {});
|
|
1060
1060
|
let patches = cached?.patches ?? 0;
|
|
@@ -1097,7 +1097,7 @@ async function load(me, stream, callback, asOf, actor) {
|
|
|
1097
1097
|
...cached ? { after: cached.event_id } : { with_snaps: true, ...asOf }
|
|
1098
1098
|
}
|
|
1099
1099
|
);
|
|
1100
|
-
if (replayed > 0 && !
|
|
1100
|
+
if (replayed > 0 && !time_travel && event) {
|
|
1101
1101
|
await cache().set(stream, {
|
|
1102
1102
|
state: state2,
|
|
1103
1103
|
version,
|
|
@@ -1108,12 +1108,12 @@ async function load(me, stream, callback, asOf, actor) {
|
|
|
1108
1108
|
}
|
|
1109
1109
|
return { event, state: state2, version, patches, snaps, cache_hit, replayed };
|
|
1110
1110
|
}
|
|
1111
|
-
async function action(me, action2, target, payload, reactingTo, skipValidation = false, correlator =
|
|
1111
|
+
async function action(me, action2, target, payload, reactingTo, skipValidation = false, correlator = default_correlator) {
|
|
1112
1112
|
const { stream, expectedVersion, actor } = target;
|
|
1113
1113
|
if (!stream) throw new Error("Missing target stream");
|
|
1114
1114
|
const validated = skipValidation ? payload : validate(action2, payload, me.actions[action2]);
|
|
1115
1115
|
const opts = me.options?.[action2];
|
|
1116
|
-
const
|
|
1116
|
+
const max_retries = opts?.maxRetries ?? 0;
|
|
1117
1117
|
for (let attempt = 0; ; attempt++) {
|
|
1118
1118
|
try {
|
|
1119
1119
|
const snapshot = await load(
|
|
@@ -1231,10 +1231,10 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
1231
1231
|
return snapshots;
|
|
1232
1232
|
} catch (error) {
|
|
1233
1233
|
if (!(error instanceof ConcurrencyError)) throw error;
|
|
1234
|
-
if (attempt >=
|
|
1234
|
+
if (attempt >= max_retries) throw error;
|
|
1235
1235
|
if (opts?.backoff) {
|
|
1236
|
-
const
|
|
1237
|
-
if (
|
|
1236
|
+
const delay_ms = compute_backoff_delay(attempt, opts.backoff);
|
|
1237
|
+
if (delay_ms > 0) await sleep(delay_ms);
|
|
1238
1238
|
}
|
|
1239
1239
|
}
|
|
1240
1240
|
}
|
|
@@ -1258,12 +1258,12 @@ var C_STREAM = "\x1B[38;5;226m";
|
|
|
1258
1258
|
var dim = (text) => PRETTY ? `${C_DIM}${text}${C_RESET}` : text;
|
|
1259
1259
|
var hue = (color, text) => PRETTY ? `${color}${text}${C_RESET}` : text;
|
|
1260
1260
|
var drain_caption = (caption, lane) => {
|
|
1261
|
-
const
|
|
1261
|
+
const show_lane = lane && lane !== "default";
|
|
1262
1262
|
if (PRETTY) {
|
|
1263
1263
|
const tag = `${C_DRAIN}>> ${caption}${C_RESET}`;
|
|
1264
|
-
return
|
|
1264
|
+
return show_lane ? `${tag} ${C_LANE}${lane}${C_RESET}` : tag;
|
|
1265
1265
|
}
|
|
1266
|
-
return
|
|
1266
|
+
return show_lane ? `>> ${caption} ${lane}` : `>> ${caption}`;
|
|
1267
1267
|
};
|
|
1268
1268
|
var cache_marker = (hit) => {
|
|
1269
1269
|
const word = hit ? "hit" : "miss";
|
|
@@ -1292,10 +1292,10 @@ var traced = (inner, exit, entry) => (async (...args) => {
|
|
|
1292
1292
|
exit?.(result, ...args);
|
|
1293
1293
|
return result;
|
|
1294
1294
|
});
|
|
1295
|
-
function
|
|
1296
|
-
const bound_action = (me,
|
|
1295
|
+
function build_es(logger, correlator = default_correlator) {
|
|
1296
|
+
const bound_action = (me, action_name, target, payload, reactingTo, skipValidation = false) => action(
|
|
1297
1297
|
me,
|
|
1298
|
-
|
|
1298
|
+
action_name,
|
|
1299
1299
|
target,
|
|
1300
1300
|
payload,
|
|
1301
1301
|
reactingTo,
|
|
@@ -1365,7 +1365,7 @@ function buildEs(logger, correlator = defaultCorrelator) {
|
|
|
1365
1365
|
})
|
|
1366
1366
|
};
|
|
1367
1367
|
}
|
|
1368
|
-
function
|
|
1368
|
+
function build_drain(logger) {
|
|
1369
1369
|
return {
|
|
1370
1370
|
claim,
|
|
1371
1371
|
fetch,
|
|
@@ -1374,46 +1374,48 @@ function buildDrain(logger) {
|
|
|
1374
1374
|
subscribe: logger.level !== "trace" ? subscribe : traced(subscribe, (result, streams) => {
|
|
1375
1375
|
if (!result.subscribed) return;
|
|
1376
1376
|
const lanes = new Set(streams.map((s) => s.lane ?? "default"));
|
|
1377
|
-
const
|
|
1377
|
+
const uniform_lane = lanes.size === 1 ? streams[0]?.lane : void 0;
|
|
1378
1378
|
const data = streams.map(
|
|
1379
|
-
({ stream, lane }) =>
|
|
1379
|
+
({ stream, lane }) => uniform_lane || !lane || lane === "default" ? hue(C_STREAM, stream) : `${hue(C_STREAM, stream)}${dim(`[${lane}]`)}`
|
|
1380
1380
|
).join(" ");
|
|
1381
|
-
logger.trace(
|
|
1381
|
+
logger.trace(
|
|
1382
|
+
`${drain_caption("correlated", uniform_lane)} ${data}`
|
|
1383
|
+
);
|
|
1382
1384
|
})
|
|
1383
1385
|
};
|
|
1384
1386
|
}
|
|
1385
|
-
function
|
|
1387
|
+
function trace_cycle(logger, leased, fetched, handled, acked, blocked) {
|
|
1386
1388
|
if (logger.level !== "trace" || !leased.length) return;
|
|
1387
1389
|
const lane = leased[0]?.lane;
|
|
1388
|
-
const
|
|
1389
|
-
const
|
|
1390
|
-
const
|
|
1391
|
-
const
|
|
1390
|
+
const fetch_by_stream = new Map(fetched.map((f) => [f.stream, f]));
|
|
1391
|
+
const acked_by_stream = new Map(acked.map((a) => [a.stream, a.at]));
|
|
1392
|
+
const blocked_by_stream = new Map(blocked.map((b) => [b.stream, b.error]));
|
|
1393
|
+
const failed_by_stream = new Map(
|
|
1392
1394
|
handled.filter((h) => h.error).map((h) => [h.lease.stream, h])
|
|
1393
1395
|
);
|
|
1394
1396
|
const detail = leased.map(({ stream, at, retry }) => {
|
|
1395
|
-
const f =
|
|
1397
|
+
const f = fetch_by_stream.get(stream);
|
|
1396
1398
|
const key = f?.source ? `${hue(C_STREAM, stream)}${dim(`<-${f.source}`)}` : hue(C_STREAM, stream);
|
|
1397
1399
|
const events = f && f.events.length ? ` ${dim(
|
|
1398
1400
|
`[${f.events.map(({ id, name }) => `#${id} ${String(name)}`).join(", ")}]`
|
|
1399
1401
|
)}` : "";
|
|
1400
|
-
const
|
|
1401
|
-
const
|
|
1402
|
-
const failure =
|
|
1403
|
-
let
|
|
1402
|
+
const acked_at = acked_by_stream.get(stream);
|
|
1403
|
+
const ack_part = acked_at !== void 0 ? hue(C_HIT, `\u2713 @${acked_at}`) : "";
|
|
1404
|
+
const failure = failed_by_stream.get(stream);
|
|
1405
|
+
let fail_part = "";
|
|
1404
1406
|
if (failure) {
|
|
1405
|
-
const
|
|
1406
|
-
const
|
|
1407
|
-
if (
|
|
1408
|
-
|
|
1407
|
+
const failed_at = failure.failed_at ?? at;
|
|
1408
|
+
const blocked_error = blocked_by_stream.get(stream);
|
|
1409
|
+
if (blocked_error !== void 0) {
|
|
1410
|
+
fail_part = `${hue(C_ERR, `\u2717 @${failed_at}/${retry}`)} ${dim(`(${blocked_error})`)}`;
|
|
1409
1411
|
} else {
|
|
1410
|
-
|
|
1412
|
+
fail_part = `${hue(C_MISS, `\u26A0 @${failed_at}/${retry}`)} ${dim(`(${failure.error})`)}`;
|
|
1411
1413
|
}
|
|
1412
1414
|
}
|
|
1413
1415
|
let tail;
|
|
1414
|
-
if (
|
|
1415
|
-
else if (
|
|
1416
|
-
else if (
|
|
1416
|
+
if (ack_part && fail_part) tail = ` ${ack_part} ${fail_part}`;
|
|
1417
|
+
else if (ack_part) tail = ` ${ack_part}`;
|
|
1418
|
+
else if (fail_part) tail = ` ${fail_part}`;
|
|
1417
1419
|
else tail = ` ${dim(`\u2298 @${at}/${retry}`)}`;
|
|
1418
1420
|
return `${key}${events}${tail}`;
|
|
1419
1421
|
}).join(", ");
|
|
@@ -1421,7 +1423,7 @@ function traceCycle(logger, leased, fetched, handled, acked, blocked) {
|
|
|
1421
1423
|
}
|
|
1422
1424
|
|
|
1423
1425
|
// src/internal/drain-cycle.ts
|
|
1424
|
-
async function
|
|
1426
|
+
async function run_drain_cycle(ops, registry, batch_handlers, handle, handle_batch, lagging, leading, eventLimit, leaseMillis, is_deferred, lane) {
|
|
1425
1427
|
const leased = await ops.claim(
|
|
1426
1428
|
lagging,
|
|
1427
1429
|
leading,
|
|
@@ -1430,7 +1432,7 @@ async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch,
|
|
|
1430
1432
|
lane
|
|
1431
1433
|
);
|
|
1432
1434
|
if (!leased.length) return void 0;
|
|
1433
|
-
const active =
|
|
1435
|
+
const active = is_deferred ? leased.filter((l) => !is_deferred(l.stream)) : leased;
|
|
1434
1436
|
if (!active.length) {
|
|
1435
1437
|
return {
|
|
1436
1438
|
leased,
|
|
@@ -1441,7 +1443,7 @@ async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch,
|
|
|
1441
1443
|
};
|
|
1442
1444
|
}
|
|
1443
1445
|
const fetched = await ops.fetch(active, eventLimit);
|
|
1444
|
-
const
|
|
1446
|
+
const fetch_map = /* @__PURE__ */ new Map();
|
|
1445
1447
|
const fetch_window_at = fetched.reduce(
|
|
1446
1448
|
(max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
|
|
1447
1449
|
0
|
|
@@ -1456,16 +1458,16 @@ async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch,
|
|
|
1456
1458
|
return resolved && resolved.target === stream;
|
|
1457
1459
|
}).map((reaction) => ({ ...reaction, event }));
|
|
1458
1460
|
});
|
|
1459
|
-
|
|
1461
|
+
fetch_map.set(stream, { fetch: f, payloads });
|
|
1460
1462
|
}
|
|
1461
1463
|
const handled = await Promise.all(
|
|
1462
1464
|
active.map((lease) => {
|
|
1463
|
-
const entry =
|
|
1465
|
+
const entry = fetch_map.get(lease.stream);
|
|
1464
1466
|
const at = entry.fetch.events.at(-1)?.id || fetch_window_at;
|
|
1465
1467
|
const { payloads } = entry;
|
|
1466
|
-
const batchHandler =
|
|
1468
|
+
const batchHandler = batch_handlers.get(lease.stream);
|
|
1467
1469
|
if (batchHandler && payloads.length > 0) {
|
|
1468
|
-
return
|
|
1470
|
+
return handle_batch({ ...lease, at }, payloads, batchHandler);
|
|
1469
1471
|
}
|
|
1470
1472
|
return handle({ ...lease, at }, payloads);
|
|
1471
1473
|
})
|
|
@@ -1489,14 +1491,14 @@ var DrainController = class {
|
|
|
1489
1491
|
_locked = false;
|
|
1490
1492
|
_ratio = 0.5;
|
|
1491
1493
|
/**
|
|
1492
|
-
* Per-stream backoff: `stream →
|
|
1493
|
-
* `_finalize` via `HandleResult.
|
|
1494
|
+
* Per-stream backoff: `stream → next_attempt_at` (ms since epoch). Set by
|
|
1495
|
+
* `_finalize` via `HandleResult.next_attempt_at`; cleared on successful
|
|
1494
1496
|
* ack or terminal block. Lives in process memory — per-worker pacing
|
|
1495
1497
|
* by design (see {@link BackoffOptions} for the multi-worker trade-off).
|
|
1496
1498
|
*/
|
|
1497
1499
|
_backoff = /* @__PURE__ */ new Map();
|
|
1498
|
-
/** Timer re-arming drain at the earliest pending `
|
|
1499
|
-
|
|
1500
|
+
/** Timer re-arming drain at the earliest pending `next_attempt_at`. */
|
|
1501
|
+
_backoff_timer;
|
|
1500
1502
|
/** Worker timer (ACT-1103). Set when `start()` is active, undefined otherwise. */
|
|
1501
1503
|
_worker;
|
|
1502
1504
|
_stopped = false;
|
|
@@ -1517,7 +1519,7 @@ var DrainController = class {
|
|
|
1517
1519
|
return this._armed;
|
|
1518
1520
|
}
|
|
1519
1521
|
/** Returns true when `stream` is currently within a backoff window. */
|
|
1520
|
-
|
|
1522
|
+
is_deferred = (stream) => {
|
|
1521
1523
|
const next = this._backoff.get(stream);
|
|
1522
1524
|
return next !== void 0 && next > Date.now();
|
|
1523
1525
|
};
|
|
@@ -1527,20 +1529,20 @@ var DrainController = class {
|
|
|
1527
1529
|
* Idempotent — collapses many simultaneously deferred streams into a
|
|
1528
1530
|
* single timer.
|
|
1529
1531
|
*/
|
|
1530
|
-
|
|
1531
|
-
if (this.
|
|
1532
|
+
schedule_backoff_wake() {
|
|
1533
|
+
if (this._backoff_timer) clearTimeout(this._backoff_timer);
|
|
1532
1534
|
let earliest = Number.POSITIVE_INFINITY;
|
|
1533
1535
|
for (const t of this._backoff.values()) if (t < earliest) earliest = t;
|
|
1534
1536
|
const delay = Math.max(0, earliest - Date.now());
|
|
1535
|
-
this.
|
|
1536
|
-
this.
|
|
1537
|
+
this._backoff_timer = setTimeout(() => {
|
|
1538
|
+
this._backoff_timer = void 0;
|
|
1537
1539
|
const now = Date.now();
|
|
1538
1540
|
for (const [stream, at] of this._backoff) {
|
|
1539
1541
|
if (at <= now) this._backoff.delete(stream);
|
|
1540
1542
|
}
|
|
1541
1543
|
this._armed = true;
|
|
1542
1544
|
}, delay);
|
|
1543
|
-
this.
|
|
1545
|
+
this._backoff_timer.unref();
|
|
1544
1546
|
}
|
|
1545
1547
|
/** Lane this controller drains (undefined = legacy single-lane span). */
|
|
1546
1548
|
get lane() {
|
|
@@ -1586,17 +1588,17 @@ var DrainController = class {
|
|
|
1586
1588
|
this._locked = true;
|
|
1587
1589
|
const lagging = Math.ceil(streamLimit * this._ratio);
|
|
1588
1590
|
const leading = streamLimit - lagging;
|
|
1589
|
-
const cycle = await
|
|
1591
|
+
const cycle = await run_drain_cycle(
|
|
1590
1592
|
this._deps.ops,
|
|
1591
1593
|
this._deps.registry,
|
|
1592
|
-
this._deps.
|
|
1594
|
+
this._deps.batch_handlers,
|
|
1593
1595
|
this._deps.handle,
|
|
1594
|
-
this._deps.
|
|
1596
|
+
this._deps.handle_batch,
|
|
1595
1597
|
lagging,
|
|
1596
1598
|
leading,
|
|
1597
1599
|
eventLimit,
|
|
1598
1600
|
leaseMillis,
|
|
1599
|
-
this._backoff.size > 0 ? this.
|
|
1601
|
+
this._backoff.size > 0 ? this.is_deferred : void 0,
|
|
1600
1602
|
this._deps.lane
|
|
1601
1603
|
);
|
|
1602
1604
|
if (!cycle) {
|
|
@@ -1604,20 +1606,20 @@ var DrainController = class {
|
|
|
1604
1606
|
return EMPTY_DRAIN;
|
|
1605
1607
|
}
|
|
1606
1608
|
const { leased, fetched, handled, acked, blocked } = cycle;
|
|
1607
|
-
|
|
1608
|
-
this._ratio =
|
|
1609
|
+
trace_cycle(this._deps.logger, leased, fetched, handled, acked, blocked);
|
|
1610
|
+
this._ratio = compute_lag_lead_ratio(handled, lagging, leading);
|
|
1609
1611
|
for (const lease of acked) this._backoff.delete(lease.stream);
|
|
1610
1612
|
for (const lease of blocked) this._backoff.delete(lease.stream);
|
|
1611
1613
|
for (const h of handled) {
|
|
1612
|
-
if (h.
|
|
1613
|
-
this._backoff.set(h.lease.stream, h.
|
|
1614
|
+
if (h.next_attempt_at !== void 0 && !h.block) {
|
|
1615
|
+
this._backoff.set(h.lease.stream, h.next_attempt_at);
|
|
1614
1616
|
}
|
|
1615
1617
|
}
|
|
1616
|
-
if (this._backoff.size > 0) this.
|
|
1617
|
-
if (acked.length) this._deps.
|
|
1618
|
-
if (blocked.length) this._deps.
|
|
1619
|
-
const
|
|
1620
|
-
if (!acked.length && !blocked.length && !
|
|
1618
|
+
if (this._backoff.size > 0) this.schedule_backoff_wake();
|
|
1619
|
+
if (acked.length) this._deps.on_acked(acked);
|
|
1620
|
+
if (blocked.length) this._deps.on_blocked(blocked);
|
|
1621
|
+
const has_errors = handled.some(({ error }) => error);
|
|
1622
|
+
if (!acked.length && !blocked.length && !has_errors) this._armed = false;
|
|
1621
1623
|
return { fetched, leased, acked, blocked };
|
|
1622
1624
|
} catch (error) {
|
|
1623
1625
|
this._deps.logger.error(error);
|
|
@@ -1630,44 +1632,44 @@ var DrainController = class {
|
|
|
1630
1632
|
|
|
1631
1633
|
// src/internal/merge.ts
|
|
1632
1634
|
import { ZodObject } from "zod";
|
|
1633
|
-
function
|
|
1635
|
+
function base_type_name(zodType) {
|
|
1634
1636
|
let t = zodType;
|
|
1635
1637
|
while (typeof t.unwrap === "function") {
|
|
1636
1638
|
t = t.unwrap();
|
|
1637
1639
|
}
|
|
1638
1640
|
return t.constructor.name;
|
|
1639
1641
|
}
|
|
1640
|
-
function
|
|
1642
|
+
function merge_schemas(existing, incoming, state_name) {
|
|
1641
1643
|
if (existing instanceof ZodObject && incoming instanceof ZodObject) {
|
|
1642
|
-
const
|
|
1643
|
-
const
|
|
1644
|
-
for (const key of Object.keys(
|
|
1645
|
-
if (key in
|
|
1646
|
-
const
|
|
1647
|
-
const
|
|
1648
|
-
if (
|
|
1644
|
+
const existing_shape = existing.shape;
|
|
1645
|
+
const incoming_shape = incoming.shape;
|
|
1646
|
+
for (const key of Object.keys(incoming_shape)) {
|
|
1647
|
+
if (key in existing_shape) {
|
|
1648
|
+
const existing_base = base_type_name(existing_shape[key]);
|
|
1649
|
+
const incoming_base = base_type_name(incoming_shape[key]);
|
|
1650
|
+
if (existing_base !== incoming_base) {
|
|
1649
1651
|
throw new Error(
|
|
1650
|
-
`Schema conflict in "${
|
|
1652
|
+
`Schema conflict in "${state_name}": key "${key}" has type "${existing_base}" but incoming partial declares "${incoming_base}"`
|
|
1651
1653
|
);
|
|
1652
1654
|
}
|
|
1653
1655
|
}
|
|
1654
1656
|
}
|
|
1655
|
-
return existing.extend(
|
|
1657
|
+
return existing.extend(incoming_shape);
|
|
1656
1658
|
}
|
|
1657
1659
|
return existing;
|
|
1658
1660
|
}
|
|
1659
|
-
function
|
|
1661
|
+
function merge_inits(existing, incoming) {
|
|
1660
1662
|
return () => ({ ...existing(), ...incoming() });
|
|
1661
1663
|
}
|
|
1662
|
-
function
|
|
1664
|
+
function register_state(state2, states, actions, events) {
|
|
1663
1665
|
const existing = states.get(state2.name);
|
|
1664
1666
|
if (existing) {
|
|
1665
|
-
|
|
1667
|
+
merge_into_existing(state2, existing, states, actions, events);
|
|
1666
1668
|
} else {
|
|
1667
|
-
|
|
1669
|
+
register_new_state(state2, states, actions, events);
|
|
1668
1670
|
}
|
|
1669
1671
|
}
|
|
1670
|
-
function
|
|
1672
|
+
function register_new_state(state2, states, actions, events) {
|
|
1671
1673
|
states.set(state2.name, state2);
|
|
1672
1674
|
for (const name of Object.keys(state2.actions)) {
|
|
1673
1675
|
if (actions[name]) throw new Error(`Duplicate action "${name}"`);
|
|
@@ -1678,7 +1680,7 @@ function registerNewState(state2, states, actions, events) {
|
|
|
1678
1680
|
events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
|
|
1679
1681
|
}
|
|
1680
1682
|
}
|
|
1681
|
-
function
|
|
1683
|
+
function merge_into_existing(state2, existing, states, actions, events) {
|
|
1682
1684
|
for (const name of Object.keys(state2.actions)) {
|
|
1683
1685
|
if (existing.actions[name] === state2.actions[name]) continue;
|
|
1684
1686
|
if (actions[name]) throw new Error(`Duplicate action "${name}"`);
|
|
@@ -1692,14 +1694,14 @@ function mergeIntoExisting(state2, existing, states, actions, events) {
|
|
|
1692
1694
|
}
|
|
1693
1695
|
if (events[name]) throw new Error(`Duplicate event "${name}"`);
|
|
1694
1696
|
}
|
|
1695
|
-
const
|
|
1697
|
+
const merged_patch = merge_patches(existing.patch, state2.patch, state2.name);
|
|
1696
1698
|
const merged = {
|
|
1697
1699
|
...existing,
|
|
1698
|
-
state:
|
|
1699
|
-
init:
|
|
1700
|
+
state: merge_schemas(existing.state, state2.state, state2.name),
|
|
1701
|
+
init: merge_inits(existing.init, state2.init),
|
|
1700
1702
|
events: { ...existing.events, ...state2.events },
|
|
1701
1703
|
actions: { ...existing.actions, ...state2.actions },
|
|
1702
|
-
patch:
|
|
1704
|
+
patch: merged_patch,
|
|
1703
1705
|
on: { ...existing.on, ...state2.on },
|
|
1704
1706
|
given: { ...existing.given, ...state2.given },
|
|
1705
1707
|
snap: state2.snap && existing.snap && state2.snap !== existing.snap ? (() => {
|
|
@@ -1717,48 +1719,48 @@ function mergeIntoExisting(state2, existing, states, actions, events) {
|
|
|
1717
1719
|
events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
|
|
1718
1720
|
}
|
|
1719
1721
|
}
|
|
1720
|
-
function
|
|
1722
|
+
function merge_patches(existing, incoming, state_name) {
|
|
1721
1723
|
const merged = { ...existing };
|
|
1722
1724
|
for (const name of Object.keys(incoming)) {
|
|
1723
|
-
const
|
|
1724
|
-
const
|
|
1725
|
-
if (!
|
|
1726
|
-
merged[name] =
|
|
1725
|
+
const existing_p = existing[name];
|
|
1726
|
+
const incoming_p = incoming[name];
|
|
1727
|
+
if (!existing_p) {
|
|
1728
|
+
merged[name] = incoming_p;
|
|
1727
1729
|
continue;
|
|
1728
1730
|
}
|
|
1729
|
-
const
|
|
1730
|
-
const
|
|
1731
|
-
if (!
|
|
1731
|
+
const existing_is_default = existing_p._passthrough;
|
|
1732
|
+
const incoming_is_default = incoming_p._passthrough;
|
|
1733
|
+
if (!existing_is_default && !incoming_is_default && existing_p !== incoming_p) {
|
|
1732
1734
|
throw new Error(
|
|
1733
|
-
`Duplicate custom patch for event "${name}" in state "${
|
|
1735
|
+
`Duplicate custom patch for event "${name}" in state "${state_name}"`
|
|
1734
1736
|
);
|
|
1735
1737
|
}
|
|
1736
|
-
if (
|
|
1737
|
-
merged[name] =
|
|
1738
|
+
if (existing_is_default && !incoming_is_default) {
|
|
1739
|
+
merged[name] = incoming_p;
|
|
1738
1740
|
}
|
|
1739
1741
|
}
|
|
1740
1742
|
return merged;
|
|
1741
1743
|
}
|
|
1742
|
-
function
|
|
1743
|
-
for (const [
|
|
1744
|
-
const
|
|
1745
|
-
if (!
|
|
1746
|
-
for (const [name, reaction] of
|
|
1747
|
-
|
|
1744
|
+
function merge_event_register(target, source) {
|
|
1745
|
+
for (const [event_name, source_reg] of Object.entries(source)) {
|
|
1746
|
+
const target_reg = target[event_name];
|
|
1747
|
+
if (!target_reg) continue;
|
|
1748
|
+
for (const [name, reaction] of source_reg.reactions) {
|
|
1749
|
+
target_reg.reactions.set(name, reaction);
|
|
1748
1750
|
}
|
|
1749
1751
|
}
|
|
1750
1752
|
}
|
|
1751
|
-
function
|
|
1752
|
-
for (const
|
|
1753
|
-
const
|
|
1754
|
-
const existing = events[
|
|
1753
|
+
function merge_projection(proj, events) {
|
|
1754
|
+
for (const event_name of Object.keys(proj.events)) {
|
|
1755
|
+
const proj_register = proj.events[event_name];
|
|
1756
|
+
const existing = events[event_name];
|
|
1755
1757
|
if (!existing) {
|
|
1756
|
-
events[
|
|
1757
|
-
schema:
|
|
1758
|
-
reactions: new Map(
|
|
1758
|
+
events[event_name] = {
|
|
1759
|
+
schema: proj_register.schema,
|
|
1760
|
+
reactions: new Map(proj_register.reactions)
|
|
1759
1761
|
};
|
|
1760
1762
|
} else {
|
|
1761
|
-
for (const [name, reaction] of
|
|
1763
|
+
for (const [name, reaction] of proj_register.reactions) {
|
|
1762
1764
|
let key = name;
|
|
1763
1765
|
while (existing.reactions.has(key)) key = `${key}_p`;
|
|
1764
1766
|
existing.reactions.set(key, reaction);
|
|
@@ -1775,24 +1777,24 @@ var _this_ = ({ stream }) => ({
|
|
|
1775
1777
|
function finalize(lease, handled, at, error, options, logger, failed_at) {
|
|
1776
1778
|
if (!error) return { lease, handled, acked_at: at };
|
|
1777
1779
|
logger.error(error);
|
|
1778
|
-
const
|
|
1779
|
-
const block2 = options.blockOnError && (
|
|
1780
|
+
const non_retryable = error instanceof NonRetryableError;
|
|
1781
|
+
const block2 = options.blockOnError && (non_retryable || lease.retry >= options.maxRetries);
|
|
1780
1782
|
if (block2)
|
|
1781
1783
|
logger.error(
|
|
1782
|
-
|
|
1784
|
+
non_retryable ? `Blocking ${lease.stream} on non-retryable error.` : `Blocking ${lease.stream} after ${lease.retry} retries.`
|
|
1783
1785
|
);
|
|
1784
|
-
const
|
|
1786
|
+
const next_attempt_at = !block2 && options.backoff ? Date.now() + compute_backoff_delay(lease.retry, options.backoff) : void 0;
|
|
1785
1787
|
return {
|
|
1786
1788
|
lease,
|
|
1787
1789
|
handled,
|
|
1788
1790
|
acked_at: at,
|
|
1789
1791
|
error: error.message,
|
|
1790
1792
|
block: block2,
|
|
1791
|
-
|
|
1793
|
+
next_attempt_at,
|
|
1792
1794
|
failed_at
|
|
1793
1795
|
};
|
|
1794
1796
|
}
|
|
1795
|
-
function
|
|
1797
|
+
function build_handle(deps) {
|
|
1796
1798
|
const {
|
|
1797
1799
|
logger,
|
|
1798
1800
|
bound_do,
|
|
@@ -1808,7 +1810,7 @@ function buildHandle(deps) {
|
|
|
1808
1810
|
let handled = 0;
|
|
1809
1811
|
if (lease.retry > 0)
|
|
1810
1812
|
logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
|
|
1811
|
-
const
|
|
1813
|
+
const scoped_app = {
|
|
1812
1814
|
do: bound_do,
|
|
1813
1815
|
load: bound_load,
|
|
1814
1816
|
query: bound_query,
|
|
@@ -1817,15 +1819,15 @@ function buildHandle(deps) {
|
|
|
1817
1819
|
};
|
|
1818
1820
|
for (const payload of payloads) {
|
|
1819
1821
|
const { event, handler } = payload;
|
|
1820
|
-
|
|
1822
|
+
scoped_app.do = (action2, target, action_payload, reactingTo, skipValidation) => bound_do(
|
|
1821
1823
|
action2,
|
|
1822
1824
|
target,
|
|
1823
|
-
|
|
1825
|
+
action_payload,
|
|
1824
1826
|
reactingTo ?? event,
|
|
1825
1827
|
skipValidation
|
|
1826
1828
|
);
|
|
1827
1829
|
try {
|
|
1828
|
-
await handler(event, stream,
|
|
1830
|
+
await handler(event, stream, scoped_app);
|
|
1829
1831
|
at = event.id;
|
|
1830
1832
|
handled++;
|
|
1831
1833
|
} catch (error) {
|
|
@@ -1843,7 +1845,7 @@ function buildHandle(deps) {
|
|
|
1843
1845
|
return finalize(lease, handled, at, void 0, payloads[0].options, logger);
|
|
1844
1846
|
};
|
|
1845
1847
|
}
|
|
1846
|
-
function
|
|
1848
|
+
function build_handle_batch(logger) {
|
|
1847
1849
|
return async (lease, payloads, batchHandler) => {
|
|
1848
1850
|
const stream = lease.stream;
|
|
1849
1851
|
const events = payloads.map(
|
|
@@ -1875,23 +1877,23 @@ var SettleLoop = class {
|
|
|
1875
1877
|
_deps;
|
|
1876
1878
|
/** Debounce window applied when the caller doesn't override via `SettleOptions.debounceMs`. */
|
|
1877
1879
|
_default_debounce_ms;
|
|
1878
|
-
constructor(deps,
|
|
1880
|
+
constructor(deps, default_debounce_ms) {
|
|
1879
1881
|
this._deps = deps;
|
|
1880
|
-
this._default_debounce_ms =
|
|
1882
|
+
this._default_debounce_ms = default_debounce_ms;
|
|
1881
1883
|
}
|
|
1882
1884
|
/**
|
|
1883
1885
|
* Schedule a settle pass. Multiple calls inside the debounce window
|
|
1884
1886
|
* coalesce into one cycle. The cycle runs correlate→drain in a loop
|
|
1885
1887
|
* until no progress is made (no new subscriptions, no acks, no blocks)
|
|
1886
1888
|
* or `maxPasses` is reached, then emits the `"settled"` lifecycle event
|
|
1887
|
-
* via {@link SettleDeps.
|
|
1889
|
+
* via {@link SettleDeps.on_settled}.
|
|
1888
1890
|
*/
|
|
1889
1891
|
schedule(options = {}) {
|
|
1890
1892
|
const {
|
|
1891
1893
|
debounceMs = this._default_debounce_ms,
|
|
1892
|
-
correlate:
|
|
1894
|
+
correlate: correlate_query = { after: -1, limit: 100 },
|
|
1893
1895
|
maxPasses = Infinity,
|
|
1894
|
-
...
|
|
1896
|
+
...drain_options
|
|
1895
1897
|
} = options;
|
|
1896
1898
|
if (this._timer) clearTimeout(this._timer);
|
|
1897
1899
|
this._timer = setTimeout(() => {
|
|
@@ -1900,17 +1902,17 @@ var SettleLoop = class {
|
|
|
1900
1902
|
this._running = true;
|
|
1901
1903
|
(async () => {
|
|
1902
1904
|
await this._deps.init();
|
|
1903
|
-
let
|
|
1905
|
+
let last_drain;
|
|
1904
1906
|
for (let i = 0; i < maxPasses; i++) {
|
|
1905
1907
|
const { subscribed } = await this._deps.correlate({
|
|
1906
|
-
...
|
|
1908
|
+
...correlate_query,
|
|
1907
1909
|
after: this._deps.checkpoint()
|
|
1908
1910
|
});
|
|
1909
|
-
|
|
1910
|
-
const made_progress = subscribed > 0 ||
|
|
1911
|
+
last_drain = await this._deps.drain(drain_options);
|
|
1912
|
+
const made_progress = subscribed > 0 || last_drain.acked.length > 0 || last_drain.blocked.length > 0;
|
|
1911
1913
|
if (!made_progress) break;
|
|
1912
1914
|
}
|
|
1913
|
-
if (
|
|
1915
|
+
if (last_drain) this._deps.on_settled(last_drain);
|
|
1914
1916
|
})().catch((err) => this._deps.logger.error(err)).finally(() => {
|
|
1915
1917
|
this._running = false;
|
|
1916
1918
|
});
|
|
@@ -1997,7 +1999,7 @@ var Act = class {
|
|
|
1997
1999
|
_event_to_state;
|
|
1998
2000
|
/**
|
|
1999
2001
|
* Event-name → lane fan-in for selective arming (ACT-1103). Built by
|
|
2000
|
-
* `
|
|
2002
|
+
* `classify_registry` once per build. `"all"` means at least one of
|
|
2001
2003
|
* the event's reactions is a dynamic resolver (lane opaque until
|
|
2002
2004
|
* runtime); a `Set<string>` lists the static lanes only that event's
|
|
2003
2005
|
* reactions target.
|
|
@@ -2020,9 +2022,9 @@ var Act = class {
|
|
|
2020
2022
|
_scoped;
|
|
2021
2023
|
/**
|
|
2022
2024
|
* Correlation-id generator for originating actions. Bound at
|
|
2023
|
-
* construction from `options.correlator ??
|
|
2025
|
+
* construction from `options.correlator ?? default_correlator`. The
|
|
2024
2026
|
* `do()` path passes this into the `_es.action` closure; close-cycle
|
|
2025
|
-
* uses it via {@link
|
|
2027
|
+
* uses it via {@link close_correlation}.
|
|
2026
2028
|
*/
|
|
2027
2029
|
_correlator;
|
|
2028
2030
|
/** Pre-bound IAct methods reused across drain cycles. Only `do` varies per
|
|
@@ -2032,7 +2034,7 @@ var Act = class {
|
|
|
2032
2034
|
_bound_query = this.query.bind(this);
|
|
2033
2035
|
_bound_query_array = this.query_array.bind(this);
|
|
2034
2036
|
_bound_forget = this.forget.bind(this);
|
|
2035
|
-
/** Reaction dispatchers built once and handed to
|
|
2037
|
+
/** Reaction dispatchers built once and handed to run_drain_cycle each cycle. */
|
|
2036
2038
|
_handle;
|
|
2037
2039
|
_handle_batch;
|
|
2038
2040
|
/** Declared drain lanes (ACT-1103). */
|
|
@@ -2049,16 +2051,16 @@ var Act = class {
|
|
|
2049
2051
|
*
|
|
2050
2052
|
* @param registry Schemas for every event and action across registered states
|
|
2051
2053
|
* @param states Merged map of state name → state definition
|
|
2052
|
-
* @param
|
|
2054
|
+
* @param batch_handlers Static-target projection batch handlers (target → handler)
|
|
2053
2055
|
* @param options Tuning knobs — see {@link ActOptions}
|
|
2054
2056
|
* @param lanes Declared drain lanes (ACT-1103). The builder collects
|
|
2055
2057
|
* these from `.withLane(...)` calls. Slice 1 records them on the
|
|
2056
2058
|
* instance; later slices fan out one `DrainController` per lane.
|
|
2057
2059
|
*/
|
|
2058
|
-
constructor(registry, states = /* @__PURE__ */ new Map(),
|
|
2060
|
+
constructor(registry, states = /* @__PURE__ */ new Map(), batch_handlers = /* @__PURE__ */ new Map(), options = {}, lanes = []) {
|
|
2059
2061
|
this.registry = registry;
|
|
2060
2062
|
this._states = states;
|
|
2061
|
-
this._batch_handlers =
|
|
2063
|
+
this._batch_handlers = batch_handlers;
|
|
2062
2064
|
this._lanes = lanes;
|
|
2063
2065
|
if (options.onlyLanes && options.onlyLanes.length > 0) {
|
|
2064
2066
|
const declared = /* @__PURE__ */ new Set([
|
|
@@ -2072,10 +2074,10 @@ var Act = class {
|
|
|
2072
2074
|
);
|
|
2073
2075
|
}
|
|
2074
2076
|
this._scoped = options.scoped ? (fn) => scoped.run(options.scoped, fn) : (fn) => fn();
|
|
2075
|
-
this._correlator = options.correlator ??
|
|
2076
|
-
this._es =
|
|
2077
|
-
this._cd =
|
|
2078
|
-
this._handle =
|
|
2077
|
+
this._correlator = options.correlator ?? default_correlator;
|
|
2078
|
+
this._es = build_es(this._logger, this._correlator);
|
|
2079
|
+
this._cd = build_drain(this._logger);
|
|
2080
|
+
this._handle = build_handle({
|
|
2079
2081
|
logger: this._logger,
|
|
2080
2082
|
bound_do: this._bound_do,
|
|
2081
2083
|
bound_load: this._bound_load,
|
|
@@ -2083,39 +2085,39 @@ var Act = class {
|
|
|
2083
2085
|
bound_query_array: this._bound_query_array,
|
|
2084
2086
|
bound_forget: this._bound_forget
|
|
2085
2087
|
});
|
|
2086
|
-
this._handle_batch =
|
|
2088
|
+
this._handle_batch = build_handle_batch(this._logger);
|
|
2087
2089
|
const {
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
} =
|
|
2094
|
-
this._reactive_events =
|
|
2090
|
+
static_targets,
|
|
2091
|
+
has_dynamic_resolvers,
|
|
2092
|
+
reactive_events,
|
|
2093
|
+
event_to_state,
|
|
2094
|
+
event_to_lanes
|
|
2095
|
+
} = classify_registry(this.registry, this._states);
|
|
2096
|
+
this._reactive_events = reactive_events;
|
|
2095
2097
|
this._listen = options.listen !== false;
|
|
2096
2098
|
this._drain = options.drain !== false;
|
|
2097
|
-
this._event_to_state =
|
|
2098
|
-
this._event_to_lanes =
|
|
2099
|
-
const
|
|
2100
|
-
const
|
|
2101
|
-
const
|
|
2102
|
-
const
|
|
2099
|
+
this._event_to_state = event_to_state;
|
|
2100
|
+
this._event_to_lanes = event_to_lanes;
|
|
2101
|
+
const all_lanes = ["default", ...lanes.map((l) => l.name)];
|
|
2102
|
+
const only_set = options.onlyLanes && options.onlyLanes.length > 0 ? new Set(options.onlyLanes) : void 0;
|
|
2103
|
+
const active_lanes = only_set ? all_lanes.filter((n) => only_set.has(n)) : all_lanes;
|
|
2104
|
+
const single_default_lane = active_lanes.length === 1 && active_lanes[0] === "default";
|
|
2103
2105
|
this._drain_controllers = /* @__PURE__ */ new Map();
|
|
2104
|
-
for (const name of
|
|
2106
|
+
for (const name of active_lanes) {
|
|
2105
2107
|
const cfg = lanes.find((l) => l.name === name);
|
|
2106
2108
|
const controller = new DrainController({
|
|
2107
2109
|
logger: this._logger,
|
|
2108
2110
|
ops: this._cd,
|
|
2109
2111
|
registry: this.registry,
|
|
2110
|
-
|
|
2112
|
+
batch_handlers: this._batch_handlers,
|
|
2111
2113
|
handle: this._handle,
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2114
|
+
handle_batch: this._handle_batch,
|
|
2115
|
+
on_acked: (acked) => this.emit("acked", acked),
|
|
2116
|
+
on_blocked: (blocked) => this.emit("blocked", blocked),
|
|
2115
2117
|
// Pass lane only when a true per-lane controller is active.
|
|
2116
2118
|
// The all-lanes (single default) case keeps lane=undefined so
|
|
2117
2119
|
// adapter SQL collapses to the pre-1103 shape.
|
|
2118
|
-
lane:
|
|
2120
|
+
lane: single_default_lane ? void 0 : name,
|
|
2119
2121
|
defaults: cfg && {
|
|
2120
2122
|
streamLimit: cfg.streamLimit,
|
|
2121
2123
|
leaseMillis: cfg.leaseMillis
|
|
@@ -2128,22 +2130,22 @@ var Act = class {
|
|
|
2128
2130
|
this._audit_deps = {
|
|
2129
2131
|
store,
|
|
2130
2132
|
logger: this._logger,
|
|
2131
|
-
event_to_state
|
|
2133
|
+
event_to_state,
|
|
2132
2134
|
states: this._states,
|
|
2133
|
-
known_events: new Set(
|
|
2135
|
+
known_events: new Set(event_to_state.keys()),
|
|
2134
2136
|
declared_lanes: new Set(this._drain_controllers.keys()),
|
|
2135
|
-
routed_events: new Set(
|
|
2137
|
+
routed_events: new Set(event_to_lanes.keys())
|
|
2136
2138
|
};
|
|
2137
2139
|
this._correlate = new CorrelateCycle(
|
|
2138
2140
|
this.registry,
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
+
static_targets,
|
|
2142
|
+
has_dynamic_resolvers,
|
|
2141
2143
|
this._cd,
|
|
2142
2144
|
options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
2143
2145
|
// Cold start: assume drain is needed (historical events may need processing).
|
|
2144
2146
|
// #803: writer-only instances skip the cold-start arm.
|
|
2145
2147
|
() => {
|
|
2146
|
-
if (this._drain && this._reactive_events.size > 0) this.
|
|
2148
|
+
if (this._drain && this._reactive_events.size > 0) this._arm_all();
|
|
2147
2149
|
}
|
|
2148
2150
|
);
|
|
2149
2151
|
this._settle = new SettleLoop(
|
|
@@ -2153,11 +2155,11 @@ var Act = class {
|
|
|
2153
2155
|
checkpoint: () => this._correlate.checkpoint,
|
|
2154
2156
|
correlate: (q) => this.correlate(q),
|
|
2155
2157
|
drain: (o) => this.drain(o),
|
|
2156
|
-
|
|
2158
|
+
on_settled: (drain) => this.emit("settled", drain)
|
|
2157
2159
|
},
|
|
2158
2160
|
options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
|
|
2159
2161
|
);
|
|
2160
|
-
this._notify_disposer = this.
|
|
2162
|
+
this._notify_disposer = this._wire_notify(options.scoped?.store ?? store());
|
|
2161
2163
|
dispose(() => this.shutdown());
|
|
2162
2164
|
}
|
|
2163
2165
|
/** True after the first `shutdown()` call. Guards idempotency. */
|
|
@@ -2191,7 +2193,7 @@ var Act = class {
|
|
|
2191
2193
|
* subscription was made). Errors during subscription are logged but
|
|
2192
2194
|
* never thrown — `notify` is a hint, not a contract.
|
|
2193
2195
|
*/
|
|
2194
|
-
async
|
|
2196
|
+
async _wire_notify(s) {
|
|
2195
2197
|
if (this._reactive_events.size === 0) return void 0;
|
|
2196
2198
|
if (!s.notify) return void 0;
|
|
2197
2199
|
if (!this._listen) return void 0;
|
|
@@ -2200,7 +2202,7 @@ var Act = class {
|
|
|
2200
2202
|
try {
|
|
2201
2203
|
this.emit("notified", notification);
|
|
2202
2204
|
if (this._drain) {
|
|
2203
|
-
const armed = this.
|
|
2205
|
+
const armed = this._arm_for_event_names(
|
|
2204
2206
|
notification.events.map((e) => e.name)
|
|
2205
2207
|
);
|
|
2206
2208
|
if (armed) this._settle.schedule({ debounceMs: 0 });
|
|
@@ -2306,7 +2308,7 @@ var Act = class {
|
|
|
2306
2308
|
skipValidation
|
|
2307
2309
|
);
|
|
2308
2310
|
if (this._reactive_events.size > 0)
|
|
2309
|
-
this.
|
|
2311
|
+
this._arm_for_event_names(
|
|
2310
2312
|
snapshots.map((s) => s.event.name)
|
|
2311
2313
|
);
|
|
2312
2314
|
this.emit("committed", snapshots);
|
|
@@ -2485,28 +2487,28 @@ var Act = class {
|
|
|
2485
2487
|
async drain(options = {}) {
|
|
2486
2488
|
if (!this._drain)
|
|
2487
2489
|
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
2488
|
-
return this._scoped(() => this.
|
|
2490
|
+
return this._scoped(() => this._drain_all(options));
|
|
2489
2491
|
}
|
|
2490
2492
|
/** Arm every active lane controller (ACT-1103). */
|
|
2491
|
-
|
|
2493
|
+
_arm_all() {
|
|
2492
2494
|
for (const c of this._drain_controllers.values()) c.arm();
|
|
2493
2495
|
}
|
|
2494
2496
|
/**
|
|
2495
2497
|
* Arm only the lane controllers whose reactions match the supplied
|
|
2496
2498
|
* event names (ACT-1103 selective arming). Events with any dynamic
|
|
2497
|
-
* resolver fall back to `
|
|
2499
|
+
* resolver fall back to `_arm_all()` via the `"all"` sentinel — the
|
|
2498
2500
|
* resolver's lane isn't known until correlate runs the function.
|
|
2499
2501
|
* Events with no reactions are skipped; `_event_to_lanes` doesn't
|
|
2500
2502
|
* carry them. Returns true when any controller was armed (used by
|
|
2501
2503
|
* the notify handler to decide whether to schedule a settle).
|
|
2502
2504
|
*/
|
|
2503
|
-
|
|
2505
|
+
_arm_for_event_names(names) {
|
|
2504
2506
|
const to_arm = /* @__PURE__ */ new Set();
|
|
2505
2507
|
for (const name of names) {
|
|
2506
2508
|
const set = this._event_to_lanes.get(name);
|
|
2507
2509
|
if (set === void 0) continue;
|
|
2508
2510
|
if (set === ALL_LANES) {
|
|
2509
|
-
this.
|
|
2511
|
+
this._arm_all();
|
|
2510
2512
|
return true;
|
|
2511
2513
|
}
|
|
2512
2514
|
for (const lane of set) to_arm.add(lane);
|
|
@@ -2523,7 +2525,7 @@ var Act = class {
|
|
|
2523
2525
|
* `SKIP LOCKED` keeps cross-controller races safe. Lifecycle events
|
|
2524
2526
|
* (`acked`, `blocked`) may interleave by lane — listeners filter via
|
|
2525
2527
|
* `lease.lane`. */
|
|
2526
|
-
async
|
|
2528
|
+
async _drain_all(options) {
|
|
2527
2529
|
const results = await Promise.all(
|
|
2528
2530
|
[...this._drain_controllers.values()].map((c) => c.drain(options))
|
|
2529
2531
|
);
|
|
@@ -2643,7 +2645,7 @@ var Act = class {
|
|
|
2643
2645
|
* @see {@link stop_correlations} to stop the worker
|
|
2644
2646
|
*/
|
|
2645
2647
|
start_correlations(query = {}, frequency = 1e4, callback) {
|
|
2646
|
-
return this._correlate.
|
|
2648
|
+
return this._correlate.start_polling(query, frequency, callback);
|
|
2647
2649
|
}
|
|
2648
2650
|
/**
|
|
2649
2651
|
* Stops the automatic correlation worker.
|
|
@@ -2663,7 +2665,7 @@ var Act = class {
|
|
|
2663
2665
|
* @see {@link start_correlations}
|
|
2664
2666
|
*/
|
|
2665
2667
|
stop_correlations() {
|
|
2666
|
-
this._correlate.
|
|
2668
|
+
this._correlate.stop_polling();
|
|
2667
2669
|
}
|
|
2668
2670
|
/**
|
|
2669
2671
|
* Cancels any pending or active settle cycle.
|
|
@@ -2709,7 +2711,7 @@ var Act = class {
|
|
|
2709
2711
|
async reset(input) {
|
|
2710
2712
|
return this._scoped(async () => {
|
|
2711
2713
|
const count = await store().reset(input);
|
|
2712
|
-
if (count > 0 && this._reactive_events.size > 0) this.
|
|
2714
|
+
if (count > 0 && this._reactive_events.size > 0) this._arm_all();
|
|
2713
2715
|
return count;
|
|
2714
2716
|
});
|
|
2715
2717
|
}
|
|
@@ -2743,7 +2745,7 @@ var Act = class {
|
|
|
2743
2745
|
async unblock(input) {
|
|
2744
2746
|
return this._scoped(async () => {
|
|
2745
2747
|
const count = await store().unblock(input);
|
|
2746
|
-
if (count > 0 && this._reactive_events.size > 0) this.
|
|
2748
|
+
if (count > 0 && this._reactive_events.size > 0) this._arm_all();
|
|
2747
2749
|
return count;
|
|
2748
2750
|
});
|
|
2749
2751
|
}
|
|
@@ -2969,14 +2971,14 @@ var Act = class {
|
|
|
2969
2971
|
if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
|
|
2970
2972
|
return this._scoped(async () => {
|
|
2971
2973
|
await this.correlate({ limit: 1e3 });
|
|
2972
|
-
const
|
|
2973
|
-
const result = await
|
|
2974
|
-
|
|
2975
|
-
|
|
2974
|
+
const close_actor = { id: "$close", name: "close" };
|
|
2975
|
+
const result = await run_close_cycle(targets, {
|
|
2976
|
+
reactive_events_size: this._reactive_events.size,
|
|
2977
|
+
event_to_state: this._event_to_state,
|
|
2976
2978
|
load: this._es.load,
|
|
2977
2979
|
tombstone: this._es.tombstone,
|
|
2978
2980
|
logger: this._logger,
|
|
2979
|
-
correlation:
|
|
2981
|
+
correlation: close_correlation(this._correlator, close_actor)
|
|
2980
2982
|
});
|
|
2981
2983
|
this.emit("closed", result);
|
|
2982
2984
|
return result;
|
|
@@ -3019,17 +3021,17 @@ var Act = class {
|
|
|
3019
3021
|
};
|
|
3020
3022
|
|
|
3021
3023
|
// src/builders/act-builder.ts
|
|
3022
|
-
function
|
|
3024
|
+
function register_batch_handler(proj, batch_handlers) {
|
|
3023
3025
|
if (!proj.batchHandler || !proj.target) return;
|
|
3024
|
-
const existing =
|
|
3026
|
+
const existing = batch_handlers.get(proj.target);
|
|
3025
3027
|
if (existing && existing !== proj.batchHandler) {
|
|
3026
3028
|
throw new Error(`Duplicate batch handler for target "${proj.target}"`);
|
|
3027
3029
|
}
|
|
3028
|
-
|
|
3030
|
+
batch_handlers.set(proj.target, proj.batchHandler);
|
|
3029
3031
|
}
|
|
3030
|
-
function
|
|
3032
|
+
function validate_lane_references(registry, lanes) {
|
|
3031
3033
|
const declared = /* @__PURE__ */ new Set([DEFAULT_LANE, ...lanes.map((l) => l.name)]);
|
|
3032
|
-
for (const [
|
|
3034
|
+
for (const [event_name, def] of Object.entries(registry.events)) {
|
|
3033
3035
|
const entry = def;
|
|
3034
3036
|
for (const [handlerName, reaction] of entry.reactions) {
|
|
3035
3037
|
const resolver = reaction.resolver;
|
|
@@ -3037,7 +3039,7 @@ function validateLaneReferences(registry, lanes) {
|
|
|
3037
3039
|
const lane = resolver.lane;
|
|
3038
3040
|
if (lane && !declared.has(lane)) {
|
|
3039
3041
|
throw new Error(
|
|
3040
|
-
`Reaction "${handlerName}" on "${
|
|
3042
|
+
`Reaction "${handlerName}" on "${event_name}" targets undeclared lane "${lane}". Declared lanes: ${[...declared].map((l) => `"${l}"`).join(", ")}. Add \`.withLane({ name: "${lane}", ... })\` to act() or correct the .to() declaration.`
|
|
3041
3043
|
);
|
|
3042
3044
|
}
|
|
3043
3045
|
}
|
|
@@ -3050,75 +3052,75 @@ function act() {
|
|
|
3050
3052
|
const registry = {
|
|
3051
3053
|
actions: {},
|
|
3052
3054
|
events: {},
|
|
3053
|
-
sensitive_fields: (
|
|
3054
|
-
disclosure_predicate: (
|
|
3055
|
+
sensitive_fields: (event_name) => _sf.get(event_name) ?? [],
|
|
3056
|
+
disclosure_predicate: (state_name) => _dp.get(state_name) ?? null
|
|
3055
3057
|
};
|
|
3056
|
-
const
|
|
3057
|
-
const
|
|
3058
|
+
const pending_projections = [];
|
|
3059
|
+
const batch_handlers = /* @__PURE__ */ new Map();
|
|
3058
3060
|
const lanes = [];
|
|
3059
3061
|
let _built = false;
|
|
3060
|
-
const
|
|
3061
|
-
const
|
|
3062
|
+
const finalize_deprecations = () => {
|
|
3063
|
+
const deprecation_summary = [];
|
|
3062
3064
|
for (const state2 of states.values()) {
|
|
3063
|
-
const
|
|
3064
|
-
const deprecated =
|
|
3065
|
+
const event_names = Object.keys(state2.events);
|
|
3066
|
+
const deprecated = deprecated_event_names(event_names);
|
|
3065
3067
|
if (deprecated.size === 0) continue;
|
|
3066
3068
|
state2._deprecated = deprecated;
|
|
3067
3069
|
for (const name of deprecated) {
|
|
3068
|
-
const current =
|
|
3069
|
-
|
|
3070
|
-
|
|
3070
|
+
const current = current_version_of(name, event_names);
|
|
3071
|
+
deprecation_summary.push({
|
|
3072
|
+
state_name: state2.name,
|
|
3071
3073
|
deprecated: name,
|
|
3072
3074
|
current
|
|
3073
3075
|
});
|
|
3074
3076
|
}
|
|
3075
|
-
for (const [
|
|
3076
|
-
const
|
|
3077
|
-
if (
|
|
3078
|
-
const current =
|
|
3077
|
+
for (const [action_name, handler] of Object.entries(state2.on)) {
|
|
3078
|
+
const static_target = handler?._static_emit;
|
|
3079
|
+
if (static_target && deprecated.has(static_target)) {
|
|
3080
|
+
const current = current_version_of(static_target, event_names);
|
|
3079
3081
|
throw new Error(
|
|
3080
|
-
`Action "${
|
|
3082
|
+
`Action "${action_name}" in state "${state2.name}" emits deprecated event "${static_target}". A newer version exists: "${current}". Update the .emit() call to target the current version. The reducer (.patch) for "${static_target}" stays as-is \u2014 historical events still need it.`
|
|
3081
3083
|
);
|
|
3082
3084
|
}
|
|
3083
3085
|
}
|
|
3084
3086
|
}
|
|
3085
|
-
if (
|
|
3086
|
-
const list =
|
|
3087
|
-
(d) => `"${d.deprecated}" (current: "${d.current}", state: "${d.
|
|
3087
|
+
if (deprecation_summary.length > 0) {
|
|
3088
|
+
const list = deprecation_summary.map(
|
|
3089
|
+
(d) => `"${d.deprecated}" (current: "${d.current}", state: "${d.state_name}")`
|
|
3088
3090
|
).join(", ");
|
|
3089
3091
|
log().info(
|
|
3090
|
-
`Act registered ${
|
|
3092
|
+
`Act registered ${deprecation_summary.length} deprecated event(s): ${list}. These are legacy versions kept for the read path. Consider truncating closed streams via app.close() when feasible to reduce historical event load. See docs/docs/architecture/event-schema-evolution.md.`
|
|
3091
3093
|
);
|
|
3092
3094
|
}
|
|
3093
3095
|
};
|
|
3094
3096
|
const builder = {
|
|
3095
3097
|
withState: (state2) => {
|
|
3096
|
-
|
|
3098
|
+
register_state(state2, states, registry.actions, registry.events);
|
|
3097
3099
|
return builder;
|
|
3098
3100
|
},
|
|
3099
3101
|
withSlice: (input) => {
|
|
3100
3102
|
for (const s of input.states.values()) {
|
|
3101
|
-
|
|
3103
|
+
register_state(s, states, registry.actions, registry.events);
|
|
3102
3104
|
}
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
for (const
|
|
3106
|
-
const existing = lanes.find((l) => l.name ===
|
|
3105
|
+
merge_event_register(registry.events, input.events);
|
|
3106
|
+
pending_projections.push(...input.projections);
|
|
3107
|
+
for (const slice_lane of input.lanes) {
|
|
3108
|
+
const existing = lanes.find((l) => l.name === slice_lane.name);
|
|
3107
3109
|
if (!existing) {
|
|
3108
|
-
lanes.push(
|
|
3110
|
+
lanes.push(slice_lane);
|
|
3109
3111
|
continue;
|
|
3110
3112
|
}
|
|
3111
|
-
if (existing.leaseMillis !==
|
|
3113
|
+
if (existing.leaseMillis !== slice_lane.leaseMillis || existing.streamLimit !== slice_lane.streamLimit || existing.cycleMs !== slice_lane.cycleMs) {
|
|
3112
3114
|
throw new Error(
|
|
3113
|
-
`Lane "${
|
|
3115
|
+
`Lane "${slice_lane.name}" was already declared with a different config`
|
|
3114
3116
|
);
|
|
3115
3117
|
}
|
|
3116
3118
|
}
|
|
3117
3119
|
return builder;
|
|
3118
3120
|
},
|
|
3119
3121
|
withProjection: (proj) => {
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
+
merge_projection(proj, registry.events);
|
|
3123
|
+
register_batch_handler(proj, batch_handlers);
|
|
3122
3124
|
return builder;
|
|
3123
3125
|
},
|
|
3124
3126
|
withActor: () => builder,
|
|
@@ -3156,18 +3158,18 @@ function act() {
|
|
|
3156
3158
|
}),
|
|
3157
3159
|
build: (options) => {
|
|
3158
3160
|
if (!_built) {
|
|
3159
|
-
for (const proj of
|
|
3160
|
-
|
|
3161
|
-
|
|
3161
|
+
for (const proj of pending_projections) {
|
|
3162
|
+
merge_projection(proj, registry.events);
|
|
3163
|
+
register_batch_handler(proj, batch_handlers);
|
|
3162
3164
|
}
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
for (const [
|
|
3165
|
+
finalize_deprecations();
|
|
3166
|
+
validate_lane_references(registry, lanes);
|
|
3167
|
+
for (const [event_name, reg] of Object.entries(
|
|
3166
3168
|
registry.events
|
|
3167
3169
|
)) {
|
|
3168
3170
|
const fields = pii_fields(reg.schema);
|
|
3169
3171
|
if (fields.length === 0) continue;
|
|
3170
|
-
_sf.set(
|
|
3172
|
+
_sf.set(event_name, fields);
|
|
3171
3173
|
for (const [name, reaction] of reg.reactions) {
|
|
3172
3174
|
const inner = reaction.handler;
|
|
3173
3175
|
const wrapped = (event, stream, app) => inner(pii_strip(event, fields), stream, app);
|
|
@@ -3179,9 +3181,9 @@ function act() {
|
|
|
3179
3181
|
for (const state2 of states.values()) {
|
|
3180
3182
|
if (state2.disclose) _dp.set(state2.name, state2.disclose);
|
|
3181
3183
|
const fields_by_event = /* @__PURE__ */ new Map();
|
|
3182
|
-
for (const
|
|
3183
|
-
const fields = _sf.get(
|
|
3184
|
-
if (fields) fields_by_event.set(
|
|
3184
|
+
for (const event_name of Object.keys(state2.events)) {
|
|
3185
|
+
const fields = _sf.get(event_name);
|
|
3186
|
+
if (fields) fields_by_event.set(event_name, fields);
|
|
3185
3187
|
}
|
|
3186
3188
|
if (fields_by_event.size === 0) continue;
|
|
3187
3189
|
if (state2.snap) {
|
|
@@ -3199,12 +3201,12 @@ function act() {
|
|
|
3199
3201
|
const fields = fields_by_event.get(validated.name);
|
|
3200
3202
|
return fields ? pii_split(validated, fields) : validated;
|
|
3201
3203
|
};
|
|
3202
|
-
for (const [
|
|
3203
|
-
const original = state2.patch[
|
|
3204
|
-
state2.patch[
|
|
3204
|
+
for (const [event_name, fields] of fields_by_event) {
|
|
3205
|
+
const original = state2.patch[event_name];
|
|
3206
|
+
state2.patch[event_name] = (event, s) => original(pii_merge(event, fields), s);
|
|
3205
3207
|
}
|
|
3206
3208
|
}
|
|
3207
|
-
for (const [target, original] of
|
|
3209
|
+
for (const [target, original] of batch_handlers) {
|
|
3208
3210
|
const wrapped = async (events, stream) => {
|
|
3209
3211
|
const stripped = events.map((e) => {
|
|
3210
3212
|
const f = _sf.get(e.name);
|
|
@@ -3212,14 +3214,14 @@ function act() {
|
|
|
3212
3214
|
});
|
|
3213
3215
|
return original(stripped, stream);
|
|
3214
3216
|
};
|
|
3215
|
-
|
|
3217
|
+
batch_handlers.set(target, wrapped);
|
|
3216
3218
|
}
|
|
3217
3219
|
_built = true;
|
|
3218
3220
|
}
|
|
3219
3221
|
return new Act(
|
|
3220
3222
|
registry,
|
|
3221
3223
|
states,
|
|
3222
|
-
|
|
3224
|
+
batch_handlers,
|
|
3223
3225
|
options,
|
|
3224
3226
|
lanes
|
|
3225
3227
|
);
|
|
@@ -3232,7 +3234,7 @@ function act() {
|
|
|
3232
3234
|
// src/builders/projection-builder.ts
|
|
3233
3235
|
function _projection(target) {
|
|
3234
3236
|
const events = {};
|
|
3235
|
-
const
|
|
3237
|
+
const default_resolver = typeof target === "string" ? { target } : void 0;
|
|
3236
3238
|
const base = {
|
|
3237
3239
|
on: (entry) => {
|
|
3238
3240
|
const keys = Object.keys(entry);
|
|
@@ -3249,7 +3251,7 @@ function _projection(target) {
|
|
|
3249
3251
|
do: (handler) => {
|
|
3250
3252
|
const reaction = {
|
|
3251
3253
|
handler,
|
|
3252
|
-
resolver:
|
|
3254
|
+
resolver: default_resolver ?? _this_,
|
|
3253
3255
|
options: {
|
|
3254
3256
|
blockOnError: true,
|
|
3255
3257
|
maxRetries: 3
|
|
@@ -3305,7 +3307,7 @@ function slice() {
|
|
|
3305
3307
|
const lanes = [];
|
|
3306
3308
|
const builder = {
|
|
3307
3309
|
withState: (state2) => {
|
|
3308
|
-
|
|
3310
|
+
register_state(state2, states, actions, events);
|
|
3309
3311
|
return builder;
|
|
3310
3312
|
},
|
|
3311
3313
|
withProjection: (proj) => {
|
|
@@ -3361,12 +3363,12 @@ function state(entry) {
|
|
|
3361
3363
|
const keys = Object.keys(entry);
|
|
3362
3364
|
if (keys.length !== 1) throw new Error("state() requires exactly one key");
|
|
3363
3365
|
const name = keys[0];
|
|
3364
|
-
const
|
|
3366
|
+
const state_schema = entry[name];
|
|
3365
3367
|
return {
|
|
3366
3368
|
init(init) {
|
|
3367
3369
|
return {
|
|
3368
3370
|
emits(events) {
|
|
3369
|
-
const
|
|
3371
|
+
const default_patch = Object.fromEntries(
|
|
3370
3372
|
Object.keys(events).map((k) => {
|
|
3371
3373
|
const fn = Object.assign(({ data }) => data, {
|
|
3372
3374
|
_passthrough: true
|
|
@@ -3377,10 +3379,10 @@ function state(entry) {
|
|
|
3377
3379
|
const internal = {
|
|
3378
3380
|
events,
|
|
3379
3381
|
actions: {},
|
|
3380
|
-
state:
|
|
3382
|
+
state: state_schema,
|
|
3381
3383
|
name,
|
|
3382
3384
|
init,
|
|
3383
|
-
patch:
|
|
3385
|
+
patch: default_patch,
|
|
3384
3386
|
on: {},
|
|
3385
3387
|
// Step delegates initialized as identity. `act().build()`
|
|
3386
3388
|
// overrides on states with `sensitive(...)` events to bake in
|
|
@@ -3422,11 +3424,14 @@ function action_builder(state2) {
|
|
|
3422
3424
|
}
|
|
3423
3425
|
function emit(handler) {
|
|
3424
3426
|
if (typeof handler === "string") {
|
|
3425
|
-
const
|
|
3426
|
-
const
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3427
|
+
const event_name = handler;
|
|
3428
|
+
const emit_fn = Object.assign(
|
|
3429
|
+
(payload) => [event_name, payload],
|
|
3430
|
+
{
|
|
3431
|
+
_static_emit: event_name
|
|
3432
|
+
}
|
|
3433
|
+
);
|
|
3434
|
+
internal.on[action2] = emit_fn;
|
|
3430
3435
|
} else {
|
|
3431
3436
|
internal.on[action2] = handler;
|
|
3432
3437
|
}
|
|
@@ -3479,7 +3484,7 @@ var CsvFile = class {
|
|
|
3479
3484
|
let header = null;
|
|
3480
3485
|
for await (const line of lines) {
|
|
3481
3486
|
if (!line.trim()) continue;
|
|
3482
|
-
const fields =
|
|
3487
|
+
const fields = parse_csv_line(line);
|
|
3483
3488
|
if (!header) {
|
|
3484
3489
|
header = fields;
|
|
3485
3490
|
const expected = CSV_COLUMNS.join(",");
|
|
@@ -3516,21 +3521,21 @@ var CsvFile = class {
|
|
|
3516
3521
|
flags: "w",
|
|
3517
3522
|
encoding: "utf8"
|
|
3518
3523
|
});
|
|
3519
|
-
let
|
|
3524
|
+
let next_id = 1;
|
|
3520
3525
|
try {
|
|
3521
|
-
await
|
|
3526
|
+
await write_line(writer, CSV_COLUMNS.join(","));
|
|
3522
3527
|
await driver(async (event) => {
|
|
3523
|
-
const id =
|
|
3528
|
+
const id = next_id++;
|
|
3524
3529
|
const row = [
|
|
3525
3530
|
String(id),
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3531
|
+
csv_escape(event.name),
|
|
3532
|
+
csv_escape(JSON.stringify(event.data)),
|
|
3533
|
+
csv_escape(event.stream),
|
|
3529
3534
|
String(event.version),
|
|
3530
3535
|
event.created.toISOString(),
|
|
3531
|
-
|
|
3536
|
+
csv_escape(JSON.stringify(event.meta))
|
|
3532
3537
|
].join(",");
|
|
3533
|
-
await
|
|
3538
|
+
await write_line(writer, row);
|
|
3534
3539
|
return id;
|
|
3535
3540
|
});
|
|
3536
3541
|
} finally {
|
|
@@ -3563,7 +3568,7 @@ async function* linesFromBlob(blob) {
|
|
|
3563
3568
|
await Promise.resolve();
|
|
3564
3569
|
}
|
|
3565
3570
|
}
|
|
3566
|
-
function
|
|
3571
|
+
function parse_csv_line(line) {
|
|
3567
3572
|
const fields = [];
|
|
3568
3573
|
let i = 0;
|
|
3569
3574
|
while (i < line.length) {
|
|
@@ -3596,11 +3601,11 @@ function parseCsvLine(line) {
|
|
|
3596
3601
|
}
|
|
3597
3602
|
return fields;
|
|
3598
3603
|
}
|
|
3599
|
-
function
|
|
3604
|
+
function csv_escape(value) {
|
|
3600
3605
|
if (/[",\n\r]/.test(value)) return `"${value.replace(/"/g, '""')}"`;
|
|
3601
3606
|
return value;
|
|
3602
3607
|
}
|
|
3603
|
-
function
|
|
3608
|
+
function write_line(writer, line) {
|
|
3604
3609
|
return new Promise((resolve, reject) => {
|
|
3605
3610
|
writer.write(`${line}
|
|
3606
3611
|
`, (err) => {
|