@rotorsoft/act 1.10.0 → 1.10.2
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 +22 -19
- 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/act-builder.d.ts.map +1 -1
- package/dist/@types/builders/slice-builder.d.ts +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 +9 -9
- 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 +8 -8
- 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 +8 -25
- package/dist/@types/internal/sensitive.d.ts.map +1 -1
- 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/@types/types/action.d.ts +78 -16
- package/dist/@types/types/action.d.ts.map +1 -1
- package/dist/@types/types/registry.d.ts +12 -0
- package/dist/@types/types/registry.d.ts.map +1 -1
- package/dist/{chunk-3ZTFNAY7.js → chunk-5PWLGL5E.js} +134 -133
- package/dist/chunk-5PWLGL5E.js.map +1 -0
- package/dist/{chunk-XSBT63QX.js → chunk-NGVWJXEF.js} +9 -24
- package/dist/{chunk-XSBT63QX.js.map → chunk-NGVWJXEF.js.map} +1 -1
- package/dist/index.cjs +638 -658
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +500 -508
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs +134 -133
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.js +4 -4
- package/dist/test/index.js.map +1 -1
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.js +1 -1
- package/package.json +4 -4
- 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-5PWLGL5E.js";
|
|
23
23
|
import {
|
|
24
24
|
ActorSchema,
|
|
25
25
|
CausationEventSchema,
|
|
@@ -40,11 +40,10 @@ import {
|
|
|
40
40
|
ZodEmpty,
|
|
41
41
|
pii_fields,
|
|
42
42
|
pii_gate,
|
|
43
|
-
pii_merge,
|
|
44
43
|
pii_split,
|
|
45
44
|
pii_strip,
|
|
46
45
|
sensitive
|
|
47
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-NGVWJXEF.js";
|
|
48
47
|
import "./chunk-5WRI5ZAA.js";
|
|
49
48
|
|
|
50
49
|
// src/signals.ts
|
|
@@ -78,7 +77,7 @@ function parse(name) {
|
|
|
78
77
|
}
|
|
79
78
|
return { base: name, version: 1 };
|
|
80
79
|
}
|
|
81
|
-
function
|
|
80
|
+
function deprecated_event_names(names) {
|
|
82
81
|
const groups = /* @__PURE__ */ new Map();
|
|
83
82
|
for (const name of names) {
|
|
84
83
|
const { base, version } = parse(name);
|
|
@@ -94,10 +93,10 @@ function deprecatedEventNames(names) {
|
|
|
94
93
|
}
|
|
95
94
|
return deprecated;
|
|
96
95
|
}
|
|
97
|
-
function
|
|
98
|
-
const target = parse(
|
|
96
|
+
function current_version_of(deprecated_name, all_names) {
|
|
97
|
+
const target = parse(deprecated_name);
|
|
99
98
|
let highest;
|
|
100
|
-
for (const name of
|
|
99
|
+
for (const name of all_names) {
|
|
101
100
|
const { base, version } = parse(name);
|
|
102
101
|
if (base !== target.base) continue;
|
|
103
102
|
if (!highest || version > highest.version) highest = { version, name };
|
|
@@ -127,27 +126,27 @@ var ALL_CATEGORIES = [
|
|
|
127
126
|
];
|
|
128
127
|
async function* audit(deps, categories, options = {}) {
|
|
129
128
|
const requested = new Set(categories ?? [...ALL_CATEGORIES]);
|
|
130
|
-
const
|
|
131
|
-
const passes =
|
|
129
|
+
const ordered_categories = ALL_CATEGORIES.filter((c) => requested.has(c));
|
|
130
|
+
const passes = ordered_categories.map(
|
|
132
131
|
(c) => PASS_FACTORIES[c](deps, options)
|
|
133
132
|
);
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
if (
|
|
133
|
+
const need_stats = passes.some((p) => p.on_stat !== void 0);
|
|
134
|
+
const need_streams = passes.some((p) => p.on_stream !== void 0);
|
|
135
|
+
const need_events = passes.some((p) => p.on_event !== void 0);
|
|
136
|
+
if (need_stats) {
|
|
138
137
|
const stats = await deps.store().query_stats({}, { count: true, names: true });
|
|
139
138
|
for (const [stream, s] of stats) {
|
|
140
|
-
for (const p of passes) p.
|
|
139
|
+
for (const p of passes) p.on_stat?.(stream, s);
|
|
141
140
|
}
|
|
142
141
|
}
|
|
143
|
-
if (
|
|
142
|
+
if (need_streams) {
|
|
144
143
|
await deps.store().query_streams((pos) => {
|
|
145
|
-
for (const p of passes) p.
|
|
144
|
+
for (const p of passes) p.on_stream?.(pos);
|
|
146
145
|
});
|
|
147
146
|
}
|
|
148
|
-
if (
|
|
147
|
+
if (need_events) {
|
|
149
148
|
await deps.store().query((event) => {
|
|
150
|
-
for (const p of passes) p.
|
|
149
|
+
for (const p of passes) p.on_event?.(event);
|
|
151
150
|
}, options.query);
|
|
152
151
|
}
|
|
153
152
|
for (const p of passes) await p.finalize?.(deps);
|
|
@@ -155,11 +154,11 @@ async function* audit(deps, categories, options = {}) {
|
|
|
155
154
|
for (const f of p.drain()) yield f;
|
|
156
155
|
}
|
|
157
156
|
}
|
|
158
|
-
var
|
|
157
|
+
var make_schema_pass = (deps) => {
|
|
159
158
|
const findings = [];
|
|
160
159
|
return {
|
|
161
160
|
category: "schema",
|
|
162
|
-
|
|
161
|
+
on_event(event) {
|
|
163
162
|
const name = String(event.name);
|
|
164
163
|
const state2 = deps.event_to_state.get(name);
|
|
165
164
|
if (!state2) {
|
|
@@ -189,19 +188,19 @@ var makeSchemaPass = (deps) => {
|
|
|
189
188
|
drain: () => findings
|
|
190
189
|
};
|
|
191
190
|
};
|
|
192
|
-
var
|
|
191
|
+
var make_deprecated_load_pass = (deps, options) => {
|
|
193
192
|
const share_min = options.thresholds?.deprecated_min ?? DEFAULTS.deprecated_min;
|
|
194
193
|
const totals = /* @__PURE__ */ new Map();
|
|
195
|
-
const
|
|
194
|
+
const per_stream = /* @__PURE__ */ new Map();
|
|
196
195
|
return {
|
|
197
196
|
category: "deprecated-load",
|
|
198
|
-
|
|
197
|
+
on_stat(stream, { names }) {
|
|
199
198
|
for (const [name, count] of Object.entries(names)) {
|
|
200
199
|
totals.set(name, (totals.get(name) ?? 0) + count);
|
|
201
|
-
let m =
|
|
200
|
+
let m = per_stream.get(name);
|
|
202
201
|
if (!m) {
|
|
203
202
|
m = /* @__PURE__ */ new Map();
|
|
204
|
-
|
|
203
|
+
per_stream.set(name, m);
|
|
205
204
|
}
|
|
206
205
|
m.set(stream, count);
|
|
207
206
|
}
|
|
@@ -210,33 +209,33 @@ var makeDeprecatedLoadPass = (deps, options) => {
|
|
|
210
209
|
const findings = [];
|
|
211
210
|
const grand = [...totals.values()].reduce((s, n) => s + n, 0);
|
|
212
211
|
if (grand === 0) return findings;
|
|
213
|
-
const deprecated =
|
|
212
|
+
const deprecated = deprecated_event_names(deps.known_events);
|
|
214
213
|
const sorted = [...deprecated].map((name) => ({ name, count: totals.get(name) ?? 0 })).sort((a, b) => b.count - a.count);
|
|
215
214
|
for (const { name, count } of sorted) {
|
|
216
215
|
if (count === 0) continue;
|
|
217
216
|
if (count / grand < share_min) continue;
|
|
218
|
-
const
|
|
219
|
-
const
|
|
217
|
+
const current_version = current_version_of(name, deps.known_events);
|
|
218
|
+
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
219
|
findings.push({
|
|
221
220
|
category: "deprecated-load",
|
|
222
221
|
name,
|
|
223
|
-
current_version
|
|
222
|
+
current_version,
|
|
224
223
|
total: count,
|
|
225
|
-
top_streams
|
|
224
|
+
top_streams
|
|
226
225
|
});
|
|
227
226
|
}
|
|
228
227
|
return findings;
|
|
229
228
|
}
|
|
230
229
|
};
|
|
231
230
|
};
|
|
232
|
-
var
|
|
231
|
+
var make_close_candidate_pass = (deps, options) => {
|
|
233
232
|
const idle_days = options.thresholds?.idle_days ?? DEFAULTS.idle_days;
|
|
234
233
|
const terminal_events = new Set(options.thresholds?.terminal_events ?? []);
|
|
235
234
|
const idle_cutoff = Date.now() - idle_days * 24 * 60 * 60 * 1e3;
|
|
236
235
|
const findings = [];
|
|
237
236
|
return {
|
|
238
237
|
category: "close-candidate",
|
|
239
|
-
|
|
238
|
+
on_stat(stream, { head }) {
|
|
240
239
|
const head_name = String(head.name);
|
|
241
240
|
if (head_name.startsWith("__")) return;
|
|
242
241
|
const head_time = head.created.getTime();
|
|
@@ -249,22 +248,22 @@ var makeCloseCandidatePass = (deps, options) => {
|
|
|
249
248
|
last_event_at: head.created.toISOString(),
|
|
250
249
|
reason: is_terminal ? "terminal" : "idle",
|
|
251
250
|
idle_days: is_idle ? Math.floor((Date.now() - head_time) / (24 * 60 * 60 * 1e3)) : void 0,
|
|
252
|
-
restart_supported:
|
|
251
|
+
restart_supported: restart_is_supported(deps, head_name)
|
|
253
252
|
});
|
|
254
253
|
},
|
|
255
254
|
drain: () => findings
|
|
256
255
|
};
|
|
257
256
|
};
|
|
258
|
-
var
|
|
257
|
+
var make_restart_candidate_pass = (deps, options) => {
|
|
259
258
|
const threshold = options.thresholds?.restart_min ?? DEFAULTS.restart_min;
|
|
260
259
|
const findings = [];
|
|
261
260
|
return {
|
|
262
261
|
category: "restart-candidate",
|
|
263
|
-
|
|
262
|
+
on_stat(stream, { head, count, names }) {
|
|
264
263
|
if (count < threshold) return;
|
|
265
264
|
const head_name = String(head.name);
|
|
266
265
|
if (head_name.startsWith("__")) return;
|
|
267
|
-
if (!
|
|
266
|
+
if (!restart_is_supported(deps, head_name)) return;
|
|
268
267
|
findings.push({
|
|
269
268
|
category: "restart-candidate",
|
|
270
269
|
stream,
|
|
@@ -278,14 +277,14 @@ var makeRestartCandidatePass = (deps, options) => {
|
|
|
278
277
|
drain: () => findings
|
|
279
278
|
};
|
|
280
279
|
};
|
|
281
|
-
var
|
|
280
|
+
var make_reaction_health_pass = (_deps, options) => {
|
|
282
281
|
const near_block = options.thresholds?.near_block ?? DEFAULTS.near_block;
|
|
283
282
|
const stuck_minutes = options.thresholds?.stuck_minutes ?? DEFAULTS.stuck_minutes;
|
|
284
283
|
const stuck_cutoff = Date.now() - stuck_minutes * 60 * 1e3;
|
|
285
284
|
const findings = [];
|
|
286
285
|
return {
|
|
287
286
|
category: "reaction-health",
|
|
288
|
-
|
|
287
|
+
on_stream(p) {
|
|
289
288
|
if (p.blocked) {
|
|
290
289
|
findings.push({
|
|
291
290
|
category: "reaction-health",
|
|
@@ -322,14 +321,14 @@ var makeReactionHealthPass = (_deps, options) => {
|
|
|
322
321
|
drain: () => findings
|
|
323
322
|
};
|
|
324
323
|
};
|
|
325
|
-
var
|
|
324
|
+
var make_snapshot_drift_pass = (deps, options) => {
|
|
326
325
|
const drift_min = options.thresholds?.drift_min ?? DEFAULTS.drift_min;
|
|
327
326
|
const candidates = [];
|
|
328
327
|
const findings = [];
|
|
329
328
|
return {
|
|
330
329
|
category: "snapshot-drift",
|
|
331
|
-
|
|
332
|
-
if (!
|
|
330
|
+
on_stat(stream, { head, count, names }) {
|
|
331
|
+
if (!restart_is_supported(deps, String(head.name))) return;
|
|
333
332
|
if (count < drift_min) return;
|
|
334
333
|
candidates.push({
|
|
335
334
|
stream,
|
|
@@ -378,12 +377,12 @@ var makeSnapshotDriftPass = (deps, options) => {
|
|
|
378
377
|
drain: () => findings
|
|
379
378
|
};
|
|
380
379
|
};
|
|
381
|
-
var
|
|
380
|
+
var make_routing_health_pass = (deps) => {
|
|
382
381
|
const findings = [];
|
|
383
|
-
const
|
|
382
|
+
const seen_event_names = /* @__PURE__ */ new Set();
|
|
384
383
|
return {
|
|
385
384
|
category: "routing-health",
|
|
386
|
-
|
|
385
|
+
on_stream(p) {
|
|
387
386
|
if (!p.lane) return;
|
|
388
387
|
if (deps.declared_lanes.has(p.lane)) return;
|
|
389
388
|
findings.push({
|
|
@@ -393,13 +392,13 @@ var makeRoutingHealthPass = (deps) => {
|
|
|
393
392
|
lane: p.lane
|
|
394
393
|
});
|
|
395
394
|
},
|
|
396
|
-
|
|
395
|
+
on_stat(_stream, { names }) {
|
|
397
396
|
for (const name of Object.keys(names)) {
|
|
398
|
-
|
|
397
|
+
seen_event_names.add(name);
|
|
399
398
|
}
|
|
400
399
|
},
|
|
401
400
|
finalize() {
|
|
402
|
-
for (const name of
|
|
401
|
+
for (const name of seen_event_names) {
|
|
403
402
|
if (name.startsWith("__")) continue;
|
|
404
403
|
if (deps.routed_events.has(name)) continue;
|
|
405
404
|
findings.push({
|
|
@@ -413,23 +412,23 @@ var makeRoutingHealthPass = (deps) => {
|
|
|
413
412
|
drain: () => findings
|
|
414
413
|
};
|
|
415
414
|
};
|
|
416
|
-
var
|
|
417
|
-
const
|
|
415
|
+
var make_correlation_gaps_pass = () => {
|
|
416
|
+
const seen_ids = /* @__PURE__ */ new Set();
|
|
418
417
|
const checks = [];
|
|
419
418
|
return {
|
|
420
419
|
category: "correlation-gaps",
|
|
421
|
-
|
|
422
|
-
|
|
420
|
+
on_event(e) {
|
|
421
|
+
seen_ids.add(e.id);
|
|
423
422
|
const causation = e.meta?.causation;
|
|
424
|
-
const
|
|
425
|
-
if (
|
|
426
|
-
checks.push({ stream: e.stream, id: e.id,
|
|
423
|
+
const parent_id = causation?.event?.id;
|
|
424
|
+
if (parent_id !== void 0) {
|
|
425
|
+
checks.push({ stream: e.stream, id: e.id, parent_id });
|
|
427
426
|
}
|
|
428
427
|
},
|
|
429
428
|
drain() {
|
|
430
429
|
const findings = [];
|
|
431
|
-
for (const { stream, id,
|
|
432
|
-
if (!
|
|
430
|
+
for (const { stream, id, parent_id } of checks) {
|
|
431
|
+
if (!seen_ids.has(parent_id)) {
|
|
433
432
|
findings.push({
|
|
434
433
|
category: "correlation-gaps",
|
|
435
434
|
stream,
|
|
@@ -442,12 +441,12 @@ var makeCorrelationGapsPass = () => {
|
|
|
442
441
|
}
|
|
443
442
|
};
|
|
444
443
|
};
|
|
445
|
-
var
|
|
444
|
+
var make_clock_anomalies_pass = () => {
|
|
446
445
|
const findings = [];
|
|
447
|
-
const
|
|
446
|
+
const last_per_stream = /* @__PURE__ */ new Map();
|
|
448
447
|
return {
|
|
449
448
|
category: "clock-anomalies",
|
|
450
|
-
|
|
449
|
+
on_event(e) {
|
|
451
450
|
const created = e.created.getTime();
|
|
452
451
|
if (created > Date.now()) {
|
|
453
452
|
findings.push({
|
|
@@ -457,7 +456,7 @@ var makeClockAnomaliesPass = () => {
|
|
|
457
456
|
reason: "future-created"
|
|
458
457
|
});
|
|
459
458
|
}
|
|
460
|
-
const prev =
|
|
459
|
+
const prev = last_per_stream.get(e.stream);
|
|
461
460
|
if (prev !== void 0 && created < prev) {
|
|
462
461
|
findings.push({
|
|
463
462
|
category: "clock-anomalies",
|
|
@@ -466,48 +465,48 @@ var makeClockAnomaliesPass = () => {
|
|
|
466
465
|
reason: "out-of-order"
|
|
467
466
|
});
|
|
468
467
|
}
|
|
469
|
-
|
|
468
|
+
last_per_stream.set(e.stream, created);
|
|
470
469
|
},
|
|
471
470
|
drain: () => findings
|
|
472
471
|
};
|
|
473
472
|
};
|
|
474
|
-
function
|
|
475
|
-
const state2 = deps.event_to_state.get(
|
|
473
|
+
function restart_is_supported(deps, head_event_name) {
|
|
474
|
+
const state2 = deps.event_to_state.get(head_event_name);
|
|
476
475
|
return state2?.snap !== void 0;
|
|
477
476
|
}
|
|
478
477
|
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":
|
|
478
|
+
schema: make_schema_pass,
|
|
479
|
+
"deprecated-load": make_deprecated_load_pass,
|
|
480
|
+
"close-candidate": make_close_candidate_pass,
|
|
481
|
+
"restart-candidate": make_restart_candidate_pass,
|
|
482
|
+
"reaction-health": make_reaction_health_pass,
|
|
483
|
+
"snapshot-drift": make_snapshot_drift_pass,
|
|
484
|
+
"routing-health": make_routing_health_pass,
|
|
485
|
+
"correlation-gaps": make_correlation_gaps_pass,
|
|
486
|
+
"clock-anomalies": make_clock_anomalies_pass
|
|
488
487
|
};
|
|
489
488
|
|
|
490
489
|
// src/internal/build-classify.ts
|
|
491
490
|
var ALL_LANES = /* @__PURE__ */ Symbol("act-1103/all-lanes");
|
|
492
|
-
function
|
|
491
|
+
function classify_registry(registry, states) {
|
|
493
492
|
const statics = /* @__PURE__ */ new Map();
|
|
494
|
-
const
|
|
495
|
-
const
|
|
496
|
-
let
|
|
493
|
+
const reactive_events = /* @__PURE__ */ new Set();
|
|
494
|
+
const event_to_lanes = /* @__PURE__ */ new Map();
|
|
495
|
+
let has_dynamic_resolvers = false;
|
|
497
496
|
for (const [name, register] of Object.entries(registry.events)) {
|
|
498
|
-
if (register.reactions.size > 0)
|
|
497
|
+
if (register.reactions.size > 0) reactive_events.add(name);
|
|
499
498
|
for (const reaction of register.reactions.values()) {
|
|
500
499
|
if (typeof reaction.resolver === "function") {
|
|
501
|
-
|
|
502
|
-
|
|
500
|
+
has_dynamic_resolvers = true;
|
|
501
|
+
event_to_lanes.set(name, ALL_LANES);
|
|
503
502
|
} else {
|
|
504
503
|
const { target, source, priority = 0, lane } = reaction.resolver;
|
|
505
504
|
const lane_name = lane ?? "default";
|
|
506
|
-
const existing_lanes =
|
|
505
|
+
const existing_lanes = event_to_lanes.get(name);
|
|
507
506
|
if (existing_lanes !== ALL_LANES) {
|
|
508
507
|
const set = existing_lanes ?? /* @__PURE__ */ new Set();
|
|
509
508
|
set.add(lane_name);
|
|
510
|
-
|
|
509
|
+
event_to_lanes.set(name, set);
|
|
511
510
|
}
|
|
512
511
|
const key = `${target}|${source ?? ""}`;
|
|
513
512
|
const existing = statics.get(key);
|
|
@@ -525,59 +524,59 @@ function classifyRegistry(registry, states) {
|
|
|
525
524
|
}
|
|
526
525
|
}
|
|
527
526
|
}
|
|
528
|
-
const
|
|
527
|
+
const event_to_state = /* @__PURE__ */ new Map();
|
|
529
528
|
for (const merged of states.values()) {
|
|
530
|
-
for (const
|
|
531
|
-
|
|
529
|
+
for (const event_name of Object.keys(merged.events)) {
|
|
530
|
+
event_to_state.set(event_name, merged);
|
|
532
531
|
}
|
|
533
532
|
}
|
|
534
533
|
return {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
534
|
+
static_targets: [...statics.values()],
|
|
535
|
+
has_dynamic_resolvers,
|
|
536
|
+
reactive_events,
|
|
537
|
+
event_to_state,
|
|
538
|
+
event_to_lanes
|
|
540
539
|
};
|
|
541
540
|
}
|
|
542
541
|
|
|
543
542
|
// src/internal/close-cycle.ts
|
|
544
|
-
async function
|
|
545
|
-
const
|
|
546
|
-
const streams = [...
|
|
543
|
+
async function run_close_cycle(targets, deps) {
|
|
544
|
+
const target_map = new Map(targets.map((t) => [t.stream, t]));
|
|
545
|
+
const streams = [...target_map.keys()];
|
|
547
546
|
const skipped = [];
|
|
548
|
-
const
|
|
549
|
-
const safe = await
|
|
550
|
-
|
|
551
|
-
deps.
|
|
547
|
+
const stream_info = await scan_stream_heads(streams);
|
|
548
|
+
const safe = await partition_by_safety(
|
|
549
|
+
stream_info,
|
|
550
|
+
deps.reactive_events_size,
|
|
552
551
|
skipped
|
|
553
552
|
);
|
|
554
553
|
if (!safe.length) return { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
555
|
-
const { guarded,
|
|
554
|
+
const { guarded, guard_events } = await guard_with_tombstones(
|
|
556
555
|
safe,
|
|
557
|
-
|
|
556
|
+
stream_info,
|
|
558
557
|
deps.correlation,
|
|
559
558
|
deps.tombstone,
|
|
560
559
|
skipped
|
|
561
560
|
);
|
|
562
561
|
if (!guarded.length) return { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
563
|
-
const
|
|
562
|
+
const seed_states = await load_restart_seeds(
|
|
564
563
|
guarded,
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
deps.
|
|
564
|
+
target_map,
|
|
565
|
+
stream_info,
|
|
566
|
+
deps.event_to_state,
|
|
568
567
|
deps.load,
|
|
569
568
|
deps.logger
|
|
570
569
|
);
|
|
571
|
-
await
|
|
572
|
-
const truncated = await
|
|
570
|
+
await run_archive_callbacks(guarded, target_map);
|
|
571
|
+
const truncated = await truncate_and_warm_cache(
|
|
573
572
|
guarded,
|
|
574
|
-
|
|
575
|
-
|
|
573
|
+
seed_states,
|
|
574
|
+
guard_events,
|
|
576
575
|
deps.correlation
|
|
577
576
|
);
|
|
578
577
|
return { truncated, skipped };
|
|
579
578
|
}
|
|
580
|
-
async function
|
|
579
|
+
async function scan_stream_heads(streams) {
|
|
581
580
|
const stats = await store().query_stats(streams, {
|
|
582
581
|
exclude: [SNAP_EVENT]
|
|
583
582
|
});
|
|
@@ -585,76 +584,76 @@ async function scanStreamHeads(streams) {
|
|
|
585
584
|
for (const [stream, { head }] of stats) {
|
|
586
585
|
if (head.name === TOMBSTONE_EVENT) continue;
|
|
587
586
|
out.set(stream, {
|
|
588
|
-
|
|
587
|
+
max_id: head.id,
|
|
589
588
|
version: head.version,
|
|
590
|
-
|
|
589
|
+
last_event_name: head.name
|
|
591
590
|
});
|
|
592
591
|
}
|
|
593
592
|
return out;
|
|
594
593
|
}
|
|
595
|
-
async function
|
|
596
|
-
if (
|
|
597
|
-
const
|
|
594
|
+
async function partition_by_safety(stream_info, reactive_events_size, skipped) {
|
|
595
|
+
if (reactive_events_size === 0) return [...stream_info.keys()];
|
|
596
|
+
const pending_set = /* @__PURE__ */ new Set();
|
|
598
597
|
await store().query_streams((position) => {
|
|
599
|
-
const
|
|
600
|
-
for (const [stream, info] of
|
|
601
|
-
if ((!
|
|
602
|
-
|
|
598
|
+
const source_re = position.source ? RegExp(position.source) : void 0;
|
|
599
|
+
for (const [stream, info] of stream_info) {
|
|
600
|
+
if ((!source_re || source_re.test(stream)) && position.at < info.max_id) {
|
|
601
|
+
pending_set.add(stream);
|
|
603
602
|
}
|
|
604
603
|
}
|
|
605
604
|
});
|
|
606
605
|
const safe = [];
|
|
607
|
-
for (const [stream] of
|
|
608
|
-
if (
|
|
606
|
+
for (const [stream] of stream_info) {
|
|
607
|
+
if (pending_set.has(stream)) skipped.push(stream);
|
|
609
608
|
else safe.push(stream);
|
|
610
609
|
}
|
|
611
610
|
return safe;
|
|
612
611
|
}
|
|
613
|
-
async function
|
|
612
|
+
async function guard_with_tombstones(safe, stream_info, correlation, tombstone2, skipped) {
|
|
614
613
|
const guarded = [];
|
|
615
|
-
const
|
|
614
|
+
const guard_events = /* @__PURE__ */ new Map();
|
|
616
615
|
await Promise.all(
|
|
617
616
|
safe.map(async (stream) => {
|
|
618
|
-
const info =
|
|
617
|
+
const info = stream_info.get(stream);
|
|
619
618
|
const committed = await tombstone2(stream, info.version, correlation);
|
|
620
619
|
if (committed) {
|
|
621
620
|
guarded.push(stream);
|
|
622
|
-
|
|
621
|
+
guard_events.set(stream, { id: committed.id, stream });
|
|
623
622
|
} else {
|
|
624
623
|
skipped.push(stream);
|
|
625
624
|
}
|
|
626
625
|
})
|
|
627
626
|
);
|
|
628
|
-
return { guarded,
|
|
627
|
+
return { guarded, guard_events };
|
|
629
628
|
}
|
|
630
|
-
async function
|
|
631
|
-
const
|
|
629
|
+
async function load_restart_seeds(guarded, target_map, stream_info, event_to_state, load2, logger) {
|
|
630
|
+
const seed_states = /* @__PURE__ */ new Map();
|
|
632
631
|
await Promise.all(
|
|
633
|
-
guarded.filter((s) =>
|
|
634
|
-
const
|
|
635
|
-
const
|
|
636
|
-
if (!
|
|
632
|
+
guarded.filter((s) => target_map.get(s)?.restart).map(async (stream) => {
|
|
633
|
+
const last_event_name = stream_info.get(stream).last_event_name;
|
|
634
|
+
const owner_state = event_to_state.get(last_event_name);
|
|
635
|
+
if (!owner_state) {
|
|
637
636
|
logger.error(
|
|
638
|
-
`Cannot seed restart for "${stream}": no registered state owns event "${
|
|
637
|
+
`Cannot seed restart for "${stream}": no registered state owns event "${last_event_name}". Stream will be tombstoned instead.`
|
|
639
638
|
);
|
|
640
639
|
return;
|
|
641
640
|
}
|
|
642
|
-
const snap2 = await load2(
|
|
643
|
-
|
|
641
|
+
const snap2 = await load2(owner_state, { stream });
|
|
642
|
+
seed_states.set(stream, snap2.state);
|
|
644
643
|
})
|
|
645
644
|
);
|
|
646
|
-
return
|
|
645
|
+
return seed_states;
|
|
647
646
|
}
|
|
648
|
-
async function
|
|
647
|
+
async function run_archive_callbacks(guarded, target_map) {
|
|
649
648
|
for (const stream of guarded) {
|
|
650
|
-
const
|
|
651
|
-
if (
|
|
649
|
+
const archive_fn = target_map.get(stream)?.archive;
|
|
650
|
+
if (archive_fn) await archive_fn();
|
|
652
651
|
}
|
|
653
652
|
}
|
|
654
|
-
async function
|
|
655
|
-
const
|
|
656
|
-
const snapshot =
|
|
657
|
-
const guard =
|
|
653
|
+
async function truncate_and_warm_cache(guarded, seed_states, guard_events, correlation) {
|
|
654
|
+
const trunc_targets = guarded.map((stream) => {
|
|
655
|
+
const snapshot = seed_states.get(stream);
|
|
656
|
+
const guard = guard_events.get(stream);
|
|
658
657
|
return {
|
|
659
658
|
stream,
|
|
660
659
|
snapshot,
|
|
@@ -666,11 +665,11 @@ async function truncateAndWarmCache(guarded, seedStates, guardEvents, correlatio
|
|
|
666
665
|
}
|
|
667
666
|
};
|
|
668
667
|
});
|
|
669
|
-
const truncated = await store().truncate(
|
|
668
|
+
const truncated = await store().truncate(trunc_targets);
|
|
670
669
|
await Promise.all(
|
|
671
670
|
guarded.map(async (stream) => {
|
|
672
671
|
const entry = truncated.get(stream);
|
|
673
|
-
const state2 =
|
|
672
|
+
const state2 = seed_states.get(stream);
|
|
674
673
|
if (state2 && entry) {
|
|
675
674
|
await cache().set(stream, {
|
|
676
675
|
state: state2,
|
|
@@ -698,13 +697,13 @@ var CorrelateCycle = class {
|
|
|
698
697
|
_has_dynamic_resolvers;
|
|
699
698
|
_cd;
|
|
700
699
|
_on_init;
|
|
701
|
-
constructor(registry,
|
|
700
|
+
constructor(registry, static_targets, has_dynamic_resolvers, cd, maxSubscribedStreams, on_init) {
|
|
702
701
|
this._subscribed = new LruSet(maxSubscribedStreams);
|
|
703
702
|
this._registry = registry;
|
|
704
|
-
this._static_targets =
|
|
705
|
-
this._has_dynamic_resolvers =
|
|
703
|
+
this._static_targets = static_targets;
|
|
704
|
+
this._has_dynamic_resolvers = has_dynamic_resolvers;
|
|
706
705
|
this._cd = cd;
|
|
707
|
-
this._on_init =
|
|
706
|
+
this._on_init = on_init;
|
|
708
707
|
}
|
|
709
708
|
/** Last correlated event id. */
|
|
710
709
|
get checkpoint() {
|
|
@@ -715,7 +714,7 @@ var CorrelateCycle = class {
|
|
|
715
714
|
* - Reads max(at) from store as cold-start checkpoint
|
|
716
715
|
* - Subscribes static resolver targets (idempotent upsert)
|
|
717
716
|
* - Populates the subscribed-streams LRU
|
|
718
|
-
* - Fires `
|
|
717
|
+
* - Fires `on_init` once (Act uses this to flag a cold-start drain)
|
|
719
718
|
*/
|
|
720
719
|
async init() {
|
|
721
720
|
if (this._initialized) return;
|
|
@@ -748,15 +747,15 @@ var CorrelateCycle = class {
|
|
|
748
747
|
if (typeof reaction.resolver !== "function") continue;
|
|
749
748
|
const resolved = reaction.resolver(event);
|
|
750
749
|
if (resolved && !this._subscribed.has(resolved.target)) {
|
|
751
|
-
const
|
|
750
|
+
const incoming_priority = resolved.priority ?? 0;
|
|
752
751
|
const entry = correlated.get(resolved.target) || {
|
|
753
752
|
source: resolved.source,
|
|
754
|
-
priority:
|
|
753
|
+
priority: incoming_priority,
|
|
755
754
|
lane: resolved.lane,
|
|
756
755
|
payloads: []
|
|
757
756
|
};
|
|
758
|
-
if (
|
|
759
|
-
entry.priority =
|
|
757
|
+
if (incoming_priority > entry.priority)
|
|
758
|
+
entry.priority = incoming_priority;
|
|
760
759
|
entry.payloads.push({
|
|
761
760
|
...reaction,
|
|
762
761
|
source: resolved.source,
|
|
@@ -795,7 +794,7 @@ var CorrelateCycle = class {
|
|
|
795
794
|
* running. Errors from `correlate()` are routed through `log()` so they
|
|
796
795
|
* land in the configured logger (the timer keeps running on failure).
|
|
797
796
|
*/
|
|
798
|
-
|
|
797
|
+
start_polling(query = {}, frequency = 1e4, callback) {
|
|
799
798
|
if (this._timer) return false;
|
|
800
799
|
const limit = query.limit || 100;
|
|
801
800
|
this._timer = setInterval(
|
|
@@ -807,7 +806,7 @@ var CorrelateCycle = class {
|
|
|
807
806
|
return true;
|
|
808
807
|
}
|
|
809
808
|
/** Stop the periodic correlation worker. Idempotent. */
|
|
810
|
-
|
|
809
|
+
stop_polling() {
|
|
811
810
|
if (this._timer) {
|
|
812
811
|
clearInterval(this._timer);
|
|
813
812
|
this._timer = void 0;
|
|
@@ -823,14 +822,14 @@ var SEG_SPACE = BASE ** SEG_WIDTH;
|
|
|
823
822
|
function seg(n) {
|
|
824
823
|
return n.toString(BASE).padStart(SEG_WIDTH, "0");
|
|
825
824
|
}
|
|
826
|
-
var
|
|
825
|
+
var default_correlator = ({ state: state2, action: action2 }) => {
|
|
827
826
|
const s = state2.slice(0, SEG_WIDTH).toLowerCase();
|
|
828
827
|
const a = action2.slice(0, SEG_WIDTH).toLowerCase();
|
|
829
828
|
const ts = seg(Date.now() % SEG_SPACE);
|
|
830
829
|
const rnd = seg(randomInt(SEG_SPACE));
|
|
831
830
|
return `${s}-${a}-${ts}${rnd}`;
|
|
832
831
|
};
|
|
833
|
-
function
|
|
832
|
+
function close_correlation(correlator, actor) {
|
|
834
833
|
return correlator({
|
|
835
834
|
state: "$close",
|
|
836
835
|
action: "close",
|
|
@@ -846,7 +845,7 @@ import { randomUUID } from "crypto";
|
|
|
846
845
|
var RATIO_MIN = 0.2;
|
|
847
846
|
var RATIO_MAX = 0.8;
|
|
848
847
|
var RATIO_DEFAULT = 0.5;
|
|
849
|
-
function
|
|
848
|
+
function compute_lag_lead_ratio(handled, lagging, leading) {
|
|
850
849
|
let lagging_handled = 0;
|
|
851
850
|
let leading_handled = 0;
|
|
852
851
|
for (const { lease, handled: count } of handled) {
|
|
@@ -883,7 +882,7 @@ var subscribe = (streams) => store().subscribe(streams);
|
|
|
883
882
|
import { patch } from "@rotorsoft/act-patch";
|
|
884
883
|
|
|
885
884
|
// src/internal/backoff.ts
|
|
886
|
-
function
|
|
885
|
+
function compute_backoff_delay(retry, opts) {
|
|
887
886
|
if (!opts || opts.baseMs <= 0) return 0;
|
|
888
887
|
const r = Math.max(0, retry);
|
|
889
888
|
let delay;
|
|
@@ -1003,7 +1002,6 @@ async function scan(source, opts = {}, callback) {
|
|
|
1003
1002
|
migrated = {
|
|
1004
1003
|
...event,
|
|
1005
1004
|
name: migration.to,
|
|
1006
|
-
// biome-ignore lint/suspicious/noExplicitAny: migration target shape is caller-defined
|
|
1007
1005
|
data: new_data
|
|
1008
1006
|
};
|
|
1009
1007
|
migrated_count++;
|
|
@@ -1052,39 +1050,37 @@ async function scan(source, opts = {}, callback) {
|
|
|
1052
1050
|
}
|
|
1053
1051
|
};
|
|
1054
1052
|
}
|
|
1055
|
-
async function load(me,
|
|
1056
|
-
const
|
|
1057
|
-
const
|
|
1053
|
+
async function load(me, target, callback) {
|
|
1054
|
+
const { stream, actor, asOf } = target;
|
|
1055
|
+
const time_travel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
|
|
1056
|
+
const cached = time_travel ? void 0 : await cache().get(stream);
|
|
1058
1057
|
const cache_hit = !!cached;
|
|
1059
1058
|
let state2 = cached?.state ?? (me.init ? me.init() : {});
|
|
1060
1059
|
let patches = cached?.patches ?? 0;
|
|
1061
1060
|
let snaps = cached?.snaps ?? 0;
|
|
1062
|
-
let version = cached?.version ?? -1;
|
|
1063
1061
|
let replayed = 0;
|
|
1064
1062
|
let event;
|
|
1065
1063
|
await store().query(
|
|
1066
|
-
(
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
if (e.name === SNAP_EVENT) {
|
|
1071
|
-
state2 = e.data;
|
|
1064
|
+
(raw) => {
|
|
1065
|
+
event = me.view(raw, actor);
|
|
1066
|
+
if (event.name === SNAP_EVENT) {
|
|
1067
|
+
state2 = event.data;
|
|
1072
1068
|
snaps++;
|
|
1073
1069
|
patches = 0;
|
|
1074
1070
|
replayed++;
|
|
1075
|
-
} else if (me.patch[
|
|
1076
|
-
state2 = patch(state2, me.patch[
|
|
1071
|
+
} else if (me.patch[event.name]) {
|
|
1072
|
+
state2 = patch(state2, me.patch[event.name](event, state2));
|
|
1077
1073
|
patches++;
|
|
1078
1074
|
replayed++;
|
|
1079
|
-
} else if (
|
|
1075
|
+
} else if (event.name !== TOMBSTONE_EVENT) {
|
|
1080
1076
|
log().warn(
|
|
1081
|
-
`Skipping unknown event "${String(
|
|
1077
|
+
`Skipping unknown event "${String(event.name)}" on stream "${stream}" (id=${event.id}) \u2014 no reducer in state "${me.name}"`
|
|
1082
1078
|
);
|
|
1083
1079
|
}
|
|
1084
1080
|
callback?.({
|
|
1085
1081
|
event,
|
|
1086
1082
|
state: state2,
|
|
1087
|
-
version,
|
|
1083
|
+
version: event.version,
|
|
1088
1084
|
patches,
|
|
1089
1085
|
snaps,
|
|
1090
1086
|
cache_hit,
|
|
@@ -1097,39 +1093,43 @@ async function load(me, stream, callback, asOf, actor) {
|
|
|
1097
1093
|
...cached ? { after: cached.event_id } : { with_snaps: true, ...asOf }
|
|
1098
1094
|
}
|
|
1099
1095
|
);
|
|
1100
|
-
if (replayed > 0 && !
|
|
1096
|
+
if (replayed > 0 && !time_travel && event && !me.pii_aware) {
|
|
1101
1097
|
await cache().set(stream, {
|
|
1102
1098
|
state: state2,
|
|
1103
|
-
version,
|
|
1099
|
+
version: event.version,
|
|
1104
1100
|
event_id: event.id,
|
|
1105
1101
|
patches,
|
|
1106
1102
|
snaps
|
|
1107
1103
|
});
|
|
1108
1104
|
}
|
|
1109
|
-
return {
|
|
1105
|
+
return {
|
|
1106
|
+
event,
|
|
1107
|
+
state: state2,
|
|
1108
|
+
version: event?.version ?? cached?.version ?? -1,
|
|
1109
|
+
patches,
|
|
1110
|
+
snaps,
|
|
1111
|
+
cache_hit,
|
|
1112
|
+
replayed
|
|
1113
|
+
};
|
|
1110
1114
|
}
|
|
1111
|
-
async function action(me, action2, target, payload,
|
|
1115
|
+
async function action(me, action2, target, payload, options) {
|
|
1112
1116
|
const { stream, expectedVersion, actor } = target;
|
|
1113
1117
|
if (!stream) throw new Error("Missing target stream");
|
|
1114
|
-
const
|
|
1118
|
+
const reactingTo = options?.reactingTo;
|
|
1119
|
+
const correlator = options?.correlator ?? default_correlator;
|
|
1120
|
+
const validated = validate(action2, payload, me.actions[action2]);
|
|
1115
1121
|
const opts = me.options?.[action2];
|
|
1116
|
-
const
|
|
1122
|
+
const max_retries = opts?.maxRetries ?? 0;
|
|
1117
1123
|
for (let attempt = 0; ; attempt++) {
|
|
1118
1124
|
try {
|
|
1119
|
-
const snapshot = await load(
|
|
1120
|
-
me,
|
|
1121
|
-
stream,
|
|
1122
|
-
void 0,
|
|
1123
|
-
void 0,
|
|
1124
|
-
target.actor
|
|
1125
|
-
);
|
|
1125
|
+
const snapshot = await load(me, { stream, actor: target.actor });
|
|
1126
1126
|
if (snapshot.event?.name === TOMBSTONE_EVENT)
|
|
1127
1127
|
throw new StreamClosedError(stream);
|
|
1128
1128
|
const expected = expectedVersion ?? snapshot.event?.version;
|
|
1129
1129
|
if (me.given) {
|
|
1130
1130
|
const invariants = me.given[action2] || [];
|
|
1131
|
-
invariants.forEach(({ valid, description }) => {
|
|
1132
|
-
if (!
|
|
1131
|
+
invariants.forEach(({ valid: valid2, description }) => {
|
|
1132
|
+
if (!valid2(snapshot.state, actor))
|
|
1133
1133
|
throw new InvariantError(
|
|
1134
1134
|
action2,
|
|
1135
1135
|
validated,
|
|
@@ -1145,24 +1145,11 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
1145
1145
|
return [snapshot];
|
|
1146
1146
|
}
|
|
1147
1147
|
const tuples = Array.isArray(result[0]) ? result : [result];
|
|
1148
|
-
const
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
const evt = name;
|
|
1154
|
-
if (deprecated.has(evt) && !warned.has(evt)) {
|
|
1155
|
-
warned.add(evt);
|
|
1156
|
-
log().warn(
|
|
1157
|
-
`Action "${String(action2)}" emitted deprecated event "${evt}". A newer version exists in the registry \u2014 update the action's .emit() to target the current version. (warned once per process)`
|
|
1158
|
-
);
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
const emitted = tuples.map(([name, data]) => {
|
|
1163
|
-
const validated2 = skipValidation ? data : validate(name, data, me.events[name]);
|
|
1164
|
-
return me.message({ name, data: validated2 });
|
|
1165
|
-
});
|
|
1148
|
+
const valid = tuples.map(([name, data]) => ({
|
|
1149
|
+
name,
|
|
1150
|
+
data: validate(name, data, me.events[name])
|
|
1151
|
+
}));
|
|
1152
|
+
const emitted = valid.map((e) => me.message(e));
|
|
1166
1153
|
const meta = {
|
|
1167
1154
|
correlation: reactingTo?.meta.correlation || correlator({
|
|
1168
1155
|
action: action2,
|
|
@@ -1203,12 +1190,13 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
1203
1190
|
throw error;
|
|
1204
1191
|
}
|
|
1205
1192
|
let { state: state2, patches } = snapshot;
|
|
1206
|
-
const snapshots = committed.map((
|
|
1193
|
+
const snapshots = committed.map((row, i) => {
|
|
1194
|
+
const event = { ...row, data: valid[i].data };
|
|
1207
1195
|
const p = me.patch[event.name](event, state2);
|
|
1208
1196
|
state2 = patch(state2, p);
|
|
1209
1197
|
patches++;
|
|
1210
1198
|
return {
|
|
1211
|
-
event
|
|
1199
|
+
event,
|
|
1212
1200
|
state: state2,
|
|
1213
1201
|
version: event.version,
|
|
1214
1202
|
patches,
|
|
@@ -1220,21 +1208,23 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
1220
1208
|
});
|
|
1221
1209
|
const last = snapshots.at(-1);
|
|
1222
1210
|
const snapped = me.snap?.(last);
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1211
|
+
if (!me.pii_aware) {
|
|
1212
|
+
cache().set(stream, {
|
|
1213
|
+
state: last.state,
|
|
1214
|
+
version: last.event.version,
|
|
1215
|
+
event_id: last.event.id,
|
|
1216
|
+
patches: snapped ? 0 : last.patches,
|
|
1217
|
+
snaps: snapped ? last.snaps + 1 : last.snaps
|
|
1218
|
+
}).catch((err) => log().error(err));
|
|
1219
|
+
}
|
|
1230
1220
|
if (snapped) void snap(last);
|
|
1231
1221
|
return snapshots;
|
|
1232
1222
|
} catch (error) {
|
|
1233
1223
|
if (!(error instanceof ConcurrencyError)) throw error;
|
|
1234
|
-
if (attempt >=
|
|
1224
|
+
if (attempt >= max_retries) throw error;
|
|
1235
1225
|
if (opts?.backoff) {
|
|
1236
|
-
const
|
|
1237
|
-
if (
|
|
1226
|
+
const delay_ms = compute_backoff_delay(attempt, opts.backoff);
|
|
1227
|
+
if (delay_ms > 0) await sleep(delay_ms);
|
|
1238
1228
|
}
|
|
1239
1229
|
}
|
|
1240
1230
|
}
|
|
@@ -1258,12 +1248,12 @@ var C_STREAM = "\x1B[38;5;226m";
|
|
|
1258
1248
|
var dim = (text) => PRETTY ? `${C_DIM}${text}${C_RESET}` : text;
|
|
1259
1249
|
var hue = (color, text) => PRETTY ? `${color}${text}${C_RESET}` : text;
|
|
1260
1250
|
var drain_caption = (caption, lane) => {
|
|
1261
|
-
const
|
|
1251
|
+
const show_lane = lane && lane !== "default";
|
|
1262
1252
|
if (PRETTY) {
|
|
1263
1253
|
const tag = `${C_DRAIN}>> ${caption}${C_RESET}`;
|
|
1264
|
-
return
|
|
1254
|
+
return show_lane ? `${tag} ${C_LANE}${lane}${C_RESET}` : tag;
|
|
1265
1255
|
}
|
|
1266
|
-
return
|
|
1256
|
+
return show_lane ? `>> ${caption} ${lane}` : `>> ${caption}`;
|
|
1267
1257
|
};
|
|
1268
1258
|
var cache_marker = (hit) => {
|
|
1269
1259
|
const word = hit ? "hit" : "miss";
|
|
@@ -1292,16 +1282,8 @@ var traced = (inner, exit, entry) => (async (...args) => {
|
|
|
1292
1282
|
exit?.(result, ...args);
|
|
1293
1283
|
return result;
|
|
1294
1284
|
});
|
|
1295
|
-
function
|
|
1296
|
-
const bound_action = (me,
|
|
1297
|
-
me,
|
|
1298
|
-
actionName,
|
|
1299
|
-
target,
|
|
1300
|
-
payload,
|
|
1301
|
-
reactingTo,
|
|
1302
|
-
skipValidation,
|
|
1303
|
-
correlator
|
|
1304
|
-
);
|
|
1285
|
+
function build_es(logger, correlator = default_correlator) {
|
|
1286
|
+
const bound_action = (me, action_name, target, payload, options) => action(me, action_name, target, payload, { correlator, ...options });
|
|
1305
1287
|
if (logger.level !== "trace") {
|
|
1306
1288
|
return {
|
|
1307
1289
|
snap,
|
|
@@ -1320,7 +1302,7 @@ function buildEs(logger, correlator = defaultCorrelator) {
|
|
|
1320
1302
|
)
|
|
1321
1303
|
);
|
|
1322
1304
|
}),
|
|
1323
|
-
load: traced(load, (result, _me,
|
|
1305
|
+
load: traced(load, (result, _me, target) => {
|
|
1324
1306
|
const stats = stats_marker(
|
|
1325
1307
|
result.version,
|
|
1326
1308
|
result.replayed,
|
|
@@ -1331,7 +1313,7 @@ function buildEs(logger, correlator = defaultCorrelator) {
|
|
|
1331
1313
|
es_caption(
|
|
1332
1314
|
"load",
|
|
1333
1315
|
C_GREEN,
|
|
1334
|
-
`${stream}${as_of_marker(asOf)} ${cache_marker(result.cache_hit)} ${stats}`
|
|
1316
|
+
`${target.stream}${as_of_marker(target.asOf)} ${cache_marker(result.cache_hit)} ${stats}`
|
|
1335
1317
|
)
|
|
1336
1318
|
);
|
|
1337
1319
|
}),
|
|
@@ -1365,7 +1347,7 @@ function buildEs(logger, correlator = defaultCorrelator) {
|
|
|
1365
1347
|
})
|
|
1366
1348
|
};
|
|
1367
1349
|
}
|
|
1368
|
-
function
|
|
1350
|
+
function build_drain(logger) {
|
|
1369
1351
|
return {
|
|
1370
1352
|
claim,
|
|
1371
1353
|
fetch,
|
|
@@ -1374,46 +1356,48 @@ function buildDrain(logger) {
|
|
|
1374
1356
|
subscribe: logger.level !== "trace" ? subscribe : traced(subscribe, (result, streams) => {
|
|
1375
1357
|
if (!result.subscribed) return;
|
|
1376
1358
|
const lanes = new Set(streams.map((s) => s.lane ?? "default"));
|
|
1377
|
-
const
|
|
1359
|
+
const uniform_lane = lanes.size === 1 ? streams[0]?.lane : void 0;
|
|
1378
1360
|
const data = streams.map(
|
|
1379
|
-
({ stream, lane }) =>
|
|
1361
|
+
({ stream, lane }) => uniform_lane || !lane || lane === "default" ? hue(C_STREAM, stream) : `${hue(C_STREAM, stream)}${dim(`[${lane}]`)}`
|
|
1380
1362
|
).join(" ");
|
|
1381
|
-
logger.trace(
|
|
1363
|
+
logger.trace(
|
|
1364
|
+
`${drain_caption("correlated", uniform_lane)} ${data}`
|
|
1365
|
+
);
|
|
1382
1366
|
})
|
|
1383
1367
|
};
|
|
1384
1368
|
}
|
|
1385
|
-
function
|
|
1369
|
+
function trace_cycle(logger, leased, fetched, handled, acked, blocked) {
|
|
1386
1370
|
if (logger.level !== "trace" || !leased.length) return;
|
|
1387
1371
|
const lane = leased[0]?.lane;
|
|
1388
|
-
const
|
|
1389
|
-
const
|
|
1390
|
-
const
|
|
1391
|
-
const
|
|
1372
|
+
const fetch_by_stream = new Map(fetched.map((f) => [f.stream, f]));
|
|
1373
|
+
const acked_by_stream = new Map(acked.map((a) => [a.stream, a.at]));
|
|
1374
|
+
const blocked_by_stream = new Map(blocked.map((b) => [b.stream, b.error]));
|
|
1375
|
+
const failed_by_stream = new Map(
|
|
1392
1376
|
handled.filter((h) => h.error).map((h) => [h.lease.stream, h])
|
|
1393
1377
|
);
|
|
1394
1378
|
const detail = leased.map(({ stream, at, retry }) => {
|
|
1395
|
-
const f =
|
|
1379
|
+
const f = fetch_by_stream.get(stream);
|
|
1396
1380
|
const key = f?.source ? `${hue(C_STREAM, stream)}${dim(`<-${f.source}`)}` : hue(C_STREAM, stream);
|
|
1397
1381
|
const events = f && f.events.length ? ` ${dim(
|
|
1398
1382
|
`[${f.events.map(({ id, name }) => `#${id} ${String(name)}`).join(", ")}]`
|
|
1399
1383
|
)}` : "";
|
|
1400
|
-
const
|
|
1401
|
-
const
|
|
1402
|
-
const failure =
|
|
1403
|
-
let
|
|
1384
|
+
const acked_at = acked_by_stream.get(stream);
|
|
1385
|
+
const ack_part = acked_at !== void 0 ? hue(C_HIT, `\u2713 @${acked_at}`) : "";
|
|
1386
|
+
const failure = failed_by_stream.get(stream);
|
|
1387
|
+
let fail_part = "";
|
|
1404
1388
|
if (failure) {
|
|
1405
|
-
const
|
|
1406
|
-
const
|
|
1407
|
-
if (
|
|
1408
|
-
|
|
1389
|
+
const failed_at = failure.failed_at ?? at;
|
|
1390
|
+
const blocked_error = blocked_by_stream.get(stream);
|
|
1391
|
+
if (blocked_error !== void 0) {
|
|
1392
|
+
fail_part = `${hue(C_ERR, `\u2717 @${failed_at}/${retry}`)} ${dim(`(${blocked_error})`)}`;
|
|
1409
1393
|
} else {
|
|
1410
|
-
|
|
1394
|
+
fail_part = `${hue(C_MISS, `\u26A0 @${failed_at}/${retry}`)} ${dim(`(${failure.error})`)}`;
|
|
1411
1395
|
}
|
|
1412
1396
|
}
|
|
1413
1397
|
let tail;
|
|
1414
|
-
if (
|
|
1415
|
-
else if (
|
|
1416
|
-
else if (
|
|
1398
|
+
if (ack_part && fail_part) tail = ` ${ack_part} ${fail_part}`;
|
|
1399
|
+
else if (ack_part) tail = ` ${ack_part}`;
|
|
1400
|
+
else if (fail_part) tail = ` ${fail_part}`;
|
|
1417
1401
|
else tail = ` ${dim(`\u2298 @${at}/${retry}`)}`;
|
|
1418
1402
|
return `${key}${events}${tail}`;
|
|
1419
1403
|
}).join(", ");
|
|
@@ -1421,7 +1405,7 @@ function traceCycle(logger, leased, fetched, handled, acked, blocked) {
|
|
|
1421
1405
|
}
|
|
1422
1406
|
|
|
1423
1407
|
// src/internal/drain-cycle.ts
|
|
1424
|
-
async function
|
|
1408
|
+
async function run_drain_cycle(ops, registry, batch_handlers, handle, handle_batch, lagging, leading, eventLimit, leaseMillis, is_deferred, lane) {
|
|
1425
1409
|
const leased = await ops.claim(
|
|
1426
1410
|
lagging,
|
|
1427
1411
|
leading,
|
|
@@ -1430,7 +1414,7 @@ async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch,
|
|
|
1430
1414
|
lane
|
|
1431
1415
|
);
|
|
1432
1416
|
if (!leased.length) return void 0;
|
|
1433
|
-
const active =
|
|
1417
|
+
const active = is_deferred ? leased.filter((l) => !is_deferred(l.stream)) : leased;
|
|
1434
1418
|
if (!active.length) {
|
|
1435
1419
|
return {
|
|
1436
1420
|
leased,
|
|
@@ -1441,7 +1425,7 @@ async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch,
|
|
|
1441
1425
|
};
|
|
1442
1426
|
}
|
|
1443
1427
|
const fetched = await ops.fetch(active, eventLimit);
|
|
1444
|
-
const
|
|
1428
|
+
const fetch_map = /* @__PURE__ */ new Map();
|
|
1445
1429
|
const fetch_window_at = fetched.reduce(
|
|
1446
1430
|
(max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
|
|
1447
1431
|
0
|
|
@@ -1456,16 +1440,16 @@ async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch,
|
|
|
1456
1440
|
return resolved && resolved.target === stream;
|
|
1457
1441
|
}).map((reaction) => ({ ...reaction, event }));
|
|
1458
1442
|
});
|
|
1459
|
-
|
|
1443
|
+
fetch_map.set(stream, { fetch: f, payloads });
|
|
1460
1444
|
}
|
|
1461
1445
|
const handled = await Promise.all(
|
|
1462
1446
|
active.map((lease) => {
|
|
1463
|
-
const entry =
|
|
1447
|
+
const entry = fetch_map.get(lease.stream);
|
|
1464
1448
|
const at = entry.fetch.events.at(-1)?.id || fetch_window_at;
|
|
1465
1449
|
const { payloads } = entry;
|
|
1466
|
-
const batchHandler =
|
|
1450
|
+
const batchHandler = batch_handlers.get(lease.stream);
|
|
1467
1451
|
if (batchHandler && payloads.length > 0) {
|
|
1468
|
-
return
|
|
1452
|
+
return handle_batch({ ...lease, at }, payloads, batchHandler);
|
|
1469
1453
|
}
|
|
1470
1454
|
return handle({ ...lease, at }, payloads);
|
|
1471
1455
|
})
|
|
@@ -1489,14 +1473,14 @@ var DrainController = class {
|
|
|
1489
1473
|
_locked = false;
|
|
1490
1474
|
_ratio = 0.5;
|
|
1491
1475
|
/**
|
|
1492
|
-
* Per-stream backoff: `stream →
|
|
1493
|
-
* `_finalize` via `HandleResult.
|
|
1476
|
+
* Per-stream backoff: `stream → next_attempt_at` (ms since epoch). Set by
|
|
1477
|
+
* `_finalize` via `HandleResult.next_attempt_at`; cleared on successful
|
|
1494
1478
|
* ack or terminal block. Lives in process memory — per-worker pacing
|
|
1495
1479
|
* by design (see {@link BackoffOptions} for the multi-worker trade-off).
|
|
1496
1480
|
*/
|
|
1497
1481
|
_backoff = /* @__PURE__ */ new Map();
|
|
1498
|
-
/** Timer re-arming drain at the earliest pending `
|
|
1499
|
-
|
|
1482
|
+
/** Timer re-arming drain at the earliest pending `next_attempt_at`. */
|
|
1483
|
+
_backoff_timer;
|
|
1500
1484
|
/** Worker timer (ACT-1103). Set when `start()` is active, undefined otherwise. */
|
|
1501
1485
|
_worker;
|
|
1502
1486
|
_stopped = false;
|
|
@@ -1517,7 +1501,7 @@ var DrainController = class {
|
|
|
1517
1501
|
return this._armed;
|
|
1518
1502
|
}
|
|
1519
1503
|
/** Returns true when `stream` is currently within a backoff window. */
|
|
1520
|
-
|
|
1504
|
+
is_deferred = (stream) => {
|
|
1521
1505
|
const next = this._backoff.get(stream);
|
|
1522
1506
|
return next !== void 0 && next > Date.now();
|
|
1523
1507
|
};
|
|
@@ -1527,20 +1511,20 @@ var DrainController = class {
|
|
|
1527
1511
|
* Idempotent — collapses many simultaneously deferred streams into a
|
|
1528
1512
|
* single timer.
|
|
1529
1513
|
*/
|
|
1530
|
-
|
|
1531
|
-
if (this.
|
|
1514
|
+
schedule_backoff_wake() {
|
|
1515
|
+
if (this._backoff_timer) clearTimeout(this._backoff_timer);
|
|
1532
1516
|
let earliest = Number.POSITIVE_INFINITY;
|
|
1533
1517
|
for (const t of this._backoff.values()) if (t < earliest) earliest = t;
|
|
1534
1518
|
const delay = Math.max(0, earliest - Date.now());
|
|
1535
|
-
this.
|
|
1536
|
-
this.
|
|
1519
|
+
this._backoff_timer = setTimeout(() => {
|
|
1520
|
+
this._backoff_timer = void 0;
|
|
1537
1521
|
const now = Date.now();
|
|
1538
1522
|
for (const [stream, at] of this._backoff) {
|
|
1539
1523
|
if (at <= now) this._backoff.delete(stream);
|
|
1540
1524
|
}
|
|
1541
1525
|
this._armed = true;
|
|
1542
1526
|
}, delay);
|
|
1543
|
-
this.
|
|
1527
|
+
this._backoff_timer.unref();
|
|
1544
1528
|
}
|
|
1545
1529
|
/** Lane this controller drains (undefined = legacy single-lane span). */
|
|
1546
1530
|
get lane() {
|
|
@@ -1586,17 +1570,17 @@ var DrainController = class {
|
|
|
1586
1570
|
this._locked = true;
|
|
1587
1571
|
const lagging = Math.ceil(streamLimit * this._ratio);
|
|
1588
1572
|
const leading = streamLimit - lagging;
|
|
1589
|
-
const cycle = await
|
|
1573
|
+
const cycle = await run_drain_cycle(
|
|
1590
1574
|
this._deps.ops,
|
|
1591
1575
|
this._deps.registry,
|
|
1592
|
-
this._deps.
|
|
1576
|
+
this._deps.batch_handlers,
|
|
1593
1577
|
this._deps.handle,
|
|
1594
|
-
this._deps.
|
|
1578
|
+
this._deps.handle_batch,
|
|
1595
1579
|
lagging,
|
|
1596
1580
|
leading,
|
|
1597
1581
|
eventLimit,
|
|
1598
1582
|
leaseMillis,
|
|
1599
|
-
this._backoff.size > 0 ? this.
|
|
1583
|
+
this._backoff.size > 0 ? this.is_deferred : void 0,
|
|
1600
1584
|
this._deps.lane
|
|
1601
1585
|
);
|
|
1602
1586
|
if (!cycle) {
|
|
@@ -1604,20 +1588,20 @@ var DrainController = class {
|
|
|
1604
1588
|
return EMPTY_DRAIN;
|
|
1605
1589
|
}
|
|
1606
1590
|
const { leased, fetched, handled, acked, blocked } = cycle;
|
|
1607
|
-
|
|
1608
|
-
this._ratio =
|
|
1591
|
+
trace_cycle(this._deps.logger, leased, fetched, handled, acked, blocked);
|
|
1592
|
+
this._ratio = compute_lag_lead_ratio(handled, lagging, leading);
|
|
1609
1593
|
for (const lease of acked) this._backoff.delete(lease.stream);
|
|
1610
1594
|
for (const lease of blocked) this._backoff.delete(lease.stream);
|
|
1611
1595
|
for (const h of handled) {
|
|
1612
|
-
if (h.
|
|
1613
|
-
this._backoff.set(h.lease.stream, h.
|
|
1596
|
+
if (h.next_attempt_at !== void 0 && !h.block) {
|
|
1597
|
+
this._backoff.set(h.lease.stream, h.next_attempt_at);
|
|
1614
1598
|
}
|
|
1615
1599
|
}
|
|
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 && !
|
|
1600
|
+
if (this._backoff.size > 0) this.schedule_backoff_wake();
|
|
1601
|
+
if (acked.length) this._deps.on_acked(acked);
|
|
1602
|
+
if (blocked.length) this._deps.on_blocked(blocked);
|
|
1603
|
+
const has_errors = handled.some(({ error }) => error);
|
|
1604
|
+
if (!acked.length && !blocked.length && !has_errors) this._armed = false;
|
|
1621
1605
|
return { fetched, leased, acked, blocked };
|
|
1622
1606
|
} catch (error) {
|
|
1623
1607
|
this._deps.logger.error(error);
|
|
@@ -1630,44 +1614,44 @@ var DrainController = class {
|
|
|
1630
1614
|
|
|
1631
1615
|
// src/internal/merge.ts
|
|
1632
1616
|
import { ZodObject } from "zod";
|
|
1633
|
-
function
|
|
1617
|
+
function base_type_name(zodType) {
|
|
1634
1618
|
let t = zodType;
|
|
1635
1619
|
while (typeof t.unwrap === "function") {
|
|
1636
1620
|
t = t.unwrap();
|
|
1637
1621
|
}
|
|
1638
1622
|
return t.constructor.name;
|
|
1639
1623
|
}
|
|
1640
|
-
function
|
|
1624
|
+
function merge_schemas(existing, incoming, state_name) {
|
|
1641
1625
|
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 (
|
|
1626
|
+
const existing_shape = existing.shape;
|
|
1627
|
+
const incoming_shape = incoming.shape;
|
|
1628
|
+
for (const key of Object.keys(incoming_shape)) {
|
|
1629
|
+
if (key in existing_shape) {
|
|
1630
|
+
const existing_base = base_type_name(existing_shape[key]);
|
|
1631
|
+
const incoming_base = base_type_name(incoming_shape[key]);
|
|
1632
|
+
if (existing_base !== incoming_base) {
|
|
1649
1633
|
throw new Error(
|
|
1650
|
-
`Schema conflict in "${
|
|
1634
|
+
`Schema conflict in "${state_name}": key "${key}" has type "${existing_base}" but incoming partial declares "${incoming_base}"`
|
|
1651
1635
|
);
|
|
1652
1636
|
}
|
|
1653
1637
|
}
|
|
1654
1638
|
}
|
|
1655
|
-
return existing.extend(
|
|
1639
|
+
return existing.extend(incoming_shape);
|
|
1656
1640
|
}
|
|
1657
1641
|
return existing;
|
|
1658
1642
|
}
|
|
1659
|
-
function
|
|
1643
|
+
function merge_inits(existing, incoming) {
|
|
1660
1644
|
return () => ({ ...existing(), ...incoming() });
|
|
1661
1645
|
}
|
|
1662
|
-
function
|
|
1646
|
+
function register_state(state2, states, actions, events) {
|
|
1663
1647
|
const existing = states.get(state2.name);
|
|
1664
1648
|
if (existing) {
|
|
1665
|
-
|
|
1649
|
+
merge_into_existing(state2, existing, states, actions, events);
|
|
1666
1650
|
} else {
|
|
1667
|
-
|
|
1651
|
+
register_new_state(state2, states, actions, events);
|
|
1668
1652
|
}
|
|
1669
1653
|
}
|
|
1670
|
-
function
|
|
1654
|
+
function register_new_state(state2, states, actions, events) {
|
|
1671
1655
|
states.set(state2.name, state2);
|
|
1672
1656
|
for (const name of Object.keys(state2.actions)) {
|
|
1673
1657
|
if (actions[name]) throw new Error(`Duplicate action "${name}"`);
|
|
@@ -1678,7 +1662,7 @@ function registerNewState(state2, states, actions, events) {
|
|
|
1678
1662
|
events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
|
|
1679
1663
|
}
|
|
1680
1664
|
}
|
|
1681
|
-
function
|
|
1665
|
+
function merge_into_existing(state2, existing, states, actions, events) {
|
|
1682
1666
|
for (const name of Object.keys(state2.actions)) {
|
|
1683
1667
|
if (existing.actions[name] === state2.actions[name]) continue;
|
|
1684
1668
|
if (actions[name]) throw new Error(`Duplicate action "${name}"`);
|
|
@@ -1692,14 +1676,14 @@ function mergeIntoExisting(state2, existing, states, actions, events) {
|
|
|
1692
1676
|
}
|
|
1693
1677
|
if (events[name]) throw new Error(`Duplicate event "${name}"`);
|
|
1694
1678
|
}
|
|
1695
|
-
const
|
|
1679
|
+
const merged_patch = merge_patches(existing.patch, state2.patch, state2.name);
|
|
1696
1680
|
const merged = {
|
|
1697
1681
|
...existing,
|
|
1698
|
-
state:
|
|
1699
|
-
init:
|
|
1682
|
+
state: merge_schemas(existing.state, state2.state, state2.name),
|
|
1683
|
+
init: merge_inits(existing.init, state2.init),
|
|
1700
1684
|
events: { ...existing.events, ...state2.events },
|
|
1701
1685
|
actions: { ...existing.actions, ...state2.actions },
|
|
1702
|
-
patch:
|
|
1686
|
+
patch: merged_patch,
|
|
1703
1687
|
on: { ...existing.on, ...state2.on },
|
|
1704
1688
|
given: { ...existing.given, ...state2.given },
|
|
1705
1689
|
snap: state2.snap && existing.snap && state2.snap !== existing.snap ? (() => {
|
|
@@ -1717,48 +1701,48 @@ function mergeIntoExisting(state2, existing, states, actions, events) {
|
|
|
1717
1701
|
events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
|
|
1718
1702
|
}
|
|
1719
1703
|
}
|
|
1720
|
-
function
|
|
1704
|
+
function merge_patches(existing, incoming, state_name) {
|
|
1721
1705
|
const merged = { ...existing };
|
|
1722
1706
|
for (const name of Object.keys(incoming)) {
|
|
1723
|
-
const
|
|
1724
|
-
const
|
|
1725
|
-
if (!
|
|
1726
|
-
merged[name] =
|
|
1707
|
+
const existing_p = existing[name];
|
|
1708
|
+
const incoming_p = incoming[name];
|
|
1709
|
+
if (!existing_p) {
|
|
1710
|
+
merged[name] = incoming_p;
|
|
1727
1711
|
continue;
|
|
1728
1712
|
}
|
|
1729
|
-
const
|
|
1730
|
-
const
|
|
1731
|
-
if (!
|
|
1713
|
+
const existing_is_default = existing_p._passthrough;
|
|
1714
|
+
const incoming_is_default = incoming_p._passthrough;
|
|
1715
|
+
if (!existing_is_default && !incoming_is_default && existing_p !== incoming_p) {
|
|
1732
1716
|
throw new Error(
|
|
1733
|
-
`Duplicate custom patch for event "${name}" in state "${
|
|
1717
|
+
`Duplicate custom patch for event "${name}" in state "${state_name}"`
|
|
1734
1718
|
);
|
|
1735
1719
|
}
|
|
1736
|
-
if (
|
|
1737
|
-
merged[name] =
|
|
1720
|
+
if (existing_is_default && !incoming_is_default) {
|
|
1721
|
+
merged[name] = incoming_p;
|
|
1738
1722
|
}
|
|
1739
1723
|
}
|
|
1740
1724
|
return merged;
|
|
1741
1725
|
}
|
|
1742
|
-
function
|
|
1743
|
-
for (const [
|
|
1744
|
-
const
|
|
1745
|
-
if (!
|
|
1746
|
-
for (const [name, reaction] of
|
|
1747
|
-
|
|
1726
|
+
function merge_event_register(target, source) {
|
|
1727
|
+
for (const [event_name, source_reg] of Object.entries(source)) {
|
|
1728
|
+
const target_reg = target[event_name];
|
|
1729
|
+
if (!target_reg) continue;
|
|
1730
|
+
for (const [name, reaction] of source_reg.reactions) {
|
|
1731
|
+
target_reg.reactions.set(name, reaction);
|
|
1748
1732
|
}
|
|
1749
1733
|
}
|
|
1750
1734
|
}
|
|
1751
|
-
function
|
|
1752
|
-
for (const
|
|
1753
|
-
const
|
|
1754
|
-
const existing = events[
|
|
1735
|
+
function merge_projection(proj, events) {
|
|
1736
|
+
for (const event_name of Object.keys(proj.events)) {
|
|
1737
|
+
const proj_register = proj.events[event_name];
|
|
1738
|
+
const existing = events[event_name];
|
|
1755
1739
|
if (!existing) {
|
|
1756
|
-
events[
|
|
1757
|
-
schema:
|
|
1758
|
-
reactions: new Map(
|
|
1740
|
+
events[event_name] = {
|
|
1741
|
+
schema: proj_register.schema,
|
|
1742
|
+
reactions: new Map(proj_register.reactions)
|
|
1759
1743
|
};
|
|
1760
1744
|
} else {
|
|
1761
|
-
for (const [name, reaction] of
|
|
1745
|
+
for (const [name, reaction] of proj_register.reactions) {
|
|
1762
1746
|
let key = name;
|
|
1763
1747
|
while (existing.reactions.has(key)) key = `${key}_p`;
|
|
1764
1748
|
existing.reactions.set(key, reaction);
|
|
@@ -1775,24 +1759,24 @@ var _this_ = ({ stream }) => ({
|
|
|
1775
1759
|
function finalize(lease, handled, at, error, options, logger, failed_at) {
|
|
1776
1760
|
if (!error) return { lease, handled, acked_at: at };
|
|
1777
1761
|
logger.error(error);
|
|
1778
|
-
const
|
|
1779
|
-
const block2 = options.blockOnError && (
|
|
1762
|
+
const non_retryable = error instanceof NonRetryableError;
|
|
1763
|
+
const block2 = options.blockOnError && (non_retryable || lease.retry >= options.maxRetries);
|
|
1780
1764
|
if (block2)
|
|
1781
1765
|
logger.error(
|
|
1782
|
-
|
|
1766
|
+
non_retryable ? `Blocking ${lease.stream} on non-retryable error.` : `Blocking ${lease.stream} after ${lease.retry} retries.`
|
|
1783
1767
|
);
|
|
1784
|
-
const
|
|
1768
|
+
const next_attempt_at = !block2 && options.backoff ? Date.now() + compute_backoff_delay(lease.retry, options.backoff) : void 0;
|
|
1785
1769
|
return {
|
|
1786
1770
|
lease,
|
|
1787
1771
|
handled,
|
|
1788
1772
|
acked_at: at,
|
|
1789
1773
|
error: error.message,
|
|
1790
1774
|
block: block2,
|
|
1791
|
-
|
|
1775
|
+
next_attempt_at,
|
|
1792
1776
|
failed_at
|
|
1793
1777
|
};
|
|
1794
1778
|
}
|
|
1795
|
-
function
|
|
1779
|
+
function build_handle(deps) {
|
|
1796
1780
|
const {
|
|
1797
1781
|
logger,
|
|
1798
1782
|
bound_do,
|
|
@@ -1808,7 +1792,7 @@ function buildHandle(deps) {
|
|
|
1808
1792
|
let handled = 0;
|
|
1809
1793
|
if (lease.retry > 0)
|
|
1810
1794
|
logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
|
|
1811
|
-
const
|
|
1795
|
+
const scoped_app = {
|
|
1812
1796
|
do: bound_do,
|
|
1813
1797
|
load: bound_load,
|
|
1814
1798
|
query: bound_query,
|
|
@@ -1817,15 +1801,12 @@ function buildHandle(deps) {
|
|
|
1817
1801
|
};
|
|
1818
1802
|
for (const payload of payloads) {
|
|
1819
1803
|
const { event, handler } = payload;
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
reactingTo ?? event,
|
|
1825
|
-
skipValidation
|
|
1826
|
-
);
|
|
1804
|
+
scoped_app.do = (action2, target, action_payload, options) => bound_do(action2, target, action_payload, {
|
|
1805
|
+
...options,
|
|
1806
|
+
reactingTo: options?.reactingTo ?? event
|
|
1807
|
+
});
|
|
1827
1808
|
try {
|
|
1828
|
-
await handler(event, stream,
|
|
1809
|
+
await handler(event, stream, scoped_app);
|
|
1829
1810
|
at = event.id;
|
|
1830
1811
|
handled++;
|
|
1831
1812
|
} catch (error) {
|
|
@@ -1843,7 +1824,7 @@ function buildHandle(deps) {
|
|
|
1843
1824
|
return finalize(lease, handled, at, void 0, payloads[0].options, logger);
|
|
1844
1825
|
};
|
|
1845
1826
|
}
|
|
1846
|
-
function
|
|
1827
|
+
function build_handle_batch(logger) {
|
|
1847
1828
|
return async (lease, payloads, batchHandler) => {
|
|
1848
1829
|
const stream = lease.stream;
|
|
1849
1830
|
const events = payloads.map(
|
|
@@ -1875,23 +1856,23 @@ var SettleLoop = class {
|
|
|
1875
1856
|
_deps;
|
|
1876
1857
|
/** Debounce window applied when the caller doesn't override via `SettleOptions.debounceMs`. */
|
|
1877
1858
|
_default_debounce_ms;
|
|
1878
|
-
constructor(deps,
|
|
1859
|
+
constructor(deps, default_debounce_ms) {
|
|
1879
1860
|
this._deps = deps;
|
|
1880
|
-
this._default_debounce_ms =
|
|
1861
|
+
this._default_debounce_ms = default_debounce_ms;
|
|
1881
1862
|
}
|
|
1882
1863
|
/**
|
|
1883
1864
|
* Schedule a settle pass. Multiple calls inside the debounce window
|
|
1884
1865
|
* coalesce into one cycle. The cycle runs correlate→drain in a loop
|
|
1885
1866
|
* until no progress is made (no new subscriptions, no acks, no blocks)
|
|
1886
1867
|
* or `maxPasses` is reached, then emits the `"settled"` lifecycle event
|
|
1887
|
-
* via {@link SettleDeps.
|
|
1868
|
+
* via {@link SettleDeps.on_settled}.
|
|
1888
1869
|
*/
|
|
1889
1870
|
schedule(options = {}) {
|
|
1890
1871
|
const {
|
|
1891
1872
|
debounceMs = this._default_debounce_ms,
|
|
1892
|
-
correlate:
|
|
1873
|
+
correlate: correlate_query = { after: -1, limit: 100 },
|
|
1893
1874
|
maxPasses = Infinity,
|
|
1894
|
-
...
|
|
1875
|
+
...drain_options
|
|
1895
1876
|
} = options;
|
|
1896
1877
|
if (this._timer) clearTimeout(this._timer);
|
|
1897
1878
|
this._timer = setTimeout(() => {
|
|
@@ -1900,17 +1881,17 @@ var SettleLoop = class {
|
|
|
1900
1881
|
this._running = true;
|
|
1901
1882
|
(async () => {
|
|
1902
1883
|
await this._deps.init();
|
|
1903
|
-
let
|
|
1884
|
+
let last_drain;
|
|
1904
1885
|
for (let i = 0; i < maxPasses; i++) {
|
|
1905
1886
|
const { subscribed } = await this._deps.correlate({
|
|
1906
|
-
...
|
|
1887
|
+
...correlate_query,
|
|
1907
1888
|
after: this._deps.checkpoint()
|
|
1908
1889
|
});
|
|
1909
|
-
|
|
1910
|
-
const made_progress = subscribed > 0 ||
|
|
1890
|
+
last_drain = await this._deps.drain(drain_options);
|
|
1891
|
+
const made_progress = subscribed > 0 || last_drain.acked.length > 0 || last_drain.blocked.length > 0;
|
|
1911
1892
|
if (!made_progress) break;
|
|
1912
1893
|
}
|
|
1913
|
-
if (
|
|
1894
|
+
if (last_drain) this._deps.on_settled(last_drain);
|
|
1914
1895
|
})().catch((err) => this._deps.logger.error(err)).finally(() => {
|
|
1915
1896
|
this._running = false;
|
|
1916
1897
|
});
|
|
@@ -1997,7 +1978,7 @@ var Act = class {
|
|
|
1997
1978
|
_event_to_state;
|
|
1998
1979
|
/**
|
|
1999
1980
|
* Event-name → lane fan-in for selective arming (ACT-1103). Built by
|
|
2000
|
-
* `
|
|
1981
|
+
* `classify_registry` once per build. `"all"` means at least one of
|
|
2001
1982
|
* the event's reactions is a dynamic resolver (lane opaque until
|
|
2002
1983
|
* runtime); a `Set<string>` lists the static lanes only that event's
|
|
2003
1984
|
* reactions target.
|
|
@@ -2020,9 +2001,9 @@ var Act = class {
|
|
|
2020
2001
|
_scoped;
|
|
2021
2002
|
/**
|
|
2022
2003
|
* Correlation-id generator for originating actions. Bound at
|
|
2023
|
-
* construction from `options.correlator ??
|
|
2004
|
+
* construction from `options.correlator ?? default_correlator`. The
|
|
2024
2005
|
* `do()` path passes this into the `_es.action` closure; close-cycle
|
|
2025
|
-
* uses it via {@link
|
|
2006
|
+
* uses it via {@link close_correlation}.
|
|
2026
2007
|
*/
|
|
2027
2008
|
_correlator;
|
|
2028
2009
|
/** Pre-bound IAct methods reused across drain cycles. Only `do` varies per
|
|
@@ -2032,7 +2013,7 @@ var Act = class {
|
|
|
2032
2013
|
_bound_query = this.query.bind(this);
|
|
2033
2014
|
_bound_query_array = this.query_array.bind(this);
|
|
2034
2015
|
_bound_forget = this.forget.bind(this);
|
|
2035
|
-
/** Reaction dispatchers built once and handed to
|
|
2016
|
+
/** Reaction dispatchers built once and handed to run_drain_cycle each cycle. */
|
|
2036
2017
|
_handle;
|
|
2037
2018
|
_handle_batch;
|
|
2038
2019
|
/** Declared drain lanes (ACT-1103). */
|
|
@@ -2049,16 +2030,16 @@ var Act = class {
|
|
|
2049
2030
|
*
|
|
2050
2031
|
* @param registry Schemas for every event and action across registered states
|
|
2051
2032
|
* @param states Merged map of state name → state definition
|
|
2052
|
-
* @param
|
|
2033
|
+
* @param batch_handlers Static-target projection batch handlers (target → handler)
|
|
2053
2034
|
* @param options Tuning knobs — see {@link ActOptions}
|
|
2054
2035
|
* @param lanes Declared drain lanes (ACT-1103). The builder collects
|
|
2055
2036
|
* these from `.withLane(...)` calls. Slice 1 records them on the
|
|
2056
2037
|
* instance; later slices fan out one `DrainController` per lane.
|
|
2057
2038
|
*/
|
|
2058
|
-
constructor(registry, states = /* @__PURE__ */ new Map(),
|
|
2039
|
+
constructor(registry, states = /* @__PURE__ */ new Map(), batch_handlers = /* @__PURE__ */ new Map(), options = {}, lanes = []) {
|
|
2059
2040
|
this.registry = registry;
|
|
2060
2041
|
this._states = states;
|
|
2061
|
-
this._batch_handlers =
|
|
2042
|
+
this._batch_handlers = batch_handlers;
|
|
2062
2043
|
this._lanes = lanes;
|
|
2063
2044
|
if (options.onlyLanes && options.onlyLanes.length > 0) {
|
|
2064
2045
|
const declared = /* @__PURE__ */ new Set([
|
|
@@ -2072,10 +2053,10 @@ var Act = class {
|
|
|
2072
2053
|
);
|
|
2073
2054
|
}
|
|
2074
2055
|
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 =
|
|
2056
|
+
this._correlator = options.correlator ?? default_correlator;
|
|
2057
|
+
this._es = build_es(this._logger, this._correlator);
|
|
2058
|
+
this._cd = build_drain(this._logger);
|
|
2059
|
+
this._handle = build_handle({
|
|
2079
2060
|
logger: this._logger,
|
|
2080
2061
|
bound_do: this._bound_do,
|
|
2081
2062
|
bound_load: this._bound_load,
|
|
@@ -2083,39 +2064,39 @@ var Act = class {
|
|
|
2083
2064
|
bound_query_array: this._bound_query_array,
|
|
2084
2065
|
bound_forget: this._bound_forget
|
|
2085
2066
|
});
|
|
2086
|
-
this._handle_batch =
|
|
2067
|
+
this._handle_batch = build_handle_batch(this._logger);
|
|
2087
2068
|
const {
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
} =
|
|
2094
|
-
this._reactive_events =
|
|
2069
|
+
static_targets,
|
|
2070
|
+
has_dynamic_resolvers,
|
|
2071
|
+
reactive_events,
|
|
2072
|
+
event_to_state,
|
|
2073
|
+
event_to_lanes
|
|
2074
|
+
} = classify_registry(this.registry, this._states);
|
|
2075
|
+
this._reactive_events = reactive_events;
|
|
2095
2076
|
this._listen = options.listen !== false;
|
|
2096
2077
|
this._drain = options.drain !== false;
|
|
2097
|
-
this._event_to_state =
|
|
2098
|
-
this._event_to_lanes =
|
|
2099
|
-
const
|
|
2100
|
-
const
|
|
2101
|
-
const
|
|
2102
|
-
const
|
|
2078
|
+
this._event_to_state = event_to_state;
|
|
2079
|
+
this._event_to_lanes = event_to_lanes;
|
|
2080
|
+
const all_lanes = ["default", ...lanes.map((l) => l.name)];
|
|
2081
|
+
const only_set = options.onlyLanes && options.onlyLanes.length > 0 ? new Set(options.onlyLanes) : void 0;
|
|
2082
|
+
const active_lanes = only_set ? all_lanes.filter((n) => only_set.has(n)) : all_lanes;
|
|
2083
|
+
const single_default_lane = active_lanes.length === 1 && active_lanes[0] === "default";
|
|
2103
2084
|
this._drain_controllers = /* @__PURE__ */ new Map();
|
|
2104
|
-
for (const name of
|
|
2085
|
+
for (const name of active_lanes) {
|
|
2105
2086
|
const cfg = lanes.find((l) => l.name === name);
|
|
2106
2087
|
const controller = new DrainController({
|
|
2107
2088
|
logger: this._logger,
|
|
2108
2089
|
ops: this._cd,
|
|
2109
2090
|
registry: this.registry,
|
|
2110
|
-
|
|
2091
|
+
batch_handlers: this._batch_handlers,
|
|
2111
2092
|
handle: this._handle,
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2093
|
+
handle_batch: this._handle_batch,
|
|
2094
|
+
on_acked: (acked) => this.emit("acked", acked),
|
|
2095
|
+
on_blocked: (blocked) => this.emit("blocked", blocked),
|
|
2115
2096
|
// Pass lane only when a true per-lane controller is active.
|
|
2116
2097
|
// The all-lanes (single default) case keeps lane=undefined so
|
|
2117
2098
|
// adapter SQL collapses to the pre-1103 shape.
|
|
2118
|
-
lane:
|
|
2099
|
+
lane: single_default_lane ? void 0 : name,
|
|
2119
2100
|
defaults: cfg && {
|
|
2120
2101
|
streamLimit: cfg.streamLimit,
|
|
2121
2102
|
leaseMillis: cfg.leaseMillis
|
|
@@ -2128,22 +2109,22 @@ var Act = class {
|
|
|
2128
2109
|
this._audit_deps = {
|
|
2129
2110
|
store,
|
|
2130
2111
|
logger: this._logger,
|
|
2131
|
-
event_to_state
|
|
2112
|
+
event_to_state,
|
|
2132
2113
|
states: this._states,
|
|
2133
|
-
known_events: new Set(
|
|
2114
|
+
known_events: new Set(event_to_state.keys()),
|
|
2134
2115
|
declared_lanes: new Set(this._drain_controllers.keys()),
|
|
2135
|
-
routed_events: new Set(
|
|
2116
|
+
routed_events: new Set(event_to_lanes.keys())
|
|
2136
2117
|
};
|
|
2137
2118
|
this._correlate = new CorrelateCycle(
|
|
2138
2119
|
this.registry,
|
|
2139
|
-
|
|
2140
|
-
|
|
2120
|
+
static_targets,
|
|
2121
|
+
has_dynamic_resolvers,
|
|
2141
2122
|
this._cd,
|
|
2142
2123
|
options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
2143
2124
|
// Cold start: assume drain is needed (historical events may need processing).
|
|
2144
2125
|
// #803: writer-only instances skip the cold-start arm.
|
|
2145
2126
|
() => {
|
|
2146
|
-
if (this._drain && this._reactive_events.size > 0) this.
|
|
2127
|
+
if (this._drain && this._reactive_events.size > 0) this._arm_all();
|
|
2147
2128
|
}
|
|
2148
2129
|
);
|
|
2149
2130
|
this._settle = new SettleLoop(
|
|
@@ -2153,11 +2134,11 @@ var Act = class {
|
|
|
2153
2134
|
checkpoint: () => this._correlate.checkpoint,
|
|
2154
2135
|
correlate: (q) => this.correlate(q),
|
|
2155
2136
|
drain: (o) => this.drain(o),
|
|
2156
|
-
|
|
2137
|
+
on_settled: (drain) => this.emit("settled", drain)
|
|
2157
2138
|
},
|
|
2158
2139
|
options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
|
|
2159
2140
|
);
|
|
2160
|
-
this._notify_disposer = this.
|
|
2141
|
+
this._notify_disposer = this._wire_notify(options.scoped?.store ?? store());
|
|
2161
2142
|
dispose(() => this.shutdown());
|
|
2162
2143
|
}
|
|
2163
2144
|
/** True after the first `shutdown()` call. Guards idempotency. */
|
|
@@ -2191,7 +2172,7 @@ var Act = class {
|
|
|
2191
2172
|
* subscription was made). Errors during subscription are logged but
|
|
2192
2173
|
* never thrown — `notify` is a hint, not a contract.
|
|
2193
2174
|
*/
|
|
2194
|
-
async
|
|
2175
|
+
async _wire_notify(s) {
|
|
2195
2176
|
if (this._reactive_events.size === 0) return void 0;
|
|
2196
2177
|
if (!s.notify) return void 0;
|
|
2197
2178
|
if (!this._listen) return void 0;
|
|
@@ -2200,7 +2181,7 @@ var Act = class {
|
|
|
2200
2181
|
try {
|
|
2201
2182
|
this.emit("notified", notification);
|
|
2202
2183
|
if (this._drain) {
|
|
2203
|
-
const armed = this.
|
|
2184
|
+
const armed = this._arm_for_event_names(
|
|
2204
2185
|
notification.events.map((e) => e.name)
|
|
2205
2186
|
);
|
|
2206
2187
|
if (armed) this._settle.schedule({ debounceMs: 0 });
|
|
@@ -2229,8 +2210,9 @@ var Act = class {
|
|
|
2229
2210
|
* @param action - The name of the action to execute
|
|
2230
2211
|
* @param target - Target specification with stream ID and actor context
|
|
2231
2212
|
* @param payload - Action payload matching the action's schema
|
|
2232
|
-
* @param
|
|
2233
|
-
*
|
|
2213
|
+
* @param options - Per-call dispatch options ({@link DoOptions}) —
|
|
2214
|
+
* `reactingTo` to thread correlation, `correlator` to override the
|
|
2215
|
+
* framework or orchestrator-level correlator for this call only.
|
|
2234
2216
|
* @returns Array of snapshots for all affected states (usually one)
|
|
2235
2217
|
*
|
|
2236
2218
|
* @throws {ValidationError} If payload doesn't match action schema
|
|
@@ -2285,7 +2267,7 @@ var Act = class {
|
|
|
2285
2267
|
* { amount: event.data.items.length }
|
|
2286
2268
|
* );
|
|
2287
2269
|
* // To use a different correlation, pass reactingTo explicitly:
|
|
2288
|
-
* // await app.do("reduceStock", target, payload, customEvent);
|
|
2270
|
+
* // await app.do("reduceStock", target, payload, { reactingTo: customEvent });
|
|
2289
2271
|
* })
|
|
2290
2272
|
* .to("inventory-1")
|
|
2291
2273
|
* .build();
|
|
@@ -2295,25 +2277,24 @@ var Act = class {
|
|
|
2295
2277
|
* @see {@link Snapshot} for return value structure
|
|
2296
2278
|
* @see {@link ValidationError}, {@link InvariantError}, {@link ConcurrencyError}
|
|
2297
2279
|
*/
|
|
2298
|
-
async do(action2, target, payload,
|
|
2280
|
+
async do(action2, target, payload, options) {
|
|
2299
2281
|
return this._scoped(async () => {
|
|
2300
2282
|
const snapshots = await this._es.action(
|
|
2301
2283
|
this.registry.actions[action2],
|
|
2302
2284
|
action2,
|
|
2303
2285
|
target,
|
|
2304
2286
|
payload,
|
|
2305
|
-
|
|
2306
|
-
skipValidation
|
|
2287
|
+
options
|
|
2307
2288
|
);
|
|
2308
2289
|
if (this._reactive_events.size > 0)
|
|
2309
|
-
this.
|
|
2290
|
+
this._arm_for_event_names(
|
|
2310
2291
|
snapshots.map((s) => s.event.name)
|
|
2311
2292
|
);
|
|
2312
2293
|
this.emit("committed", snapshots);
|
|
2313
2294
|
return snapshots;
|
|
2314
2295
|
});
|
|
2315
2296
|
}
|
|
2316
|
-
async load(stateOrName,
|
|
2297
|
+
async load(stateOrName, streamOrTarget, callback, asOf) {
|
|
2317
2298
|
return this._scoped(async () => {
|
|
2318
2299
|
let merged;
|
|
2319
2300
|
if (typeof stateOrName === "string") {
|
|
@@ -2323,7 +2304,12 @@ var Act = class {
|
|
|
2323
2304
|
} else {
|
|
2324
2305
|
merged = this._states.get(stateOrName.name) || stateOrName;
|
|
2325
2306
|
}
|
|
2326
|
-
|
|
2307
|
+
const target = typeof streamOrTarget === "string" ? {
|
|
2308
|
+
stream: streamOrTarget,
|
|
2309
|
+
actor: void 0,
|
|
2310
|
+
asOf
|
|
2311
|
+
} : streamOrTarget;
|
|
2312
|
+
return await this._es.load(merged, target, callback);
|
|
2327
2313
|
});
|
|
2328
2314
|
}
|
|
2329
2315
|
/**
|
|
@@ -2485,28 +2471,28 @@ var Act = class {
|
|
|
2485
2471
|
async drain(options = {}) {
|
|
2486
2472
|
if (!this._drain)
|
|
2487
2473
|
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
2488
|
-
return this._scoped(() => this.
|
|
2474
|
+
return this._scoped(() => this._drain_all(options));
|
|
2489
2475
|
}
|
|
2490
2476
|
/** Arm every active lane controller (ACT-1103). */
|
|
2491
|
-
|
|
2477
|
+
_arm_all() {
|
|
2492
2478
|
for (const c of this._drain_controllers.values()) c.arm();
|
|
2493
2479
|
}
|
|
2494
2480
|
/**
|
|
2495
2481
|
* Arm only the lane controllers whose reactions match the supplied
|
|
2496
2482
|
* event names (ACT-1103 selective arming). Events with any dynamic
|
|
2497
|
-
* resolver fall back to `
|
|
2483
|
+
* resolver fall back to `_arm_all()` via the `"all"` sentinel — the
|
|
2498
2484
|
* resolver's lane isn't known until correlate runs the function.
|
|
2499
2485
|
* Events with no reactions are skipped; `_event_to_lanes` doesn't
|
|
2500
2486
|
* carry them. Returns true when any controller was armed (used by
|
|
2501
2487
|
* the notify handler to decide whether to schedule a settle).
|
|
2502
2488
|
*/
|
|
2503
|
-
|
|
2489
|
+
_arm_for_event_names(names) {
|
|
2504
2490
|
const to_arm = /* @__PURE__ */ new Set();
|
|
2505
2491
|
for (const name of names) {
|
|
2506
2492
|
const set = this._event_to_lanes.get(name);
|
|
2507
2493
|
if (set === void 0) continue;
|
|
2508
2494
|
if (set === ALL_LANES) {
|
|
2509
|
-
this.
|
|
2495
|
+
this._arm_all();
|
|
2510
2496
|
return true;
|
|
2511
2497
|
}
|
|
2512
2498
|
for (const lane of set) to_arm.add(lane);
|
|
@@ -2523,7 +2509,7 @@ var Act = class {
|
|
|
2523
2509
|
* `SKIP LOCKED` keeps cross-controller races safe. Lifecycle events
|
|
2524
2510
|
* (`acked`, `blocked`) may interleave by lane — listeners filter via
|
|
2525
2511
|
* `lease.lane`. */
|
|
2526
|
-
async
|
|
2512
|
+
async _drain_all(options) {
|
|
2527
2513
|
const results = await Promise.all(
|
|
2528
2514
|
[...this._drain_controllers.values()].map((c) => c.drain(options))
|
|
2529
2515
|
);
|
|
@@ -2643,7 +2629,7 @@ var Act = class {
|
|
|
2643
2629
|
* @see {@link stop_correlations} to stop the worker
|
|
2644
2630
|
*/
|
|
2645
2631
|
start_correlations(query = {}, frequency = 1e4, callback) {
|
|
2646
|
-
return this._correlate.
|
|
2632
|
+
return this._correlate.start_polling(query, frequency, callback);
|
|
2647
2633
|
}
|
|
2648
2634
|
/**
|
|
2649
2635
|
* Stops the automatic correlation worker.
|
|
@@ -2663,7 +2649,7 @@ var Act = class {
|
|
|
2663
2649
|
* @see {@link start_correlations}
|
|
2664
2650
|
*/
|
|
2665
2651
|
stop_correlations() {
|
|
2666
|
-
this._correlate.
|
|
2652
|
+
this._correlate.stop_polling();
|
|
2667
2653
|
}
|
|
2668
2654
|
/**
|
|
2669
2655
|
* Cancels any pending or active settle cycle.
|
|
@@ -2709,7 +2695,7 @@ var Act = class {
|
|
|
2709
2695
|
async reset(input) {
|
|
2710
2696
|
return this._scoped(async () => {
|
|
2711
2697
|
const count = await store().reset(input);
|
|
2712
|
-
if (count > 0 && this._reactive_events.size > 0) this.
|
|
2698
|
+
if (count > 0 && this._reactive_events.size > 0) this._arm_all();
|
|
2713
2699
|
return count;
|
|
2714
2700
|
});
|
|
2715
2701
|
}
|
|
@@ -2743,7 +2729,7 @@ var Act = class {
|
|
|
2743
2729
|
async unblock(input) {
|
|
2744
2730
|
return this._scoped(async () => {
|
|
2745
2731
|
const count = await store().unblock(input);
|
|
2746
|
-
if (count > 0 && this._reactive_events.size > 0) this.
|
|
2732
|
+
if (count > 0 && this._reactive_events.size > 0) this._arm_all();
|
|
2747
2733
|
return count;
|
|
2748
2734
|
});
|
|
2749
2735
|
}
|
|
@@ -2969,14 +2955,14 @@ var Act = class {
|
|
|
2969
2955
|
if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
|
|
2970
2956
|
return this._scoped(async () => {
|
|
2971
2957
|
await this.correlate({ limit: 1e3 });
|
|
2972
|
-
const
|
|
2973
|
-
const result = await
|
|
2974
|
-
|
|
2975
|
-
|
|
2958
|
+
const close_actor = { id: "$close", name: "close" };
|
|
2959
|
+
const result = await run_close_cycle(targets, {
|
|
2960
|
+
reactive_events_size: this._reactive_events.size,
|
|
2961
|
+
event_to_state: this._event_to_state,
|
|
2976
2962
|
load: this._es.load,
|
|
2977
2963
|
tombstone: this._es.tombstone,
|
|
2978
2964
|
logger: this._logger,
|
|
2979
|
-
correlation:
|
|
2965
|
+
correlation: close_correlation(this._correlator, close_actor)
|
|
2980
2966
|
});
|
|
2981
2967
|
this.emit("closed", result);
|
|
2982
2968
|
return result;
|
|
@@ -3019,17 +3005,17 @@ var Act = class {
|
|
|
3019
3005
|
};
|
|
3020
3006
|
|
|
3021
3007
|
// src/builders/act-builder.ts
|
|
3022
|
-
function
|
|
3008
|
+
function register_batch_handler(proj, batch_handlers) {
|
|
3023
3009
|
if (!proj.batchHandler || !proj.target) return;
|
|
3024
|
-
const existing =
|
|
3010
|
+
const existing = batch_handlers.get(proj.target);
|
|
3025
3011
|
if (existing && existing !== proj.batchHandler) {
|
|
3026
3012
|
throw new Error(`Duplicate batch handler for target "${proj.target}"`);
|
|
3027
3013
|
}
|
|
3028
|
-
|
|
3014
|
+
batch_handlers.set(proj.target, proj.batchHandler);
|
|
3029
3015
|
}
|
|
3030
|
-
function
|
|
3016
|
+
function validate_lane_references(registry, lanes) {
|
|
3031
3017
|
const declared = /* @__PURE__ */ new Set([DEFAULT_LANE, ...lanes.map((l) => l.name)]);
|
|
3032
|
-
for (const [
|
|
3018
|
+
for (const [event_name, def] of Object.entries(registry.events)) {
|
|
3033
3019
|
const entry = def;
|
|
3034
3020
|
for (const [handlerName, reaction] of entry.reactions) {
|
|
3035
3021
|
const resolver = reaction.resolver;
|
|
@@ -3037,7 +3023,7 @@ function validateLaneReferences(registry, lanes) {
|
|
|
3037
3023
|
const lane = resolver.lane;
|
|
3038
3024
|
if (lane && !declared.has(lane)) {
|
|
3039
3025
|
throw new Error(
|
|
3040
|
-
`Reaction "${handlerName}" on "${
|
|
3026
|
+
`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
3027
|
);
|
|
3042
3028
|
}
|
|
3043
3029
|
}
|
|
@@ -3047,78 +3033,81 @@ function act() {
|
|
|
3047
3033
|
const states = /* @__PURE__ */ new Map();
|
|
3048
3034
|
const _sf = /* @__PURE__ */ new Map();
|
|
3049
3035
|
const _dp = /* @__PURE__ */ new Map();
|
|
3036
|
+
const _de = /* @__PURE__ */ new Map();
|
|
3037
|
+
const EMPTY_DEPRECATED = /* @__PURE__ */ new Set();
|
|
3050
3038
|
const registry = {
|
|
3051
3039
|
actions: {},
|
|
3052
3040
|
events: {},
|
|
3053
|
-
sensitive_fields: (
|
|
3054
|
-
disclosure_predicate: (
|
|
3041
|
+
sensitive_fields: (event_name) => _sf.get(event_name) ?? [],
|
|
3042
|
+
disclosure_predicate: (state_name) => _dp.get(state_name) ?? null,
|
|
3043
|
+
deprecated_events: (state_name) => _de.get(state_name) ?? EMPTY_DEPRECATED
|
|
3055
3044
|
};
|
|
3056
|
-
const
|
|
3057
|
-
const
|
|
3045
|
+
const pending_projections = [];
|
|
3046
|
+
const batch_handlers = /* @__PURE__ */ new Map();
|
|
3058
3047
|
const lanes = [];
|
|
3059
3048
|
let _built = false;
|
|
3060
|
-
const
|
|
3061
|
-
const
|
|
3049
|
+
const finalize_deprecations = () => {
|
|
3050
|
+
const deprecation_summary = [];
|
|
3062
3051
|
for (const state2 of states.values()) {
|
|
3063
|
-
const
|
|
3064
|
-
const deprecated =
|
|
3052
|
+
const event_names = Object.keys(state2.events);
|
|
3053
|
+
const deprecated = deprecated_event_names(event_names);
|
|
3065
3054
|
if (deprecated.size === 0) continue;
|
|
3066
|
-
state2.
|
|
3055
|
+
_de.set(state2.name, deprecated);
|
|
3067
3056
|
for (const name of deprecated) {
|
|
3068
|
-
const current =
|
|
3069
|
-
|
|
3070
|
-
|
|
3057
|
+
const current = current_version_of(name, event_names);
|
|
3058
|
+
deprecation_summary.push({
|
|
3059
|
+
state_name: state2.name,
|
|
3071
3060
|
deprecated: name,
|
|
3072
3061
|
current
|
|
3073
3062
|
});
|
|
3074
3063
|
}
|
|
3075
|
-
for (const [
|
|
3076
|
-
const
|
|
3077
|
-
if (
|
|
3078
|
-
const current =
|
|
3064
|
+
for (const [action_name, handler] of Object.entries(state2.on)) {
|
|
3065
|
+
const static_target = handler?._static_emit;
|
|
3066
|
+
if (static_target && deprecated.has(static_target)) {
|
|
3067
|
+
const current = current_version_of(static_target, event_names);
|
|
3079
3068
|
throw new Error(
|
|
3080
|
-
`Action "${
|
|
3069
|
+
`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
3070
|
);
|
|
3082
3071
|
}
|
|
3083
3072
|
}
|
|
3084
3073
|
}
|
|
3085
|
-
if (
|
|
3086
|
-
const list =
|
|
3087
|
-
(d) => `"${d.deprecated}" (current: "${d.current}", state: "${d.
|
|
3074
|
+
if (deprecation_summary.length > 0) {
|
|
3075
|
+
const list = deprecation_summary.map(
|
|
3076
|
+
(d) => `"${d.deprecated}" (current: "${d.current}", state: "${d.state_name}")`
|
|
3088
3077
|
).join(", ");
|
|
3089
3078
|
log().info(
|
|
3090
|
-
`Act registered ${
|
|
3079
|
+
`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
3080
|
);
|
|
3092
3081
|
}
|
|
3093
3082
|
};
|
|
3094
3083
|
const builder = {
|
|
3095
3084
|
withState: (state2) => {
|
|
3096
|
-
|
|
3085
|
+
register_state(state2, states, registry.actions, registry.events);
|
|
3097
3086
|
return builder;
|
|
3098
3087
|
},
|
|
3099
3088
|
withSlice: (input) => {
|
|
3100
3089
|
for (const s of input.states.values()) {
|
|
3101
|
-
|
|
3090
|
+
register_state(s, states, registry.actions, registry.events);
|
|
3102
3091
|
}
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
for (const
|
|
3106
|
-
const existing = lanes.find((l) => l.name ===
|
|
3092
|
+
merge_event_register(registry.events, input.events);
|
|
3093
|
+
pending_projections.push(...input.projections);
|
|
3094
|
+
for (const slice_lane of input.lanes) {
|
|
3095
|
+
const existing = lanes.find((l) => l.name === slice_lane.name);
|
|
3107
3096
|
if (!existing) {
|
|
3108
|
-
lanes.push(
|
|
3097
|
+
lanes.push(slice_lane);
|
|
3109
3098
|
continue;
|
|
3110
3099
|
}
|
|
3111
|
-
if (existing.leaseMillis !==
|
|
3100
|
+
if (existing.leaseMillis !== slice_lane.leaseMillis || existing.streamLimit !== slice_lane.streamLimit || existing.cycleMs !== slice_lane.cycleMs) {
|
|
3112
3101
|
throw new Error(
|
|
3113
|
-
`Lane "${
|
|
3102
|
+
`Lane "${slice_lane.name}" was already declared with a different config`
|
|
3114
3103
|
);
|
|
3115
3104
|
}
|
|
3116
3105
|
}
|
|
3117
3106
|
return builder;
|
|
3118
3107
|
},
|
|
3119
3108
|
withProjection: (proj) => {
|
|
3120
|
-
|
|
3121
|
-
|
|
3109
|
+
merge_projection(proj, registry.events);
|
|
3110
|
+
register_batch_handler(proj, batch_handlers);
|
|
3122
3111
|
return builder;
|
|
3123
3112
|
},
|
|
3124
3113
|
withActor: () => builder,
|
|
@@ -3156,18 +3145,18 @@ function act() {
|
|
|
3156
3145
|
}),
|
|
3157
3146
|
build: (options) => {
|
|
3158
3147
|
if (!_built) {
|
|
3159
|
-
for (const proj of
|
|
3160
|
-
|
|
3161
|
-
|
|
3148
|
+
for (const proj of pending_projections) {
|
|
3149
|
+
merge_projection(proj, registry.events);
|
|
3150
|
+
register_batch_handler(proj, batch_handlers);
|
|
3162
3151
|
}
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
for (const [
|
|
3152
|
+
finalize_deprecations();
|
|
3153
|
+
validate_lane_references(registry, lanes);
|
|
3154
|
+
for (const [event_name, reg] of Object.entries(
|
|
3166
3155
|
registry.events
|
|
3167
3156
|
)) {
|
|
3168
3157
|
const fields = pii_fields(reg.schema);
|
|
3169
3158
|
if (fields.length === 0) continue;
|
|
3170
|
-
_sf.set(
|
|
3159
|
+
_sf.set(event_name, fields);
|
|
3171
3160
|
for (const [name, reaction] of reg.reactions) {
|
|
3172
3161
|
const inner = reaction.handler;
|
|
3173
3162
|
const wrapped = (event, stream, app) => inner(pii_strip(event, fields), stream, app);
|
|
@@ -3179,9 +3168,9 @@ function act() {
|
|
|
3179
3168
|
for (const state2 of states.values()) {
|
|
3180
3169
|
if (state2.disclose) _dp.set(state2.name, state2.disclose);
|
|
3181
3170
|
const fields_by_event = /* @__PURE__ */ new Map();
|
|
3182
|
-
for (const
|
|
3183
|
-
const fields = _sf.get(
|
|
3184
|
-
if (fields) fields_by_event.set(
|
|
3171
|
+
for (const event_name of Object.keys(state2.events)) {
|
|
3172
|
+
const fields = _sf.get(event_name);
|
|
3173
|
+
if (fields) fields_by_event.set(event_name, fields);
|
|
3185
3174
|
}
|
|
3186
3175
|
if (fields_by_event.size === 0) continue;
|
|
3187
3176
|
if (state2.snap) {
|
|
@@ -3191,20 +3180,19 @@ function act() {
|
|
|
3191
3180
|
);
|
|
3192
3181
|
}
|
|
3193
3182
|
const disclose = state2.disclose ?? null;
|
|
3194
|
-
state2.
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3183
|
+
state2.pii_aware = true;
|
|
3184
|
+
state2.view = (event, actor) => pii_gate(
|
|
3185
|
+
event,
|
|
3186
|
+
fields_by_event.get(event.name) ?? [],
|
|
3187
|
+
disclose,
|
|
3188
|
+
actor
|
|
3189
|
+
);
|
|
3198
3190
|
state2.message = (validated) => {
|
|
3199
3191
|
const fields = fields_by_event.get(validated.name);
|
|
3200
3192
|
return fields ? pii_split(validated, fields) : validated;
|
|
3201
3193
|
};
|
|
3202
|
-
for (const [eventName, fields] of fields_by_event) {
|
|
3203
|
-
const original = state2.patch[eventName];
|
|
3204
|
-
state2.patch[eventName] = (event, s) => original(pii_merge(event, fields), s);
|
|
3205
|
-
}
|
|
3206
3194
|
}
|
|
3207
|
-
for (const [target, original] of
|
|
3195
|
+
for (const [target, original] of batch_handlers) {
|
|
3208
3196
|
const wrapped = async (events, stream) => {
|
|
3209
3197
|
const stripped = events.map((e) => {
|
|
3210
3198
|
const f = _sf.get(e.name);
|
|
@@ -3212,14 +3200,14 @@ function act() {
|
|
|
3212
3200
|
});
|
|
3213
3201
|
return original(stripped, stream);
|
|
3214
3202
|
};
|
|
3215
|
-
|
|
3203
|
+
batch_handlers.set(target, wrapped);
|
|
3216
3204
|
}
|
|
3217
3205
|
_built = true;
|
|
3218
3206
|
}
|
|
3219
3207
|
return new Act(
|
|
3220
3208
|
registry,
|
|
3221
3209
|
states,
|
|
3222
|
-
|
|
3210
|
+
batch_handlers,
|
|
3223
3211
|
options,
|
|
3224
3212
|
lanes
|
|
3225
3213
|
);
|
|
@@ -3232,7 +3220,7 @@ function act() {
|
|
|
3232
3220
|
// src/builders/projection-builder.ts
|
|
3233
3221
|
function _projection(target) {
|
|
3234
3222
|
const events = {};
|
|
3235
|
-
const
|
|
3223
|
+
const default_resolver = typeof target === "string" ? { target } : void 0;
|
|
3236
3224
|
const base = {
|
|
3237
3225
|
on: (entry) => {
|
|
3238
3226
|
const keys = Object.keys(entry);
|
|
@@ -3249,7 +3237,7 @@ function _projection(target) {
|
|
|
3249
3237
|
do: (handler) => {
|
|
3250
3238
|
const reaction = {
|
|
3251
3239
|
handler,
|
|
3252
|
-
resolver:
|
|
3240
|
+
resolver: default_resolver ?? _this_,
|
|
3253
3241
|
options: {
|
|
3254
3242
|
blockOnError: true,
|
|
3255
3243
|
maxRetries: 3
|
|
@@ -3305,7 +3293,7 @@ function slice() {
|
|
|
3305
3293
|
const lanes = [];
|
|
3306
3294
|
const builder = {
|
|
3307
3295
|
withState: (state2) => {
|
|
3308
|
-
|
|
3296
|
+
register_state(state2, states, actions, events);
|
|
3309
3297
|
return builder;
|
|
3310
3298
|
},
|
|
3311
3299
|
withProjection: (proj) => {
|
|
@@ -3361,12 +3349,12 @@ function state(entry) {
|
|
|
3361
3349
|
const keys = Object.keys(entry);
|
|
3362
3350
|
if (keys.length !== 1) throw new Error("state() requires exactly one key");
|
|
3363
3351
|
const name = keys[0];
|
|
3364
|
-
const
|
|
3352
|
+
const state_schema = entry[name];
|
|
3365
3353
|
return {
|
|
3366
3354
|
init(init) {
|
|
3367
3355
|
return {
|
|
3368
3356
|
emits(events) {
|
|
3369
|
-
const
|
|
3357
|
+
const default_patch = Object.fromEntries(
|
|
3370
3358
|
Object.keys(events).map((k) => {
|
|
3371
3359
|
const fn = Object.assign(({ data }) => data, {
|
|
3372
3360
|
_passthrough: true
|
|
@@ -3377,14 +3365,15 @@ function state(entry) {
|
|
|
3377
3365
|
const internal = {
|
|
3378
3366
|
events,
|
|
3379
3367
|
actions: {},
|
|
3380
|
-
state:
|
|
3368
|
+
state: state_schema,
|
|
3381
3369
|
name,
|
|
3382
3370
|
init,
|
|
3383
|
-
patch:
|
|
3371
|
+
patch: default_patch,
|
|
3384
3372
|
on: {},
|
|
3385
|
-
// Step delegates initialized as identity
|
|
3386
|
-
//
|
|
3387
|
-
// the gate / split.
|
|
3373
|
+
// Step delegates initialized as identity, `pii_aware` false.
|
|
3374
|
+
// `act().build()` flips both on states with `sensitive(...)`
|
|
3375
|
+
// events to bake in the gate / split and gate the cache write.
|
|
3376
|
+
pii_aware: false,
|
|
3388
3377
|
view: (event) => event,
|
|
3389
3378
|
message: (validated) => validated
|
|
3390
3379
|
};
|
|
@@ -3422,11 +3411,14 @@ function action_builder(state2) {
|
|
|
3422
3411
|
}
|
|
3423
3412
|
function emit(handler) {
|
|
3424
3413
|
if (typeof handler === "string") {
|
|
3425
|
-
const
|
|
3426
|
-
const
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3414
|
+
const event_name = handler;
|
|
3415
|
+
const emit_fn = Object.assign(
|
|
3416
|
+
(payload) => [event_name, payload],
|
|
3417
|
+
{
|
|
3418
|
+
_static_emit: event_name
|
|
3419
|
+
}
|
|
3420
|
+
);
|
|
3421
|
+
internal.on[action2] = emit_fn;
|
|
3430
3422
|
} else {
|
|
3431
3423
|
internal.on[action2] = handler;
|
|
3432
3424
|
}
|
|
@@ -3479,7 +3471,7 @@ var CsvFile = class {
|
|
|
3479
3471
|
let header = null;
|
|
3480
3472
|
for await (const line of lines) {
|
|
3481
3473
|
if (!line.trim()) continue;
|
|
3482
|
-
const fields =
|
|
3474
|
+
const fields = parse_csv_line(line);
|
|
3483
3475
|
if (!header) {
|
|
3484
3476
|
header = fields;
|
|
3485
3477
|
const expected = CSV_COLUMNS.join(",");
|
|
@@ -3516,21 +3508,21 @@ var CsvFile = class {
|
|
|
3516
3508
|
flags: "w",
|
|
3517
3509
|
encoding: "utf8"
|
|
3518
3510
|
});
|
|
3519
|
-
let
|
|
3511
|
+
let next_id = 1;
|
|
3520
3512
|
try {
|
|
3521
|
-
await
|
|
3513
|
+
await write_line(writer, CSV_COLUMNS.join(","));
|
|
3522
3514
|
await driver(async (event) => {
|
|
3523
|
-
const id =
|
|
3515
|
+
const id = next_id++;
|
|
3524
3516
|
const row = [
|
|
3525
3517
|
String(id),
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3518
|
+
csv_escape(event.name),
|
|
3519
|
+
csv_escape(JSON.stringify(event.data)),
|
|
3520
|
+
csv_escape(event.stream),
|
|
3529
3521
|
String(event.version),
|
|
3530
3522
|
event.created.toISOString(),
|
|
3531
|
-
|
|
3523
|
+
csv_escape(JSON.stringify(event.meta))
|
|
3532
3524
|
].join(",");
|
|
3533
|
-
await
|
|
3525
|
+
await write_line(writer, row);
|
|
3534
3526
|
return id;
|
|
3535
3527
|
});
|
|
3536
3528
|
} finally {
|
|
@@ -3563,7 +3555,7 @@ async function* linesFromBlob(blob) {
|
|
|
3563
3555
|
await Promise.resolve();
|
|
3564
3556
|
}
|
|
3565
3557
|
}
|
|
3566
|
-
function
|
|
3558
|
+
function parse_csv_line(line) {
|
|
3567
3559
|
const fields = [];
|
|
3568
3560
|
let i = 0;
|
|
3569
3561
|
while (i < line.length) {
|
|
@@ -3596,11 +3588,11 @@ function parseCsvLine(line) {
|
|
|
3596
3588
|
}
|
|
3597
3589
|
return fields;
|
|
3598
3590
|
}
|
|
3599
|
-
function
|
|
3591
|
+
function csv_escape(value) {
|
|
3600
3592
|
if (/[",\n\r]/.test(value)) return `"${value.replace(/"/g, '""')}"`;
|
|
3601
3593
|
return value;
|
|
3602
3594
|
}
|
|
3603
|
-
function
|
|
3595
|
+
function write_line(writer, line) {
|
|
3604
3596
|
return new Promise((resolve, reject) => {
|
|
3605
3597
|
writer.write(`${line}
|
|
3606
3598
|
`, (err) => {
|