@rotorsoft/act 1.9.0 → 1.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/act.d.ts +46 -14
- 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/state-builder.d.ts +33 -3
- 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 -4
- 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 -7
- package/dist/@types/internal/index.d.ts.map +1 -1
- package/dist/@types/internal/merge.d.ts +3 -3
- package/dist/@types/internal/merge.d.ts.map +1 -1
- package/dist/@types/internal/reactions.d.ts +11 -10
- package/dist/@types/internal/reactions.d.ts.map +1 -1
- package/dist/@types/internal/sensitive.d.ts +147 -0
- package/dist/@types/internal/sensitive.d.ts.map +1 -0
- 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 +57 -0
- package/dist/@types/types/action.d.ts.map +1 -1
- package/dist/@types/types/registry.d.ts +9 -1
- package/dist/@types/types/registry.d.ts.map +1 -1
- package/dist/@types/types/schemas.d.ts +36 -0
- package/dist/@types/types/schemas.d.ts.map +1 -1
- package/dist/{chunk-F4S2JOPN.js → chunk-3Z2HU726.js} +134 -133
- package/dist/chunk-3Z2HU726.js.map +1 -0
- package/dist/chunk-BY5JPOZR.js +267 -0
- package/dist/chunk-BY5JPOZR.js.map +1 -0
- package/dist/index.cjs +835 -616
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +567 -439
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs +194 -187
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.js +11 -11
- package/dist/test/index.js.map +1 -1
- package/dist/types/index.cjs +52 -34
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.js +9 -3
- package/package.json +3 -3
- package/dist/chunk-F4S2JOPN.js.map +0 -1
- package/dist/chunk-PMAZTOSO.js +0 -164
- package/dist/chunk-PMAZTOSO.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
sleep,
|
|
20
20
|
store,
|
|
21
21
|
validate
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-3Z2HU726.js";
|
|
23
23
|
import {
|
|
24
24
|
ActorSchema,
|
|
25
25
|
CausationEventSchema,
|
|
@@ -32,11 +32,19 @@ import {
|
|
|
32
32
|
LogLevels,
|
|
33
33
|
NonRetryableError,
|
|
34
34
|
QuerySchema,
|
|
35
|
+
REDACTED,
|
|
36
|
+
SHREDDED,
|
|
35
37
|
StreamClosedError,
|
|
36
38
|
TargetSchema,
|
|
37
39
|
ValidationError,
|
|
38
|
-
ZodEmpty
|
|
39
|
-
|
|
40
|
+
ZodEmpty,
|
|
41
|
+
pii_fields,
|
|
42
|
+
pii_gate,
|
|
43
|
+
pii_merge,
|
|
44
|
+
pii_split,
|
|
45
|
+
pii_strip,
|
|
46
|
+
sensitive
|
|
47
|
+
} from "./chunk-BY5JPOZR.js";
|
|
40
48
|
import "./chunk-5WRI5ZAA.js";
|
|
41
49
|
|
|
42
50
|
// src/signals.ts
|
|
@@ -70,7 +78,7 @@ function parse(name) {
|
|
|
70
78
|
}
|
|
71
79
|
return { base: name, version: 1 };
|
|
72
80
|
}
|
|
73
|
-
function
|
|
81
|
+
function deprecated_event_names(names) {
|
|
74
82
|
const groups = /* @__PURE__ */ new Map();
|
|
75
83
|
for (const name of names) {
|
|
76
84
|
const { base, version } = parse(name);
|
|
@@ -86,10 +94,10 @@ function deprecatedEventNames(names) {
|
|
|
86
94
|
}
|
|
87
95
|
return deprecated;
|
|
88
96
|
}
|
|
89
|
-
function
|
|
90
|
-
const target = parse(
|
|
97
|
+
function current_version_of(deprecated_name, all_names) {
|
|
98
|
+
const target = parse(deprecated_name);
|
|
91
99
|
let highest;
|
|
92
|
-
for (const name of
|
|
100
|
+
for (const name of all_names) {
|
|
93
101
|
const { base, version } = parse(name);
|
|
94
102
|
if (base !== target.base) continue;
|
|
95
103
|
if (!highest || version > highest.version) highest = { version, name };
|
|
@@ -119,27 +127,27 @@ var ALL_CATEGORIES = [
|
|
|
119
127
|
];
|
|
120
128
|
async function* audit(deps, categories, options = {}) {
|
|
121
129
|
const requested = new Set(categories ?? [...ALL_CATEGORIES]);
|
|
122
|
-
const
|
|
123
|
-
const passes =
|
|
130
|
+
const ordered_categories = ALL_CATEGORIES.filter((c) => requested.has(c));
|
|
131
|
+
const passes = ordered_categories.map(
|
|
124
132
|
(c) => PASS_FACTORIES[c](deps, options)
|
|
125
133
|
);
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
if (
|
|
134
|
+
const need_stats = passes.some((p) => p.on_stat !== void 0);
|
|
135
|
+
const need_streams = passes.some((p) => p.on_stream !== void 0);
|
|
136
|
+
const need_events = passes.some((p) => p.on_event !== void 0);
|
|
137
|
+
if (need_stats) {
|
|
130
138
|
const stats = await deps.store().query_stats({}, { count: true, names: true });
|
|
131
139
|
for (const [stream, s] of stats) {
|
|
132
|
-
for (const p of passes) p.
|
|
140
|
+
for (const p of passes) p.on_stat?.(stream, s);
|
|
133
141
|
}
|
|
134
142
|
}
|
|
135
|
-
if (
|
|
143
|
+
if (need_streams) {
|
|
136
144
|
await deps.store().query_streams((pos) => {
|
|
137
|
-
for (const p of passes) p.
|
|
145
|
+
for (const p of passes) p.on_stream?.(pos);
|
|
138
146
|
});
|
|
139
147
|
}
|
|
140
|
-
if (
|
|
148
|
+
if (need_events) {
|
|
141
149
|
await deps.store().query((event) => {
|
|
142
|
-
for (const p of passes) p.
|
|
150
|
+
for (const p of passes) p.on_event?.(event);
|
|
143
151
|
}, options.query);
|
|
144
152
|
}
|
|
145
153
|
for (const p of passes) await p.finalize?.(deps);
|
|
@@ -147,11 +155,11 @@ async function* audit(deps, categories, options = {}) {
|
|
|
147
155
|
for (const f of p.drain()) yield f;
|
|
148
156
|
}
|
|
149
157
|
}
|
|
150
|
-
var
|
|
158
|
+
var make_schema_pass = (deps) => {
|
|
151
159
|
const findings = [];
|
|
152
160
|
return {
|
|
153
161
|
category: "schema",
|
|
154
|
-
|
|
162
|
+
on_event(event) {
|
|
155
163
|
const name = String(event.name);
|
|
156
164
|
const state2 = deps.event_to_state.get(name);
|
|
157
165
|
if (!state2) {
|
|
@@ -181,19 +189,19 @@ var makeSchemaPass = (deps) => {
|
|
|
181
189
|
drain: () => findings
|
|
182
190
|
};
|
|
183
191
|
};
|
|
184
|
-
var
|
|
192
|
+
var make_deprecated_load_pass = (deps, options) => {
|
|
185
193
|
const share_min = options.thresholds?.deprecated_min ?? DEFAULTS.deprecated_min;
|
|
186
194
|
const totals = /* @__PURE__ */ new Map();
|
|
187
|
-
const
|
|
195
|
+
const per_stream = /* @__PURE__ */ new Map();
|
|
188
196
|
return {
|
|
189
197
|
category: "deprecated-load",
|
|
190
|
-
|
|
198
|
+
on_stat(stream, { names }) {
|
|
191
199
|
for (const [name, count] of Object.entries(names)) {
|
|
192
200
|
totals.set(name, (totals.get(name) ?? 0) + count);
|
|
193
|
-
let m =
|
|
201
|
+
let m = per_stream.get(name);
|
|
194
202
|
if (!m) {
|
|
195
203
|
m = /* @__PURE__ */ new Map();
|
|
196
|
-
|
|
204
|
+
per_stream.set(name, m);
|
|
197
205
|
}
|
|
198
206
|
m.set(stream, count);
|
|
199
207
|
}
|
|
@@ -202,33 +210,33 @@ var makeDeprecatedLoadPass = (deps, options) => {
|
|
|
202
210
|
const findings = [];
|
|
203
211
|
const grand = [...totals.values()].reduce((s, n) => s + n, 0);
|
|
204
212
|
if (grand === 0) return findings;
|
|
205
|
-
const deprecated =
|
|
213
|
+
const deprecated = deprecated_event_names(deps.known_events);
|
|
206
214
|
const sorted = [...deprecated].map((name) => ({ name, count: totals.get(name) ?? 0 })).sort((a, b) => b.count - a.count);
|
|
207
215
|
for (const { name, count } of sorted) {
|
|
208
216
|
if (count === 0) continue;
|
|
209
217
|
if (count / grand < share_min) continue;
|
|
210
|
-
const
|
|
211
|
-
const
|
|
218
|
+
const current_version = current_version_of(name, deps.known_events);
|
|
219
|
+
const top_streams = [...per_stream.get(name).entries()].map(([stream, c]) => ({ stream, count: c })).sort((a, b) => b.count - a.count).slice(0, 10);
|
|
212
220
|
findings.push({
|
|
213
221
|
category: "deprecated-load",
|
|
214
222
|
name,
|
|
215
|
-
current_version
|
|
223
|
+
current_version,
|
|
216
224
|
total: count,
|
|
217
|
-
top_streams
|
|
225
|
+
top_streams
|
|
218
226
|
});
|
|
219
227
|
}
|
|
220
228
|
return findings;
|
|
221
229
|
}
|
|
222
230
|
};
|
|
223
231
|
};
|
|
224
|
-
var
|
|
232
|
+
var make_close_candidate_pass = (deps, options) => {
|
|
225
233
|
const idle_days = options.thresholds?.idle_days ?? DEFAULTS.idle_days;
|
|
226
234
|
const terminal_events = new Set(options.thresholds?.terminal_events ?? []);
|
|
227
235
|
const idle_cutoff = Date.now() - idle_days * 24 * 60 * 60 * 1e3;
|
|
228
236
|
const findings = [];
|
|
229
237
|
return {
|
|
230
238
|
category: "close-candidate",
|
|
231
|
-
|
|
239
|
+
on_stat(stream, { head }) {
|
|
232
240
|
const head_name = String(head.name);
|
|
233
241
|
if (head_name.startsWith("__")) return;
|
|
234
242
|
const head_time = head.created.getTime();
|
|
@@ -241,22 +249,22 @@ var makeCloseCandidatePass = (deps, options) => {
|
|
|
241
249
|
last_event_at: head.created.toISOString(),
|
|
242
250
|
reason: is_terminal ? "terminal" : "idle",
|
|
243
251
|
idle_days: is_idle ? Math.floor((Date.now() - head_time) / (24 * 60 * 60 * 1e3)) : void 0,
|
|
244
|
-
restart_supported:
|
|
252
|
+
restart_supported: restart_is_supported(deps, head_name)
|
|
245
253
|
});
|
|
246
254
|
},
|
|
247
255
|
drain: () => findings
|
|
248
256
|
};
|
|
249
257
|
};
|
|
250
|
-
var
|
|
258
|
+
var make_restart_candidate_pass = (deps, options) => {
|
|
251
259
|
const threshold = options.thresholds?.restart_min ?? DEFAULTS.restart_min;
|
|
252
260
|
const findings = [];
|
|
253
261
|
return {
|
|
254
262
|
category: "restart-candidate",
|
|
255
|
-
|
|
263
|
+
on_stat(stream, { head, count, names }) {
|
|
256
264
|
if (count < threshold) return;
|
|
257
265
|
const head_name = String(head.name);
|
|
258
266
|
if (head_name.startsWith("__")) return;
|
|
259
|
-
if (!
|
|
267
|
+
if (!restart_is_supported(deps, head_name)) return;
|
|
260
268
|
findings.push({
|
|
261
269
|
category: "restart-candidate",
|
|
262
270
|
stream,
|
|
@@ -270,14 +278,14 @@ var makeRestartCandidatePass = (deps, options) => {
|
|
|
270
278
|
drain: () => findings
|
|
271
279
|
};
|
|
272
280
|
};
|
|
273
|
-
var
|
|
281
|
+
var make_reaction_health_pass = (_deps, options) => {
|
|
274
282
|
const near_block = options.thresholds?.near_block ?? DEFAULTS.near_block;
|
|
275
283
|
const stuck_minutes = options.thresholds?.stuck_minutes ?? DEFAULTS.stuck_minutes;
|
|
276
284
|
const stuck_cutoff = Date.now() - stuck_minutes * 60 * 1e3;
|
|
277
285
|
const findings = [];
|
|
278
286
|
return {
|
|
279
287
|
category: "reaction-health",
|
|
280
|
-
|
|
288
|
+
on_stream(p) {
|
|
281
289
|
if (p.blocked) {
|
|
282
290
|
findings.push({
|
|
283
291
|
category: "reaction-health",
|
|
@@ -314,14 +322,14 @@ var makeReactionHealthPass = (_deps, options) => {
|
|
|
314
322
|
drain: () => findings
|
|
315
323
|
};
|
|
316
324
|
};
|
|
317
|
-
var
|
|
325
|
+
var make_snapshot_drift_pass = (deps, options) => {
|
|
318
326
|
const drift_min = options.thresholds?.drift_min ?? DEFAULTS.drift_min;
|
|
319
327
|
const candidates = [];
|
|
320
328
|
const findings = [];
|
|
321
329
|
return {
|
|
322
330
|
category: "snapshot-drift",
|
|
323
|
-
|
|
324
|
-
if (!
|
|
331
|
+
on_stat(stream, { head, count, names }) {
|
|
332
|
+
if (!restart_is_supported(deps, String(head.name))) return;
|
|
325
333
|
if (count < drift_min) return;
|
|
326
334
|
candidates.push({
|
|
327
335
|
stream,
|
|
@@ -370,12 +378,12 @@ var makeSnapshotDriftPass = (deps, options) => {
|
|
|
370
378
|
drain: () => findings
|
|
371
379
|
};
|
|
372
380
|
};
|
|
373
|
-
var
|
|
381
|
+
var make_routing_health_pass = (deps) => {
|
|
374
382
|
const findings = [];
|
|
375
|
-
const
|
|
383
|
+
const seen_event_names = /* @__PURE__ */ new Set();
|
|
376
384
|
return {
|
|
377
385
|
category: "routing-health",
|
|
378
|
-
|
|
386
|
+
on_stream(p) {
|
|
379
387
|
if (!p.lane) return;
|
|
380
388
|
if (deps.declared_lanes.has(p.lane)) return;
|
|
381
389
|
findings.push({
|
|
@@ -385,13 +393,13 @@ var makeRoutingHealthPass = (deps) => {
|
|
|
385
393
|
lane: p.lane
|
|
386
394
|
});
|
|
387
395
|
},
|
|
388
|
-
|
|
396
|
+
on_stat(_stream, { names }) {
|
|
389
397
|
for (const name of Object.keys(names)) {
|
|
390
|
-
|
|
398
|
+
seen_event_names.add(name);
|
|
391
399
|
}
|
|
392
400
|
},
|
|
393
401
|
finalize() {
|
|
394
|
-
for (const name of
|
|
402
|
+
for (const name of seen_event_names) {
|
|
395
403
|
if (name.startsWith("__")) continue;
|
|
396
404
|
if (deps.routed_events.has(name)) continue;
|
|
397
405
|
findings.push({
|
|
@@ -405,23 +413,23 @@ var makeRoutingHealthPass = (deps) => {
|
|
|
405
413
|
drain: () => findings
|
|
406
414
|
};
|
|
407
415
|
};
|
|
408
|
-
var
|
|
409
|
-
const
|
|
416
|
+
var make_correlation_gaps_pass = () => {
|
|
417
|
+
const seen_ids = /* @__PURE__ */ new Set();
|
|
410
418
|
const checks = [];
|
|
411
419
|
return {
|
|
412
420
|
category: "correlation-gaps",
|
|
413
|
-
|
|
414
|
-
|
|
421
|
+
on_event(e) {
|
|
422
|
+
seen_ids.add(e.id);
|
|
415
423
|
const causation = e.meta?.causation;
|
|
416
|
-
const
|
|
417
|
-
if (
|
|
418
|
-
checks.push({ stream: e.stream, id: e.id,
|
|
424
|
+
const parent_id = causation?.event?.id;
|
|
425
|
+
if (parent_id !== void 0) {
|
|
426
|
+
checks.push({ stream: e.stream, id: e.id, parent_id });
|
|
419
427
|
}
|
|
420
428
|
},
|
|
421
429
|
drain() {
|
|
422
430
|
const findings = [];
|
|
423
|
-
for (const { stream, id,
|
|
424
|
-
if (!
|
|
431
|
+
for (const { stream, id, parent_id } of checks) {
|
|
432
|
+
if (!seen_ids.has(parent_id)) {
|
|
425
433
|
findings.push({
|
|
426
434
|
category: "correlation-gaps",
|
|
427
435
|
stream,
|
|
@@ -434,12 +442,12 @@ var makeCorrelationGapsPass = () => {
|
|
|
434
442
|
}
|
|
435
443
|
};
|
|
436
444
|
};
|
|
437
|
-
var
|
|
445
|
+
var make_clock_anomalies_pass = () => {
|
|
438
446
|
const findings = [];
|
|
439
|
-
const
|
|
447
|
+
const last_per_stream = /* @__PURE__ */ new Map();
|
|
440
448
|
return {
|
|
441
449
|
category: "clock-anomalies",
|
|
442
|
-
|
|
450
|
+
on_event(e) {
|
|
443
451
|
const created = e.created.getTime();
|
|
444
452
|
if (created > Date.now()) {
|
|
445
453
|
findings.push({
|
|
@@ -449,7 +457,7 @@ var makeClockAnomaliesPass = () => {
|
|
|
449
457
|
reason: "future-created"
|
|
450
458
|
});
|
|
451
459
|
}
|
|
452
|
-
const prev =
|
|
460
|
+
const prev = last_per_stream.get(e.stream);
|
|
453
461
|
if (prev !== void 0 && created < prev) {
|
|
454
462
|
findings.push({
|
|
455
463
|
category: "clock-anomalies",
|
|
@@ -458,48 +466,48 @@ var makeClockAnomaliesPass = () => {
|
|
|
458
466
|
reason: "out-of-order"
|
|
459
467
|
});
|
|
460
468
|
}
|
|
461
|
-
|
|
469
|
+
last_per_stream.set(e.stream, created);
|
|
462
470
|
},
|
|
463
471
|
drain: () => findings
|
|
464
472
|
};
|
|
465
473
|
};
|
|
466
|
-
function
|
|
467
|
-
const state2 = deps.event_to_state.get(
|
|
474
|
+
function restart_is_supported(deps, head_event_name) {
|
|
475
|
+
const state2 = deps.event_to_state.get(head_event_name);
|
|
468
476
|
return state2?.snap !== void 0;
|
|
469
477
|
}
|
|
470
478
|
var PASS_FACTORIES = {
|
|
471
|
-
schema:
|
|
472
|
-
"deprecated-load":
|
|
473
|
-
"close-candidate":
|
|
474
|
-
"restart-candidate":
|
|
475
|
-
"reaction-health":
|
|
476
|
-
"snapshot-drift":
|
|
477
|
-
"routing-health":
|
|
478
|
-
"correlation-gaps":
|
|
479
|
-
"clock-anomalies":
|
|
479
|
+
schema: make_schema_pass,
|
|
480
|
+
"deprecated-load": make_deprecated_load_pass,
|
|
481
|
+
"close-candidate": make_close_candidate_pass,
|
|
482
|
+
"restart-candidate": make_restart_candidate_pass,
|
|
483
|
+
"reaction-health": make_reaction_health_pass,
|
|
484
|
+
"snapshot-drift": make_snapshot_drift_pass,
|
|
485
|
+
"routing-health": make_routing_health_pass,
|
|
486
|
+
"correlation-gaps": make_correlation_gaps_pass,
|
|
487
|
+
"clock-anomalies": make_clock_anomalies_pass
|
|
480
488
|
};
|
|
481
489
|
|
|
482
490
|
// src/internal/build-classify.ts
|
|
483
491
|
var ALL_LANES = /* @__PURE__ */ Symbol("act-1103/all-lanes");
|
|
484
|
-
function
|
|
492
|
+
function classify_registry(registry, states) {
|
|
485
493
|
const statics = /* @__PURE__ */ new Map();
|
|
486
|
-
const
|
|
487
|
-
const
|
|
488
|
-
let
|
|
494
|
+
const reactive_events = /* @__PURE__ */ new Set();
|
|
495
|
+
const event_to_lanes = /* @__PURE__ */ new Map();
|
|
496
|
+
let has_dynamic_resolvers = false;
|
|
489
497
|
for (const [name, register] of Object.entries(registry.events)) {
|
|
490
|
-
if (register.reactions.size > 0)
|
|
498
|
+
if (register.reactions.size > 0) reactive_events.add(name);
|
|
491
499
|
for (const reaction of register.reactions.values()) {
|
|
492
500
|
if (typeof reaction.resolver === "function") {
|
|
493
|
-
|
|
494
|
-
|
|
501
|
+
has_dynamic_resolvers = true;
|
|
502
|
+
event_to_lanes.set(name, ALL_LANES);
|
|
495
503
|
} else {
|
|
496
504
|
const { target, source, priority = 0, lane } = reaction.resolver;
|
|
497
505
|
const lane_name = lane ?? "default";
|
|
498
|
-
const existing_lanes =
|
|
506
|
+
const existing_lanes = event_to_lanes.get(name);
|
|
499
507
|
if (existing_lanes !== ALL_LANES) {
|
|
500
508
|
const set = existing_lanes ?? /* @__PURE__ */ new Set();
|
|
501
509
|
set.add(lane_name);
|
|
502
|
-
|
|
510
|
+
event_to_lanes.set(name, set);
|
|
503
511
|
}
|
|
504
512
|
const key = `${target}|${source ?? ""}`;
|
|
505
513
|
const existing = statics.get(key);
|
|
@@ -517,59 +525,59 @@ function classifyRegistry(registry, states) {
|
|
|
517
525
|
}
|
|
518
526
|
}
|
|
519
527
|
}
|
|
520
|
-
const
|
|
528
|
+
const event_to_state = /* @__PURE__ */ new Map();
|
|
521
529
|
for (const merged of states.values()) {
|
|
522
|
-
for (const
|
|
523
|
-
|
|
530
|
+
for (const event_name of Object.keys(merged.events)) {
|
|
531
|
+
event_to_state.set(event_name, merged);
|
|
524
532
|
}
|
|
525
533
|
}
|
|
526
534
|
return {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
535
|
+
static_targets: [...statics.values()],
|
|
536
|
+
has_dynamic_resolvers,
|
|
537
|
+
reactive_events,
|
|
538
|
+
event_to_state,
|
|
539
|
+
event_to_lanes
|
|
532
540
|
};
|
|
533
541
|
}
|
|
534
542
|
|
|
535
543
|
// src/internal/close-cycle.ts
|
|
536
|
-
async function
|
|
537
|
-
const
|
|
538
|
-
const streams = [...
|
|
544
|
+
async function run_close_cycle(targets, deps) {
|
|
545
|
+
const target_map = new Map(targets.map((t) => [t.stream, t]));
|
|
546
|
+
const streams = [...target_map.keys()];
|
|
539
547
|
const skipped = [];
|
|
540
|
-
const
|
|
541
|
-
const safe = await
|
|
542
|
-
|
|
543
|
-
deps.
|
|
548
|
+
const stream_info = await scan_stream_heads(streams);
|
|
549
|
+
const safe = await partition_by_safety(
|
|
550
|
+
stream_info,
|
|
551
|
+
deps.reactive_events_size,
|
|
544
552
|
skipped
|
|
545
553
|
);
|
|
546
554
|
if (!safe.length) return { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
547
|
-
const { guarded,
|
|
555
|
+
const { guarded, guard_events } = await guard_with_tombstones(
|
|
548
556
|
safe,
|
|
549
|
-
|
|
557
|
+
stream_info,
|
|
550
558
|
deps.correlation,
|
|
551
559
|
deps.tombstone,
|
|
552
560
|
skipped
|
|
553
561
|
);
|
|
554
562
|
if (!guarded.length) return { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
555
|
-
const
|
|
563
|
+
const seed_states = await load_restart_seeds(
|
|
556
564
|
guarded,
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
deps.
|
|
565
|
+
target_map,
|
|
566
|
+
stream_info,
|
|
567
|
+
deps.event_to_state,
|
|
560
568
|
deps.load,
|
|
561
569
|
deps.logger
|
|
562
570
|
);
|
|
563
|
-
await
|
|
564
|
-
const truncated = await
|
|
571
|
+
await run_archive_callbacks(guarded, target_map);
|
|
572
|
+
const truncated = await truncate_and_warm_cache(
|
|
565
573
|
guarded,
|
|
566
|
-
|
|
567
|
-
|
|
574
|
+
seed_states,
|
|
575
|
+
guard_events,
|
|
568
576
|
deps.correlation
|
|
569
577
|
);
|
|
570
578
|
return { truncated, skipped };
|
|
571
579
|
}
|
|
572
|
-
async function
|
|
580
|
+
async function scan_stream_heads(streams) {
|
|
573
581
|
const stats = await store().query_stats(streams, {
|
|
574
582
|
exclude: [SNAP_EVENT]
|
|
575
583
|
});
|
|
@@ -577,76 +585,76 @@ async function scanStreamHeads(streams) {
|
|
|
577
585
|
for (const [stream, { head }] of stats) {
|
|
578
586
|
if (head.name === TOMBSTONE_EVENT) continue;
|
|
579
587
|
out.set(stream, {
|
|
580
|
-
|
|
588
|
+
max_id: head.id,
|
|
581
589
|
version: head.version,
|
|
582
|
-
|
|
590
|
+
last_event_name: head.name
|
|
583
591
|
});
|
|
584
592
|
}
|
|
585
593
|
return out;
|
|
586
594
|
}
|
|
587
|
-
async function
|
|
588
|
-
if (
|
|
589
|
-
const
|
|
595
|
+
async function partition_by_safety(stream_info, reactive_events_size, skipped) {
|
|
596
|
+
if (reactive_events_size === 0) return [...stream_info.keys()];
|
|
597
|
+
const pending_set = /* @__PURE__ */ new Set();
|
|
590
598
|
await store().query_streams((position) => {
|
|
591
|
-
const
|
|
592
|
-
for (const [stream, info] of
|
|
593
|
-
if ((!
|
|
594
|
-
|
|
599
|
+
const source_re = position.source ? RegExp(position.source) : void 0;
|
|
600
|
+
for (const [stream, info] of stream_info) {
|
|
601
|
+
if ((!source_re || source_re.test(stream)) && position.at < info.max_id) {
|
|
602
|
+
pending_set.add(stream);
|
|
595
603
|
}
|
|
596
604
|
}
|
|
597
605
|
});
|
|
598
606
|
const safe = [];
|
|
599
|
-
for (const [stream] of
|
|
600
|
-
if (
|
|
607
|
+
for (const [stream] of stream_info) {
|
|
608
|
+
if (pending_set.has(stream)) skipped.push(stream);
|
|
601
609
|
else safe.push(stream);
|
|
602
610
|
}
|
|
603
611
|
return safe;
|
|
604
612
|
}
|
|
605
|
-
async function
|
|
613
|
+
async function guard_with_tombstones(safe, stream_info, correlation, tombstone2, skipped) {
|
|
606
614
|
const guarded = [];
|
|
607
|
-
const
|
|
615
|
+
const guard_events = /* @__PURE__ */ new Map();
|
|
608
616
|
await Promise.all(
|
|
609
617
|
safe.map(async (stream) => {
|
|
610
|
-
const info =
|
|
618
|
+
const info = stream_info.get(stream);
|
|
611
619
|
const committed = await tombstone2(stream, info.version, correlation);
|
|
612
620
|
if (committed) {
|
|
613
621
|
guarded.push(stream);
|
|
614
|
-
|
|
622
|
+
guard_events.set(stream, { id: committed.id, stream });
|
|
615
623
|
} else {
|
|
616
624
|
skipped.push(stream);
|
|
617
625
|
}
|
|
618
626
|
})
|
|
619
627
|
);
|
|
620
|
-
return { guarded,
|
|
628
|
+
return { guarded, guard_events };
|
|
621
629
|
}
|
|
622
|
-
async function
|
|
623
|
-
const
|
|
630
|
+
async function load_restart_seeds(guarded, target_map, stream_info, event_to_state, load2, logger) {
|
|
631
|
+
const seed_states = /* @__PURE__ */ new Map();
|
|
624
632
|
await Promise.all(
|
|
625
|
-
guarded.filter((s) =>
|
|
626
|
-
const
|
|
627
|
-
const
|
|
628
|
-
if (!
|
|
633
|
+
guarded.filter((s) => target_map.get(s)?.restart).map(async (stream) => {
|
|
634
|
+
const last_event_name = stream_info.get(stream).last_event_name;
|
|
635
|
+
const owner_state = event_to_state.get(last_event_name);
|
|
636
|
+
if (!owner_state) {
|
|
629
637
|
logger.error(
|
|
630
|
-
`Cannot seed restart for "${stream}": no registered state owns event "${
|
|
638
|
+
`Cannot seed restart for "${stream}": no registered state owns event "${last_event_name}". Stream will be tombstoned instead.`
|
|
631
639
|
);
|
|
632
640
|
return;
|
|
633
641
|
}
|
|
634
|
-
const snap2 = await load2(
|
|
635
|
-
|
|
642
|
+
const snap2 = await load2(owner_state, stream);
|
|
643
|
+
seed_states.set(stream, snap2.state);
|
|
636
644
|
})
|
|
637
645
|
);
|
|
638
|
-
return
|
|
646
|
+
return seed_states;
|
|
639
647
|
}
|
|
640
|
-
async function
|
|
648
|
+
async function run_archive_callbacks(guarded, target_map) {
|
|
641
649
|
for (const stream of guarded) {
|
|
642
|
-
const
|
|
643
|
-
if (
|
|
650
|
+
const archive_fn = target_map.get(stream)?.archive;
|
|
651
|
+
if (archive_fn) await archive_fn();
|
|
644
652
|
}
|
|
645
653
|
}
|
|
646
|
-
async function
|
|
647
|
-
const
|
|
648
|
-
const snapshot =
|
|
649
|
-
const guard =
|
|
654
|
+
async function truncate_and_warm_cache(guarded, seed_states, guard_events, correlation) {
|
|
655
|
+
const trunc_targets = guarded.map((stream) => {
|
|
656
|
+
const snapshot = seed_states.get(stream);
|
|
657
|
+
const guard = guard_events.get(stream);
|
|
650
658
|
return {
|
|
651
659
|
stream,
|
|
652
660
|
snapshot,
|
|
@@ -658,11 +666,11 @@ async function truncateAndWarmCache(guarded, seedStates, guardEvents, correlatio
|
|
|
658
666
|
}
|
|
659
667
|
};
|
|
660
668
|
});
|
|
661
|
-
const truncated = await store().truncate(
|
|
669
|
+
const truncated = await store().truncate(trunc_targets);
|
|
662
670
|
await Promise.all(
|
|
663
671
|
guarded.map(async (stream) => {
|
|
664
672
|
const entry = truncated.get(stream);
|
|
665
|
-
const state2 =
|
|
673
|
+
const state2 = seed_states.get(stream);
|
|
666
674
|
if (state2 && entry) {
|
|
667
675
|
await cache().set(stream, {
|
|
668
676
|
state: state2,
|
|
@@ -690,13 +698,13 @@ var CorrelateCycle = class {
|
|
|
690
698
|
_has_dynamic_resolvers;
|
|
691
699
|
_cd;
|
|
692
700
|
_on_init;
|
|
693
|
-
constructor(registry,
|
|
701
|
+
constructor(registry, static_targets, has_dynamic_resolvers, cd, maxSubscribedStreams, on_init) {
|
|
694
702
|
this._subscribed = new LruSet(maxSubscribedStreams);
|
|
695
703
|
this._registry = registry;
|
|
696
|
-
this._static_targets =
|
|
697
|
-
this._has_dynamic_resolvers =
|
|
704
|
+
this._static_targets = static_targets;
|
|
705
|
+
this._has_dynamic_resolvers = has_dynamic_resolvers;
|
|
698
706
|
this._cd = cd;
|
|
699
|
-
this._on_init =
|
|
707
|
+
this._on_init = on_init;
|
|
700
708
|
}
|
|
701
709
|
/** Last correlated event id. */
|
|
702
710
|
get checkpoint() {
|
|
@@ -707,7 +715,7 @@ var CorrelateCycle = class {
|
|
|
707
715
|
* - Reads max(at) from store as cold-start checkpoint
|
|
708
716
|
* - Subscribes static resolver targets (idempotent upsert)
|
|
709
717
|
* - Populates the subscribed-streams LRU
|
|
710
|
-
* - Fires `
|
|
718
|
+
* - Fires `on_init` once (Act uses this to flag a cold-start drain)
|
|
711
719
|
*/
|
|
712
720
|
async init() {
|
|
713
721
|
if (this._initialized) return;
|
|
@@ -740,15 +748,15 @@ var CorrelateCycle = class {
|
|
|
740
748
|
if (typeof reaction.resolver !== "function") continue;
|
|
741
749
|
const resolved = reaction.resolver(event);
|
|
742
750
|
if (resolved && !this._subscribed.has(resolved.target)) {
|
|
743
|
-
const
|
|
751
|
+
const incoming_priority = resolved.priority ?? 0;
|
|
744
752
|
const entry = correlated.get(resolved.target) || {
|
|
745
753
|
source: resolved.source,
|
|
746
|
-
priority:
|
|
754
|
+
priority: incoming_priority,
|
|
747
755
|
lane: resolved.lane,
|
|
748
756
|
payloads: []
|
|
749
757
|
};
|
|
750
|
-
if (
|
|
751
|
-
entry.priority =
|
|
758
|
+
if (incoming_priority > entry.priority)
|
|
759
|
+
entry.priority = incoming_priority;
|
|
752
760
|
entry.payloads.push({
|
|
753
761
|
...reaction,
|
|
754
762
|
source: resolved.source,
|
|
@@ -787,7 +795,7 @@ var CorrelateCycle = class {
|
|
|
787
795
|
* running. Errors from `correlate()` are routed through `log()` so they
|
|
788
796
|
* land in the configured logger (the timer keeps running on failure).
|
|
789
797
|
*/
|
|
790
|
-
|
|
798
|
+
start_polling(query = {}, frequency = 1e4, callback) {
|
|
791
799
|
if (this._timer) return false;
|
|
792
800
|
const limit = query.limit || 100;
|
|
793
801
|
this._timer = setInterval(
|
|
@@ -799,7 +807,7 @@ var CorrelateCycle = class {
|
|
|
799
807
|
return true;
|
|
800
808
|
}
|
|
801
809
|
/** Stop the periodic correlation worker. Idempotent. */
|
|
802
|
-
|
|
810
|
+
stop_polling() {
|
|
803
811
|
if (this._timer) {
|
|
804
812
|
clearInterval(this._timer);
|
|
805
813
|
this._timer = void 0;
|
|
@@ -815,14 +823,14 @@ var SEG_SPACE = BASE ** SEG_WIDTH;
|
|
|
815
823
|
function seg(n) {
|
|
816
824
|
return n.toString(BASE).padStart(SEG_WIDTH, "0");
|
|
817
825
|
}
|
|
818
|
-
var
|
|
826
|
+
var default_correlator = ({ state: state2, action: action2 }) => {
|
|
819
827
|
const s = state2.slice(0, SEG_WIDTH).toLowerCase();
|
|
820
828
|
const a = action2.slice(0, SEG_WIDTH).toLowerCase();
|
|
821
829
|
const ts = seg(Date.now() % SEG_SPACE);
|
|
822
830
|
const rnd = seg(randomInt(SEG_SPACE));
|
|
823
831
|
return `${s}-${a}-${ts}${rnd}`;
|
|
824
832
|
};
|
|
825
|
-
function
|
|
833
|
+
function close_correlation(correlator, actor) {
|
|
826
834
|
return correlator({
|
|
827
835
|
state: "$close",
|
|
828
836
|
action: "close",
|
|
@@ -838,7 +846,7 @@ import { randomUUID } from "crypto";
|
|
|
838
846
|
var RATIO_MIN = 0.2;
|
|
839
847
|
var RATIO_MAX = 0.8;
|
|
840
848
|
var RATIO_DEFAULT = 0.5;
|
|
841
|
-
function
|
|
849
|
+
function compute_lag_lead_ratio(handled, lagging, leading) {
|
|
842
850
|
let lagging_handled = 0;
|
|
843
851
|
let leading_handled = 0;
|
|
844
852
|
for (const { lease, handled: count } of handled) {
|
|
@@ -875,7 +883,7 @@ var subscribe = (streams) => store().subscribe(streams);
|
|
|
875
883
|
import { patch } from "@rotorsoft/act-patch";
|
|
876
884
|
|
|
877
885
|
// src/internal/backoff.ts
|
|
878
|
-
function
|
|
886
|
+
function compute_backoff_delay(retry, opts) {
|
|
879
887
|
if (!opts || opts.baseMs <= 0) return 0;
|
|
880
888
|
const r = Math.max(0, retry);
|
|
881
889
|
let delay;
|
|
@@ -1044,9 +1052,9 @@ async function scan(source, opts = {}, callback) {
|
|
|
1044
1052
|
}
|
|
1045
1053
|
};
|
|
1046
1054
|
}
|
|
1047
|
-
async function load(me, stream, callback, asOf) {
|
|
1048
|
-
const
|
|
1049
|
-
const cached =
|
|
1055
|
+
async function load(me, stream, callback, asOf, actor) {
|
|
1056
|
+
const time_travel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
|
|
1057
|
+
const cached = time_travel ? void 0 : await cache().get(stream);
|
|
1050
1058
|
const cache_hit = !!cached;
|
|
1051
1059
|
let state2 = cached?.state ?? (me.init ? me.init() : {});
|
|
1052
1060
|
let patches = cached?.patches ?? 0;
|
|
@@ -1056,15 +1064,16 @@ async function load(me, stream, callback, asOf) {
|
|
|
1056
1064
|
let event;
|
|
1057
1065
|
await store().query(
|
|
1058
1066
|
(e) => {
|
|
1059
|
-
event = e;
|
|
1060
1067
|
version = e.version;
|
|
1068
|
+
const typed = e;
|
|
1069
|
+
event = me.view(typed, actor);
|
|
1061
1070
|
if (e.name === SNAP_EVENT) {
|
|
1062
1071
|
state2 = e.data;
|
|
1063
1072
|
snaps++;
|
|
1064
1073
|
patches = 0;
|
|
1065
1074
|
replayed++;
|
|
1066
1075
|
} else if (me.patch[e.name]) {
|
|
1067
|
-
state2 = patch(state2, me.patch[e.name](
|
|
1076
|
+
state2 = patch(state2, me.patch[e.name](typed, state2));
|
|
1068
1077
|
patches++;
|
|
1069
1078
|
replayed++;
|
|
1070
1079
|
} else if (e.name !== TOMBSTONE_EVENT) {
|
|
@@ -1088,7 +1097,7 @@ async function load(me, stream, callback, asOf) {
|
|
|
1088
1097
|
...cached ? { after: cached.event_id } : { with_snaps: true, ...asOf }
|
|
1089
1098
|
}
|
|
1090
1099
|
);
|
|
1091
|
-
if (replayed > 0 && !
|
|
1100
|
+
if (replayed > 0 && !time_travel && event) {
|
|
1092
1101
|
await cache().set(stream, {
|
|
1093
1102
|
state: state2,
|
|
1094
1103
|
version,
|
|
@@ -1099,15 +1108,21 @@ async function load(me, stream, callback, asOf) {
|
|
|
1099
1108
|
}
|
|
1100
1109
|
return { event, state: state2, version, patches, snaps, cache_hit, replayed };
|
|
1101
1110
|
}
|
|
1102
|
-
async function action(me, action2, target, payload, reactingTo, skipValidation = false, correlator =
|
|
1111
|
+
async function action(me, action2, target, payload, reactingTo, skipValidation = false, correlator = default_correlator) {
|
|
1103
1112
|
const { stream, expectedVersion, actor } = target;
|
|
1104
1113
|
if (!stream) throw new Error("Missing target stream");
|
|
1105
1114
|
const validated = skipValidation ? payload : validate(action2, payload, me.actions[action2]);
|
|
1106
1115
|
const opts = me.options?.[action2];
|
|
1107
|
-
const
|
|
1116
|
+
const max_retries = opts?.maxRetries ?? 0;
|
|
1108
1117
|
for (let attempt = 0; ; attempt++) {
|
|
1109
1118
|
try {
|
|
1110
|
-
const snapshot = await load(
|
|
1119
|
+
const snapshot = await load(
|
|
1120
|
+
me,
|
|
1121
|
+
stream,
|
|
1122
|
+
void 0,
|
|
1123
|
+
void 0,
|
|
1124
|
+
target.actor
|
|
1125
|
+
);
|
|
1111
1126
|
if (snapshot.event?.name === TOMBSTONE_EVENT)
|
|
1112
1127
|
throw new StreamClosedError(stream);
|
|
1113
1128
|
const expected = expectedVersion ?? snapshot.event?.version;
|
|
@@ -1144,10 +1159,10 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
1144
1159
|
}
|
|
1145
1160
|
}
|
|
1146
1161
|
}
|
|
1147
|
-
const emitted = tuples.map(([name, data]) =>
|
|
1148
|
-
name,
|
|
1149
|
-
|
|
1150
|
-
})
|
|
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
|
+
});
|
|
1151
1166
|
const meta = {
|
|
1152
1167
|
correlation: reactingTo?.meta.correlation || correlator({
|
|
1153
1168
|
action: action2,
|
|
@@ -1159,8 +1174,8 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
1159
1174
|
action: {
|
|
1160
1175
|
name: action2,
|
|
1161
1176
|
...target
|
|
1162
|
-
// payload intentionally omitted
|
|
1163
|
-
//
|
|
1177
|
+
// payload intentionally omitted from causation metadata —
|
|
1178
|
+
// callers correlate via the correlation id when they need it.
|
|
1164
1179
|
},
|
|
1165
1180
|
event: reactingTo ? {
|
|
1166
1181
|
id: reactingTo.id,
|
|
@@ -1193,7 +1208,7 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
1193
1208
|
state2 = patch(state2, p);
|
|
1194
1209
|
patches++;
|
|
1195
1210
|
return {
|
|
1196
|
-
event,
|
|
1211
|
+
event: me.view(event, target.actor),
|
|
1197
1212
|
state: state2,
|
|
1198
1213
|
version: event.version,
|
|
1199
1214
|
patches,
|
|
@@ -1216,10 +1231,10 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
1216
1231
|
return snapshots;
|
|
1217
1232
|
} catch (error) {
|
|
1218
1233
|
if (!(error instanceof ConcurrencyError)) throw error;
|
|
1219
|
-
if (attempt >=
|
|
1234
|
+
if (attempt >= max_retries) throw error;
|
|
1220
1235
|
if (opts?.backoff) {
|
|
1221
|
-
const
|
|
1222
|
-
if (
|
|
1236
|
+
const delay_ms = compute_backoff_delay(attempt, opts.backoff);
|
|
1237
|
+
if (delay_ms > 0) await sleep(delay_ms);
|
|
1223
1238
|
}
|
|
1224
1239
|
}
|
|
1225
1240
|
}
|
|
@@ -1243,12 +1258,12 @@ var C_STREAM = "\x1B[38;5;226m";
|
|
|
1243
1258
|
var dim = (text) => PRETTY ? `${C_DIM}${text}${C_RESET}` : text;
|
|
1244
1259
|
var hue = (color, text) => PRETTY ? `${color}${text}${C_RESET}` : text;
|
|
1245
1260
|
var drain_caption = (caption, lane) => {
|
|
1246
|
-
const
|
|
1261
|
+
const show_lane = lane && lane !== "default";
|
|
1247
1262
|
if (PRETTY) {
|
|
1248
1263
|
const tag = `${C_DRAIN}>> ${caption}${C_RESET}`;
|
|
1249
|
-
return
|
|
1264
|
+
return show_lane ? `${tag} ${C_LANE}${lane}${C_RESET}` : tag;
|
|
1250
1265
|
}
|
|
1251
|
-
return
|
|
1266
|
+
return show_lane ? `>> ${caption} ${lane}` : `>> ${caption}`;
|
|
1252
1267
|
};
|
|
1253
1268
|
var cache_marker = (hit) => {
|
|
1254
1269
|
const word = hit ? "hit" : "miss";
|
|
@@ -1277,10 +1292,10 @@ var traced = (inner, exit, entry) => (async (...args) => {
|
|
|
1277
1292
|
exit?.(result, ...args);
|
|
1278
1293
|
return result;
|
|
1279
1294
|
});
|
|
1280
|
-
function
|
|
1281
|
-
const
|
|
1295
|
+
function build_es(logger, correlator = default_correlator) {
|
|
1296
|
+
const bound_action = (me, action_name, target, payload, reactingTo, skipValidation = false) => action(
|
|
1282
1297
|
me,
|
|
1283
|
-
|
|
1298
|
+
action_name,
|
|
1284
1299
|
target,
|
|
1285
1300
|
payload,
|
|
1286
1301
|
reactingTo,
|
|
@@ -1291,7 +1306,7 @@ function buildEs(logger, correlator = defaultCorrelator) {
|
|
|
1291
1306
|
return {
|
|
1292
1307
|
snap,
|
|
1293
1308
|
load,
|
|
1294
|
-
action:
|
|
1309
|
+
action: bound_action,
|
|
1295
1310
|
tombstone
|
|
1296
1311
|
};
|
|
1297
1312
|
}
|
|
@@ -1321,7 +1336,7 @@ function buildEs(logger, correlator = defaultCorrelator) {
|
|
|
1321
1336
|
);
|
|
1322
1337
|
}),
|
|
1323
1338
|
action: traced(
|
|
1324
|
-
|
|
1339
|
+
bound_action,
|
|
1325
1340
|
(snapshots, _me, _action, target) => {
|
|
1326
1341
|
const committed = snapshots.filter((s) => s.event);
|
|
1327
1342
|
if (committed.length) {
|
|
@@ -1350,7 +1365,7 @@ function buildEs(logger, correlator = defaultCorrelator) {
|
|
|
1350
1365
|
})
|
|
1351
1366
|
};
|
|
1352
1367
|
}
|
|
1353
|
-
function
|
|
1368
|
+
function build_drain(logger) {
|
|
1354
1369
|
return {
|
|
1355
1370
|
claim,
|
|
1356
1371
|
fetch,
|
|
@@ -1359,46 +1374,48 @@ function buildDrain(logger) {
|
|
|
1359
1374
|
subscribe: logger.level !== "trace" ? subscribe : traced(subscribe, (result, streams) => {
|
|
1360
1375
|
if (!result.subscribed) return;
|
|
1361
1376
|
const lanes = new Set(streams.map((s) => s.lane ?? "default"));
|
|
1362
|
-
const
|
|
1377
|
+
const uniform_lane = lanes.size === 1 ? streams[0]?.lane : void 0;
|
|
1363
1378
|
const data = streams.map(
|
|
1364
|
-
({ stream, lane }) =>
|
|
1379
|
+
({ stream, lane }) => uniform_lane || !lane || lane === "default" ? hue(C_STREAM, stream) : `${hue(C_STREAM, stream)}${dim(`[${lane}]`)}`
|
|
1365
1380
|
).join(" ");
|
|
1366
|
-
logger.trace(
|
|
1381
|
+
logger.trace(
|
|
1382
|
+
`${drain_caption("correlated", uniform_lane)} ${data}`
|
|
1383
|
+
);
|
|
1367
1384
|
})
|
|
1368
1385
|
};
|
|
1369
1386
|
}
|
|
1370
|
-
function
|
|
1387
|
+
function trace_cycle(logger, leased, fetched, handled, acked, blocked) {
|
|
1371
1388
|
if (logger.level !== "trace" || !leased.length) return;
|
|
1372
1389
|
const lane = leased[0]?.lane;
|
|
1373
|
-
const
|
|
1374
|
-
const
|
|
1375
|
-
const
|
|
1376
|
-
const
|
|
1390
|
+
const fetch_by_stream = new Map(fetched.map((f) => [f.stream, f]));
|
|
1391
|
+
const acked_by_stream = new Map(acked.map((a) => [a.stream, a.at]));
|
|
1392
|
+
const blocked_by_stream = new Map(blocked.map((b) => [b.stream, b.error]));
|
|
1393
|
+
const failed_by_stream = new Map(
|
|
1377
1394
|
handled.filter((h) => h.error).map((h) => [h.lease.stream, h])
|
|
1378
1395
|
);
|
|
1379
1396
|
const detail = leased.map(({ stream, at, retry }) => {
|
|
1380
|
-
const f =
|
|
1397
|
+
const f = fetch_by_stream.get(stream);
|
|
1381
1398
|
const key = f?.source ? `${hue(C_STREAM, stream)}${dim(`<-${f.source}`)}` : hue(C_STREAM, stream);
|
|
1382
1399
|
const events = f && f.events.length ? ` ${dim(
|
|
1383
1400
|
`[${f.events.map(({ id, name }) => `#${id} ${String(name)}`).join(", ")}]`
|
|
1384
1401
|
)}` : "";
|
|
1385
|
-
const
|
|
1386
|
-
const
|
|
1387
|
-
const failure =
|
|
1388
|
-
let
|
|
1402
|
+
const acked_at = acked_by_stream.get(stream);
|
|
1403
|
+
const ack_part = acked_at !== void 0 ? hue(C_HIT, `\u2713 @${acked_at}`) : "";
|
|
1404
|
+
const failure = failed_by_stream.get(stream);
|
|
1405
|
+
let fail_part = "";
|
|
1389
1406
|
if (failure) {
|
|
1390
|
-
const
|
|
1391
|
-
const
|
|
1392
|
-
if (
|
|
1393
|
-
|
|
1407
|
+
const failed_at = failure.failed_at ?? at;
|
|
1408
|
+
const blocked_error = blocked_by_stream.get(stream);
|
|
1409
|
+
if (blocked_error !== void 0) {
|
|
1410
|
+
fail_part = `${hue(C_ERR, `\u2717 @${failed_at}/${retry}`)} ${dim(`(${blocked_error})`)}`;
|
|
1394
1411
|
} else {
|
|
1395
|
-
|
|
1412
|
+
fail_part = `${hue(C_MISS, `\u26A0 @${failed_at}/${retry}`)} ${dim(`(${failure.error})`)}`;
|
|
1396
1413
|
}
|
|
1397
1414
|
}
|
|
1398
1415
|
let tail;
|
|
1399
|
-
if (
|
|
1400
|
-
else if (
|
|
1401
|
-
else if (
|
|
1416
|
+
if (ack_part && fail_part) tail = ` ${ack_part} ${fail_part}`;
|
|
1417
|
+
else if (ack_part) tail = ` ${ack_part}`;
|
|
1418
|
+
else if (fail_part) tail = ` ${fail_part}`;
|
|
1402
1419
|
else tail = ` ${dim(`\u2298 @${at}/${retry}`)}`;
|
|
1403
1420
|
return `${key}${events}${tail}`;
|
|
1404
1421
|
}).join(", ");
|
|
@@ -1406,7 +1423,7 @@ function traceCycle(logger, leased, fetched, handled, acked, blocked) {
|
|
|
1406
1423
|
}
|
|
1407
1424
|
|
|
1408
1425
|
// src/internal/drain-cycle.ts
|
|
1409
|
-
async function
|
|
1426
|
+
async function run_drain_cycle(ops, registry, batch_handlers, handle, handle_batch, lagging, leading, eventLimit, leaseMillis, is_deferred, lane) {
|
|
1410
1427
|
const leased = await ops.claim(
|
|
1411
1428
|
lagging,
|
|
1412
1429
|
leading,
|
|
@@ -1415,7 +1432,7 @@ async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch,
|
|
|
1415
1432
|
lane
|
|
1416
1433
|
);
|
|
1417
1434
|
if (!leased.length) return void 0;
|
|
1418
|
-
const active =
|
|
1435
|
+
const active = is_deferred ? leased.filter((l) => !is_deferred(l.stream)) : leased;
|
|
1419
1436
|
if (!active.length) {
|
|
1420
1437
|
return {
|
|
1421
1438
|
leased,
|
|
@@ -1426,7 +1443,7 @@ async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch,
|
|
|
1426
1443
|
};
|
|
1427
1444
|
}
|
|
1428
1445
|
const fetched = await ops.fetch(active, eventLimit);
|
|
1429
|
-
const
|
|
1446
|
+
const fetch_map = /* @__PURE__ */ new Map();
|
|
1430
1447
|
const fetch_window_at = fetched.reduce(
|
|
1431
1448
|
(max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
|
|
1432
1449
|
0
|
|
@@ -1441,16 +1458,16 @@ async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch,
|
|
|
1441
1458
|
return resolved && resolved.target === stream;
|
|
1442
1459
|
}).map((reaction) => ({ ...reaction, event }));
|
|
1443
1460
|
});
|
|
1444
|
-
|
|
1461
|
+
fetch_map.set(stream, { fetch: f, payloads });
|
|
1445
1462
|
}
|
|
1446
1463
|
const handled = await Promise.all(
|
|
1447
1464
|
active.map((lease) => {
|
|
1448
|
-
const entry =
|
|
1465
|
+
const entry = fetch_map.get(lease.stream);
|
|
1449
1466
|
const at = entry.fetch.events.at(-1)?.id || fetch_window_at;
|
|
1450
1467
|
const { payloads } = entry;
|
|
1451
|
-
const batchHandler =
|
|
1468
|
+
const batchHandler = batch_handlers.get(lease.stream);
|
|
1452
1469
|
if (batchHandler && payloads.length > 0) {
|
|
1453
|
-
return
|
|
1470
|
+
return handle_batch({ ...lease, at }, payloads, batchHandler);
|
|
1454
1471
|
}
|
|
1455
1472
|
return handle({ ...lease, at }, payloads);
|
|
1456
1473
|
})
|
|
@@ -1474,14 +1491,14 @@ var DrainController = class {
|
|
|
1474
1491
|
_locked = false;
|
|
1475
1492
|
_ratio = 0.5;
|
|
1476
1493
|
/**
|
|
1477
|
-
* Per-stream backoff: `stream →
|
|
1478
|
-
* `_finalize` via `HandleResult.
|
|
1494
|
+
* Per-stream backoff: `stream → next_attempt_at` (ms since epoch). Set by
|
|
1495
|
+
* `_finalize` via `HandleResult.next_attempt_at`; cleared on successful
|
|
1479
1496
|
* ack or terminal block. Lives in process memory — per-worker pacing
|
|
1480
1497
|
* by design (see {@link BackoffOptions} for the multi-worker trade-off).
|
|
1481
1498
|
*/
|
|
1482
1499
|
_backoff = /* @__PURE__ */ new Map();
|
|
1483
|
-
/** Timer re-arming drain at the earliest pending `
|
|
1484
|
-
|
|
1500
|
+
/** Timer re-arming drain at the earliest pending `next_attempt_at`. */
|
|
1501
|
+
_backoff_timer;
|
|
1485
1502
|
/** Worker timer (ACT-1103). Set when `start()` is active, undefined otherwise. */
|
|
1486
1503
|
_worker;
|
|
1487
1504
|
_stopped = false;
|
|
@@ -1502,7 +1519,7 @@ var DrainController = class {
|
|
|
1502
1519
|
return this._armed;
|
|
1503
1520
|
}
|
|
1504
1521
|
/** Returns true when `stream` is currently within a backoff window. */
|
|
1505
|
-
|
|
1522
|
+
is_deferred = (stream) => {
|
|
1506
1523
|
const next = this._backoff.get(stream);
|
|
1507
1524
|
return next !== void 0 && next > Date.now();
|
|
1508
1525
|
};
|
|
@@ -1512,20 +1529,20 @@ var DrainController = class {
|
|
|
1512
1529
|
* Idempotent — collapses many simultaneously deferred streams into a
|
|
1513
1530
|
* single timer.
|
|
1514
1531
|
*/
|
|
1515
|
-
|
|
1516
|
-
if (this.
|
|
1532
|
+
schedule_backoff_wake() {
|
|
1533
|
+
if (this._backoff_timer) clearTimeout(this._backoff_timer);
|
|
1517
1534
|
let earliest = Number.POSITIVE_INFINITY;
|
|
1518
1535
|
for (const t of this._backoff.values()) if (t < earliest) earliest = t;
|
|
1519
1536
|
const delay = Math.max(0, earliest - Date.now());
|
|
1520
|
-
this.
|
|
1521
|
-
this.
|
|
1537
|
+
this._backoff_timer = setTimeout(() => {
|
|
1538
|
+
this._backoff_timer = void 0;
|
|
1522
1539
|
const now = Date.now();
|
|
1523
1540
|
for (const [stream, at] of this._backoff) {
|
|
1524
1541
|
if (at <= now) this._backoff.delete(stream);
|
|
1525
1542
|
}
|
|
1526
1543
|
this._armed = true;
|
|
1527
1544
|
}, delay);
|
|
1528
|
-
this.
|
|
1545
|
+
this._backoff_timer.unref();
|
|
1529
1546
|
}
|
|
1530
1547
|
/** Lane this controller drains (undefined = legacy single-lane span). */
|
|
1531
1548
|
get lane() {
|
|
@@ -1571,17 +1588,17 @@ var DrainController = class {
|
|
|
1571
1588
|
this._locked = true;
|
|
1572
1589
|
const lagging = Math.ceil(streamLimit * this._ratio);
|
|
1573
1590
|
const leading = streamLimit - lagging;
|
|
1574
|
-
const cycle = await
|
|
1591
|
+
const cycle = await run_drain_cycle(
|
|
1575
1592
|
this._deps.ops,
|
|
1576
1593
|
this._deps.registry,
|
|
1577
|
-
this._deps.
|
|
1594
|
+
this._deps.batch_handlers,
|
|
1578
1595
|
this._deps.handle,
|
|
1579
|
-
this._deps.
|
|
1596
|
+
this._deps.handle_batch,
|
|
1580
1597
|
lagging,
|
|
1581
1598
|
leading,
|
|
1582
1599
|
eventLimit,
|
|
1583
1600
|
leaseMillis,
|
|
1584
|
-
this._backoff.size > 0 ? this.
|
|
1601
|
+
this._backoff.size > 0 ? this.is_deferred : void 0,
|
|
1585
1602
|
this._deps.lane
|
|
1586
1603
|
);
|
|
1587
1604
|
if (!cycle) {
|
|
@@ -1589,20 +1606,20 @@ var DrainController = class {
|
|
|
1589
1606
|
return EMPTY_DRAIN;
|
|
1590
1607
|
}
|
|
1591
1608
|
const { leased, fetched, handled, acked, blocked } = cycle;
|
|
1592
|
-
|
|
1593
|
-
this._ratio =
|
|
1609
|
+
trace_cycle(this._deps.logger, leased, fetched, handled, acked, blocked);
|
|
1610
|
+
this._ratio = compute_lag_lead_ratio(handled, lagging, leading);
|
|
1594
1611
|
for (const lease of acked) this._backoff.delete(lease.stream);
|
|
1595
1612
|
for (const lease of blocked) this._backoff.delete(lease.stream);
|
|
1596
1613
|
for (const h of handled) {
|
|
1597
|
-
if (h.
|
|
1598
|
-
this._backoff.set(h.lease.stream, h.
|
|
1614
|
+
if (h.next_attempt_at !== void 0 && !h.block) {
|
|
1615
|
+
this._backoff.set(h.lease.stream, h.next_attempt_at);
|
|
1599
1616
|
}
|
|
1600
1617
|
}
|
|
1601
|
-
if (this._backoff.size > 0) this.
|
|
1602
|
-
if (acked.length) this._deps.
|
|
1603
|
-
if (blocked.length) this._deps.
|
|
1604
|
-
const
|
|
1605
|
-
if (!acked.length && !blocked.length && !
|
|
1618
|
+
if (this._backoff.size > 0) this.schedule_backoff_wake();
|
|
1619
|
+
if (acked.length) this._deps.on_acked(acked);
|
|
1620
|
+
if (blocked.length) this._deps.on_blocked(blocked);
|
|
1621
|
+
const has_errors = handled.some(({ error }) => error);
|
|
1622
|
+
if (!acked.length && !blocked.length && !has_errors) this._armed = false;
|
|
1606
1623
|
return { fetched, leased, acked, blocked };
|
|
1607
1624
|
} catch (error) {
|
|
1608
1625
|
this._deps.logger.error(error);
|
|
@@ -1615,44 +1632,44 @@ var DrainController = class {
|
|
|
1615
1632
|
|
|
1616
1633
|
// src/internal/merge.ts
|
|
1617
1634
|
import { ZodObject } from "zod";
|
|
1618
|
-
function
|
|
1635
|
+
function base_type_name(zodType) {
|
|
1619
1636
|
let t = zodType;
|
|
1620
1637
|
while (typeof t.unwrap === "function") {
|
|
1621
1638
|
t = t.unwrap();
|
|
1622
1639
|
}
|
|
1623
1640
|
return t.constructor.name;
|
|
1624
1641
|
}
|
|
1625
|
-
function
|
|
1642
|
+
function merge_schemas(existing, incoming, state_name) {
|
|
1626
1643
|
if (existing instanceof ZodObject && incoming instanceof ZodObject) {
|
|
1627
|
-
const
|
|
1628
|
-
const
|
|
1629
|
-
for (const key of Object.keys(
|
|
1630
|
-
if (key in
|
|
1631
|
-
const
|
|
1632
|
-
const
|
|
1633
|
-
if (
|
|
1644
|
+
const existing_shape = existing.shape;
|
|
1645
|
+
const incoming_shape = incoming.shape;
|
|
1646
|
+
for (const key of Object.keys(incoming_shape)) {
|
|
1647
|
+
if (key in existing_shape) {
|
|
1648
|
+
const existing_base = base_type_name(existing_shape[key]);
|
|
1649
|
+
const incoming_base = base_type_name(incoming_shape[key]);
|
|
1650
|
+
if (existing_base !== incoming_base) {
|
|
1634
1651
|
throw new Error(
|
|
1635
|
-
`Schema conflict in "${
|
|
1652
|
+
`Schema conflict in "${state_name}": key "${key}" has type "${existing_base}" but incoming partial declares "${incoming_base}"`
|
|
1636
1653
|
);
|
|
1637
1654
|
}
|
|
1638
1655
|
}
|
|
1639
1656
|
}
|
|
1640
|
-
return existing.extend(
|
|
1657
|
+
return existing.extend(incoming_shape);
|
|
1641
1658
|
}
|
|
1642
1659
|
return existing;
|
|
1643
1660
|
}
|
|
1644
|
-
function
|
|
1661
|
+
function merge_inits(existing, incoming) {
|
|
1645
1662
|
return () => ({ ...existing(), ...incoming() });
|
|
1646
1663
|
}
|
|
1647
|
-
function
|
|
1664
|
+
function register_state(state2, states, actions, events) {
|
|
1648
1665
|
const existing = states.get(state2.name);
|
|
1649
1666
|
if (existing) {
|
|
1650
|
-
|
|
1667
|
+
merge_into_existing(state2, existing, states, actions, events);
|
|
1651
1668
|
} else {
|
|
1652
|
-
|
|
1669
|
+
register_new_state(state2, states, actions, events);
|
|
1653
1670
|
}
|
|
1654
1671
|
}
|
|
1655
|
-
function
|
|
1672
|
+
function register_new_state(state2, states, actions, events) {
|
|
1656
1673
|
states.set(state2.name, state2);
|
|
1657
1674
|
for (const name of Object.keys(state2.actions)) {
|
|
1658
1675
|
if (actions[name]) throw new Error(`Duplicate action "${name}"`);
|
|
@@ -1663,7 +1680,7 @@ function registerNewState(state2, states, actions, events) {
|
|
|
1663
1680
|
events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
|
|
1664
1681
|
}
|
|
1665
1682
|
}
|
|
1666
|
-
function
|
|
1683
|
+
function merge_into_existing(state2, existing, states, actions, events) {
|
|
1667
1684
|
for (const name of Object.keys(state2.actions)) {
|
|
1668
1685
|
if (existing.actions[name] === state2.actions[name]) continue;
|
|
1669
1686
|
if (actions[name]) throw new Error(`Duplicate action "${name}"`);
|
|
@@ -1677,14 +1694,14 @@ function mergeIntoExisting(state2, existing, states, actions, events) {
|
|
|
1677
1694
|
}
|
|
1678
1695
|
if (events[name]) throw new Error(`Duplicate event "${name}"`);
|
|
1679
1696
|
}
|
|
1680
|
-
const
|
|
1697
|
+
const merged_patch = merge_patches(existing.patch, state2.patch, state2.name);
|
|
1681
1698
|
const merged = {
|
|
1682
1699
|
...existing,
|
|
1683
|
-
state:
|
|
1684
|
-
init:
|
|
1700
|
+
state: merge_schemas(existing.state, state2.state, state2.name),
|
|
1701
|
+
init: merge_inits(existing.init, state2.init),
|
|
1685
1702
|
events: { ...existing.events, ...state2.events },
|
|
1686
1703
|
actions: { ...existing.actions, ...state2.actions },
|
|
1687
|
-
patch:
|
|
1704
|
+
patch: merged_patch,
|
|
1688
1705
|
on: { ...existing.on, ...state2.on },
|
|
1689
1706
|
given: { ...existing.given, ...state2.given },
|
|
1690
1707
|
snap: state2.snap && existing.snap && state2.snap !== existing.snap ? (() => {
|
|
@@ -1702,48 +1719,48 @@ function mergeIntoExisting(state2, existing, states, actions, events) {
|
|
|
1702
1719
|
events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
|
|
1703
1720
|
}
|
|
1704
1721
|
}
|
|
1705
|
-
function
|
|
1722
|
+
function merge_patches(existing, incoming, state_name) {
|
|
1706
1723
|
const merged = { ...existing };
|
|
1707
1724
|
for (const name of Object.keys(incoming)) {
|
|
1708
|
-
const
|
|
1709
|
-
const
|
|
1710
|
-
if (!
|
|
1711
|
-
merged[name] =
|
|
1725
|
+
const existing_p = existing[name];
|
|
1726
|
+
const incoming_p = incoming[name];
|
|
1727
|
+
if (!existing_p) {
|
|
1728
|
+
merged[name] = incoming_p;
|
|
1712
1729
|
continue;
|
|
1713
1730
|
}
|
|
1714
|
-
const
|
|
1715
|
-
const
|
|
1716
|
-
if (!
|
|
1731
|
+
const existing_is_default = existing_p._passthrough;
|
|
1732
|
+
const incoming_is_default = incoming_p._passthrough;
|
|
1733
|
+
if (!existing_is_default && !incoming_is_default && existing_p !== incoming_p) {
|
|
1717
1734
|
throw new Error(
|
|
1718
|
-
`Duplicate custom patch for event "${name}" in state "${
|
|
1735
|
+
`Duplicate custom patch for event "${name}" in state "${state_name}"`
|
|
1719
1736
|
);
|
|
1720
1737
|
}
|
|
1721
|
-
if (
|
|
1722
|
-
merged[name] =
|
|
1738
|
+
if (existing_is_default && !incoming_is_default) {
|
|
1739
|
+
merged[name] = incoming_p;
|
|
1723
1740
|
}
|
|
1724
1741
|
}
|
|
1725
1742
|
return merged;
|
|
1726
1743
|
}
|
|
1727
|
-
function
|
|
1728
|
-
for (const [
|
|
1729
|
-
const
|
|
1730
|
-
if (!
|
|
1731
|
-
for (const [name, reaction] of
|
|
1732
|
-
|
|
1744
|
+
function merge_event_register(target, source) {
|
|
1745
|
+
for (const [event_name, source_reg] of Object.entries(source)) {
|
|
1746
|
+
const target_reg = target[event_name];
|
|
1747
|
+
if (!target_reg) continue;
|
|
1748
|
+
for (const [name, reaction] of source_reg.reactions) {
|
|
1749
|
+
target_reg.reactions.set(name, reaction);
|
|
1733
1750
|
}
|
|
1734
1751
|
}
|
|
1735
1752
|
}
|
|
1736
|
-
function
|
|
1737
|
-
for (const
|
|
1738
|
-
const
|
|
1739
|
-
const existing = events[
|
|
1753
|
+
function merge_projection(proj, events) {
|
|
1754
|
+
for (const event_name of Object.keys(proj.events)) {
|
|
1755
|
+
const proj_register = proj.events[event_name];
|
|
1756
|
+
const existing = events[event_name];
|
|
1740
1757
|
if (!existing) {
|
|
1741
|
-
events[
|
|
1742
|
-
schema:
|
|
1743
|
-
reactions: new Map(
|
|
1758
|
+
events[event_name] = {
|
|
1759
|
+
schema: proj_register.schema,
|
|
1760
|
+
reactions: new Map(proj_register.reactions)
|
|
1744
1761
|
};
|
|
1745
1762
|
} else {
|
|
1746
|
-
for (const [name, reaction] of
|
|
1763
|
+
for (const [name, reaction] of proj_register.reactions) {
|
|
1747
1764
|
let key = name;
|
|
1748
1765
|
while (existing.reactions.has(key)) key = `${key}_p`;
|
|
1749
1766
|
existing.reactions.set(key, reaction);
|
|
@@ -1760,25 +1777,32 @@ var _this_ = ({ stream }) => ({
|
|
|
1760
1777
|
function finalize(lease, handled, at, error, options, logger, failed_at) {
|
|
1761
1778
|
if (!error) return { lease, handled, acked_at: at };
|
|
1762
1779
|
logger.error(error);
|
|
1763
|
-
const
|
|
1764
|
-
const block2 = options.blockOnError && (
|
|
1780
|
+
const non_retryable = error instanceof NonRetryableError;
|
|
1781
|
+
const block2 = options.blockOnError && (non_retryable || lease.retry >= options.maxRetries);
|
|
1765
1782
|
if (block2)
|
|
1766
1783
|
logger.error(
|
|
1767
|
-
|
|
1784
|
+
non_retryable ? `Blocking ${lease.stream} on non-retryable error.` : `Blocking ${lease.stream} after ${lease.retry} retries.`
|
|
1768
1785
|
);
|
|
1769
|
-
const
|
|
1786
|
+
const next_attempt_at = !block2 && options.backoff ? Date.now() + compute_backoff_delay(lease.retry, options.backoff) : void 0;
|
|
1770
1787
|
return {
|
|
1771
1788
|
lease,
|
|
1772
1789
|
handled,
|
|
1773
1790
|
acked_at: at,
|
|
1774
1791
|
error: error.message,
|
|
1775
1792
|
block: block2,
|
|
1776
|
-
|
|
1793
|
+
next_attempt_at,
|
|
1777
1794
|
failed_at
|
|
1778
1795
|
};
|
|
1779
1796
|
}
|
|
1780
|
-
function
|
|
1781
|
-
const {
|
|
1797
|
+
function build_handle(deps) {
|
|
1798
|
+
const {
|
|
1799
|
+
logger,
|
|
1800
|
+
bound_do,
|
|
1801
|
+
bound_load,
|
|
1802
|
+
bound_query,
|
|
1803
|
+
bound_query_array,
|
|
1804
|
+
bound_forget
|
|
1805
|
+
} = deps;
|
|
1782
1806
|
return async (lease, payloads) => {
|
|
1783
1807
|
if (payloads.length === 0) return { lease, handled: 0, acked_at: lease.at };
|
|
1784
1808
|
const stream = lease.stream;
|
|
@@ -1786,23 +1810,24 @@ function buildHandle(deps) {
|
|
|
1786
1810
|
let handled = 0;
|
|
1787
1811
|
if (lease.retry > 0)
|
|
1788
1812
|
logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
|
|
1789
|
-
const
|
|
1790
|
-
do:
|
|
1791
|
-
load:
|
|
1792
|
-
query:
|
|
1793
|
-
query_array:
|
|
1813
|
+
const scoped_app = {
|
|
1814
|
+
do: bound_do,
|
|
1815
|
+
load: bound_load,
|
|
1816
|
+
query: bound_query,
|
|
1817
|
+
query_array: bound_query_array,
|
|
1818
|
+
forget: bound_forget
|
|
1794
1819
|
};
|
|
1795
1820
|
for (const payload of payloads) {
|
|
1796
1821
|
const { event, handler } = payload;
|
|
1797
|
-
|
|
1822
|
+
scoped_app.do = (action2, target, action_payload, reactingTo, skipValidation) => bound_do(
|
|
1798
1823
|
action2,
|
|
1799
1824
|
target,
|
|
1800
|
-
|
|
1825
|
+
action_payload,
|
|
1801
1826
|
reactingTo ?? event,
|
|
1802
1827
|
skipValidation
|
|
1803
1828
|
);
|
|
1804
1829
|
try {
|
|
1805
|
-
await handler(event, stream,
|
|
1830
|
+
await handler(event, stream, scoped_app);
|
|
1806
1831
|
at = event.id;
|
|
1807
1832
|
handled++;
|
|
1808
1833
|
} catch (error) {
|
|
@@ -1820,10 +1845,12 @@ function buildHandle(deps) {
|
|
|
1820
1845
|
return finalize(lease, handled, at, void 0, payloads[0].options, logger);
|
|
1821
1846
|
};
|
|
1822
1847
|
}
|
|
1823
|
-
function
|
|
1848
|
+
function build_handle_batch(logger) {
|
|
1824
1849
|
return async (lease, payloads, batchHandler) => {
|
|
1825
1850
|
const stream = lease.stream;
|
|
1826
|
-
const events = payloads.map(
|
|
1851
|
+
const events = payloads.map(
|
|
1852
|
+
(p) => p.event
|
|
1853
|
+
);
|
|
1827
1854
|
const options = payloads[0].options;
|
|
1828
1855
|
if (lease.retry > 0)
|
|
1829
1856
|
logger.warn(`Retrying batch ${stream}@${events[0].id} (${lease.retry}).`);
|
|
@@ -1850,23 +1877,23 @@ var SettleLoop = class {
|
|
|
1850
1877
|
_deps;
|
|
1851
1878
|
/** Debounce window applied when the caller doesn't override via `SettleOptions.debounceMs`. */
|
|
1852
1879
|
_default_debounce_ms;
|
|
1853
|
-
constructor(deps,
|
|
1880
|
+
constructor(deps, default_debounce_ms) {
|
|
1854
1881
|
this._deps = deps;
|
|
1855
|
-
this._default_debounce_ms =
|
|
1882
|
+
this._default_debounce_ms = default_debounce_ms;
|
|
1856
1883
|
}
|
|
1857
1884
|
/**
|
|
1858
1885
|
* Schedule a settle pass. Multiple calls inside the debounce window
|
|
1859
1886
|
* coalesce into one cycle. The cycle runs correlate→drain in a loop
|
|
1860
1887
|
* until no progress is made (no new subscriptions, no acks, no blocks)
|
|
1861
1888
|
* or `maxPasses` is reached, then emits the `"settled"` lifecycle event
|
|
1862
|
-
* via {@link SettleDeps.
|
|
1889
|
+
* via {@link SettleDeps.on_settled}.
|
|
1863
1890
|
*/
|
|
1864
1891
|
schedule(options = {}) {
|
|
1865
1892
|
const {
|
|
1866
1893
|
debounceMs = this._default_debounce_ms,
|
|
1867
|
-
correlate:
|
|
1894
|
+
correlate: correlate_query = { after: -1, limit: 100 },
|
|
1868
1895
|
maxPasses = Infinity,
|
|
1869
|
-
...
|
|
1896
|
+
...drain_options
|
|
1870
1897
|
} = options;
|
|
1871
1898
|
if (this._timer) clearTimeout(this._timer);
|
|
1872
1899
|
this._timer = setTimeout(() => {
|
|
@@ -1875,17 +1902,17 @@ var SettleLoop = class {
|
|
|
1875
1902
|
this._running = true;
|
|
1876
1903
|
(async () => {
|
|
1877
1904
|
await this._deps.init();
|
|
1878
|
-
let
|
|
1905
|
+
let last_drain;
|
|
1879
1906
|
for (let i = 0; i < maxPasses; i++) {
|
|
1880
1907
|
const { subscribed } = await this._deps.correlate({
|
|
1881
|
-
...
|
|
1908
|
+
...correlate_query,
|
|
1882
1909
|
after: this._deps.checkpoint()
|
|
1883
1910
|
});
|
|
1884
|
-
|
|
1885
|
-
const made_progress = subscribed > 0 ||
|
|
1911
|
+
last_drain = await this._deps.drain(drain_options);
|
|
1912
|
+
const made_progress = subscribed > 0 || last_drain.acked.length > 0 || last_drain.blocked.length > 0;
|
|
1886
1913
|
if (!made_progress) break;
|
|
1887
1914
|
}
|
|
1888
|
-
if (
|
|
1915
|
+
if (last_drain) this._deps.on_settled(last_drain);
|
|
1889
1916
|
})().catch((err) => this._deps.logger.error(err)).finally(() => {
|
|
1890
1917
|
this._running = false;
|
|
1891
1918
|
});
|
|
@@ -1972,7 +1999,7 @@ var Act = class {
|
|
|
1972
1999
|
_event_to_state;
|
|
1973
2000
|
/**
|
|
1974
2001
|
* Event-name → lane fan-in for selective arming (ACT-1103). Built by
|
|
1975
|
-
* `
|
|
2002
|
+
* `classify_registry` once per build. `"all"` means at least one of
|
|
1976
2003
|
* the event's reactions is a dynamic resolver (lane opaque until
|
|
1977
2004
|
* runtime); a `Set<string>` lists the static lanes only that event's
|
|
1978
2005
|
* reactions target.
|
|
@@ -1995,9 +2022,9 @@ var Act = class {
|
|
|
1995
2022
|
_scoped;
|
|
1996
2023
|
/**
|
|
1997
2024
|
* Correlation-id generator for originating actions. Bound at
|
|
1998
|
-
* construction from `options.correlator ??
|
|
2025
|
+
* construction from `options.correlator ?? default_correlator`. The
|
|
1999
2026
|
* `do()` path passes this into the `_es.action` closure; close-cycle
|
|
2000
|
-
* uses it via {@link
|
|
2027
|
+
* uses it via {@link close_correlation}.
|
|
2001
2028
|
*/
|
|
2002
2029
|
_correlator;
|
|
2003
2030
|
/** Pre-bound IAct methods reused across drain cycles. Only `do` varies per
|
|
@@ -2006,7 +2033,8 @@ var Act = class {
|
|
|
2006
2033
|
_bound_load = this.load.bind(this);
|
|
2007
2034
|
_bound_query = this.query.bind(this);
|
|
2008
2035
|
_bound_query_array = this.query_array.bind(this);
|
|
2009
|
-
|
|
2036
|
+
_bound_forget = this.forget.bind(this);
|
|
2037
|
+
/** Reaction dispatchers built once and handed to run_drain_cycle each cycle. */
|
|
2010
2038
|
_handle;
|
|
2011
2039
|
_handle_batch;
|
|
2012
2040
|
/** Declared drain lanes (ACT-1103). */
|
|
@@ -2023,16 +2051,16 @@ var Act = class {
|
|
|
2023
2051
|
*
|
|
2024
2052
|
* @param registry Schemas for every event and action across registered states
|
|
2025
2053
|
* @param states Merged map of state name → state definition
|
|
2026
|
-
* @param
|
|
2054
|
+
* @param batch_handlers Static-target projection batch handlers (target → handler)
|
|
2027
2055
|
* @param options Tuning knobs — see {@link ActOptions}
|
|
2028
2056
|
* @param lanes Declared drain lanes (ACT-1103). The builder collects
|
|
2029
2057
|
* these from `.withLane(...)` calls. Slice 1 records them on the
|
|
2030
2058
|
* instance; later slices fan out one `DrainController` per lane.
|
|
2031
2059
|
*/
|
|
2032
|
-
constructor(registry, states = /* @__PURE__ */ new Map(),
|
|
2060
|
+
constructor(registry, states = /* @__PURE__ */ new Map(), batch_handlers = /* @__PURE__ */ new Map(), options = {}, lanes = []) {
|
|
2033
2061
|
this.registry = registry;
|
|
2034
2062
|
this._states = states;
|
|
2035
|
-
this._batch_handlers =
|
|
2063
|
+
this._batch_handlers = batch_handlers;
|
|
2036
2064
|
this._lanes = lanes;
|
|
2037
2065
|
if (options.onlyLanes && options.onlyLanes.length > 0) {
|
|
2038
2066
|
const declared = /* @__PURE__ */ new Set([
|
|
@@ -2046,49 +2074,50 @@ var Act = class {
|
|
|
2046
2074
|
);
|
|
2047
2075
|
}
|
|
2048
2076
|
this._scoped = options.scoped ? (fn) => scoped.run(options.scoped, fn) : (fn) => fn();
|
|
2049
|
-
this._correlator = options.correlator ??
|
|
2050
|
-
this._es =
|
|
2051
|
-
this._cd =
|
|
2052
|
-
this._handle =
|
|
2077
|
+
this._correlator = options.correlator ?? default_correlator;
|
|
2078
|
+
this._es = build_es(this._logger, this._correlator);
|
|
2079
|
+
this._cd = build_drain(this._logger);
|
|
2080
|
+
this._handle = build_handle({
|
|
2053
2081
|
logger: this._logger,
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2082
|
+
bound_do: this._bound_do,
|
|
2083
|
+
bound_load: this._bound_load,
|
|
2084
|
+
bound_query: this._bound_query,
|
|
2085
|
+
bound_query_array: this._bound_query_array,
|
|
2086
|
+
bound_forget: this._bound_forget
|
|
2058
2087
|
});
|
|
2059
|
-
this._handle_batch =
|
|
2088
|
+
this._handle_batch = build_handle_batch(this._logger);
|
|
2060
2089
|
const {
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
} =
|
|
2067
|
-
this._reactive_events =
|
|
2090
|
+
static_targets,
|
|
2091
|
+
has_dynamic_resolvers,
|
|
2092
|
+
reactive_events,
|
|
2093
|
+
event_to_state,
|
|
2094
|
+
event_to_lanes
|
|
2095
|
+
} = classify_registry(this.registry, this._states);
|
|
2096
|
+
this._reactive_events = reactive_events;
|
|
2068
2097
|
this._listen = options.listen !== false;
|
|
2069
2098
|
this._drain = options.drain !== false;
|
|
2070
|
-
this._event_to_state =
|
|
2071
|
-
this._event_to_lanes =
|
|
2072
|
-
const
|
|
2073
|
-
const
|
|
2074
|
-
const
|
|
2075
|
-
const
|
|
2099
|
+
this._event_to_state = event_to_state;
|
|
2100
|
+
this._event_to_lanes = event_to_lanes;
|
|
2101
|
+
const all_lanes = ["default", ...lanes.map((l) => l.name)];
|
|
2102
|
+
const only_set = options.onlyLanes && options.onlyLanes.length > 0 ? new Set(options.onlyLanes) : void 0;
|
|
2103
|
+
const active_lanes = only_set ? all_lanes.filter((n) => only_set.has(n)) : all_lanes;
|
|
2104
|
+
const single_default_lane = active_lanes.length === 1 && active_lanes[0] === "default";
|
|
2076
2105
|
this._drain_controllers = /* @__PURE__ */ new Map();
|
|
2077
|
-
for (const name of
|
|
2106
|
+
for (const name of active_lanes) {
|
|
2078
2107
|
const cfg = lanes.find((l) => l.name === name);
|
|
2079
2108
|
const controller = new DrainController({
|
|
2080
2109
|
logger: this._logger,
|
|
2081
2110
|
ops: this._cd,
|
|
2082
2111
|
registry: this.registry,
|
|
2083
|
-
|
|
2112
|
+
batch_handlers: this._batch_handlers,
|
|
2084
2113
|
handle: this._handle,
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2114
|
+
handle_batch: this._handle_batch,
|
|
2115
|
+
on_acked: (acked) => this.emit("acked", acked),
|
|
2116
|
+
on_blocked: (blocked) => this.emit("blocked", blocked),
|
|
2088
2117
|
// Pass lane only when a true per-lane controller is active.
|
|
2089
2118
|
// The all-lanes (single default) case keeps lane=undefined so
|
|
2090
2119
|
// adapter SQL collapses to the pre-1103 shape.
|
|
2091
|
-
lane:
|
|
2120
|
+
lane: single_default_lane ? void 0 : name,
|
|
2092
2121
|
defaults: cfg && {
|
|
2093
2122
|
streamLimit: cfg.streamLimit,
|
|
2094
2123
|
leaseMillis: cfg.leaseMillis
|
|
@@ -2101,22 +2130,22 @@ var Act = class {
|
|
|
2101
2130
|
this._audit_deps = {
|
|
2102
2131
|
store,
|
|
2103
2132
|
logger: this._logger,
|
|
2104
|
-
event_to_state
|
|
2133
|
+
event_to_state,
|
|
2105
2134
|
states: this._states,
|
|
2106
|
-
known_events: new Set(
|
|
2135
|
+
known_events: new Set(event_to_state.keys()),
|
|
2107
2136
|
declared_lanes: new Set(this._drain_controllers.keys()),
|
|
2108
|
-
routed_events: new Set(
|
|
2137
|
+
routed_events: new Set(event_to_lanes.keys())
|
|
2109
2138
|
};
|
|
2110
2139
|
this._correlate = new CorrelateCycle(
|
|
2111
2140
|
this.registry,
|
|
2112
|
-
|
|
2113
|
-
|
|
2141
|
+
static_targets,
|
|
2142
|
+
has_dynamic_resolvers,
|
|
2114
2143
|
this._cd,
|
|
2115
2144
|
options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
2116
2145
|
// Cold start: assume drain is needed (historical events may need processing).
|
|
2117
2146
|
// #803: writer-only instances skip the cold-start arm.
|
|
2118
2147
|
() => {
|
|
2119
|
-
if (this._drain && this._reactive_events.size > 0) this.
|
|
2148
|
+
if (this._drain && this._reactive_events.size > 0) this._arm_all();
|
|
2120
2149
|
}
|
|
2121
2150
|
);
|
|
2122
2151
|
this._settle = new SettleLoop(
|
|
@@ -2126,11 +2155,11 @@ var Act = class {
|
|
|
2126
2155
|
checkpoint: () => this._correlate.checkpoint,
|
|
2127
2156
|
correlate: (q) => this.correlate(q),
|
|
2128
2157
|
drain: (o) => this.drain(o),
|
|
2129
|
-
|
|
2158
|
+
on_settled: (drain) => this.emit("settled", drain)
|
|
2130
2159
|
},
|
|
2131
2160
|
options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
|
|
2132
2161
|
);
|
|
2133
|
-
this._notify_disposer = this.
|
|
2162
|
+
this._notify_disposer = this._wire_notify(options.scoped?.store ?? store());
|
|
2134
2163
|
dispose(() => this.shutdown());
|
|
2135
2164
|
}
|
|
2136
2165
|
/** True after the first `shutdown()` call. Guards idempotency. */
|
|
@@ -2164,7 +2193,7 @@ var Act = class {
|
|
|
2164
2193
|
* subscription was made). Errors during subscription are logged but
|
|
2165
2194
|
* never thrown — `notify` is a hint, not a contract.
|
|
2166
2195
|
*/
|
|
2167
|
-
async
|
|
2196
|
+
async _wire_notify(s) {
|
|
2168
2197
|
if (this._reactive_events.size === 0) return void 0;
|
|
2169
2198
|
if (!s.notify) return void 0;
|
|
2170
2199
|
if (!this._listen) return void 0;
|
|
@@ -2173,7 +2202,7 @@ var Act = class {
|
|
|
2173
2202
|
try {
|
|
2174
2203
|
this.emit("notified", notification);
|
|
2175
2204
|
if (this._drain) {
|
|
2176
|
-
const armed = this.
|
|
2205
|
+
const armed = this._arm_for_event_names(
|
|
2177
2206
|
notification.events.map((e) => e.name)
|
|
2178
2207
|
);
|
|
2179
2208
|
if (armed) this._settle.schedule({ debounceMs: 0 });
|
|
@@ -2279,14 +2308,14 @@ var Act = class {
|
|
|
2279
2308
|
skipValidation
|
|
2280
2309
|
);
|
|
2281
2310
|
if (this._reactive_events.size > 0)
|
|
2282
|
-
this.
|
|
2311
|
+
this._arm_for_event_names(
|
|
2283
2312
|
snapshots.map((s) => s.event.name)
|
|
2284
2313
|
);
|
|
2285
2314
|
this.emit("committed", snapshots);
|
|
2286
2315
|
return snapshots;
|
|
2287
2316
|
});
|
|
2288
2317
|
}
|
|
2289
|
-
async load(stateOrName, stream, callback, asOf) {
|
|
2318
|
+
async load(stateOrName, stream, callback, asOf, actor) {
|
|
2290
2319
|
return this._scoped(async () => {
|
|
2291
2320
|
let merged;
|
|
2292
2321
|
if (typeof stateOrName === "string") {
|
|
@@ -2296,7 +2325,7 @@ var Act = class {
|
|
|
2296
2325
|
} else {
|
|
2297
2326
|
merged = this._states.get(stateOrName.name) || stateOrName;
|
|
2298
2327
|
}
|
|
2299
|
-
return await this._es.load(merged, stream, callback, asOf);
|
|
2328
|
+
return await this._es.load(merged, stream, callback, asOf, actor);
|
|
2300
2329
|
});
|
|
2301
2330
|
}
|
|
2302
2331
|
/**
|
|
@@ -2390,6 +2419,34 @@ var Act = class {
|
|
|
2390
2419
|
return events;
|
|
2391
2420
|
});
|
|
2392
2421
|
}
|
|
2422
|
+
/**
|
|
2423
|
+
* Wipe the sensitive-data payload for every event on the stream — see
|
|
2424
|
+
* {@link IAct.forget}. Application-level half of #566.
|
|
2425
|
+
*
|
|
2426
|
+
* Throws on adapters without `Store.forget_pii`, invalidates the cache
|
|
2427
|
+
* entry for the stream, emits the `forgotten` lifecycle event with the
|
|
2428
|
+
* row count. Idempotent: a second call returns `{eventCount: 0}` and
|
|
2429
|
+
* does NOT re-emit.
|
|
2430
|
+
*
|
|
2431
|
+
* @param stream - Target stream.
|
|
2432
|
+
* @returns `{eventCount}` — number of events whose PII column was wiped.
|
|
2433
|
+
*/
|
|
2434
|
+
async forget(stream) {
|
|
2435
|
+
return this._scoped(async () => {
|
|
2436
|
+
const s = store();
|
|
2437
|
+
if (!s.forget_pii) {
|
|
2438
|
+
throw new Error(
|
|
2439
|
+
`Store does not implement forget_pii \u2014 adapter cannot comply with sensitive-data erasure. Use an adapter that declares pii_isolation: true (e.g. @rotorsoft/act on the in-memory store).`
|
|
2440
|
+
);
|
|
2441
|
+
}
|
|
2442
|
+
const eventCount = await s.forget_pii(stream);
|
|
2443
|
+
await cache().invalidate(stream);
|
|
2444
|
+
if (eventCount > 0) {
|
|
2445
|
+
this.emit("forgotten", { stream, at: /* @__PURE__ */ new Date(), eventCount });
|
|
2446
|
+
}
|
|
2447
|
+
return { eventCount };
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2393
2450
|
/**
|
|
2394
2451
|
* Processes pending reactions by draining uncommitted events from the event store.
|
|
2395
2452
|
*
|
|
@@ -2430,28 +2487,28 @@ var Act = class {
|
|
|
2430
2487
|
async drain(options = {}) {
|
|
2431
2488
|
if (!this._drain)
|
|
2432
2489
|
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
2433
|
-
return this._scoped(() => this.
|
|
2490
|
+
return this._scoped(() => this._drain_all(options));
|
|
2434
2491
|
}
|
|
2435
2492
|
/** Arm every active lane controller (ACT-1103). */
|
|
2436
|
-
|
|
2493
|
+
_arm_all() {
|
|
2437
2494
|
for (const c of this._drain_controllers.values()) c.arm();
|
|
2438
2495
|
}
|
|
2439
2496
|
/**
|
|
2440
2497
|
* Arm only the lane controllers whose reactions match the supplied
|
|
2441
2498
|
* event names (ACT-1103 selective arming). Events with any dynamic
|
|
2442
|
-
* resolver fall back to `
|
|
2499
|
+
* resolver fall back to `_arm_all()` via the `"all"` sentinel — the
|
|
2443
2500
|
* resolver's lane isn't known until correlate runs the function.
|
|
2444
2501
|
* Events with no reactions are skipped; `_event_to_lanes` doesn't
|
|
2445
2502
|
* carry them. Returns true when any controller was armed (used by
|
|
2446
2503
|
* the notify handler to decide whether to schedule a settle).
|
|
2447
2504
|
*/
|
|
2448
|
-
|
|
2505
|
+
_arm_for_event_names(names) {
|
|
2449
2506
|
const to_arm = /* @__PURE__ */ new Set();
|
|
2450
2507
|
for (const name of names) {
|
|
2451
2508
|
const set = this._event_to_lanes.get(name);
|
|
2452
2509
|
if (set === void 0) continue;
|
|
2453
2510
|
if (set === ALL_LANES) {
|
|
2454
|
-
this.
|
|
2511
|
+
this._arm_all();
|
|
2455
2512
|
return true;
|
|
2456
2513
|
}
|
|
2457
2514
|
for (const lane of set) to_arm.add(lane);
|
|
@@ -2468,7 +2525,7 @@ var Act = class {
|
|
|
2468
2525
|
* `SKIP LOCKED` keeps cross-controller races safe. Lifecycle events
|
|
2469
2526
|
* (`acked`, `blocked`) may interleave by lane — listeners filter via
|
|
2470
2527
|
* `lease.lane`. */
|
|
2471
|
-
async
|
|
2528
|
+
async _drain_all(options) {
|
|
2472
2529
|
const results = await Promise.all(
|
|
2473
2530
|
[...this._drain_controllers.values()].map((c) => c.drain(options))
|
|
2474
2531
|
);
|
|
@@ -2588,7 +2645,7 @@ var Act = class {
|
|
|
2588
2645
|
* @see {@link stop_correlations} to stop the worker
|
|
2589
2646
|
*/
|
|
2590
2647
|
start_correlations(query = {}, frequency = 1e4, callback) {
|
|
2591
|
-
return this._correlate.
|
|
2648
|
+
return this._correlate.start_polling(query, frequency, callback);
|
|
2592
2649
|
}
|
|
2593
2650
|
/**
|
|
2594
2651
|
* Stops the automatic correlation worker.
|
|
@@ -2608,7 +2665,7 @@ var Act = class {
|
|
|
2608
2665
|
* @see {@link start_correlations}
|
|
2609
2666
|
*/
|
|
2610
2667
|
stop_correlations() {
|
|
2611
|
-
this._correlate.
|
|
2668
|
+
this._correlate.stop_polling();
|
|
2612
2669
|
}
|
|
2613
2670
|
/**
|
|
2614
2671
|
* Cancels any pending or active settle cycle.
|
|
@@ -2654,7 +2711,7 @@ var Act = class {
|
|
|
2654
2711
|
async reset(input) {
|
|
2655
2712
|
return this._scoped(async () => {
|
|
2656
2713
|
const count = await store().reset(input);
|
|
2657
|
-
if (count > 0 && this._reactive_events.size > 0) this.
|
|
2714
|
+
if (count > 0 && this._reactive_events.size > 0) this._arm_all();
|
|
2658
2715
|
return count;
|
|
2659
2716
|
});
|
|
2660
2717
|
}
|
|
@@ -2688,7 +2745,7 @@ var Act = class {
|
|
|
2688
2745
|
async unblock(input) {
|
|
2689
2746
|
return this._scoped(async () => {
|
|
2690
2747
|
const count = await store().unblock(input);
|
|
2691
|
-
if (count > 0 && this._reactive_events.size > 0) this.
|
|
2748
|
+
if (count > 0 && this._reactive_events.size > 0) this._arm_all();
|
|
2692
2749
|
return count;
|
|
2693
2750
|
});
|
|
2694
2751
|
}
|
|
@@ -2914,14 +2971,14 @@ var Act = class {
|
|
|
2914
2971
|
if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
|
|
2915
2972
|
return this._scoped(async () => {
|
|
2916
2973
|
await this.correlate({ limit: 1e3 });
|
|
2917
|
-
const
|
|
2918
|
-
const result = await
|
|
2919
|
-
|
|
2920
|
-
|
|
2974
|
+
const close_actor = { id: "$close", name: "close" };
|
|
2975
|
+
const result = await run_close_cycle(targets, {
|
|
2976
|
+
reactive_events_size: this._reactive_events.size,
|
|
2977
|
+
event_to_state: this._event_to_state,
|
|
2921
2978
|
load: this._es.load,
|
|
2922
2979
|
tombstone: this._es.tombstone,
|
|
2923
2980
|
logger: this._logger,
|
|
2924
|
-
correlation:
|
|
2981
|
+
correlation: close_correlation(this._correlator, close_actor)
|
|
2925
2982
|
});
|
|
2926
2983
|
this.emit("closed", result);
|
|
2927
2984
|
return result;
|
|
@@ -2964,17 +3021,17 @@ var Act = class {
|
|
|
2964
3021
|
};
|
|
2965
3022
|
|
|
2966
3023
|
// src/builders/act-builder.ts
|
|
2967
|
-
function
|
|
3024
|
+
function register_batch_handler(proj, batch_handlers) {
|
|
2968
3025
|
if (!proj.batchHandler || !proj.target) return;
|
|
2969
|
-
const existing =
|
|
3026
|
+
const existing = batch_handlers.get(proj.target);
|
|
2970
3027
|
if (existing && existing !== proj.batchHandler) {
|
|
2971
3028
|
throw new Error(`Duplicate batch handler for target "${proj.target}"`);
|
|
2972
3029
|
}
|
|
2973
|
-
|
|
3030
|
+
batch_handlers.set(proj.target, proj.batchHandler);
|
|
2974
3031
|
}
|
|
2975
|
-
function
|
|
3032
|
+
function validate_lane_references(registry, lanes) {
|
|
2976
3033
|
const declared = /* @__PURE__ */ new Set([DEFAULT_LANE, ...lanes.map((l) => l.name)]);
|
|
2977
|
-
for (const [
|
|
3034
|
+
for (const [event_name, def] of Object.entries(registry.events)) {
|
|
2978
3035
|
const entry = def;
|
|
2979
3036
|
for (const [handlerName, reaction] of entry.reactions) {
|
|
2980
3037
|
const resolver = reaction.resolver;
|
|
@@ -2982,7 +3039,7 @@ function validateLaneReferences(registry, lanes) {
|
|
|
2982
3039
|
const lane = resolver.lane;
|
|
2983
3040
|
if (lane && !declared.has(lane)) {
|
|
2984
3041
|
throw new Error(
|
|
2985
|
-
`Reaction "${handlerName}" on "${
|
|
3042
|
+
`Reaction "${handlerName}" on "${event_name}" targets undeclared lane "${lane}". Declared lanes: ${[...declared].map((l) => `"${l}"`).join(", ")}. Add \`.withLane({ name: "${lane}", ... })\` to act() or correct the .to() declaration.`
|
|
2986
3043
|
);
|
|
2987
3044
|
}
|
|
2988
3045
|
}
|
|
@@ -2990,76 +3047,80 @@ function validateLaneReferences(registry, lanes) {
|
|
|
2990
3047
|
}
|
|
2991
3048
|
function act() {
|
|
2992
3049
|
const states = /* @__PURE__ */ new Map();
|
|
3050
|
+
const _sf = /* @__PURE__ */ new Map();
|
|
3051
|
+
const _dp = /* @__PURE__ */ new Map();
|
|
2993
3052
|
const registry = {
|
|
2994
3053
|
actions: {},
|
|
2995
|
-
events: {}
|
|
3054
|
+
events: {},
|
|
3055
|
+
sensitive_fields: (event_name) => _sf.get(event_name) ?? [],
|
|
3056
|
+
disclosure_predicate: (state_name) => _dp.get(state_name) ?? null
|
|
2996
3057
|
};
|
|
2997
|
-
const
|
|
2998
|
-
const
|
|
3058
|
+
const pending_projections = [];
|
|
3059
|
+
const batch_handlers = /* @__PURE__ */ new Map();
|
|
2999
3060
|
const lanes = [];
|
|
3000
3061
|
let _built = false;
|
|
3001
|
-
const
|
|
3002
|
-
const
|
|
3062
|
+
const finalize_deprecations = () => {
|
|
3063
|
+
const deprecation_summary = [];
|
|
3003
3064
|
for (const state2 of states.values()) {
|
|
3004
|
-
const
|
|
3005
|
-
const deprecated =
|
|
3065
|
+
const event_names = Object.keys(state2.events);
|
|
3066
|
+
const deprecated = deprecated_event_names(event_names);
|
|
3006
3067
|
if (deprecated.size === 0) continue;
|
|
3007
3068
|
state2._deprecated = deprecated;
|
|
3008
3069
|
for (const name of deprecated) {
|
|
3009
|
-
const current =
|
|
3010
|
-
|
|
3011
|
-
|
|
3070
|
+
const current = current_version_of(name, event_names);
|
|
3071
|
+
deprecation_summary.push({
|
|
3072
|
+
state_name: state2.name,
|
|
3012
3073
|
deprecated: name,
|
|
3013
3074
|
current
|
|
3014
3075
|
});
|
|
3015
3076
|
}
|
|
3016
|
-
for (const [
|
|
3017
|
-
const
|
|
3018
|
-
if (
|
|
3019
|
-
const current =
|
|
3077
|
+
for (const [action_name, handler] of Object.entries(state2.on)) {
|
|
3078
|
+
const static_target = handler?._static_emit;
|
|
3079
|
+
if (static_target && deprecated.has(static_target)) {
|
|
3080
|
+
const current = current_version_of(static_target, event_names);
|
|
3020
3081
|
throw new Error(
|
|
3021
|
-
`Action "${
|
|
3082
|
+
`Action "${action_name}" in state "${state2.name}" emits deprecated event "${static_target}". A newer version exists: "${current}". Update the .emit() call to target the current version. The reducer (.patch) for "${static_target}" stays as-is \u2014 historical events still need it.`
|
|
3022
3083
|
);
|
|
3023
3084
|
}
|
|
3024
3085
|
}
|
|
3025
3086
|
}
|
|
3026
|
-
if (
|
|
3027
|
-
const list =
|
|
3028
|
-
(d) => `"${d.deprecated}" (current: "${d.current}", state: "${d.
|
|
3087
|
+
if (deprecation_summary.length > 0) {
|
|
3088
|
+
const list = deprecation_summary.map(
|
|
3089
|
+
(d) => `"${d.deprecated}" (current: "${d.current}", state: "${d.state_name}")`
|
|
3029
3090
|
).join(", ");
|
|
3030
3091
|
log().info(
|
|
3031
|
-
`Act registered ${
|
|
3092
|
+
`Act registered ${deprecation_summary.length} deprecated event(s): ${list}. These are legacy versions kept for the read path. Consider truncating closed streams via app.close() when feasible to reduce historical event load. See docs/docs/architecture/event-schema-evolution.md.`
|
|
3032
3093
|
);
|
|
3033
3094
|
}
|
|
3034
3095
|
};
|
|
3035
3096
|
const builder = {
|
|
3036
3097
|
withState: (state2) => {
|
|
3037
|
-
|
|
3098
|
+
register_state(state2, states, registry.actions, registry.events);
|
|
3038
3099
|
return builder;
|
|
3039
3100
|
},
|
|
3040
3101
|
withSlice: (input) => {
|
|
3041
3102
|
for (const s of input.states.values()) {
|
|
3042
|
-
|
|
3103
|
+
register_state(s, states, registry.actions, registry.events);
|
|
3043
3104
|
}
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
for (const
|
|
3047
|
-
const existing = lanes.find((l) => l.name ===
|
|
3105
|
+
merge_event_register(registry.events, input.events);
|
|
3106
|
+
pending_projections.push(...input.projections);
|
|
3107
|
+
for (const slice_lane of input.lanes) {
|
|
3108
|
+
const existing = lanes.find((l) => l.name === slice_lane.name);
|
|
3048
3109
|
if (!existing) {
|
|
3049
|
-
lanes.push(
|
|
3110
|
+
lanes.push(slice_lane);
|
|
3050
3111
|
continue;
|
|
3051
3112
|
}
|
|
3052
|
-
if (existing.leaseMillis !==
|
|
3113
|
+
if (existing.leaseMillis !== slice_lane.leaseMillis || existing.streamLimit !== slice_lane.streamLimit || existing.cycleMs !== slice_lane.cycleMs) {
|
|
3053
3114
|
throw new Error(
|
|
3054
|
-
`Lane "${
|
|
3115
|
+
`Lane "${slice_lane.name}" was already declared with a different config`
|
|
3055
3116
|
);
|
|
3056
3117
|
}
|
|
3057
3118
|
}
|
|
3058
3119
|
return builder;
|
|
3059
3120
|
},
|
|
3060
3121
|
withProjection: (proj) => {
|
|
3061
|
-
|
|
3062
|
-
|
|
3122
|
+
merge_projection(proj, registry.events);
|
|
3123
|
+
register_batch_handler(proj, batch_handlers);
|
|
3063
3124
|
return builder;
|
|
3064
3125
|
},
|
|
3065
3126
|
withActor: () => builder,
|
|
@@ -3097,18 +3158,70 @@ function act() {
|
|
|
3097
3158
|
}),
|
|
3098
3159
|
build: (options) => {
|
|
3099
3160
|
if (!_built) {
|
|
3100
|
-
for (const proj of
|
|
3101
|
-
|
|
3102
|
-
|
|
3161
|
+
for (const proj of pending_projections) {
|
|
3162
|
+
merge_projection(proj, registry.events);
|
|
3163
|
+
register_batch_handler(proj, batch_handlers);
|
|
3164
|
+
}
|
|
3165
|
+
finalize_deprecations();
|
|
3166
|
+
validate_lane_references(registry, lanes);
|
|
3167
|
+
for (const [event_name, reg] of Object.entries(
|
|
3168
|
+
registry.events
|
|
3169
|
+
)) {
|
|
3170
|
+
const fields = pii_fields(reg.schema);
|
|
3171
|
+
if (fields.length === 0) continue;
|
|
3172
|
+
_sf.set(event_name, fields);
|
|
3173
|
+
for (const [name, reaction] of reg.reactions) {
|
|
3174
|
+
const inner = reaction.handler;
|
|
3175
|
+
const wrapped = (event, stream, app) => inner(pii_strip(event, fields), stream, app);
|
|
3176
|
+
Object.defineProperty(wrapped, "name", { value: inner.name });
|
|
3177
|
+
reaction.handler = wrapped;
|
|
3178
|
+
reg.reactions.set(name, reaction);
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
for (const state2 of states.values()) {
|
|
3182
|
+
if (state2.disclose) _dp.set(state2.name, state2.disclose);
|
|
3183
|
+
const fields_by_event = /* @__PURE__ */ new Map();
|
|
3184
|
+
for (const event_name of Object.keys(state2.events)) {
|
|
3185
|
+
const fields = _sf.get(event_name);
|
|
3186
|
+
if (fields) fields_by_event.set(event_name, fields);
|
|
3187
|
+
}
|
|
3188
|
+
if (fields_by_event.size === 0) continue;
|
|
3189
|
+
if (state2.snap) {
|
|
3190
|
+
const offending = [...fields_by_event.keys()];
|
|
3191
|
+
throw new Error(
|
|
3192
|
+
`State "${state2.name}" cannot snapshot \u2014 events {${offending.join(", ")}} carry sensitive fields. Snapshots write derived state into __snapshot__.data, which forget_pii cannot reach. Remove .snap() or remove sensitive(...) markers.`
|
|
3193
|
+
);
|
|
3194
|
+
}
|
|
3195
|
+
const disclose = state2.disclose ?? null;
|
|
3196
|
+
state2.view = (event, actor) => {
|
|
3197
|
+
const fields = fields_by_event.get(event.name);
|
|
3198
|
+
return fields ? pii_gate(event, fields, disclose, actor) : event;
|
|
3199
|
+
};
|
|
3200
|
+
state2.message = (validated) => {
|
|
3201
|
+
const fields = fields_by_event.get(validated.name);
|
|
3202
|
+
return fields ? pii_split(validated, fields) : validated;
|
|
3203
|
+
};
|
|
3204
|
+
for (const [event_name, fields] of fields_by_event) {
|
|
3205
|
+
const original = state2.patch[event_name];
|
|
3206
|
+
state2.patch[event_name] = (event, s) => original(pii_merge(event, fields), s);
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
for (const [target, original] of batch_handlers) {
|
|
3210
|
+
const wrapped = async (events, stream) => {
|
|
3211
|
+
const stripped = events.map((e) => {
|
|
3212
|
+
const f = _sf.get(e.name);
|
|
3213
|
+
return f ? pii_strip(e, f) : e;
|
|
3214
|
+
});
|
|
3215
|
+
return original(stripped, stream);
|
|
3216
|
+
};
|
|
3217
|
+
batch_handlers.set(target, wrapped);
|
|
3103
3218
|
}
|
|
3104
|
-
finalizeDeprecations();
|
|
3105
|
-
validateLaneReferences(registry, lanes);
|
|
3106
3219
|
_built = true;
|
|
3107
3220
|
}
|
|
3108
3221
|
return new Act(
|
|
3109
3222
|
registry,
|
|
3110
3223
|
states,
|
|
3111
|
-
|
|
3224
|
+
batch_handlers,
|
|
3112
3225
|
options,
|
|
3113
3226
|
lanes
|
|
3114
3227
|
);
|
|
@@ -3121,7 +3234,7 @@ function act() {
|
|
|
3121
3234
|
// src/builders/projection-builder.ts
|
|
3122
3235
|
function _projection(target) {
|
|
3123
3236
|
const events = {};
|
|
3124
|
-
const
|
|
3237
|
+
const default_resolver = typeof target === "string" ? { target } : void 0;
|
|
3125
3238
|
const base = {
|
|
3126
3239
|
on: (entry) => {
|
|
3127
3240
|
const keys = Object.keys(entry);
|
|
@@ -3138,7 +3251,7 @@ function _projection(target) {
|
|
|
3138
3251
|
do: (handler) => {
|
|
3139
3252
|
const reaction = {
|
|
3140
3253
|
handler,
|
|
3141
|
-
resolver:
|
|
3254
|
+
resolver: default_resolver ?? _this_,
|
|
3142
3255
|
options: {
|
|
3143
3256
|
blockOnError: true,
|
|
3144
3257
|
maxRetries: 3
|
|
@@ -3194,7 +3307,7 @@ function slice() {
|
|
|
3194
3307
|
const lanes = [];
|
|
3195
3308
|
const builder = {
|
|
3196
3309
|
withState: (state2) => {
|
|
3197
|
-
|
|
3310
|
+
register_state(state2, states, actions, events);
|
|
3198
3311
|
return builder;
|
|
3199
3312
|
},
|
|
3200
3313
|
withProjection: (proj) => {
|
|
@@ -3250,12 +3363,12 @@ function state(entry) {
|
|
|
3250
3363
|
const keys = Object.keys(entry);
|
|
3251
3364
|
if (keys.length !== 1) throw new Error("state() requires exactly one key");
|
|
3252
3365
|
const name = keys[0];
|
|
3253
|
-
const
|
|
3366
|
+
const state_schema = entry[name];
|
|
3254
3367
|
return {
|
|
3255
3368
|
init(init) {
|
|
3256
3369
|
return {
|
|
3257
3370
|
emits(events) {
|
|
3258
|
-
const
|
|
3371
|
+
const default_patch = Object.fromEntries(
|
|
3259
3372
|
Object.keys(events).map((k) => {
|
|
3260
3373
|
const fn = Object.assign(({ data }) => data, {
|
|
3261
3374
|
_passthrough: true
|
|
@@ -3266,11 +3379,16 @@ function state(entry) {
|
|
|
3266
3379
|
const internal = {
|
|
3267
3380
|
events,
|
|
3268
3381
|
actions: {},
|
|
3269
|
-
state:
|
|
3382
|
+
state: state_schema,
|
|
3270
3383
|
name,
|
|
3271
3384
|
init,
|
|
3272
|
-
patch:
|
|
3273
|
-
on: {}
|
|
3385
|
+
patch: default_patch,
|
|
3386
|
+
on: {},
|
|
3387
|
+
// Step delegates initialized as identity. `act().build()`
|
|
3388
|
+
// overrides on states with `sensitive(...)` events to bake in
|
|
3389
|
+
// the gate / split.
|
|
3390
|
+
view: (event) => event,
|
|
3391
|
+
message: (validated) => validated
|
|
3274
3392
|
};
|
|
3275
3393
|
const builder = action_builder(internal);
|
|
3276
3394
|
return Object.assign(builder, {
|
|
@@ -3306,11 +3424,14 @@ function action_builder(state2) {
|
|
|
3306
3424
|
}
|
|
3307
3425
|
function emit(handler) {
|
|
3308
3426
|
if (typeof handler === "string") {
|
|
3309
|
-
const
|
|
3310
|
-
const
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3427
|
+
const event_name = handler;
|
|
3428
|
+
const emit_fn = Object.assign(
|
|
3429
|
+
(payload) => [event_name, payload],
|
|
3430
|
+
{
|
|
3431
|
+
_static_emit: event_name
|
|
3432
|
+
}
|
|
3433
|
+
);
|
|
3434
|
+
internal.on[action2] = emit_fn;
|
|
3314
3435
|
} else {
|
|
3315
3436
|
internal.on[action2] = handler;
|
|
3316
3437
|
}
|
|
@@ -3322,6 +3443,10 @@ function action_builder(state2) {
|
|
|
3322
3443
|
internal.snap = snap2;
|
|
3323
3444
|
return builder;
|
|
3324
3445
|
},
|
|
3446
|
+
discloses(disclose) {
|
|
3447
|
+
internal.disclose = disclose;
|
|
3448
|
+
return builder;
|
|
3449
|
+
},
|
|
3325
3450
|
build() {
|
|
3326
3451
|
return internal;
|
|
3327
3452
|
}
|
|
@@ -3359,7 +3484,7 @@ var CsvFile = class {
|
|
|
3359
3484
|
let header = null;
|
|
3360
3485
|
for await (const line of lines) {
|
|
3361
3486
|
if (!line.trim()) continue;
|
|
3362
|
-
const fields =
|
|
3487
|
+
const fields = parse_csv_line(line);
|
|
3363
3488
|
if (!header) {
|
|
3364
3489
|
header = fields;
|
|
3365
3490
|
const expected = CSV_COLUMNS.join(",");
|
|
@@ -3396,21 +3521,21 @@ var CsvFile = class {
|
|
|
3396
3521
|
flags: "w",
|
|
3397
3522
|
encoding: "utf8"
|
|
3398
3523
|
});
|
|
3399
|
-
let
|
|
3524
|
+
let next_id = 1;
|
|
3400
3525
|
try {
|
|
3401
|
-
await
|
|
3526
|
+
await write_line(writer, CSV_COLUMNS.join(","));
|
|
3402
3527
|
await driver(async (event) => {
|
|
3403
|
-
const id =
|
|
3528
|
+
const id = next_id++;
|
|
3404
3529
|
const row = [
|
|
3405
3530
|
String(id),
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3531
|
+
csv_escape(event.name),
|
|
3532
|
+
csv_escape(JSON.stringify(event.data)),
|
|
3533
|
+
csv_escape(event.stream),
|
|
3409
3534
|
String(event.version),
|
|
3410
3535
|
event.created.toISOString(),
|
|
3411
|
-
|
|
3536
|
+
csv_escape(JSON.stringify(event.meta))
|
|
3412
3537
|
].join(",");
|
|
3413
|
-
await
|
|
3538
|
+
await write_line(writer, row);
|
|
3414
3539
|
return id;
|
|
3415
3540
|
});
|
|
3416
3541
|
} finally {
|
|
@@ -3443,7 +3568,7 @@ async function* linesFromBlob(blob) {
|
|
|
3443
3568
|
await Promise.resolve();
|
|
3444
3569
|
}
|
|
3445
3570
|
}
|
|
3446
|
-
function
|
|
3571
|
+
function parse_csv_line(line) {
|
|
3447
3572
|
const fields = [];
|
|
3448
3573
|
let i = 0;
|
|
3449
3574
|
while (i < line.length) {
|
|
@@ -3476,11 +3601,11 @@ function parseCsvLine(line) {
|
|
|
3476
3601
|
}
|
|
3477
3602
|
return fields;
|
|
3478
3603
|
}
|
|
3479
|
-
function
|
|
3604
|
+
function csv_escape(value) {
|
|
3480
3605
|
if (/[",\n\r]/.test(value)) return `"${value.replace(/"/g, '""')}"`;
|
|
3481
3606
|
return value;
|
|
3482
3607
|
}
|
|
3483
|
-
function
|
|
3608
|
+
function write_line(writer, line) {
|
|
3484
3609
|
return new Promise((resolve, reject) => {
|
|
3485
3610
|
writer.write(`${line}
|
|
3486
3611
|
`, (err) => {
|
|
@@ -3511,6 +3636,8 @@ export {
|
|
|
3511
3636
|
NonRetryableError,
|
|
3512
3637
|
PackageSchema,
|
|
3513
3638
|
QuerySchema,
|
|
3639
|
+
REDACTED,
|
|
3640
|
+
SHREDDED,
|
|
3514
3641
|
SNAP_EVENT,
|
|
3515
3642
|
StreamClosedError,
|
|
3516
3643
|
TOMBSTONE_EVENT,
|
|
@@ -3527,6 +3654,7 @@ export {
|
|
|
3527
3654
|
port,
|
|
3528
3655
|
projection,
|
|
3529
3656
|
scoped,
|
|
3657
|
+
sensitive,
|
|
3530
3658
|
sleep,
|
|
3531
3659
|
slice,
|
|
3532
3660
|
state,
|