@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.cjs
CHANGED
|
@@ -51,6 +51,8 @@ __export(index_exports, {
|
|
|
51
51
|
NonRetryableError: () => NonRetryableError,
|
|
52
52
|
PackageSchema: () => PackageSchema,
|
|
53
53
|
QuerySchema: () => QuerySchema,
|
|
54
|
+
REDACTED: () => REDACTED,
|
|
55
|
+
SHREDDED: () => SHREDDED,
|
|
54
56
|
SNAP_EVENT: () => SNAP_EVENT,
|
|
55
57
|
StreamClosedError: () => StreamClosedError,
|
|
56
58
|
TOMBSTONE_EVENT: () => TOMBSTONE_EVENT,
|
|
@@ -67,6 +69,7 @@ __export(index_exports, {
|
|
|
67
69
|
port: () => port,
|
|
68
70
|
projection: () => projection,
|
|
69
71
|
scoped: () => scoped,
|
|
72
|
+
sensitive: () => sensitive,
|
|
70
73
|
sleep: () => sleep,
|
|
71
74
|
slice: () => slice,
|
|
72
75
|
state: () => state,
|
|
@@ -122,7 +125,7 @@ var ConsoleLogger = class _ConsoleLogger {
|
|
|
122
125
|
this._pretty = pretty;
|
|
123
126
|
this.level = level;
|
|
124
127
|
const threshold = LEVEL_VALUES[level] ?? 30;
|
|
125
|
-
const write = pretty ? this.
|
|
128
|
+
const write = pretty ? this._pretty_write.bind(this, bindings) : this._json_write.bind(this, bindings);
|
|
126
129
|
this.fatal = write.bind(this, "fatal", 60);
|
|
127
130
|
this.error = threshold <= 50 ? write.bind(this, "error", 50) : noop;
|
|
128
131
|
this.warn = threshold <= 40 ? write.bind(this, "warn", 40) : noop;
|
|
@@ -141,24 +144,24 @@ var ConsoleLogger = class _ConsoleLogger {
|
|
|
141
144
|
bindings
|
|
142
145
|
});
|
|
143
146
|
}
|
|
144
|
-
|
|
147
|
+
_json_write(bindings, level, _num, obj_or_msg, msg) {
|
|
145
148
|
let obj;
|
|
146
149
|
let message;
|
|
147
|
-
if (typeof
|
|
148
|
-
message =
|
|
150
|
+
if (typeof obj_or_msg === "string") {
|
|
151
|
+
message = obj_or_msg;
|
|
149
152
|
obj = {};
|
|
150
|
-
} else if (
|
|
151
|
-
message = msg ??
|
|
153
|
+
} else if (obj_or_msg instanceof Error) {
|
|
154
|
+
message = msg ?? obj_or_msg.message;
|
|
152
155
|
obj = {
|
|
153
|
-
error: { message:
|
|
154
|
-
stack:
|
|
156
|
+
error: { message: obj_or_msg.message, name: obj_or_msg.name },
|
|
157
|
+
stack: obj_or_msg.stack
|
|
155
158
|
};
|
|
156
|
-
} else if (
|
|
159
|
+
} else if (obj_or_msg !== null && typeof obj_or_msg === "object") {
|
|
157
160
|
message = msg;
|
|
158
|
-
obj = { ...
|
|
161
|
+
obj = { ...obj_or_msg };
|
|
159
162
|
} else {
|
|
160
163
|
message = msg;
|
|
161
|
-
obj = { value:
|
|
164
|
+
obj = { value: obj_or_msg };
|
|
162
165
|
}
|
|
163
166
|
const entry = Object.assign({ level, time: Date.now() }, bindings, obj);
|
|
164
167
|
if (message) entry.msg = message;
|
|
@@ -175,29 +178,29 @@ var ConsoleLogger = class _ConsoleLogger {
|
|
|
175
178
|
}
|
|
176
179
|
process.stdout.write(line + "\n");
|
|
177
180
|
}
|
|
178
|
-
|
|
181
|
+
_pretty_write(bindings, level, _num, obj_or_msg, msg) {
|
|
179
182
|
const color = LEVEL_COLORS[level];
|
|
180
183
|
const tag = `${color}${level.toUpperCase().padEnd(5)}${RESET}`;
|
|
181
184
|
const ts = (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
|
|
182
185
|
let message;
|
|
183
186
|
let data;
|
|
184
|
-
if (typeof
|
|
185
|
-
message =
|
|
186
|
-
} else if (
|
|
187
|
-
message = msg ??
|
|
188
|
-
data =
|
|
187
|
+
if (typeof obj_or_msg === "string") {
|
|
188
|
+
message = obj_or_msg;
|
|
189
|
+
} else if (obj_or_msg instanceof Error) {
|
|
190
|
+
message = msg ?? obj_or_msg.message;
|
|
191
|
+
data = obj_or_msg.stack;
|
|
189
192
|
} else {
|
|
190
193
|
message = msg ?? "";
|
|
191
|
-
if (
|
|
194
|
+
if (obj_or_msg !== void 0 && obj_or_msg !== null) {
|
|
192
195
|
try {
|
|
193
|
-
data = JSON.stringify(
|
|
196
|
+
data = JSON.stringify(obj_or_msg);
|
|
194
197
|
} catch {
|
|
195
198
|
data = "[unserializable]";
|
|
196
199
|
}
|
|
197
200
|
}
|
|
198
201
|
}
|
|
199
|
-
const
|
|
200
|
-
const parts = [ts, tag, message, data,
|
|
202
|
+
const bind_str = bindings && Object.keys(bindings).length ? ` ${JSON.stringify(bindings)}` : "";
|
|
203
|
+
const parts = [ts, tag, message, data, bind_str].filter(Boolean);
|
|
201
204
|
process.stdout.write(parts.join(" ") + "\n");
|
|
202
205
|
}
|
|
203
206
|
};
|
|
@@ -380,55 +383,150 @@ var NonRetryableError = class extends Error {
|
|
|
380
383
|
};
|
|
381
384
|
|
|
382
385
|
// src/utils.ts
|
|
383
|
-
var
|
|
386
|
+
var import_zod4 = require("zod");
|
|
384
387
|
|
|
385
388
|
// src/config.ts
|
|
386
389
|
var fs = __toESM(require("fs"), 1);
|
|
387
|
-
var
|
|
390
|
+
var import_zod3 = require("zod");
|
|
388
391
|
|
|
389
392
|
// src/types/schemas.ts
|
|
393
|
+
var import_zod2 = require("zod");
|
|
394
|
+
|
|
395
|
+
// src/internal/sensitive.ts
|
|
390
396
|
var import_zod = require("zod");
|
|
391
|
-
var
|
|
392
|
-
var
|
|
393
|
-
|
|
394
|
-
|
|
397
|
+
var REDACTED = "[REDACTED]";
|
|
398
|
+
var SHREDDED = "[SHREDDED]";
|
|
399
|
+
var _registry = import_zod.z.registry();
|
|
400
|
+
function is_pii(schema) {
|
|
401
|
+
let cur = schema;
|
|
402
|
+
while (true) {
|
|
403
|
+
if (_registry.has(cur)) return true;
|
|
404
|
+
const inner = cur._def?.innerType;
|
|
405
|
+
if (!inner || inner === cur) return false;
|
|
406
|
+
cur = inner;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function pii_fields(schema) {
|
|
410
|
+
const shape = schema.shape;
|
|
411
|
+
if (!shape || typeof shape !== "object") return [];
|
|
412
|
+
const fields = [];
|
|
413
|
+
for (const key of Object.keys(shape)) {
|
|
414
|
+
if (is_pii(shape[key])) fields.push(key);
|
|
415
|
+
}
|
|
416
|
+
return fields;
|
|
417
|
+
}
|
|
418
|
+
function pii_split(emitted, fields) {
|
|
419
|
+
const rec = emitted.data;
|
|
420
|
+
const clean = {};
|
|
421
|
+
const pii = {};
|
|
422
|
+
for (const k of Object.keys(rec)) {
|
|
423
|
+
if (fields.includes(k)) pii[k] = rec[k];
|
|
424
|
+
else clean[k] = rec[k];
|
|
425
|
+
}
|
|
426
|
+
return { name: emitted.name, data: clean, pii };
|
|
427
|
+
}
|
|
428
|
+
function pii_merge(event, fields) {
|
|
429
|
+
const data = event.data;
|
|
430
|
+
const pii = event.pii;
|
|
431
|
+
if (pii != null) {
|
|
432
|
+
return {
|
|
433
|
+
...event,
|
|
434
|
+
data: { ...data, ...pii }
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
const shredded = { ...data };
|
|
438
|
+
for (const f of fields) shredded[f] = SHREDDED;
|
|
439
|
+
return {
|
|
440
|
+
...event,
|
|
441
|
+
data: shredded
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
function pii_gate(event, fields, predicate, actor) {
|
|
445
|
+
const data = event.data;
|
|
446
|
+
if (event.pii == null) {
|
|
447
|
+
const shredded = { ...data };
|
|
448
|
+
for (const f of fields) shredded[f] = SHREDDED;
|
|
449
|
+
return {
|
|
450
|
+
...event,
|
|
451
|
+
data: shredded
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
const allowed = !!actor && !!predicate && predicate(event, actor);
|
|
455
|
+
if (allowed) {
|
|
456
|
+
return {
|
|
457
|
+
...event,
|
|
458
|
+
data: {
|
|
459
|
+
...data,
|
|
460
|
+
...event.pii
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
const redacted = { ...data };
|
|
465
|
+
for (const f of fields) redacted[f] = REDACTED;
|
|
466
|
+
return {
|
|
467
|
+
...event,
|
|
468
|
+
data: redacted
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
function pii_strip(event, fields) {
|
|
472
|
+
const data = event.data;
|
|
473
|
+
const stripped = {};
|
|
474
|
+
for (const k of Object.keys(data)) {
|
|
475
|
+
if (!fields.includes(k)) stripped[k] = data[k];
|
|
476
|
+
}
|
|
477
|
+
const { pii: _drop_pii, ...rest } = event;
|
|
478
|
+
return {
|
|
479
|
+
...rest,
|
|
480
|
+
data: stripped
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// src/types/schemas.ts
|
|
485
|
+
var ZodEmpty = import_zod2.z.record(import_zod2.z.string(), import_zod2.z.never());
|
|
486
|
+
function sensitive(schema) {
|
|
487
|
+
_registry.add(schema, { sensitive: true });
|
|
488
|
+
return schema;
|
|
489
|
+
}
|
|
490
|
+
var ActorSchema = import_zod2.z.object({
|
|
491
|
+
id: import_zod2.z.string(),
|
|
492
|
+
name: import_zod2.z.string()
|
|
395
493
|
}).loose().readonly();
|
|
396
|
-
var TargetSchema =
|
|
397
|
-
stream:
|
|
494
|
+
var TargetSchema = import_zod2.z.object({
|
|
495
|
+
stream: import_zod2.z.string(),
|
|
398
496
|
actor: ActorSchema,
|
|
399
|
-
expectedVersion:
|
|
497
|
+
expectedVersion: import_zod2.z.number().optional()
|
|
400
498
|
}).loose().readonly();
|
|
401
|
-
var CausationEventSchema =
|
|
402
|
-
id:
|
|
403
|
-
name:
|
|
404
|
-
stream:
|
|
499
|
+
var CausationEventSchema = import_zod2.z.object({
|
|
500
|
+
id: import_zod2.z.number(),
|
|
501
|
+
name: import_zod2.z.string(),
|
|
502
|
+
stream: import_zod2.z.string()
|
|
405
503
|
});
|
|
406
|
-
var EventMetaSchema =
|
|
407
|
-
correlation:
|
|
408
|
-
causation:
|
|
409
|
-
action: TargetSchema.and(
|
|
504
|
+
var EventMetaSchema = import_zod2.z.object({
|
|
505
|
+
correlation: import_zod2.z.string(),
|
|
506
|
+
causation: import_zod2.z.object({
|
|
507
|
+
action: TargetSchema.and(import_zod2.z.object({ name: import_zod2.z.string() })).optional(),
|
|
410
508
|
event: CausationEventSchema.optional()
|
|
411
509
|
})
|
|
412
510
|
}).readonly();
|
|
413
|
-
var CommittedMetaSchema =
|
|
414
|
-
id:
|
|
415
|
-
stream:
|
|
416
|
-
version:
|
|
417
|
-
created:
|
|
511
|
+
var CommittedMetaSchema = import_zod2.z.object({
|
|
512
|
+
id: import_zod2.z.number(),
|
|
513
|
+
stream: import_zod2.z.string(),
|
|
514
|
+
version: import_zod2.z.number(),
|
|
515
|
+
created: import_zod2.z.date(),
|
|
418
516
|
meta: EventMetaSchema
|
|
419
517
|
}).readonly();
|
|
420
|
-
var QuerySchema =
|
|
421
|
-
stream:
|
|
422
|
-
names:
|
|
423
|
-
before:
|
|
424
|
-
after:
|
|
425
|
-
limit:
|
|
426
|
-
created_before:
|
|
427
|
-
created_after:
|
|
428
|
-
backward:
|
|
429
|
-
correlation:
|
|
430
|
-
with_snaps:
|
|
431
|
-
stream_exact:
|
|
518
|
+
var QuerySchema = import_zod2.z.object({
|
|
519
|
+
stream: import_zod2.z.string().optional(),
|
|
520
|
+
names: import_zod2.z.string().array().optional(),
|
|
521
|
+
before: import_zod2.z.number().optional(),
|
|
522
|
+
after: import_zod2.z.number().optional(),
|
|
523
|
+
limit: import_zod2.z.number().optional(),
|
|
524
|
+
created_before: import_zod2.z.date().optional(),
|
|
525
|
+
created_after: import_zod2.z.date().optional(),
|
|
526
|
+
backward: import_zod2.z.boolean().optional(),
|
|
527
|
+
correlation: import_zod2.z.string().optional(),
|
|
528
|
+
with_snaps: import_zod2.z.boolean().optional(),
|
|
529
|
+
stream_exact: import_zod2.z.boolean().optional()
|
|
432
530
|
}).readonly();
|
|
433
531
|
|
|
434
532
|
// src/types/index.ts
|
|
@@ -448,41 +546,41 @@ var LogLevels = [
|
|
|
448
546
|
];
|
|
449
547
|
|
|
450
548
|
// src/config.ts
|
|
451
|
-
var PackageSchema =
|
|
452
|
-
name:
|
|
453
|
-
version:
|
|
454
|
-
description:
|
|
455
|
-
author:
|
|
456
|
-
license:
|
|
457
|
-
dependencies:
|
|
549
|
+
var PackageSchema = import_zod3.z.object({
|
|
550
|
+
name: import_zod3.z.string().min(1),
|
|
551
|
+
version: import_zod3.z.string().min(1),
|
|
552
|
+
description: import_zod3.z.string().min(1).optional(),
|
|
553
|
+
author: import_zod3.z.object({ name: import_zod3.z.string().min(1), email: import_zod3.z.string().optional() }).optional().or(import_zod3.z.string().min(1)).optional(),
|
|
554
|
+
license: import_zod3.z.string().min(1).optional(),
|
|
555
|
+
dependencies: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.string()).optional()
|
|
458
556
|
});
|
|
459
557
|
var FALLBACK_PACKAGE = {
|
|
460
558
|
name: "act-fallback",
|
|
461
559
|
version: "0.0.0-fallback",
|
|
462
560
|
description: "Synthetic fallback \u2014 package.json could not be loaded"
|
|
463
561
|
};
|
|
464
|
-
var
|
|
562
|
+
var get_package = () => {
|
|
465
563
|
try {
|
|
466
564
|
const raw = fs.readFileSync("package.json");
|
|
467
565
|
return JSON.parse(raw.toString());
|
|
468
566
|
} catch (err) {
|
|
469
|
-
|
|
567
|
+
pkg_load_error = err;
|
|
470
568
|
return FALLBACK_PACKAGE;
|
|
471
569
|
}
|
|
472
570
|
};
|
|
473
|
-
var
|
|
571
|
+
var pkg_load_error;
|
|
474
572
|
var BaseSchema = PackageSchema.extend({
|
|
475
|
-
env:
|
|
476
|
-
logLevel:
|
|
477
|
-
logSingleLine:
|
|
478
|
-
sleepMs:
|
|
573
|
+
env: import_zod3.z.enum(Environments),
|
|
574
|
+
logLevel: import_zod3.z.enum(LogLevels),
|
|
575
|
+
logSingleLine: import_zod3.z.boolean(),
|
|
576
|
+
sleepMs: import_zod3.z.number().int().min(0).max(5e3)
|
|
479
577
|
});
|
|
480
578
|
var { NODE_ENV, LOG_LEVEL, LOG_SINGLE_LINE, SLEEP_MS } = process.env;
|
|
481
579
|
var env = NODE_ENV || "development";
|
|
482
580
|
var logLevel = LOG_LEVEL || (NODE_ENV === "test" ? "fatal" : NODE_ENV === "production" ? "info" : "trace");
|
|
483
581
|
var logSingleLine = (LOG_SINGLE_LINE || "true") === "true";
|
|
484
582
|
var sleepMs = parseInt(NODE_ENV === "test" ? "0" : SLEEP_MS ?? "100", 10);
|
|
485
|
-
var pkg =
|
|
583
|
+
var pkg = get_package();
|
|
486
584
|
var _validated;
|
|
487
585
|
var config = () => {
|
|
488
586
|
if (!_validated) {
|
|
@@ -490,12 +588,12 @@ var config = () => {
|
|
|
490
588
|
{ ...pkg, env, logLevel, logSingleLine, sleepMs },
|
|
491
589
|
BaseSchema
|
|
492
590
|
);
|
|
493
|
-
if (
|
|
494
|
-
const msg =
|
|
591
|
+
if (pkg_load_error) {
|
|
592
|
+
const msg = pkg_load_error instanceof Error ? pkg_load_error.message : typeof pkg_load_error === "string" ? pkg_load_error : "unknown error";
|
|
495
593
|
log().warn(
|
|
496
594
|
`[act] Could not read package.json (${msg}); using synthetic name="${FALLBACK_PACKAGE.name}" version="${FALLBACK_PACKAGE.version}".`
|
|
497
595
|
);
|
|
498
|
-
|
|
596
|
+
pkg_load_error = void 0;
|
|
499
597
|
}
|
|
500
598
|
}
|
|
501
599
|
return _validated;
|
|
@@ -506,8 +604,8 @@ var validate = (target, payload, schema) => {
|
|
|
506
604
|
try {
|
|
507
605
|
return schema ? schema.parse(payload) : payload;
|
|
508
606
|
} catch (error) {
|
|
509
|
-
if (error instanceof
|
|
510
|
-
throw new ValidationError(target, payload, (0,
|
|
607
|
+
if (error instanceof import_zod4.ZodError) {
|
|
608
|
+
throw new ValidationError(target, payload, (0, import_zod4.prettifyError)(error));
|
|
511
609
|
}
|
|
512
610
|
throw new ValidationError(target, payload, error);
|
|
513
611
|
}
|
|
@@ -552,14 +650,14 @@ var InMemoryStream = class {
|
|
|
552
650
|
* Bump the priority via {@link subscribe}: keeps the maximum across
|
|
553
651
|
* reactions so the highest-priority registrant wins.
|
|
554
652
|
*/
|
|
555
|
-
|
|
653
|
+
bump_priority(priority) {
|
|
556
654
|
if (priority > this._priority) this._priority = priority;
|
|
557
655
|
}
|
|
558
656
|
/**
|
|
559
657
|
* Set the priority outright via {@link prioritize}: operator
|
|
560
658
|
* runtime override that ignores the build-time `max()` invariant.
|
|
561
659
|
*/
|
|
562
|
-
|
|
660
|
+
set_priority(priority) {
|
|
563
661
|
this._priority = priority;
|
|
564
662
|
}
|
|
565
663
|
get is_available() {
|
|
@@ -681,28 +779,28 @@ var InMemoryStore = class {
|
|
|
681
779
|
// stored stream positions and other metadata
|
|
682
780
|
_streams = /* @__PURE__ */ new Map();
|
|
683
781
|
// last committed version per stream — O(1) replacement for filter-on-commit
|
|
684
|
-
|
|
782
|
+
_stream_versions = /* @__PURE__ */ new Map();
|
|
685
783
|
// max non-snapshot event id per stream — drives the source-pattern probe in claim()
|
|
686
784
|
// without scanning the full event log.
|
|
687
|
-
|
|
785
|
+
_max_event_id_by_stream = /* @__PURE__ */ new Map();
|
|
688
786
|
// global max non-snapshot event id — fast pre-check for source-less streams in claim()
|
|
689
|
-
|
|
787
|
+
_max_non_snap_event_id = -1;
|
|
690
788
|
// stream → (event_id → cloned sensitive payload). Two-level so `forget_pii`
|
|
691
789
|
// is O(1) — drop the inner Map for the stream and the wipe is done — mirroring
|
|
692
790
|
// the `DELETE WHERE stream = ?` scope that durable adapters get from their
|
|
693
791
|
// stream index. Entries exist only for events committed with a non-null
|
|
694
792
|
// `pii` field; absence means "no PII" (returned as `null` on load).
|
|
695
793
|
_pii = /* @__PURE__ */ new Map();
|
|
696
|
-
|
|
794
|
+
_reset_indexes() {
|
|
697
795
|
this._events.length = 0;
|
|
698
|
-
this.
|
|
699
|
-
this.
|
|
700
|
-
this.
|
|
796
|
+
this._stream_versions.clear();
|
|
797
|
+
this._max_event_id_by_stream.clear();
|
|
798
|
+
this._max_non_snap_event_id = -1;
|
|
701
799
|
this._pii.clear();
|
|
702
800
|
}
|
|
703
801
|
// Attach the isolated PII payload (or null) to an event before handing it to
|
|
704
802
|
// a caller. Allocation-free for events without PII — by far the common case.
|
|
705
|
-
|
|
803
|
+
_with_pii(e) {
|
|
706
804
|
const pii = this._pii.get(e.stream)?.get(e.id);
|
|
707
805
|
return pii ? { ...e, pii } : e;
|
|
708
806
|
}
|
|
@@ -712,7 +810,7 @@ var InMemoryStore = class {
|
|
|
712
810
|
*/
|
|
713
811
|
async dispose() {
|
|
714
812
|
await sleep();
|
|
715
|
-
this.
|
|
813
|
+
this._reset_indexes();
|
|
716
814
|
}
|
|
717
815
|
/**
|
|
718
816
|
* Seed the store with initial data (no-op for in-memory).
|
|
@@ -727,7 +825,7 @@ var InMemoryStore = class {
|
|
|
727
825
|
*/
|
|
728
826
|
async drop() {
|
|
729
827
|
await sleep();
|
|
730
|
-
this.
|
|
828
|
+
this._reset_indexes();
|
|
731
829
|
this._streams = /* @__PURE__ */ new Map();
|
|
732
830
|
}
|
|
733
831
|
in_query(query, e) {
|
|
@@ -761,7 +859,7 @@ var InMemoryStore = class {
|
|
|
761
859
|
if (query.after && e.id <= query.after) break;
|
|
762
860
|
if (query.created_after && e.created <= query.created_after) break;
|
|
763
861
|
await Promise.resolve(
|
|
764
|
-
callback(this.
|
|
862
|
+
callback(this._with_pii(e))
|
|
765
863
|
);
|
|
766
864
|
count++;
|
|
767
865
|
if (query?.limit && count >= query.limit) break;
|
|
@@ -775,7 +873,7 @@ var InMemoryStore = class {
|
|
|
775
873
|
if (query?.before && e.id >= query.before) break;
|
|
776
874
|
if (query?.created_before && e.created >= query.created_before) break;
|
|
777
875
|
await Promise.resolve(
|
|
778
|
-
callback(this.
|
|
876
|
+
callback(this._with_pii(e))
|
|
779
877
|
);
|
|
780
878
|
count++;
|
|
781
879
|
if (query?.limit && count >= query.limit) break;
|
|
@@ -794,17 +892,17 @@ var InMemoryStore = class {
|
|
|
794
892
|
*/
|
|
795
893
|
async commit(stream, msgs, meta, expectedVersion) {
|
|
796
894
|
await sleep();
|
|
797
|
-
const
|
|
798
|
-
if (typeof expectedVersion === "number" &&
|
|
895
|
+
const current_version = this._stream_versions.get(stream) ?? -1;
|
|
896
|
+
if (typeof expectedVersion === "number" && current_version !== expectedVersion) {
|
|
799
897
|
throw new ConcurrencyError(
|
|
800
898
|
stream,
|
|
801
|
-
|
|
899
|
+
current_version,
|
|
802
900
|
msgs,
|
|
803
901
|
expectedVersion
|
|
804
902
|
);
|
|
805
903
|
}
|
|
806
|
-
let version =
|
|
807
|
-
let
|
|
904
|
+
let version = current_version + 1;
|
|
905
|
+
let last_non_snap_id = -1;
|
|
808
906
|
const committed = msgs.map(({ name, data, pii }) => {
|
|
809
907
|
const c = {
|
|
810
908
|
id: this._events.length,
|
|
@@ -817,21 +915,21 @@ var InMemoryStore = class {
|
|
|
817
915
|
};
|
|
818
916
|
this._events.push(c);
|
|
819
917
|
if (pii != null) {
|
|
820
|
-
let
|
|
821
|
-
if (!
|
|
822
|
-
|
|
823
|
-
this._pii.set(stream,
|
|
918
|
+
let per_stream = this._pii.get(stream);
|
|
919
|
+
if (!per_stream) {
|
|
920
|
+
per_stream = /* @__PURE__ */ new Map();
|
|
921
|
+
this._pii.set(stream, per_stream);
|
|
824
922
|
}
|
|
825
|
-
|
|
923
|
+
per_stream.set(c.id, structuredClone(pii));
|
|
826
924
|
}
|
|
827
|
-
if (name !== SNAP_EVENT)
|
|
925
|
+
if (name !== SNAP_EVENT) last_non_snap_id = c.id;
|
|
828
926
|
version++;
|
|
829
|
-
return this.
|
|
927
|
+
return this._with_pii(c);
|
|
830
928
|
});
|
|
831
|
-
this.
|
|
832
|
-
if (
|
|
833
|
-
this.
|
|
834
|
-
this.
|
|
929
|
+
this._stream_versions.set(stream, version - 1);
|
|
930
|
+
if (last_non_snap_id >= 0) {
|
|
931
|
+
this._max_event_id_by_stream.set(stream, last_non_snap_id);
|
|
932
|
+
this._max_non_snap_event_id = last_non_snap_id;
|
|
835
933
|
}
|
|
836
934
|
return committed;
|
|
837
935
|
}
|
|
@@ -846,26 +944,26 @@ var InMemoryStore = class {
|
|
|
846
944
|
*/
|
|
847
945
|
async claim(lagging, leading, by, millis, lane) {
|
|
848
946
|
await sleep();
|
|
849
|
-
const
|
|
850
|
-
const
|
|
851
|
-
let re =
|
|
947
|
+
const source_regex = /* @__PURE__ */ new Map();
|
|
948
|
+
const get_regex = (source) => {
|
|
949
|
+
let re = source_regex.get(source);
|
|
852
950
|
if (!re) {
|
|
853
951
|
re = new RegExp(source);
|
|
854
|
-
|
|
952
|
+
source_regex.set(source, re);
|
|
855
953
|
}
|
|
856
954
|
return re;
|
|
857
955
|
};
|
|
858
|
-
const
|
|
956
|
+
const has_work = (s) => {
|
|
859
957
|
if (s.at < 0) return true;
|
|
860
|
-
if (!s.source) return s.at < this.
|
|
861
|
-
const re =
|
|
862
|
-
for (const [
|
|
863
|
-
if (maxId > s.at && re.test(
|
|
958
|
+
if (!s.source) return s.at < this._max_non_snap_event_id;
|
|
959
|
+
const re = get_regex(s.source);
|
|
960
|
+
for (const [stream_name, maxId] of this._max_event_id_by_stream) {
|
|
961
|
+
if (maxId > s.at && re.test(stream_name)) return true;
|
|
864
962
|
}
|
|
865
963
|
return false;
|
|
866
964
|
};
|
|
867
965
|
const available = [...this._streams.values()].filter(
|
|
868
|
-
(s) => s.is_available &&
|
|
966
|
+
(s) => s.is_available && has_work(s) && (lane === void 0 || s.lane === lane)
|
|
869
967
|
);
|
|
870
968
|
const lag = available.sort((a, b) => b.priority - a.priority || a.at - b.at).slice(0, lagging).map((s) => ({
|
|
871
969
|
stream: s.stream,
|
|
@@ -909,7 +1007,7 @@ var InMemoryStore = class {
|
|
|
909
1007
|
} of streams) {
|
|
910
1008
|
const existing = this._streams.get(stream);
|
|
911
1009
|
if (existing) {
|
|
912
|
-
existing.
|
|
1010
|
+
existing.bump_priority(priority);
|
|
913
1011
|
existing.lane = lane;
|
|
914
1012
|
} else {
|
|
915
1013
|
this._streams.set(
|
|
@@ -947,17 +1045,17 @@ var InMemoryStore = class {
|
|
|
947
1045
|
* cached in the closure so callers can apply it across the streams
|
|
948
1046
|
* map without re-compiling per iteration.
|
|
949
1047
|
*/
|
|
950
|
-
|
|
951
|
-
const
|
|
952
|
-
const
|
|
1048
|
+
_filter_predicate(filter) {
|
|
1049
|
+
const stream_re = filter.stream && !filter.stream_exact ? new RegExp(filter.stream) : void 0;
|
|
1050
|
+
const source_re = filter.source && !filter.source_exact ? new RegExp(filter.source) : void 0;
|
|
953
1051
|
return (s) => {
|
|
954
1052
|
if (filter.stream !== void 0) {
|
|
955
|
-
if (filter.stream_exact ? s.stream !== filter.stream : !
|
|
1053
|
+
if (filter.stream_exact ? s.stream !== filter.stream : !stream_re.test(s.stream))
|
|
956
1054
|
return false;
|
|
957
1055
|
}
|
|
958
1056
|
if (filter.source !== void 0) {
|
|
959
1057
|
if (s.source === void 0) return false;
|
|
960
|
-
if (filter.source_exact ? s.source !== filter.source : !
|
|
1058
|
+
if (filter.source_exact ? s.source !== filter.source : !source_re.test(s.source))
|
|
961
1059
|
return false;
|
|
962
1060
|
}
|
|
963
1061
|
if (filter.blocked !== void 0 && s.blocked !== filter.blocked)
|
|
@@ -986,7 +1084,7 @@ var InMemoryStore = class {
|
|
|
986
1084
|
}
|
|
987
1085
|
}
|
|
988
1086
|
} else {
|
|
989
|
-
const matches = this.
|
|
1087
|
+
const matches = this._filter_predicate(input);
|
|
990
1088
|
for (const s of this._streams.values()) {
|
|
991
1089
|
if (matches(s)) {
|
|
992
1090
|
s.reset();
|
|
@@ -1031,7 +1129,7 @@ var InMemoryStore = class {
|
|
|
1031
1129
|
if (s?.unblock()) count++;
|
|
1032
1130
|
}
|
|
1033
1131
|
} else {
|
|
1034
|
-
const matches = this.
|
|
1132
|
+
const matches = this._filter_predicate({ ...input, blocked: true });
|
|
1035
1133
|
for (const s of this._streams.values()) {
|
|
1036
1134
|
if (matches(s) && s.unblock()) count++;
|
|
1037
1135
|
}
|
|
@@ -1049,12 +1147,12 @@ var InMemoryStore = class {
|
|
|
1049
1147
|
*/
|
|
1050
1148
|
async prioritize(filter, priority) {
|
|
1051
1149
|
await sleep();
|
|
1052
|
-
const matches = this.
|
|
1150
|
+
const matches = this._filter_predicate(filter);
|
|
1053
1151
|
let count = 0;
|
|
1054
1152
|
for (const s of this._streams.values()) {
|
|
1055
1153
|
if (!matches(s)) continue;
|
|
1056
1154
|
if (s.priority !== priority) {
|
|
1057
|
-
s.
|
|
1155
|
+
s.set_priority(priority);
|
|
1058
1156
|
count++;
|
|
1059
1157
|
}
|
|
1060
1158
|
}
|
|
@@ -1070,8 +1168,8 @@ var InMemoryStore = class {
|
|
|
1070
1168
|
const limit = query?.limit ?? 100;
|
|
1071
1169
|
const after = query?.after;
|
|
1072
1170
|
const blocked = query?.blocked;
|
|
1073
|
-
const
|
|
1074
|
-
const
|
|
1171
|
+
const stream_re = query?.stream && !query.stream_exact ? new RegExp(query.stream) : void 0;
|
|
1172
|
+
const source_re = query?.source && !query.source_exact ? new RegExp(query.source) : void 0;
|
|
1075
1173
|
const sorted = [...this._streams.values()].sort(
|
|
1076
1174
|
(a, b) => a.stream.localeCompare(b.stream)
|
|
1077
1175
|
);
|
|
@@ -1079,12 +1177,12 @@ var InMemoryStore = class {
|
|
|
1079
1177
|
for (const s of sorted) {
|
|
1080
1178
|
if (after !== void 0 && s.stream <= after) continue;
|
|
1081
1179
|
if (query?.stream !== void 0) {
|
|
1082
|
-
if (query.stream_exact ? s.stream !== query.stream : !
|
|
1180
|
+
if (query.stream_exact ? s.stream !== query.stream : !stream_re.test(s.stream))
|
|
1083
1181
|
continue;
|
|
1084
1182
|
}
|
|
1085
1183
|
if (query?.source !== void 0) {
|
|
1086
1184
|
if (s.source === void 0) continue;
|
|
1087
|
-
if (query.source_exact ? s.source !== query.source : !
|
|
1185
|
+
if (query.source_exact ? s.source !== query.source : !source_re.test(s.source))
|
|
1088
1186
|
continue;
|
|
1089
1187
|
}
|
|
1090
1188
|
if (blocked !== void 0 && s.blocked !== blocked) continue;
|
|
@@ -1125,44 +1223,44 @@ var InMemoryStore = class {
|
|
|
1125
1223
|
async query_stats(input, options) {
|
|
1126
1224
|
await sleep();
|
|
1127
1225
|
const exclude = new Set(options?.exclude ?? []);
|
|
1128
|
-
const
|
|
1129
|
-
const
|
|
1130
|
-
const
|
|
1226
|
+
const want_tail = options?.tail ?? false;
|
|
1227
|
+
const want_count = options?.count ?? false;
|
|
1228
|
+
const want_names = options?.names ?? false;
|
|
1131
1229
|
const before = options?.before;
|
|
1132
|
-
const
|
|
1230
|
+
const array_targets = Array.isArray(input) ? new Set(input) : null;
|
|
1133
1231
|
const filter = Array.isArray(input) ? null : input;
|
|
1134
|
-
const
|
|
1135
|
-
const
|
|
1136
|
-
const
|
|
1137
|
-
const cached =
|
|
1232
|
+
const stream_re = filter?.stream && !filter.stream_exact ? new RegExp(filter.stream) : void 0;
|
|
1233
|
+
const scope_cache = /* @__PURE__ */ new Map();
|
|
1234
|
+
const in_scope = (stream) => {
|
|
1235
|
+
const cached = scope_cache.get(stream);
|
|
1138
1236
|
if (cached !== void 0) return cached;
|
|
1139
1237
|
let ok = true;
|
|
1140
|
-
if (
|
|
1141
|
-
ok =
|
|
1238
|
+
if (array_targets) {
|
|
1239
|
+
ok = array_targets.has(stream);
|
|
1142
1240
|
} else if (filter?.stream !== void 0) {
|
|
1143
1241
|
ok = filter.stream_exact ? stream === filter.stream : (
|
|
1144
|
-
// biome-ignore lint/style/noNonNullAssertion:
|
|
1145
|
-
|
|
1242
|
+
// biome-ignore lint/style/noNonNullAssertion: stream_re set when stream is regex
|
|
1243
|
+
stream_re.test(stream)
|
|
1146
1244
|
);
|
|
1147
1245
|
}
|
|
1148
|
-
|
|
1246
|
+
scope_cache.set(stream, ok);
|
|
1149
1247
|
return ok;
|
|
1150
1248
|
};
|
|
1151
1249
|
const acc = /* @__PURE__ */ new Map();
|
|
1152
1250
|
for (const e of this._events) {
|
|
1153
1251
|
if (before !== void 0 && e.id >= before) continue;
|
|
1154
|
-
if (!
|
|
1252
|
+
if (!in_scope(e.stream)) continue;
|
|
1155
1253
|
if (exclude.has(e.name)) continue;
|
|
1156
1254
|
let a = acc.get(e.stream);
|
|
1157
1255
|
if (!a) {
|
|
1158
1256
|
a = { head: e, count: 0 };
|
|
1159
|
-
if (
|
|
1160
|
-
if (
|
|
1257
|
+
if (want_tail) a.tail = e;
|
|
1258
|
+
if (want_names) a.names = {};
|
|
1161
1259
|
acc.set(e.stream, a);
|
|
1162
1260
|
}
|
|
1163
1261
|
a.head = e;
|
|
1164
1262
|
a.count++;
|
|
1165
|
-
if (
|
|
1263
|
+
if (want_names) {
|
|
1166
1264
|
const n = String(e.name);
|
|
1167
1265
|
a.names[n] = (a.names[n] ?? 0) + 1;
|
|
1168
1266
|
}
|
|
@@ -1170,9 +1268,9 @@ var InMemoryStore = class {
|
|
|
1170
1268
|
const out = /* @__PURE__ */ new Map();
|
|
1171
1269
|
for (const [stream, a] of acc) {
|
|
1172
1270
|
const stats = { head: a.head };
|
|
1173
|
-
if (
|
|
1174
|
-
if (
|
|
1175
|
-
if (
|
|
1271
|
+
if (want_tail) stats.tail = a.tail;
|
|
1272
|
+
if (want_count) stats.count = a.count;
|
|
1273
|
+
if (want_names) stats.names = a.names;
|
|
1176
1274
|
out.set(stream, stats);
|
|
1177
1275
|
}
|
|
1178
1276
|
return out;
|
|
@@ -1184,18 +1282,18 @@ var InMemoryStore = class {
|
|
|
1184
1282
|
*/
|
|
1185
1283
|
async truncate(targets) {
|
|
1186
1284
|
await sleep();
|
|
1187
|
-
const
|
|
1188
|
-
const
|
|
1285
|
+
const deleted_counts = /* @__PURE__ */ new Map();
|
|
1286
|
+
const stream_set = new Set(targets.map((t) => t.stream));
|
|
1189
1287
|
for (const e of this._events) {
|
|
1190
|
-
if (
|
|
1191
|
-
|
|
1288
|
+
if (stream_set.has(e.stream)) {
|
|
1289
|
+
deleted_counts.set(e.stream, (deleted_counts.get(e.stream) ?? 0) + 1);
|
|
1192
1290
|
}
|
|
1193
1291
|
}
|
|
1194
|
-
this._events = this._events.filter((e) => !
|
|
1195
|
-
for (const stream of
|
|
1292
|
+
this._events = this._events.filter((e) => !stream_set.has(e.stream));
|
|
1293
|
+
for (const stream of stream_set) {
|
|
1196
1294
|
this._streams.delete(stream);
|
|
1197
|
-
this.
|
|
1198
|
-
this.
|
|
1295
|
+
this._stream_versions.delete(stream);
|
|
1296
|
+
this._max_event_id_by_stream.delete(stream);
|
|
1199
1297
|
}
|
|
1200
1298
|
const result = /* @__PURE__ */ new Map();
|
|
1201
1299
|
for (const { stream, snapshot, meta } of targets) {
|
|
@@ -1209,18 +1307,19 @@ var InMemoryStore = class {
|
|
|
1209
1307
|
meta: meta ?? { correlation: "", causation: {} }
|
|
1210
1308
|
};
|
|
1211
1309
|
this._events.push(event);
|
|
1212
|
-
this.
|
|
1310
|
+
this._stream_versions.set(stream, 0);
|
|
1213
1311
|
if (event.name !== SNAP_EVENT) {
|
|
1214
|
-
this.
|
|
1312
|
+
this._max_event_id_by_stream.set(stream, event.id);
|
|
1215
1313
|
}
|
|
1216
1314
|
result.set(stream, {
|
|
1217
|
-
deleted:
|
|
1315
|
+
deleted: deleted_counts.get(stream) ?? 0,
|
|
1218
1316
|
committed: event
|
|
1219
1317
|
});
|
|
1220
1318
|
}
|
|
1221
1319
|
let max = -1;
|
|
1222
|
-
for (const id of this.
|
|
1223
|
-
|
|
1320
|
+
for (const id of this._max_event_id_by_stream.values())
|
|
1321
|
+
if (id > max) max = id;
|
|
1322
|
+
this._max_non_snap_event_id = max;
|
|
1224
1323
|
return result;
|
|
1225
1324
|
}
|
|
1226
1325
|
/**
|
|
@@ -1237,34 +1336,34 @@ var InMemoryStore = class {
|
|
|
1237
1336
|
*/
|
|
1238
1337
|
async restore(driver) {
|
|
1239
1338
|
await sleep();
|
|
1240
|
-
const
|
|
1241
|
-
const
|
|
1242
|
-
const
|
|
1243
|
-
const
|
|
1244
|
-
const
|
|
1339
|
+
const prev_events = this._events;
|
|
1340
|
+
const prev_streams = this._streams;
|
|
1341
|
+
const prev_stream_versions = this._stream_versions;
|
|
1342
|
+
const prev_max_event_id_by_stream = this._max_event_id_by_stream;
|
|
1343
|
+
const prev_max_non_snap_event_id = this._max_non_snap_event_id;
|
|
1245
1344
|
this._events = [];
|
|
1246
1345
|
this._streams = /* @__PURE__ */ new Map();
|
|
1247
|
-
this.
|
|
1248
|
-
this.
|
|
1249
|
-
this.
|
|
1346
|
+
this._stream_versions = /* @__PURE__ */ new Map();
|
|
1347
|
+
this._max_event_id_by_stream = /* @__PURE__ */ new Map();
|
|
1348
|
+
this._max_non_snap_event_id = -1;
|
|
1250
1349
|
try {
|
|
1251
1350
|
await driver(async (event) => {
|
|
1252
1351
|
const id = this._events.length;
|
|
1253
1352
|
const committed = { ...event, id };
|
|
1254
1353
|
this._events.push(committed);
|
|
1255
|
-
this.
|
|
1354
|
+
this._stream_versions.set(event.stream, event.version);
|
|
1256
1355
|
if (event.name !== SNAP_EVENT) {
|
|
1257
|
-
this.
|
|
1258
|
-
this.
|
|
1356
|
+
this._max_event_id_by_stream.set(event.stream, id);
|
|
1357
|
+
this._max_non_snap_event_id = id;
|
|
1259
1358
|
}
|
|
1260
1359
|
return id;
|
|
1261
1360
|
});
|
|
1262
1361
|
} catch (err) {
|
|
1263
|
-
this._events =
|
|
1264
|
-
this._streams =
|
|
1265
|
-
this.
|
|
1266
|
-
this.
|
|
1267
|
-
this.
|
|
1362
|
+
this._events = prev_events;
|
|
1363
|
+
this._streams = prev_streams;
|
|
1364
|
+
this._stream_versions = prev_stream_versions;
|
|
1365
|
+
this._max_event_id_by_stream = prev_max_event_id_by_stream;
|
|
1366
|
+
this._max_non_snap_event_id = prev_max_non_snap_event_id;
|
|
1268
1367
|
throw err;
|
|
1269
1368
|
}
|
|
1270
1369
|
}
|
|
@@ -1360,7 +1459,7 @@ function parse(name) {
|
|
|
1360
1459
|
}
|
|
1361
1460
|
return { base: name, version: 1 };
|
|
1362
1461
|
}
|
|
1363
|
-
function
|
|
1462
|
+
function deprecated_event_names(names) {
|
|
1364
1463
|
const groups = /* @__PURE__ */ new Map();
|
|
1365
1464
|
for (const name of names) {
|
|
1366
1465
|
const { base, version } = parse(name);
|
|
@@ -1376,10 +1475,10 @@ function deprecatedEventNames(names) {
|
|
|
1376
1475
|
}
|
|
1377
1476
|
return deprecated;
|
|
1378
1477
|
}
|
|
1379
|
-
function
|
|
1380
|
-
const target = parse(
|
|
1478
|
+
function current_version_of(deprecated_name, all_names) {
|
|
1479
|
+
const target = parse(deprecated_name);
|
|
1381
1480
|
let highest;
|
|
1382
|
-
for (const name of
|
|
1481
|
+
for (const name of all_names) {
|
|
1383
1482
|
const { base, version } = parse(name);
|
|
1384
1483
|
if (base !== target.base) continue;
|
|
1385
1484
|
if (!highest || version > highest.version) highest = { version, name };
|
|
@@ -1409,27 +1508,27 @@ var ALL_CATEGORIES = [
|
|
|
1409
1508
|
];
|
|
1410
1509
|
async function* audit(deps, categories, options = {}) {
|
|
1411
1510
|
const requested = new Set(categories ?? [...ALL_CATEGORIES]);
|
|
1412
|
-
const
|
|
1413
|
-
const passes =
|
|
1511
|
+
const ordered_categories = ALL_CATEGORIES.filter((c) => requested.has(c));
|
|
1512
|
+
const passes = ordered_categories.map(
|
|
1414
1513
|
(c) => PASS_FACTORIES[c](deps, options)
|
|
1415
1514
|
);
|
|
1416
|
-
const
|
|
1417
|
-
const
|
|
1418
|
-
const
|
|
1419
|
-
if (
|
|
1515
|
+
const need_stats = passes.some((p) => p.on_stat !== void 0);
|
|
1516
|
+
const need_streams = passes.some((p) => p.on_stream !== void 0);
|
|
1517
|
+
const need_events = passes.some((p) => p.on_event !== void 0);
|
|
1518
|
+
if (need_stats) {
|
|
1420
1519
|
const stats = await deps.store().query_stats({}, { count: true, names: true });
|
|
1421
1520
|
for (const [stream, s] of stats) {
|
|
1422
|
-
for (const p of passes) p.
|
|
1521
|
+
for (const p of passes) p.on_stat?.(stream, s);
|
|
1423
1522
|
}
|
|
1424
1523
|
}
|
|
1425
|
-
if (
|
|
1524
|
+
if (need_streams) {
|
|
1426
1525
|
await deps.store().query_streams((pos) => {
|
|
1427
|
-
for (const p of passes) p.
|
|
1526
|
+
for (const p of passes) p.on_stream?.(pos);
|
|
1428
1527
|
});
|
|
1429
1528
|
}
|
|
1430
|
-
if (
|
|
1529
|
+
if (need_events) {
|
|
1431
1530
|
await deps.store().query((event) => {
|
|
1432
|
-
for (const p of passes) p.
|
|
1531
|
+
for (const p of passes) p.on_event?.(event);
|
|
1433
1532
|
}, options.query);
|
|
1434
1533
|
}
|
|
1435
1534
|
for (const p of passes) await p.finalize?.(deps);
|
|
@@ -1437,11 +1536,11 @@ async function* audit(deps, categories, options = {}) {
|
|
|
1437
1536
|
for (const f of p.drain()) yield f;
|
|
1438
1537
|
}
|
|
1439
1538
|
}
|
|
1440
|
-
var
|
|
1539
|
+
var make_schema_pass = (deps) => {
|
|
1441
1540
|
const findings = [];
|
|
1442
1541
|
return {
|
|
1443
1542
|
category: "schema",
|
|
1444
|
-
|
|
1543
|
+
on_event(event) {
|
|
1445
1544
|
const name = String(event.name);
|
|
1446
1545
|
const state2 = deps.event_to_state.get(name);
|
|
1447
1546
|
if (!state2) {
|
|
@@ -1471,19 +1570,19 @@ var makeSchemaPass = (deps) => {
|
|
|
1471
1570
|
drain: () => findings
|
|
1472
1571
|
};
|
|
1473
1572
|
};
|
|
1474
|
-
var
|
|
1573
|
+
var make_deprecated_load_pass = (deps, options) => {
|
|
1475
1574
|
const share_min = options.thresholds?.deprecated_min ?? DEFAULTS.deprecated_min;
|
|
1476
1575
|
const totals = /* @__PURE__ */ new Map();
|
|
1477
|
-
const
|
|
1576
|
+
const per_stream = /* @__PURE__ */ new Map();
|
|
1478
1577
|
return {
|
|
1479
1578
|
category: "deprecated-load",
|
|
1480
|
-
|
|
1579
|
+
on_stat(stream, { names }) {
|
|
1481
1580
|
for (const [name, count] of Object.entries(names)) {
|
|
1482
1581
|
totals.set(name, (totals.get(name) ?? 0) + count);
|
|
1483
|
-
let m =
|
|
1582
|
+
let m = per_stream.get(name);
|
|
1484
1583
|
if (!m) {
|
|
1485
1584
|
m = /* @__PURE__ */ new Map();
|
|
1486
|
-
|
|
1585
|
+
per_stream.set(name, m);
|
|
1487
1586
|
}
|
|
1488
1587
|
m.set(stream, count);
|
|
1489
1588
|
}
|
|
@@ -1492,33 +1591,33 @@ var makeDeprecatedLoadPass = (deps, options) => {
|
|
|
1492
1591
|
const findings = [];
|
|
1493
1592
|
const grand = [...totals.values()].reduce((s, n) => s + n, 0);
|
|
1494
1593
|
if (grand === 0) return findings;
|
|
1495
|
-
const deprecated =
|
|
1594
|
+
const deprecated = deprecated_event_names(deps.known_events);
|
|
1496
1595
|
const sorted = [...deprecated].map((name) => ({ name, count: totals.get(name) ?? 0 })).sort((a, b) => b.count - a.count);
|
|
1497
1596
|
for (const { name, count } of sorted) {
|
|
1498
1597
|
if (count === 0) continue;
|
|
1499
1598
|
if (count / grand < share_min) continue;
|
|
1500
|
-
const
|
|
1501
|
-
const
|
|
1599
|
+
const current_version = current_version_of(name, deps.known_events);
|
|
1600
|
+
const top_streams = [...per_stream.get(name).entries()].map(([stream, c]) => ({ stream, count: c })).sort((a, b) => b.count - a.count).slice(0, 10);
|
|
1502
1601
|
findings.push({
|
|
1503
1602
|
category: "deprecated-load",
|
|
1504
1603
|
name,
|
|
1505
|
-
current_version
|
|
1604
|
+
current_version,
|
|
1506
1605
|
total: count,
|
|
1507
|
-
top_streams
|
|
1606
|
+
top_streams
|
|
1508
1607
|
});
|
|
1509
1608
|
}
|
|
1510
1609
|
return findings;
|
|
1511
1610
|
}
|
|
1512
1611
|
};
|
|
1513
1612
|
};
|
|
1514
|
-
var
|
|
1613
|
+
var make_close_candidate_pass = (deps, options) => {
|
|
1515
1614
|
const idle_days = options.thresholds?.idle_days ?? DEFAULTS.idle_days;
|
|
1516
1615
|
const terminal_events = new Set(options.thresholds?.terminal_events ?? []);
|
|
1517
1616
|
const idle_cutoff = Date.now() - idle_days * 24 * 60 * 60 * 1e3;
|
|
1518
1617
|
const findings = [];
|
|
1519
1618
|
return {
|
|
1520
1619
|
category: "close-candidate",
|
|
1521
|
-
|
|
1620
|
+
on_stat(stream, { head }) {
|
|
1522
1621
|
const head_name = String(head.name);
|
|
1523
1622
|
if (head_name.startsWith("__")) return;
|
|
1524
1623
|
const head_time = head.created.getTime();
|
|
@@ -1531,22 +1630,22 @@ var makeCloseCandidatePass = (deps, options) => {
|
|
|
1531
1630
|
last_event_at: head.created.toISOString(),
|
|
1532
1631
|
reason: is_terminal ? "terminal" : "idle",
|
|
1533
1632
|
idle_days: is_idle ? Math.floor((Date.now() - head_time) / (24 * 60 * 60 * 1e3)) : void 0,
|
|
1534
|
-
restart_supported:
|
|
1633
|
+
restart_supported: restart_is_supported(deps, head_name)
|
|
1535
1634
|
});
|
|
1536
1635
|
},
|
|
1537
1636
|
drain: () => findings
|
|
1538
1637
|
};
|
|
1539
1638
|
};
|
|
1540
|
-
var
|
|
1639
|
+
var make_restart_candidate_pass = (deps, options) => {
|
|
1541
1640
|
const threshold = options.thresholds?.restart_min ?? DEFAULTS.restart_min;
|
|
1542
1641
|
const findings = [];
|
|
1543
1642
|
return {
|
|
1544
1643
|
category: "restart-candidate",
|
|
1545
|
-
|
|
1644
|
+
on_stat(stream, { head, count, names }) {
|
|
1546
1645
|
if (count < threshold) return;
|
|
1547
1646
|
const head_name = String(head.name);
|
|
1548
1647
|
if (head_name.startsWith("__")) return;
|
|
1549
|
-
if (!
|
|
1648
|
+
if (!restart_is_supported(deps, head_name)) return;
|
|
1550
1649
|
findings.push({
|
|
1551
1650
|
category: "restart-candidate",
|
|
1552
1651
|
stream,
|
|
@@ -1560,14 +1659,14 @@ var makeRestartCandidatePass = (deps, options) => {
|
|
|
1560
1659
|
drain: () => findings
|
|
1561
1660
|
};
|
|
1562
1661
|
};
|
|
1563
|
-
var
|
|
1662
|
+
var make_reaction_health_pass = (_deps, options) => {
|
|
1564
1663
|
const near_block = options.thresholds?.near_block ?? DEFAULTS.near_block;
|
|
1565
1664
|
const stuck_minutes = options.thresholds?.stuck_minutes ?? DEFAULTS.stuck_minutes;
|
|
1566
1665
|
const stuck_cutoff = Date.now() - stuck_minutes * 60 * 1e3;
|
|
1567
1666
|
const findings = [];
|
|
1568
1667
|
return {
|
|
1569
1668
|
category: "reaction-health",
|
|
1570
|
-
|
|
1669
|
+
on_stream(p) {
|
|
1571
1670
|
if (p.blocked) {
|
|
1572
1671
|
findings.push({
|
|
1573
1672
|
category: "reaction-health",
|
|
@@ -1604,14 +1703,14 @@ var makeReactionHealthPass = (_deps, options) => {
|
|
|
1604
1703
|
drain: () => findings
|
|
1605
1704
|
};
|
|
1606
1705
|
};
|
|
1607
|
-
var
|
|
1706
|
+
var make_snapshot_drift_pass = (deps, options) => {
|
|
1608
1707
|
const drift_min = options.thresholds?.drift_min ?? DEFAULTS.drift_min;
|
|
1609
1708
|
const candidates = [];
|
|
1610
1709
|
const findings = [];
|
|
1611
1710
|
return {
|
|
1612
1711
|
category: "snapshot-drift",
|
|
1613
|
-
|
|
1614
|
-
if (!
|
|
1712
|
+
on_stat(stream, { head, count, names }) {
|
|
1713
|
+
if (!restart_is_supported(deps, String(head.name))) return;
|
|
1615
1714
|
if (count < drift_min) return;
|
|
1616
1715
|
candidates.push({
|
|
1617
1716
|
stream,
|
|
@@ -1660,12 +1759,12 @@ var makeSnapshotDriftPass = (deps, options) => {
|
|
|
1660
1759
|
drain: () => findings
|
|
1661
1760
|
};
|
|
1662
1761
|
};
|
|
1663
|
-
var
|
|
1762
|
+
var make_routing_health_pass = (deps) => {
|
|
1664
1763
|
const findings = [];
|
|
1665
|
-
const
|
|
1764
|
+
const seen_event_names = /* @__PURE__ */ new Set();
|
|
1666
1765
|
return {
|
|
1667
1766
|
category: "routing-health",
|
|
1668
|
-
|
|
1767
|
+
on_stream(p) {
|
|
1669
1768
|
if (!p.lane) return;
|
|
1670
1769
|
if (deps.declared_lanes.has(p.lane)) return;
|
|
1671
1770
|
findings.push({
|
|
@@ -1675,13 +1774,13 @@ var makeRoutingHealthPass = (deps) => {
|
|
|
1675
1774
|
lane: p.lane
|
|
1676
1775
|
});
|
|
1677
1776
|
},
|
|
1678
|
-
|
|
1777
|
+
on_stat(_stream, { names }) {
|
|
1679
1778
|
for (const name of Object.keys(names)) {
|
|
1680
|
-
|
|
1779
|
+
seen_event_names.add(name);
|
|
1681
1780
|
}
|
|
1682
1781
|
},
|
|
1683
1782
|
finalize() {
|
|
1684
|
-
for (const name of
|
|
1783
|
+
for (const name of seen_event_names) {
|
|
1685
1784
|
if (name.startsWith("__")) continue;
|
|
1686
1785
|
if (deps.routed_events.has(name)) continue;
|
|
1687
1786
|
findings.push({
|
|
@@ -1695,23 +1794,23 @@ var makeRoutingHealthPass = (deps) => {
|
|
|
1695
1794
|
drain: () => findings
|
|
1696
1795
|
};
|
|
1697
1796
|
};
|
|
1698
|
-
var
|
|
1699
|
-
const
|
|
1797
|
+
var make_correlation_gaps_pass = () => {
|
|
1798
|
+
const seen_ids = /* @__PURE__ */ new Set();
|
|
1700
1799
|
const checks = [];
|
|
1701
1800
|
return {
|
|
1702
1801
|
category: "correlation-gaps",
|
|
1703
|
-
|
|
1704
|
-
|
|
1802
|
+
on_event(e) {
|
|
1803
|
+
seen_ids.add(e.id);
|
|
1705
1804
|
const causation = e.meta?.causation;
|
|
1706
|
-
const
|
|
1707
|
-
if (
|
|
1708
|
-
checks.push({ stream: e.stream, id: e.id,
|
|
1805
|
+
const parent_id = causation?.event?.id;
|
|
1806
|
+
if (parent_id !== void 0) {
|
|
1807
|
+
checks.push({ stream: e.stream, id: e.id, parent_id });
|
|
1709
1808
|
}
|
|
1710
1809
|
},
|
|
1711
1810
|
drain() {
|
|
1712
1811
|
const findings = [];
|
|
1713
|
-
for (const { stream, id,
|
|
1714
|
-
if (!
|
|
1812
|
+
for (const { stream, id, parent_id } of checks) {
|
|
1813
|
+
if (!seen_ids.has(parent_id)) {
|
|
1715
1814
|
findings.push({
|
|
1716
1815
|
category: "correlation-gaps",
|
|
1717
1816
|
stream,
|
|
@@ -1724,12 +1823,12 @@ var makeCorrelationGapsPass = () => {
|
|
|
1724
1823
|
}
|
|
1725
1824
|
};
|
|
1726
1825
|
};
|
|
1727
|
-
var
|
|
1826
|
+
var make_clock_anomalies_pass = () => {
|
|
1728
1827
|
const findings = [];
|
|
1729
|
-
const
|
|
1828
|
+
const last_per_stream = /* @__PURE__ */ new Map();
|
|
1730
1829
|
return {
|
|
1731
1830
|
category: "clock-anomalies",
|
|
1732
|
-
|
|
1831
|
+
on_event(e) {
|
|
1733
1832
|
const created = e.created.getTime();
|
|
1734
1833
|
if (created > Date.now()) {
|
|
1735
1834
|
findings.push({
|
|
@@ -1739,7 +1838,7 @@ var makeClockAnomaliesPass = () => {
|
|
|
1739
1838
|
reason: "future-created"
|
|
1740
1839
|
});
|
|
1741
1840
|
}
|
|
1742
|
-
const prev =
|
|
1841
|
+
const prev = last_per_stream.get(e.stream);
|
|
1743
1842
|
if (prev !== void 0 && created < prev) {
|
|
1744
1843
|
findings.push({
|
|
1745
1844
|
category: "clock-anomalies",
|
|
@@ -1748,48 +1847,48 @@ var makeClockAnomaliesPass = () => {
|
|
|
1748
1847
|
reason: "out-of-order"
|
|
1749
1848
|
});
|
|
1750
1849
|
}
|
|
1751
|
-
|
|
1850
|
+
last_per_stream.set(e.stream, created);
|
|
1752
1851
|
},
|
|
1753
1852
|
drain: () => findings
|
|
1754
1853
|
};
|
|
1755
1854
|
};
|
|
1756
|
-
function
|
|
1757
|
-
const state2 = deps.event_to_state.get(
|
|
1855
|
+
function restart_is_supported(deps, head_event_name) {
|
|
1856
|
+
const state2 = deps.event_to_state.get(head_event_name);
|
|
1758
1857
|
return state2?.snap !== void 0;
|
|
1759
1858
|
}
|
|
1760
1859
|
var PASS_FACTORIES = {
|
|
1761
|
-
schema:
|
|
1762
|
-
"deprecated-load":
|
|
1763
|
-
"close-candidate":
|
|
1764
|
-
"restart-candidate":
|
|
1765
|
-
"reaction-health":
|
|
1766
|
-
"snapshot-drift":
|
|
1767
|
-
"routing-health":
|
|
1768
|
-
"correlation-gaps":
|
|
1769
|
-
"clock-anomalies":
|
|
1860
|
+
schema: make_schema_pass,
|
|
1861
|
+
"deprecated-load": make_deprecated_load_pass,
|
|
1862
|
+
"close-candidate": make_close_candidate_pass,
|
|
1863
|
+
"restart-candidate": make_restart_candidate_pass,
|
|
1864
|
+
"reaction-health": make_reaction_health_pass,
|
|
1865
|
+
"snapshot-drift": make_snapshot_drift_pass,
|
|
1866
|
+
"routing-health": make_routing_health_pass,
|
|
1867
|
+
"correlation-gaps": make_correlation_gaps_pass,
|
|
1868
|
+
"clock-anomalies": make_clock_anomalies_pass
|
|
1770
1869
|
};
|
|
1771
1870
|
|
|
1772
1871
|
// src/internal/build-classify.ts
|
|
1773
1872
|
var ALL_LANES = /* @__PURE__ */ Symbol("act-1103/all-lanes");
|
|
1774
|
-
function
|
|
1873
|
+
function classify_registry(registry, states) {
|
|
1775
1874
|
const statics = /* @__PURE__ */ new Map();
|
|
1776
|
-
const
|
|
1777
|
-
const
|
|
1778
|
-
let
|
|
1875
|
+
const reactive_events = /* @__PURE__ */ new Set();
|
|
1876
|
+
const event_to_lanes = /* @__PURE__ */ new Map();
|
|
1877
|
+
let has_dynamic_resolvers = false;
|
|
1779
1878
|
for (const [name, register] of Object.entries(registry.events)) {
|
|
1780
|
-
if (register.reactions.size > 0)
|
|
1879
|
+
if (register.reactions.size > 0) reactive_events.add(name);
|
|
1781
1880
|
for (const reaction of register.reactions.values()) {
|
|
1782
1881
|
if (typeof reaction.resolver === "function") {
|
|
1783
|
-
|
|
1784
|
-
|
|
1882
|
+
has_dynamic_resolvers = true;
|
|
1883
|
+
event_to_lanes.set(name, ALL_LANES);
|
|
1785
1884
|
} else {
|
|
1786
1885
|
const { target, source, priority = 0, lane } = reaction.resolver;
|
|
1787
1886
|
const lane_name = lane ?? "default";
|
|
1788
|
-
const existing_lanes =
|
|
1887
|
+
const existing_lanes = event_to_lanes.get(name);
|
|
1789
1888
|
if (existing_lanes !== ALL_LANES) {
|
|
1790
1889
|
const set = existing_lanes ?? /* @__PURE__ */ new Set();
|
|
1791
1890
|
set.add(lane_name);
|
|
1792
|
-
|
|
1891
|
+
event_to_lanes.set(name, set);
|
|
1793
1892
|
}
|
|
1794
1893
|
const key = `${target}|${source ?? ""}`;
|
|
1795
1894
|
const existing = statics.get(key);
|
|
@@ -1807,59 +1906,59 @@ function classifyRegistry(registry, states) {
|
|
|
1807
1906
|
}
|
|
1808
1907
|
}
|
|
1809
1908
|
}
|
|
1810
|
-
const
|
|
1909
|
+
const event_to_state = /* @__PURE__ */ new Map();
|
|
1811
1910
|
for (const merged of states.values()) {
|
|
1812
|
-
for (const
|
|
1813
|
-
|
|
1911
|
+
for (const event_name of Object.keys(merged.events)) {
|
|
1912
|
+
event_to_state.set(event_name, merged);
|
|
1814
1913
|
}
|
|
1815
1914
|
}
|
|
1816
1915
|
return {
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1916
|
+
static_targets: [...statics.values()],
|
|
1917
|
+
has_dynamic_resolvers,
|
|
1918
|
+
reactive_events,
|
|
1919
|
+
event_to_state,
|
|
1920
|
+
event_to_lanes
|
|
1822
1921
|
};
|
|
1823
1922
|
}
|
|
1824
1923
|
|
|
1825
1924
|
// src/internal/close-cycle.ts
|
|
1826
|
-
async function
|
|
1827
|
-
const
|
|
1828
|
-
const streams = [...
|
|
1925
|
+
async function run_close_cycle(targets, deps) {
|
|
1926
|
+
const target_map = new Map(targets.map((t) => [t.stream, t]));
|
|
1927
|
+
const streams = [...target_map.keys()];
|
|
1829
1928
|
const skipped = [];
|
|
1830
|
-
const
|
|
1831
|
-
const safe = await
|
|
1832
|
-
|
|
1833
|
-
deps.
|
|
1929
|
+
const stream_info = await scan_stream_heads(streams);
|
|
1930
|
+
const safe = await partition_by_safety(
|
|
1931
|
+
stream_info,
|
|
1932
|
+
deps.reactive_events_size,
|
|
1834
1933
|
skipped
|
|
1835
1934
|
);
|
|
1836
1935
|
if (!safe.length) return { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
1837
|
-
const { guarded,
|
|
1936
|
+
const { guarded, guard_events } = await guard_with_tombstones(
|
|
1838
1937
|
safe,
|
|
1839
|
-
|
|
1938
|
+
stream_info,
|
|
1840
1939
|
deps.correlation,
|
|
1841
1940
|
deps.tombstone,
|
|
1842
1941
|
skipped
|
|
1843
1942
|
);
|
|
1844
1943
|
if (!guarded.length) return { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
1845
|
-
const
|
|
1944
|
+
const seed_states = await load_restart_seeds(
|
|
1846
1945
|
guarded,
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
deps.
|
|
1946
|
+
target_map,
|
|
1947
|
+
stream_info,
|
|
1948
|
+
deps.event_to_state,
|
|
1850
1949
|
deps.load,
|
|
1851
1950
|
deps.logger
|
|
1852
1951
|
);
|
|
1853
|
-
await
|
|
1854
|
-
const truncated = await
|
|
1952
|
+
await run_archive_callbacks(guarded, target_map);
|
|
1953
|
+
const truncated = await truncate_and_warm_cache(
|
|
1855
1954
|
guarded,
|
|
1856
|
-
|
|
1857
|
-
|
|
1955
|
+
seed_states,
|
|
1956
|
+
guard_events,
|
|
1858
1957
|
deps.correlation
|
|
1859
1958
|
);
|
|
1860
1959
|
return { truncated, skipped };
|
|
1861
1960
|
}
|
|
1862
|
-
async function
|
|
1961
|
+
async function scan_stream_heads(streams) {
|
|
1863
1962
|
const stats = await store2().query_stats(streams, {
|
|
1864
1963
|
exclude: [SNAP_EVENT]
|
|
1865
1964
|
});
|
|
@@ -1867,76 +1966,76 @@ async function scanStreamHeads(streams) {
|
|
|
1867
1966
|
for (const [stream, { head }] of stats) {
|
|
1868
1967
|
if (head.name === TOMBSTONE_EVENT) continue;
|
|
1869
1968
|
out.set(stream, {
|
|
1870
|
-
|
|
1969
|
+
max_id: head.id,
|
|
1871
1970
|
version: head.version,
|
|
1872
|
-
|
|
1971
|
+
last_event_name: head.name
|
|
1873
1972
|
});
|
|
1874
1973
|
}
|
|
1875
1974
|
return out;
|
|
1876
1975
|
}
|
|
1877
|
-
async function
|
|
1878
|
-
if (
|
|
1879
|
-
const
|
|
1976
|
+
async function partition_by_safety(stream_info, reactive_events_size, skipped) {
|
|
1977
|
+
if (reactive_events_size === 0) return [...stream_info.keys()];
|
|
1978
|
+
const pending_set = /* @__PURE__ */ new Set();
|
|
1880
1979
|
await store2().query_streams((position) => {
|
|
1881
|
-
const
|
|
1882
|
-
for (const [stream, info] of
|
|
1883
|
-
if ((!
|
|
1884
|
-
|
|
1980
|
+
const source_re = position.source ? RegExp(position.source) : void 0;
|
|
1981
|
+
for (const [stream, info] of stream_info) {
|
|
1982
|
+
if ((!source_re || source_re.test(stream)) && position.at < info.max_id) {
|
|
1983
|
+
pending_set.add(stream);
|
|
1885
1984
|
}
|
|
1886
1985
|
}
|
|
1887
1986
|
});
|
|
1888
1987
|
const safe = [];
|
|
1889
|
-
for (const [stream] of
|
|
1890
|
-
if (
|
|
1988
|
+
for (const [stream] of stream_info) {
|
|
1989
|
+
if (pending_set.has(stream)) skipped.push(stream);
|
|
1891
1990
|
else safe.push(stream);
|
|
1892
1991
|
}
|
|
1893
1992
|
return safe;
|
|
1894
1993
|
}
|
|
1895
|
-
async function
|
|
1994
|
+
async function guard_with_tombstones(safe, stream_info, correlation, tombstone2, skipped) {
|
|
1896
1995
|
const guarded = [];
|
|
1897
|
-
const
|
|
1996
|
+
const guard_events = /* @__PURE__ */ new Map();
|
|
1898
1997
|
await Promise.all(
|
|
1899
1998
|
safe.map(async (stream) => {
|
|
1900
|
-
const info =
|
|
1999
|
+
const info = stream_info.get(stream);
|
|
1901
2000
|
const committed = await tombstone2(stream, info.version, correlation);
|
|
1902
2001
|
if (committed) {
|
|
1903
2002
|
guarded.push(stream);
|
|
1904
|
-
|
|
2003
|
+
guard_events.set(stream, { id: committed.id, stream });
|
|
1905
2004
|
} else {
|
|
1906
2005
|
skipped.push(stream);
|
|
1907
2006
|
}
|
|
1908
2007
|
})
|
|
1909
2008
|
);
|
|
1910
|
-
return { guarded,
|
|
2009
|
+
return { guarded, guard_events };
|
|
1911
2010
|
}
|
|
1912
|
-
async function
|
|
1913
|
-
const
|
|
2011
|
+
async function load_restart_seeds(guarded, target_map, stream_info, event_to_state, load2, logger) {
|
|
2012
|
+
const seed_states = /* @__PURE__ */ new Map();
|
|
1914
2013
|
await Promise.all(
|
|
1915
|
-
guarded.filter((s) =>
|
|
1916
|
-
const
|
|
1917
|
-
const
|
|
1918
|
-
if (!
|
|
2014
|
+
guarded.filter((s) => target_map.get(s)?.restart).map(async (stream) => {
|
|
2015
|
+
const last_event_name = stream_info.get(stream).last_event_name;
|
|
2016
|
+
const owner_state = event_to_state.get(last_event_name);
|
|
2017
|
+
if (!owner_state) {
|
|
1919
2018
|
logger.error(
|
|
1920
|
-
`Cannot seed restart for "${stream}": no registered state owns event "${
|
|
2019
|
+
`Cannot seed restart for "${stream}": no registered state owns event "${last_event_name}". Stream will be tombstoned instead.`
|
|
1921
2020
|
);
|
|
1922
2021
|
return;
|
|
1923
2022
|
}
|
|
1924
|
-
const snap2 = await load2(
|
|
1925
|
-
|
|
2023
|
+
const snap2 = await load2(owner_state, stream);
|
|
2024
|
+
seed_states.set(stream, snap2.state);
|
|
1926
2025
|
})
|
|
1927
2026
|
);
|
|
1928
|
-
return
|
|
2027
|
+
return seed_states;
|
|
1929
2028
|
}
|
|
1930
|
-
async function
|
|
2029
|
+
async function run_archive_callbacks(guarded, target_map) {
|
|
1931
2030
|
for (const stream of guarded) {
|
|
1932
|
-
const
|
|
1933
|
-
if (
|
|
2031
|
+
const archive_fn = target_map.get(stream)?.archive;
|
|
2032
|
+
if (archive_fn) await archive_fn();
|
|
1934
2033
|
}
|
|
1935
2034
|
}
|
|
1936
|
-
async function
|
|
1937
|
-
const
|
|
1938
|
-
const snapshot =
|
|
1939
|
-
const guard =
|
|
2035
|
+
async function truncate_and_warm_cache(guarded, seed_states, guard_events, correlation) {
|
|
2036
|
+
const trunc_targets = guarded.map((stream) => {
|
|
2037
|
+
const snapshot = seed_states.get(stream);
|
|
2038
|
+
const guard = guard_events.get(stream);
|
|
1940
2039
|
return {
|
|
1941
2040
|
stream,
|
|
1942
2041
|
snapshot,
|
|
@@ -1948,11 +2047,11 @@ async function truncateAndWarmCache(guarded, seedStates, guardEvents, correlatio
|
|
|
1948
2047
|
}
|
|
1949
2048
|
};
|
|
1950
2049
|
});
|
|
1951
|
-
const truncated = await store2().truncate(
|
|
2050
|
+
const truncated = await store2().truncate(trunc_targets);
|
|
1952
2051
|
await Promise.all(
|
|
1953
2052
|
guarded.map(async (stream) => {
|
|
1954
2053
|
const entry = truncated.get(stream);
|
|
1955
|
-
const state2 =
|
|
2054
|
+
const state2 = seed_states.get(stream);
|
|
1956
2055
|
if (state2 && entry) {
|
|
1957
2056
|
await cache2().set(stream, {
|
|
1958
2057
|
state: state2,
|
|
@@ -1980,13 +2079,13 @@ var CorrelateCycle = class {
|
|
|
1980
2079
|
_has_dynamic_resolvers;
|
|
1981
2080
|
_cd;
|
|
1982
2081
|
_on_init;
|
|
1983
|
-
constructor(registry,
|
|
2082
|
+
constructor(registry, static_targets, has_dynamic_resolvers, cd, maxSubscribedStreams, on_init) {
|
|
1984
2083
|
this._subscribed = new LruSet(maxSubscribedStreams);
|
|
1985
2084
|
this._registry = registry;
|
|
1986
|
-
this._static_targets =
|
|
1987
|
-
this._has_dynamic_resolvers =
|
|
2085
|
+
this._static_targets = static_targets;
|
|
2086
|
+
this._has_dynamic_resolvers = has_dynamic_resolvers;
|
|
1988
2087
|
this._cd = cd;
|
|
1989
|
-
this._on_init =
|
|
2088
|
+
this._on_init = on_init;
|
|
1990
2089
|
}
|
|
1991
2090
|
/** Last correlated event id. */
|
|
1992
2091
|
get checkpoint() {
|
|
@@ -1997,7 +2096,7 @@ var CorrelateCycle = class {
|
|
|
1997
2096
|
* - Reads max(at) from store as cold-start checkpoint
|
|
1998
2097
|
* - Subscribes static resolver targets (idempotent upsert)
|
|
1999
2098
|
* - Populates the subscribed-streams LRU
|
|
2000
|
-
* - Fires `
|
|
2099
|
+
* - Fires `on_init` once (Act uses this to flag a cold-start drain)
|
|
2001
2100
|
*/
|
|
2002
2101
|
async init() {
|
|
2003
2102
|
if (this._initialized) return;
|
|
@@ -2030,15 +2129,15 @@ var CorrelateCycle = class {
|
|
|
2030
2129
|
if (typeof reaction.resolver !== "function") continue;
|
|
2031
2130
|
const resolved = reaction.resolver(event);
|
|
2032
2131
|
if (resolved && !this._subscribed.has(resolved.target)) {
|
|
2033
|
-
const
|
|
2132
|
+
const incoming_priority = resolved.priority ?? 0;
|
|
2034
2133
|
const entry = correlated.get(resolved.target) || {
|
|
2035
2134
|
source: resolved.source,
|
|
2036
|
-
priority:
|
|
2135
|
+
priority: incoming_priority,
|
|
2037
2136
|
lane: resolved.lane,
|
|
2038
2137
|
payloads: []
|
|
2039
2138
|
};
|
|
2040
|
-
if (
|
|
2041
|
-
entry.priority =
|
|
2139
|
+
if (incoming_priority > entry.priority)
|
|
2140
|
+
entry.priority = incoming_priority;
|
|
2042
2141
|
entry.payloads.push({
|
|
2043
2142
|
...reaction,
|
|
2044
2143
|
source: resolved.source,
|
|
@@ -2077,7 +2176,7 @@ var CorrelateCycle = class {
|
|
|
2077
2176
|
* running. Errors from `correlate()` are routed through `log()` so they
|
|
2078
2177
|
* land in the configured logger (the timer keeps running on failure).
|
|
2079
2178
|
*/
|
|
2080
|
-
|
|
2179
|
+
start_polling(query = {}, frequency = 1e4, callback) {
|
|
2081
2180
|
if (this._timer) return false;
|
|
2082
2181
|
const limit = query.limit || 100;
|
|
2083
2182
|
this._timer = setInterval(
|
|
@@ -2089,7 +2188,7 @@ var CorrelateCycle = class {
|
|
|
2089
2188
|
return true;
|
|
2090
2189
|
}
|
|
2091
2190
|
/** Stop the periodic correlation worker. Idempotent. */
|
|
2092
|
-
|
|
2191
|
+
stop_polling() {
|
|
2093
2192
|
if (this._timer) {
|
|
2094
2193
|
clearInterval(this._timer);
|
|
2095
2194
|
this._timer = void 0;
|
|
@@ -2105,14 +2204,14 @@ var SEG_SPACE = BASE ** SEG_WIDTH;
|
|
|
2105
2204
|
function seg(n) {
|
|
2106
2205
|
return n.toString(BASE).padStart(SEG_WIDTH, "0");
|
|
2107
2206
|
}
|
|
2108
|
-
var
|
|
2207
|
+
var default_correlator = ({ state: state2, action: action2 }) => {
|
|
2109
2208
|
const s = state2.slice(0, SEG_WIDTH).toLowerCase();
|
|
2110
2209
|
const a = action2.slice(0, SEG_WIDTH).toLowerCase();
|
|
2111
2210
|
const ts = seg(Date.now() % SEG_SPACE);
|
|
2112
2211
|
const rnd = seg((0, import_node_crypto.randomInt)(SEG_SPACE));
|
|
2113
2212
|
return `${s}-${a}-${ts}${rnd}`;
|
|
2114
2213
|
};
|
|
2115
|
-
function
|
|
2214
|
+
function close_correlation(correlator, actor) {
|
|
2116
2215
|
return correlator({
|
|
2117
2216
|
state: "$close",
|
|
2118
2217
|
action: "close",
|
|
@@ -2128,7 +2227,7 @@ var import_node_crypto2 = require("crypto");
|
|
|
2128
2227
|
var RATIO_MIN = 0.2;
|
|
2129
2228
|
var RATIO_MAX = 0.8;
|
|
2130
2229
|
var RATIO_DEFAULT = 0.5;
|
|
2131
|
-
function
|
|
2230
|
+
function compute_lag_lead_ratio(handled, lagging, leading) {
|
|
2132
2231
|
let lagging_handled = 0;
|
|
2133
2232
|
let leading_handled = 0;
|
|
2134
2233
|
for (const { lease, handled: count } of handled) {
|
|
@@ -2165,7 +2264,7 @@ var subscribe = (streams) => store2().subscribe(streams);
|
|
|
2165
2264
|
var import_act_patch = require("@rotorsoft/act-patch");
|
|
2166
2265
|
|
|
2167
2266
|
// src/internal/backoff.ts
|
|
2168
|
-
function
|
|
2267
|
+
function compute_backoff_delay(retry, opts) {
|
|
2169
2268
|
if (!opts || opts.baseMs <= 0) return 0;
|
|
2170
2269
|
const r = Math.max(0, retry);
|
|
2171
2270
|
let delay;
|
|
@@ -2334,9 +2433,9 @@ async function scan(source, opts = {}, callback) {
|
|
|
2334
2433
|
}
|
|
2335
2434
|
};
|
|
2336
2435
|
}
|
|
2337
|
-
async function load(me, stream, callback, asOf) {
|
|
2338
|
-
const
|
|
2339
|
-
const cached =
|
|
2436
|
+
async function load(me, stream, callback, asOf, actor) {
|
|
2437
|
+
const time_travel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
|
|
2438
|
+
const cached = time_travel ? void 0 : await cache2().get(stream);
|
|
2340
2439
|
const cache_hit = !!cached;
|
|
2341
2440
|
let state2 = cached?.state ?? (me.init ? me.init() : {});
|
|
2342
2441
|
let patches = cached?.patches ?? 0;
|
|
@@ -2346,15 +2445,16 @@ async function load(me, stream, callback, asOf) {
|
|
|
2346
2445
|
let event;
|
|
2347
2446
|
await store2().query(
|
|
2348
2447
|
(e) => {
|
|
2349
|
-
event = e;
|
|
2350
2448
|
version = e.version;
|
|
2449
|
+
const typed = e;
|
|
2450
|
+
event = me.view(typed, actor);
|
|
2351
2451
|
if (e.name === SNAP_EVENT) {
|
|
2352
2452
|
state2 = e.data;
|
|
2353
2453
|
snaps++;
|
|
2354
2454
|
patches = 0;
|
|
2355
2455
|
replayed++;
|
|
2356
2456
|
} else if (me.patch[e.name]) {
|
|
2357
|
-
state2 = (0, import_act_patch.patch)(state2, me.patch[e.name](
|
|
2457
|
+
state2 = (0, import_act_patch.patch)(state2, me.patch[e.name](typed, state2));
|
|
2358
2458
|
patches++;
|
|
2359
2459
|
replayed++;
|
|
2360
2460
|
} else if (e.name !== TOMBSTONE_EVENT) {
|
|
@@ -2378,7 +2478,7 @@ async function load(me, stream, callback, asOf) {
|
|
|
2378
2478
|
...cached ? { after: cached.event_id } : { with_snaps: true, ...asOf }
|
|
2379
2479
|
}
|
|
2380
2480
|
);
|
|
2381
|
-
if (replayed > 0 && !
|
|
2481
|
+
if (replayed > 0 && !time_travel && event) {
|
|
2382
2482
|
await cache2().set(stream, {
|
|
2383
2483
|
state: state2,
|
|
2384
2484
|
version,
|
|
@@ -2389,15 +2489,21 @@ async function load(me, stream, callback, asOf) {
|
|
|
2389
2489
|
}
|
|
2390
2490
|
return { event, state: state2, version, patches, snaps, cache_hit, replayed };
|
|
2391
2491
|
}
|
|
2392
|
-
async function action(me, action2, target, payload, reactingTo, skipValidation = false, correlator =
|
|
2492
|
+
async function action(me, action2, target, payload, reactingTo, skipValidation = false, correlator = default_correlator) {
|
|
2393
2493
|
const { stream, expectedVersion, actor } = target;
|
|
2394
2494
|
if (!stream) throw new Error("Missing target stream");
|
|
2395
2495
|
const validated = skipValidation ? payload : validate(action2, payload, me.actions[action2]);
|
|
2396
2496
|
const opts = me.options?.[action2];
|
|
2397
|
-
const
|
|
2497
|
+
const max_retries = opts?.maxRetries ?? 0;
|
|
2398
2498
|
for (let attempt = 0; ; attempt++) {
|
|
2399
2499
|
try {
|
|
2400
|
-
const snapshot = await load(
|
|
2500
|
+
const snapshot = await load(
|
|
2501
|
+
me,
|
|
2502
|
+
stream,
|
|
2503
|
+
void 0,
|
|
2504
|
+
void 0,
|
|
2505
|
+
target.actor
|
|
2506
|
+
);
|
|
2401
2507
|
if (snapshot.event?.name === TOMBSTONE_EVENT)
|
|
2402
2508
|
throw new StreamClosedError(stream);
|
|
2403
2509
|
const expected = expectedVersion ?? snapshot.event?.version;
|
|
@@ -2434,10 +2540,10 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
2434
2540
|
}
|
|
2435
2541
|
}
|
|
2436
2542
|
}
|
|
2437
|
-
const emitted = tuples.map(([name, data]) =>
|
|
2438
|
-
name,
|
|
2439
|
-
|
|
2440
|
-
})
|
|
2543
|
+
const emitted = tuples.map(([name, data]) => {
|
|
2544
|
+
const validated2 = skipValidation ? data : validate(name, data, me.events[name]);
|
|
2545
|
+
return me.message({ name, data: validated2 });
|
|
2546
|
+
});
|
|
2441
2547
|
const meta = {
|
|
2442
2548
|
correlation: reactingTo?.meta.correlation || correlator({
|
|
2443
2549
|
action: action2,
|
|
@@ -2449,8 +2555,8 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
2449
2555
|
action: {
|
|
2450
2556
|
name: action2,
|
|
2451
2557
|
...target
|
|
2452
|
-
// payload intentionally omitted
|
|
2453
|
-
//
|
|
2558
|
+
// payload intentionally omitted from causation metadata —
|
|
2559
|
+
// callers correlate via the correlation id when they need it.
|
|
2454
2560
|
},
|
|
2455
2561
|
event: reactingTo ? {
|
|
2456
2562
|
id: reactingTo.id,
|
|
@@ -2483,7 +2589,7 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
2483
2589
|
state2 = (0, import_act_patch.patch)(state2, p);
|
|
2484
2590
|
patches++;
|
|
2485
2591
|
return {
|
|
2486
|
-
event,
|
|
2592
|
+
event: me.view(event, target.actor),
|
|
2487
2593
|
state: state2,
|
|
2488
2594
|
version: event.version,
|
|
2489
2595
|
patches,
|
|
@@ -2506,10 +2612,10 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
2506
2612
|
return snapshots;
|
|
2507
2613
|
} catch (error) {
|
|
2508
2614
|
if (!(error instanceof ConcurrencyError)) throw error;
|
|
2509
|
-
if (attempt >=
|
|
2615
|
+
if (attempt >= max_retries) throw error;
|
|
2510
2616
|
if (opts?.backoff) {
|
|
2511
|
-
const
|
|
2512
|
-
if (
|
|
2617
|
+
const delay_ms = compute_backoff_delay(attempt, opts.backoff);
|
|
2618
|
+
if (delay_ms > 0) await sleep(delay_ms);
|
|
2513
2619
|
}
|
|
2514
2620
|
}
|
|
2515
2621
|
}
|
|
@@ -2533,12 +2639,12 @@ var C_STREAM = "\x1B[38;5;226m";
|
|
|
2533
2639
|
var dim = (text) => PRETTY ? `${C_DIM}${text}${C_RESET}` : text;
|
|
2534
2640
|
var hue = (color, text) => PRETTY ? `${color}${text}${C_RESET}` : text;
|
|
2535
2641
|
var drain_caption = (caption, lane) => {
|
|
2536
|
-
const
|
|
2642
|
+
const show_lane = lane && lane !== "default";
|
|
2537
2643
|
if (PRETTY) {
|
|
2538
2644
|
const tag = `${C_DRAIN}>> ${caption}${C_RESET}`;
|
|
2539
|
-
return
|
|
2645
|
+
return show_lane ? `${tag} ${C_LANE}${lane}${C_RESET}` : tag;
|
|
2540
2646
|
}
|
|
2541
|
-
return
|
|
2647
|
+
return show_lane ? `>> ${caption} ${lane}` : `>> ${caption}`;
|
|
2542
2648
|
};
|
|
2543
2649
|
var cache_marker = (hit) => {
|
|
2544
2650
|
const word = hit ? "hit" : "miss";
|
|
@@ -2567,10 +2673,10 @@ var traced = (inner, exit, entry) => (async (...args) => {
|
|
|
2567
2673
|
exit?.(result, ...args);
|
|
2568
2674
|
return result;
|
|
2569
2675
|
});
|
|
2570
|
-
function
|
|
2571
|
-
const
|
|
2676
|
+
function build_es(logger, correlator = default_correlator) {
|
|
2677
|
+
const bound_action = (me, action_name, target, payload, reactingTo, skipValidation = false) => action(
|
|
2572
2678
|
me,
|
|
2573
|
-
|
|
2679
|
+
action_name,
|
|
2574
2680
|
target,
|
|
2575
2681
|
payload,
|
|
2576
2682
|
reactingTo,
|
|
@@ -2581,7 +2687,7 @@ function buildEs(logger, correlator = defaultCorrelator) {
|
|
|
2581
2687
|
return {
|
|
2582
2688
|
snap,
|
|
2583
2689
|
load,
|
|
2584
|
-
action:
|
|
2690
|
+
action: bound_action,
|
|
2585
2691
|
tombstone
|
|
2586
2692
|
};
|
|
2587
2693
|
}
|
|
@@ -2611,7 +2717,7 @@ function buildEs(logger, correlator = defaultCorrelator) {
|
|
|
2611
2717
|
);
|
|
2612
2718
|
}),
|
|
2613
2719
|
action: traced(
|
|
2614
|
-
|
|
2720
|
+
bound_action,
|
|
2615
2721
|
(snapshots, _me, _action, target) => {
|
|
2616
2722
|
const committed = snapshots.filter((s) => s.event);
|
|
2617
2723
|
if (committed.length) {
|
|
@@ -2640,7 +2746,7 @@ function buildEs(logger, correlator = defaultCorrelator) {
|
|
|
2640
2746
|
})
|
|
2641
2747
|
};
|
|
2642
2748
|
}
|
|
2643
|
-
function
|
|
2749
|
+
function build_drain(logger) {
|
|
2644
2750
|
return {
|
|
2645
2751
|
claim,
|
|
2646
2752
|
fetch,
|
|
@@ -2649,46 +2755,48 @@ function buildDrain(logger) {
|
|
|
2649
2755
|
subscribe: logger.level !== "trace" ? subscribe : traced(subscribe, (result, streams) => {
|
|
2650
2756
|
if (!result.subscribed) return;
|
|
2651
2757
|
const lanes = new Set(streams.map((s) => s.lane ?? "default"));
|
|
2652
|
-
const
|
|
2758
|
+
const uniform_lane = lanes.size === 1 ? streams[0]?.lane : void 0;
|
|
2653
2759
|
const data = streams.map(
|
|
2654
|
-
({ stream, lane }) =>
|
|
2760
|
+
({ stream, lane }) => uniform_lane || !lane || lane === "default" ? hue(C_STREAM, stream) : `${hue(C_STREAM, stream)}${dim(`[${lane}]`)}`
|
|
2655
2761
|
).join(" ");
|
|
2656
|
-
logger.trace(
|
|
2762
|
+
logger.trace(
|
|
2763
|
+
`${drain_caption("correlated", uniform_lane)} ${data}`
|
|
2764
|
+
);
|
|
2657
2765
|
})
|
|
2658
2766
|
};
|
|
2659
2767
|
}
|
|
2660
|
-
function
|
|
2768
|
+
function trace_cycle(logger, leased, fetched, handled, acked, blocked) {
|
|
2661
2769
|
if (logger.level !== "trace" || !leased.length) return;
|
|
2662
2770
|
const lane = leased[0]?.lane;
|
|
2663
|
-
const
|
|
2664
|
-
const
|
|
2665
|
-
const
|
|
2666
|
-
const
|
|
2771
|
+
const fetch_by_stream = new Map(fetched.map((f) => [f.stream, f]));
|
|
2772
|
+
const acked_by_stream = new Map(acked.map((a) => [a.stream, a.at]));
|
|
2773
|
+
const blocked_by_stream = new Map(blocked.map((b) => [b.stream, b.error]));
|
|
2774
|
+
const failed_by_stream = new Map(
|
|
2667
2775
|
handled.filter((h) => h.error).map((h) => [h.lease.stream, h])
|
|
2668
2776
|
);
|
|
2669
2777
|
const detail = leased.map(({ stream, at, retry }) => {
|
|
2670
|
-
const f =
|
|
2778
|
+
const f = fetch_by_stream.get(stream);
|
|
2671
2779
|
const key = f?.source ? `${hue(C_STREAM, stream)}${dim(`<-${f.source}`)}` : hue(C_STREAM, stream);
|
|
2672
2780
|
const events = f && f.events.length ? ` ${dim(
|
|
2673
2781
|
`[${f.events.map(({ id, name }) => `#${id} ${String(name)}`).join(", ")}]`
|
|
2674
2782
|
)}` : "";
|
|
2675
|
-
const
|
|
2676
|
-
const
|
|
2677
|
-
const failure =
|
|
2678
|
-
let
|
|
2783
|
+
const acked_at = acked_by_stream.get(stream);
|
|
2784
|
+
const ack_part = acked_at !== void 0 ? hue(C_HIT, `\u2713 @${acked_at}`) : "";
|
|
2785
|
+
const failure = failed_by_stream.get(stream);
|
|
2786
|
+
let fail_part = "";
|
|
2679
2787
|
if (failure) {
|
|
2680
|
-
const
|
|
2681
|
-
const
|
|
2682
|
-
if (
|
|
2683
|
-
|
|
2788
|
+
const failed_at = failure.failed_at ?? at;
|
|
2789
|
+
const blocked_error = blocked_by_stream.get(stream);
|
|
2790
|
+
if (blocked_error !== void 0) {
|
|
2791
|
+
fail_part = `${hue(C_ERR, `\u2717 @${failed_at}/${retry}`)} ${dim(`(${blocked_error})`)}`;
|
|
2684
2792
|
} else {
|
|
2685
|
-
|
|
2793
|
+
fail_part = `${hue(C_MISS, `\u26A0 @${failed_at}/${retry}`)} ${dim(`(${failure.error})`)}`;
|
|
2686
2794
|
}
|
|
2687
2795
|
}
|
|
2688
2796
|
let tail;
|
|
2689
|
-
if (
|
|
2690
|
-
else if (
|
|
2691
|
-
else if (
|
|
2797
|
+
if (ack_part && fail_part) tail = ` ${ack_part} ${fail_part}`;
|
|
2798
|
+
else if (ack_part) tail = ` ${ack_part}`;
|
|
2799
|
+
else if (fail_part) tail = ` ${fail_part}`;
|
|
2692
2800
|
else tail = ` ${dim(`\u2298 @${at}/${retry}`)}`;
|
|
2693
2801
|
return `${key}${events}${tail}`;
|
|
2694
2802
|
}).join(", ");
|
|
@@ -2696,7 +2804,7 @@ function traceCycle(logger, leased, fetched, handled, acked, blocked) {
|
|
|
2696
2804
|
}
|
|
2697
2805
|
|
|
2698
2806
|
// src/internal/drain-cycle.ts
|
|
2699
|
-
async function
|
|
2807
|
+
async function run_drain_cycle(ops, registry, batch_handlers, handle, handle_batch, lagging, leading, eventLimit, leaseMillis, is_deferred, lane) {
|
|
2700
2808
|
const leased = await ops.claim(
|
|
2701
2809
|
lagging,
|
|
2702
2810
|
leading,
|
|
@@ -2705,7 +2813,7 @@ async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch,
|
|
|
2705
2813
|
lane
|
|
2706
2814
|
);
|
|
2707
2815
|
if (!leased.length) return void 0;
|
|
2708
|
-
const active =
|
|
2816
|
+
const active = is_deferred ? leased.filter((l) => !is_deferred(l.stream)) : leased;
|
|
2709
2817
|
if (!active.length) {
|
|
2710
2818
|
return {
|
|
2711
2819
|
leased,
|
|
@@ -2716,7 +2824,7 @@ async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch,
|
|
|
2716
2824
|
};
|
|
2717
2825
|
}
|
|
2718
2826
|
const fetched = await ops.fetch(active, eventLimit);
|
|
2719
|
-
const
|
|
2827
|
+
const fetch_map = /* @__PURE__ */ new Map();
|
|
2720
2828
|
const fetch_window_at = fetched.reduce(
|
|
2721
2829
|
(max, { at, events }) => Math.max(max, events.at(-1)?.id || at),
|
|
2722
2830
|
0
|
|
@@ -2731,16 +2839,16 @@ async function runDrainCycle(ops, registry, batchHandlers, handle, handleBatch,
|
|
|
2731
2839
|
return resolved && resolved.target === stream;
|
|
2732
2840
|
}).map((reaction) => ({ ...reaction, event }));
|
|
2733
2841
|
});
|
|
2734
|
-
|
|
2842
|
+
fetch_map.set(stream, { fetch: f, payloads });
|
|
2735
2843
|
}
|
|
2736
2844
|
const handled = await Promise.all(
|
|
2737
2845
|
active.map((lease) => {
|
|
2738
|
-
const entry =
|
|
2846
|
+
const entry = fetch_map.get(lease.stream);
|
|
2739
2847
|
const at = entry.fetch.events.at(-1)?.id || fetch_window_at;
|
|
2740
2848
|
const { payloads } = entry;
|
|
2741
|
-
const batchHandler =
|
|
2849
|
+
const batchHandler = batch_handlers.get(lease.stream);
|
|
2742
2850
|
if (batchHandler && payloads.length > 0) {
|
|
2743
|
-
return
|
|
2851
|
+
return handle_batch({ ...lease, at }, payloads, batchHandler);
|
|
2744
2852
|
}
|
|
2745
2853
|
return handle({ ...lease, at }, payloads);
|
|
2746
2854
|
})
|
|
@@ -2764,14 +2872,14 @@ var DrainController = class {
|
|
|
2764
2872
|
_locked = false;
|
|
2765
2873
|
_ratio = 0.5;
|
|
2766
2874
|
/**
|
|
2767
|
-
* Per-stream backoff: `stream →
|
|
2768
|
-
* `_finalize` via `HandleResult.
|
|
2875
|
+
* Per-stream backoff: `stream → next_attempt_at` (ms since epoch). Set by
|
|
2876
|
+
* `_finalize` via `HandleResult.next_attempt_at`; cleared on successful
|
|
2769
2877
|
* ack or terminal block. Lives in process memory — per-worker pacing
|
|
2770
2878
|
* by design (see {@link BackoffOptions} for the multi-worker trade-off).
|
|
2771
2879
|
*/
|
|
2772
2880
|
_backoff = /* @__PURE__ */ new Map();
|
|
2773
|
-
/** Timer re-arming drain at the earliest pending `
|
|
2774
|
-
|
|
2881
|
+
/** Timer re-arming drain at the earliest pending `next_attempt_at`. */
|
|
2882
|
+
_backoff_timer;
|
|
2775
2883
|
/** Worker timer (ACT-1103). Set when `start()` is active, undefined otherwise. */
|
|
2776
2884
|
_worker;
|
|
2777
2885
|
_stopped = false;
|
|
@@ -2792,7 +2900,7 @@ var DrainController = class {
|
|
|
2792
2900
|
return this._armed;
|
|
2793
2901
|
}
|
|
2794
2902
|
/** Returns true when `stream` is currently within a backoff window. */
|
|
2795
|
-
|
|
2903
|
+
is_deferred = (stream) => {
|
|
2796
2904
|
const next = this._backoff.get(stream);
|
|
2797
2905
|
return next !== void 0 && next > Date.now();
|
|
2798
2906
|
};
|
|
@@ -2802,20 +2910,20 @@ var DrainController = class {
|
|
|
2802
2910
|
* Idempotent — collapses many simultaneously deferred streams into a
|
|
2803
2911
|
* single timer.
|
|
2804
2912
|
*/
|
|
2805
|
-
|
|
2806
|
-
if (this.
|
|
2913
|
+
schedule_backoff_wake() {
|
|
2914
|
+
if (this._backoff_timer) clearTimeout(this._backoff_timer);
|
|
2807
2915
|
let earliest = Number.POSITIVE_INFINITY;
|
|
2808
2916
|
for (const t of this._backoff.values()) if (t < earliest) earliest = t;
|
|
2809
2917
|
const delay = Math.max(0, earliest - Date.now());
|
|
2810
|
-
this.
|
|
2811
|
-
this.
|
|
2918
|
+
this._backoff_timer = setTimeout(() => {
|
|
2919
|
+
this._backoff_timer = void 0;
|
|
2812
2920
|
const now = Date.now();
|
|
2813
2921
|
for (const [stream, at] of this._backoff) {
|
|
2814
2922
|
if (at <= now) this._backoff.delete(stream);
|
|
2815
2923
|
}
|
|
2816
2924
|
this._armed = true;
|
|
2817
2925
|
}, delay);
|
|
2818
|
-
this.
|
|
2926
|
+
this._backoff_timer.unref();
|
|
2819
2927
|
}
|
|
2820
2928
|
/** Lane this controller drains (undefined = legacy single-lane span). */
|
|
2821
2929
|
get lane() {
|
|
@@ -2861,17 +2969,17 @@ var DrainController = class {
|
|
|
2861
2969
|
this._locked = true;
|
|
2862
2970
|
const lagging = Math.ceil(streamLimit * this._ratio);
|
|
2863
2971
|
const leading = streamLimit - lagging;
|
|
2864
|
-
const cycle = await
|
|
2972
|
+
const cycle = await run_drain_cycle(
|
|
2865
2973
|
this._deps.ops,
|
|
2866
2974
|
this._deps.registry,
|
|
2867
|
-
this._deps.
|
|
2975
|
+
this._deps.batch_handlers,
|
|
2868
2976
|
this._deps.handle,
|
|
2869
|
-
this._deps.
|
|
2977
|
+
this._deps.handle_batch,
|
|
2870
2978
|
lagging,
|
|
2871
2979
|
leading,
|
|
2872
2980
|
eventLimit,
|
|
2873
2981
|
leaseMillis,
|
|
2874
|
-
this._backoff.size > 0 ? this.
|
|
2982
|
+
this._backoff.size > 0 ? this.is_deferred : void 0,
|
|
2875
2983
|
this._deps.lane
|
|
2876
2984
|
);
|
|
2877
2985
|
if (!cycle) {
|
|
@@ -2879,20 +2987,20 @@ var DrainController = class {
|
|
|
2879
2987
|
return EMPTY_DRAIN;
|
|
2880
2988
|
}
|
|
2881
2989
|
const { leased, fetched, handled, acked, blocked } = cycle;
|
|
2882
|
-
|
|
2883
|
-
this._ratio =
|
|
2990
|
+
trace_cycle(this._deps.logger, leased, fetched, handled, acked, blocked);
|
|
2991
|
+
this._ratio = compute_lag_lead_ratio(handled, lagging, leading);
|
|
2884
2992
|
for (const lease of acked) this._backoff.delete(lease.stream);
|
|
2885
2993
|
for (const lease of blocked) this._backoff.delete(lease.stream);
|
|
2886
2994
|
for (const h of handled) {
|
|
2887
|
-
if (h.
|
|
2888
|
-
this._backoff.set(h.lease.stream, h.
|
|
2995
|
+
if (h.next_attempt_at !== void 0 && !h.block) {
|
|
2996
|
+
this._backoff.set(h.lease.stream, h.next_attempt_at);
|
|
2889
2997
|
}
|
|
2890
2998
|
}
|
|
2891
|
-
if (this._backoff.size > 0) this.
|
|
2892
|
-
if (acked.length) this._deps.
|
|
2893
|
-
if (blocked.length) this._deps.
|
|
2894
|
-
const
|
|
2895
|
-
if (!acked.length && !blocked.length && !
|
|
2999
|
+
if (this._backoff.size > 0) this.schedule_backoff_wake();
|
|
3000
|
+
if (acked.length) this._deps.on_acked(acked);
|
|
3001
|
+
if (blocked.length) this._deps.on_blocked(blocked);
|
|
3002
|
+
const has_errors = handled.some(({ error }) => error);
|
|
3003
|
+
if (!acked.length && !blocked.length && !has_errors) this._armed = false;
|
|
2896
3004
|
return { fetched, leased, acked, blocked };
|
|
2897
3005
|
} catch (error) {
|
|
2898
3006
|
this._deps.logger.error(error);
|
|
@@ -2904,45 +3012,45 @@ var DrainController = class {
|
|
|
2904
3012
|
};
|
|
2905
3013
|
|
|
2906
3014
|
// src/internal/merge.ts
|
|
2907
|
-
var
|
|
2908
|
-
function
|
|
3015
|
+
var import_zod5 = require("zod");
|
|
3016
|
+
function base_type_name(zodType) {
|
|
2909
3017
|
let t = zodType;
|
|
2910
3018
|
while (typeof t.unwrap === "function") {
|
|
2911
3019
|
t = t.unwrap();
|
|
2912
3020
|
}
|
|
2913
3021
|
return t.constructor.name;
|
|
2914
3022
|
}
|
|
2915
|
-
function
|
|
2916
|
-
if (existing instanceof
|
|
2917
|
-
const
|
|
2918
|
-
const
|
|
2919
|
-
for (const key of Object.keys(
|
|
2920
|
-
if (key in
|
|
2921
|
-
const
|
|
2922
|
-
const
|
|
2923
|
-
if (
|
|
3023
|
+
function merge_schemas(existing, incoming, state_name) {
|
|
3024
|
+
if (existing instanceof import_zod5.ZodObject && incoming instanceof import_zod5.ZodObject) {
|
|
3025
|
+
const existing_shape = existing.shape;
|
|
3026
|
+
const incoming_shape = incoming.shape;
|
|
3027
|
+
for (const key of Object.keys(incoming_shape)) {
|
|
3028
|
+
if (key in existing_shape) {
|
|
3029
|
+
const existing_base = base_type_name(existing_shape[key]);
|
|
3030
|
+
const incoming_base = base_type_name(incoming_shape[key]);
|
|
3031
|
+
if (existing_base !== incoming_base) {
|
|
2924
3032
|
throw new Error(
|
|
2925
|
-
`Schema conflict in "${
|
|
3033
|
+
`Schema conflict in "${state_name}": key "${key}" has type "${existing_base}" but incoming partial declares "${incoming_base}"`
|
|
2926
3034
|
);
|
|
2927
3035
|
}
|
|
2928
3036
|
}
|
|
2929
3037
|
}
|
|
2930
|
-
return existing.extend(
|
|
3038
|
+
return existing.extend(incoming_shape);
|
|
2931
3039
|
}
|
|
2932
3040
|
return existing;
|
|
2933
3041
|
}
|
|
2934
|
-
function
|
|
3042
|
+
function merge_inits(existing, incoming) {
|
|
2935
3043
|
return () => ({ ...existing(), ...incoming() });
|
|
2936
3044
|
}
|
|
2937
|
-
function
|
|
3045
|
+
function register_state(state2, states, actions, events) {
|
|
2938
3046
|
const existing = states.get(state2.name);
|
|
2939
3047
|
if (existing) {
|
|
2940
|
-
|
|
3048
|
+
merge_into_existing(state2, existing, states, actions, events);
|
|
2941
3049
|
} else {
|
|
2942
|
-
|
|
3050
|
+
register_new_state(state2, states, actions, events);
|
|
2943
3051
|
}
|
|
2944
3052
|
}
|
|
2945
|
-
function
|
|
3053
|
+
function register_new_state(state2, states, actions, events) {
|
|
2946
3054
|
states.set(state2.name, state2);
|
|
2947
3055
|
for (const name of Object.keys(state2.actions)) {
|
|
2948
3056
|
if (actions[name]) throw new Error(`Duplicate action "${name}"`);
|
|
@@ -2953,7 +3061,7 @@ function registerNewState(state2, states, actions, events) {
|
|
|
2953
3061
|
events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
|
|
2954
3062
|
}
|
|
2955
3063
|
}
|
|
2956
|
-
function
|
|
3064
|
+
function merge_into_existing(state2, existing, states, actions, events) {
|
|
2957
3065
|
for (const name of Object.keys(state2.actions)) {
|
|
2958
3066
|
if (existing.actions[name] === state2.actions[name]) continue;
|
|
2959
3067
|
if (actions[name]) throw new Error(`Duplicate action "${name}"`);
|
|
@@ -2967,14 +3075,14 @@ function mergeIntoExisting(state2, existing, states, actions, events) {
|
|
|
2967
3075
|
}
|
|
2968
3076
|
if (events[name]) throw new Error(`Duplicate event "${name}"`);
|
|
2969
3077
|
}
|
|
2970
|
-
const
|
|
3078
|
+
const merged_patch = merge_patches(existing.patch, state2.patch, state2.name);
|
|
2971
3079
|
const merged = {
|
|
2972
3080
|
...existing,
|
|
2973
|
-
state:
|
|
2974
|
-
init:
|
|
3081
|
+
state: merge_schemas(existing.state, state2.state, state2.name),
|
|
3082
|
+
init: merge_inits(existing.init, state2.init),
|
|
2975
3083
|
events: { ...existing.events, ...state2.events },
|
|
2976
3084
|
actions: { ...existing.actions, ...state2.actions },
|
|
2977
|
-
patch:
|
|
3085
|
+
patch: merged_patch,
|
|
2978
3086
|
on: { ...existing.on, ...state2.on },
|
|
2979
3087
|
given: { ...existing.given, ...state2.given },
|
|
2980
3088
|
snap: state2.snap && existing.snap && state2.snap !== existing.snap ? (() => {
|
|
@@ -2992,48 +3100,48 @@ function mergeIntoExisting(state2, existing, states, actions, events) {
|
|
|
2992
3100
|
events[name] = { schema: state2.events[name], reactions: /* @__PURE__ */ new Map() };
|
|
2993
3101
|
}
|
|
2994
3102
|
}
|
|
2995
|
-
function
|
|
3103
|
+
function merge_patches(existing, incoming, state_name) {
|
|
2996
3104
|
const merged = { ...existing };
|
|
2997
3105
|
for (const name of Object.keys(incoming)) {
|
|
2998
|
-
const
|
|
2999
|
-
const
|
|
3000
|
-
if (!
|
|
3001
|
-
merged[name] =
|
|
3106
|
+
const existing_p = existing[name];
|
|
3107
|
+
const incoming_p = incoming[name];
|
|
3108
|
+
if (!existing_p) {
|
|
3109
|
+
merged[name] = incoming_p;
|
|
3002
3110
|
continue;
|
|
3003
3111
|
}
|
|
3004
|
-
const
|
|
3005
|
-
const
|
|
3006
|
-
if (!
|
|
3112
|
+
const existing_is_default = existing_p._passthrough;
|
|
3113
|
+
const incoming_is_default = incoming_p._passthrough;
|
|
3114
|
+
if (!existing_is_default && !incoming_is_default && existing_p !== incoming_p) {
|
|
3007
3115
|
throw new Error(
|
|
3008
|
-
`Duplicate custom patch for event "${name}" in state "${
|
|
3116
|
+
`Duplicate custom patch for event "${name}" in state "${state_name}"`
|
|
3009
3117
|
);
|
|
3010
3118
|
}
|
|
3011
|
-
if (
|
|
3012
|
-
merged[name] =
|
|
3119
|
+
if (existing_is_default && !incoming_is_default) {
|
|
3120
|
+
merged[name] = incoming_p;
|
|
3013
3121
|
}
|
|
3014
3122
|
}
|
|
3015
3123
|
return merged;
|
|
3016
3124
|
}
|
|
3017
|
-
function
|
|
3018
|
-
for (const [
|
|
3019
|
-
const
|
|
3020
|
-
if (!
|
|
3021
|
-
for (const [name, reaction] of
|
|
3022
|
-
|
|
3125
|
+
function merge_event_register(target, source) {
|
|
3126
|
+
for (const [event_name, source_reg] of Object.entries(source)) {
|
|
3127
|
+
const target_reg = target[event_name];
|
|
3128
|
+
if (!target_reg) continue;
|
|
3129
|
+
for (const [name, reaction] of source_reg.reactions) {
|
|
3130
|
+
target_reg.reactions.set(name, reaction);
|
|
3023
3131
|
}
|
|
3024
3132
|
}
|
|
3025
3133
|
}
|
|
3026
|
-
function
|
|
3027
|
-
for (const
|
|
3028
|
-
const
|
|
3029
|
-
const existing = events[
|
|
3134
|
+
function merge_projection(proj, events) {
|
|
3135
|
+
for (const event_name of Object.keys(proj.events)) {
|
|
3136
|
+
const proj_register = proj.events[event_name];
|
|
3137
|
+
const existing = events[event_name];
|
|
3030
3138
|
if (!existing) {
|
|
3031
|
-
events[
|
|
3032
|
-
schema:
|
|
3033
|
-
reactions: new Map(
|
|
3139
|
+
events[event_name] = {
|
|
3140
|
+
schema: proj_register.schema,
|
|
3141
|
+
reactions: new Map(proj_register.reactions)
|
|
3034
3142
|
};
|
|
3035
3143
|
} else {
|
|
3036
|
-
for (const [name, reaction] of
|
|
3144
|
+
for (const [name, reaction] of proj_register.reactions) {
|
|
3037
3145
|
let key = name;
|
|
3038
3146
|
while (existing.reactions.has(key)) key = `${key}_p`;
|
|
3039
3147
|
existing.reactions.set(key, reaction);
|
|
@@ -3050,25 +3158,32 @@ var _this_ = ({ stream }) => ({
|
|
|
3050
3158
|
function finalize(lease, handled, at, error, options, logger, failed_at) {
|
|
3051
3159
|
if (!error) return { lease, handled, acked_at: at };
|
|
3052
3160
|
logger.error(error);
|
|
3053
|
-
const
|
|
3054
|
-
const block2 = options.blockOnError && (
|
|
3161
|
+
const non_retryable = error instanceof NonRetryableError;
|
|
3162
|
+
const block2 = options.blockOnError && (non_retryable || lease.retry >= options.maxRetries);
|
|
3055
3163
|
if (block2)
|
|
3056
3164
|
logger.error(
|
|
3057
|
-
|
|
3165
|
+
non_retryable ? `Blocking ${lease.stream} on non-retryable error.` : `Blocking ${lease.stream} after ${lease.retry} retries.`
|
|
3058
3166
|
);
|
|
3059
|
-
const
|
|
3167
|
+
const next_attempt_at = !block2 && options.backoff ? Date.now() + compute_backoff_delay(lease.retry, options.backoff) : void 0;
|
|
3060
3168
|
return {
|
|
3061
3169
|
lease,
|
|
3062
3170
|
handled,
|
|
3063
3171
|
acked_at: at,
|
|
3064
3172
|
error: error.message,
|
|
3065
3173
|
block: block2,
|
|
3066
|
-
|
|
3174
|
+
next_attempt_at,
|
|
3067
3175
|
failed_at
|
|
3068
3176
|
};
|
|
3069
3177
|
}
|
|
3070
|
-
function
|
|
3071
|
-
const {
|
|
3178
|
+
function build_handle(deps) {
|
|
3179
|
+
const {
|
|
3180
|
+
logger,
|
|
3181
|
+
bound_do,
|
|
3182
|
+
bound_load,
|
|
3183
|
+
bound_query,
|
|
3184
|
+
bound_query_array,
|
|
3185
|
+
bound_forget
|
|
3186
|
+
} = deps;
|
|
3072
3187
|
return async (lease, payloads) => {
|
|
3073
3188
|
if (payloads.length === 0) return { lease, handled: 0, acked_at: lease.at };
|
|
3074
3189
|
const stream = lease.stream;
|
|
@@ -3076,23 +3191,24 @@ function buildHandle(deps) {
|
|
|
3076
3191
|
let handled = 0;
|
|
3077
3192
|
if (lease.retry > 0)
|
|
3078
3193
|
logger.warn(`Retrying ${stream}@${at} (${lease.retry}).`);
|
|
3079
|
-
const
|
|
3080
|
-
do:
|
|
3081
|
-
load:
|
|
3082
|
-
query:
|
|
3083
|
-
query_array:
|
|
3194
|
+
const scoped_app = {
|
|
3195
|
+
do: bound_do,
|
|
3196
|
+
load: bound_load,
|
|
3197
|
+
query: bound_query,
|
|
3198
|
+
query_array: bound_query_array,
|
|
3199
|
+
forget: bound_forget
|
|
3084
3200
|
};
|
|
3085
3201
|
for (const payload of payloads) {
|
|
3086
3202
|
const { event, handler } = payload;
|
|
3087
|
-
|
|
3203
|
+
scoped_app.do = (action2, target, action_payload, reactingTo, skipValidation) => bound_do(
|
|
3088
3204
|
action2,
|
|
3089
3205
|
target,
|
|
3090
|
-
|
|
3206
|
+
action_payload,
|
|
3091
3207
|
reactingTo ?? event,
|
|
3092
3208
|
skipValidation
|
|
3093
3209
|
);
|
|
3094
3210
|
try {
|
|
3095
|
-
await handler(event, stream,
|
|
3211
|
+
await handler(event, stream, scoped_app);
|
|
3096
3212
|
at = event.id;
|
|
3097
3213
|
handled++;
|
|
3098
3214
|
} catch (error) {
|
|
@@ -3110,10 +3226,12 @@ function buildHandle(deps) {
|
|
|
3110
3226
|
return finalize(lease, handled, at, void 0, payloads[0].options, logger);
|
|
3111
3227
|
};
|
|
3112
3228
|
}
|
|
3113
|
-
function
|
|
3229
|
+
function build_handle_batch(logger) {
|
|
3114
3230
|
return async (lease, payloads, batchHandler) => {
|
|
3115
3231
|
const stream = lease.stream;
|
|
3116
|
-
const events = payloads.map(
|
|
3232
|
+
const events = payloads.map(
|
|
3233
|
+
(p) => p.event
|
|
3234
|
+
);
|
|
3117
3235
|
const options = payloads[0].options;
|
|
3118
3236
|
if (lease.retry > 0)
|
|
3119
3237
|
logger.warn(`Retrying batch ${stream}@${events[0].id} (${lease.retry}).`);
|
|
@@ -3140,23 +3258,23 @@ var SettleLoop = class {
|
|
|
3140
3258
|
_deps;
|
|
3141
3259
|
/** Debounce window applied when the caller doesn't override via `SettleOptions.debounceMs`. */
|
|
3142
3260
|
_default_debounce_ms;
|
|
3143
|
-
constructor(deps,
|
|
3261
|
+
constructor(deps, default_debounce_ms) {
|
|
3144
3262
|
this._deps = deps;
|
|
3145
|
-
this._default_debounce_ms =
|
|
3263
|
+
this._default_debounce_ms = default_debounce_ms;
|
|
3146
3264
|
}
|
|
3147
3265
|
/**
|
|
3148
3266
|
* Schedule a settle pass. Multiple calls inside the debounce window
|
|
3149
3267
|
* coalesce into one cycle. The cycle runs correlate→drain in a loop
|
|
3150
3268
|
* until no progress is made (no new subscriptions, no acks, no blocks)
|
|
3151
3269
|
* or `maxPasses` is reached, then emits the `"settled"` lifecycle event
|
|
3152
|
-
* via {@link SettleDeps.
|
|
3270
|
+
* via {@link SettleDeps.on_settled}.
|
|
3153
3271
|
*/
|
|
3154
3272
|
schedule(options = {}) {
|
|
3155
3273
|
const {
|
|
3156
3274
|
debounceMs = this._default_debounce_ms,
|
|
3157
|
-
correlate:
|
|
3275
|
+
correlate: correlate_query = { after: -1, limit: 100 },
|
|
3158
3276
|
maxPasses = Infinity,
|
|
3159
|
-
...
|
|
3277
|
+
...drain_options
|
|
3160
3278
|
} = options;
|
|
3161
3279
|
if (this._timer) clearTimeout(this._timer);
|
|
3162
3280
|
this._timer = setTimeout(() => {
|
|
@@ -3165,17 +3283,17 @@ var SettleLoop = class {
|
|
|
3165
3283
|
this._running = true;
|
|
3166
3284
|
(async () => {
|
|
3167
3285
|
await this._deps.init();
|
|
3168
|
-
let
|
|
3286
|
+
let last_drain;
|
|
3169
3287
|
for (let i = 0; i < maxPasses; i++) {
|
|
3170
3288
|
const { subscribed } = await this._deps.correlate({
|
|
3171
|
-
...
|
|
3289
|
+
...correlate_query,
|
|
3172
3290
|
after: this._deps.checkpoint()
|
|
3173
3291
|
});
|
|
3174
|
-
|
|
3175
|
-
const made_progress = subscribed > 0 ||
|
|
3292
|
+
last_drain = await this._deps.drain(drain_options);
|
|
3293
|
+
const made_progress = subscribed > 0 || last_drain.acked.length > 0 || last_drain.blocked.length > 0;
|
|
3176
3294
|
if (!made_progress) break;
|
|
3177
3295
|
}
|
|
3178
|
-
if (
|
|
3296
|
+
if (last_drain) this._deps.on_settled(last_drain);
|
|
3179
3297
|
})().catch((err) => this._deps.logger.error(err)).finally(() => {
|
|
3180
3298
|
this._running = false;
|
|
3181
3299
|
});
|
|
@@ -3262,7 +3380,7 @@ var Act = class {
|
|
|
3262
3380
|
_event_to_state;
|
|
3263
3381
|
/**
|
|
3264
3382
|
* Event-name → lane fan-in for selective arming (ACT-1103). Built by
|
|
3265
|
-
* `
|
|
3383
|
+
* `classify_registry` once per build. `"all"` means at least one of
|
|
3266
3384
|
* the event's reactions is a dynamic resolver (lane opaque until
|
|
3267
3385
|
* runtime); a `Set<string>` lists the static lanes only that event's
|
|
3268
3386
|
* reactions target.
|
|
@@ -3285,9 +3403,9 @@ var Act = class {
|
|
|
3285
3403
|
_scoped;
|
|
3286
3404
|
/**
|
|
3287
3405
|
* Correlation-id generator for originating actions. Bound at
|
|
3288
|
-
* construction from `options.correlator ??
|
|
3406
|
+
* construction from `options.correlator ?? default_correlator`. The
|
|
3289
3407
|
* `do()` path passes this into the `_es.action` closure; close-cycle
|
|
3290
|
-
* uses it via {@link
|
|
3408
|
+
* uses it via {@link close_correlation}.
|
|
3291
3409
|
*/
|
|
3292
3410
|
_correlator;
|
|
3293
3411
|
/** Pre-bound IAct methods reused across drain cycles. Only `do` varies per
|
|
@@ -3296,7 +3414,8 @@ var Act = class {
|
|
|
3296
3414
|
_bound_load = this.load.bind(this);
|
|
3297
3415
|
_bound_query = this.query.bind(this);
|
|
3298
3416
|
_bound_query_array = this.query_array.bind(this);
|
|
3299
|
-
|
|
3417
|
+
_bound_forget = this.forget.bind(this);
|
|
3418
|
+
/** Reaction dispatchers built once and handed to run_drain_cycle each cycle. */
|
|
3300
3419
|
_handle;
|
|
3301
3420
|
_handle_batch;
|
|
3302
3421
|
/** Declared drain lanes (ACT-1103). */
|
|
@@ -3313,16 +3432,16 @@ var Act = class {
|
|
|
3313
3432
|
*
|
|
3314
3433
|
* @param registry Schemas for every event and action across registered states
|
|
3315
3434
|
* @param states Merged map of state name → state definition
|
|
3316
|
-
* @param
|
|
3435
|
+
* @param batch_handlers Static-target projection batch handlers (target → handler)
|
|
3317
3436
|
* @param options Tuning knobs — see {@link ActOptions}
|
|
3318
3437
|
* @param lanes Declared drain lanes (ACT-1103). The builder collects
|
|
3319
3438
|
* these from `.withLane(...)` calls. Slice 1 records them on the
|
|
3320
3439
|
* instance; later slices fan out one `DrainController` per lane.
|
|
3321
3440
|
*/
|
|
3322
|
-
constructor(registry, states = /* @__PURE__ */ new Map(),
|
|
3441
|
+
constructor(registry, states = /* @__PURE__ */ new Map(), batch_handlers = /* @__PURE__ */ new Map(), options = {}, lanes = []) {
|
|
3323
3442
|
this.registry = registry;
|
|
3324
3443
|
this._states = states;
|
|
3325
|
-
this._batch_handlers =
|
|
3444
|
+
this._batch_handlers = batch_handlers;
|
|
3326
3445
|
this._lanes = lanes;
|
|
3327
3446
|
if (options.onlyLanes && options.onlyLanes.length > 0) {
|
|
3328
3447
|
const declared = /* @__PURE__ */ new Set([
|
|
@@ -3336,49 +3455,50 @@ var Act = class {
|
|
|
3336
3455
|
);
|
|
3337
3456
|
}
|
|
3338
3457
|
this._scoped = options.scoped ? (fn) => scoped.run(options.scoped, fn) : (fn) => fn();
|
|
3339
|
-
this._correlator = options.correlator ??
|
|
3340
|
-
this._es =
|
|
3341
|
-
this._cd =
|
|
3342
|
-
this._handle =
|
|
3458
|
+
this._correlator = options.correlator ?? default_correlator;
|
|
3459
|
+
this._es = build_es(this._logger, this._correlator);
|
|
3460
|
+
this._cd = build_drain(this._logger);
|
|
3461
|
+
this._handle = build_handle({
|
|
3343
3462
|
logger: this._logger,
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3463
|
+
bound_do: this._bound_do,
|
|
3464
|
+
bound_load: this._bound_load,
|
|
3465
|
+
bound_query: this._bound_query,
|
|
3466
|
+
bound_query_array: this._bound_query_array,
|
|
3467
|
+
bound_forget: this._bound_forget
|
|
3348
3468
|
});
|
|
3349
|
-
this._handle_batch =
|
|
3469
|
+
this._handle_batch = build_handle_batch(this._logger);
|
|
3350
3470
|
const {
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
} =
|
|
3357
|
-
this._reactive_events =
|
|
3471
|
+
static_targets,
|
|
3472
|
+
has_dynamic_resolvers,
|
|
3473
|
+
reactive_events,
|
|
3474
|
+
event_to_state,
|
|
3475
|
+
event_to_lanes
|
|
3476
|
+
} = classify_registry(this.registry, this._states);
|
|
3477
|
+
this._reactive_events = reactive_events;
|
|
3358
3478
|
this._listen = options.listen !== false;
|
|
3359
3479
|
this._drain = options.drain !== false;
|
|
3360
|
-
this._event_to_state =
|
|
3361
|
-
this._event_to_lanes =
|
|
3362
|
-
const
|
|
3363
|
-
const
|
|
3364
|
-
const
|
|
3365
|
-
const
|
|
3480
|
+
this._event_to_state = event_to_state;
|
|
3481
|
+
this._event_to_lanes = event_to_lanes;
|
|
3482
|
+
const all_lanes = ["default", ...lanes.map((l) => l.name)];
|
|
3483
|
+
const only_set = options.onlyLanes && options.onlyLanes.length > 0 ? new Set(options.onlyLanes) : void 0;
|
|
3484
|
+
const active_lanes = only_set ? all_lanes.filter((n) => only_set.has(n)) : all_lanes;
|
|
3485
|
+
const single_default_lane = active_lanes.length === 1 && active_lanes[0] === "default";
|
|
3366
3486
|
this._drain_controllers = /* @__PURE__ */ new Map();
|
|
3367
|
-
for (const name of
|
|
3487
|
+
for (const name of active_lanes) {
|
|
3368
3488
|
const cfg = lanes.find((l) => l.name === name);
|
|
3369
3489
|
const controller = new DrainController({
|
|
3370
3490
|
logger: this._logger,
|
|
3371
3491
|
ops: this._cd,
|
|
3372
3492
|
registry: this.registry,
|
|
3373
|
-
|
|
3493
|
+
batch_handlers: this._batch_handlers,
|
|
3374
3494
|
handle: this._handle,
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3495
|
+
handle_batch: this._handle_batch,
|
|
3496
|
+
on_acked: (acked) => this.emit("acked", acked),
|
|
3497
|
+
on_blocked: (blocked) => this.emit("blocked", blocked),
|
|
3378
3498
|
// Pass lane only when a true per-lane controller is active.
|
|
3379
3499
|
// The all-lanes (single default) case keeps lane=undefined so
|
|
3380
3500
|
// adapter SQL collapses to the pre-1103 shape.
|
|
3381
|
-
lane:
|
|
3501
|
+
lane: single_default_lane ? void 0 : name,
|
|
3382
3502
|
defaults: cfg && {
|
|
3383
3503
|
streamLimit: cfg.streamLimit,
|
|
3384
3504
|
leaseMillis: cfg.leaseMillis
|
|
@@ -3391,22 +3511,22 @@ var Act = class {
|
|
|
3391
3511
|
this._audit_deps = {
|
|
3392
3512
|
store: store2,
|
|
3393
3513
|
logger: this._logger,
|
|
3394
|
-
event_to_state
|
|
3514
|
+
event_to_state,
|
|
3395
3515
|
states: this._states,
|
|
3396
|
-
known_events: new Set(
|
|
3516
|
+
known_events: new Set(event_to_state.keys()),
|
|
3397
3517
|
declared_lanes: new Set(this._drain_controllers.keys()),
|
|
3398
|
-
routed_events: new Set(
|
|
3518
|
+
routed_events: new Set(event_to_lanes.keys())
|
|
3399
3519
|
};
|
|
3400
3520
|
this._correlate = new CorrelateCycle(
|
|
3401
3521
|
this.registry,
|
|
3402
|
-
|
|
3403
|
-
|
|
3522
|
+
static_targets,
|
|
3523
|
+
has_dynamic_resolvers,
|
|
3404
3524
|
this._cd,
|
|
3405
3525
|
options.maxSubscribedStreams ?? DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
3406
3526
|
// Cold start: assume drain is needed (historical events may need processing).
|
|
3407
3527
|
// #803: writer-only instances skip the cold-start arm.
|
|
3408
3528
|
() => {
|
|
3409
|
-
if (this._drain && this._reactive_events.size > 0) this.
|
|
3529
|
+
if (this._drain && this._reactive_events.size > 0) this._arm_all();
|
|
3410
3530
|
}
|
|
3411
3531
|
);
|
|
3412
3532
|
this._settle = new SettleLoop(
|
|
@@ -3416,11 +3536,11 @@ var Act = class {
|
|
|
3416
3536
|
checkpoint: () => this._correlate.checkpoint,
|
|
3417
3537
|
correlate: (q) => this.correlate(q),
|
|
3418
3538
|
drain: (o) => this.drain(o),
|
|
3419
|
-
|
|
3539
|
+
on_settled: (drain) => this.emit("settled", drain)
|
|
3420
3540
|
},
|
|
3421
3541
|
options.settleDebounceMs ?? DEFAULT_SETTLE_DEBOUNCE_MS
|
|
3422
3542
|
);
|
|
3423
|
-
this._notify_disposer = this.
|
|
3543
|
+
this._notify_disposer = this._wire_notify(options.scoped?.store ?? store2());
|
|
3424
3544
|
dispose(() => this.shutdown());
|
|
3425
3545
|
}
|
|
3426
3546
|
/** True after the first `shutdown()` call. Guards idempotency. */
|
|
@@ -3454,7 +3574,7 @@ var Act = class {
|
|
|
3454
3574
|
* subscription was made). Errors during subscription are logged but
|
|
3455
3575
|
* never thrown — `notify` is a hint, not a contract.
|
|
3456
3576
|
*/
|
|
3457
|
-
async
|
|
3577
|
+
async _wire_notify(s) {
|
|
3458
3578
|
if (this._reactive_events.size === 0) return void 0;
|
|
3459
3579
|
if (!s.notify) return void 0;
|
|
3460
3580
|
if (!this._listen) return void 0;
|
|
@@ -3463,7 +3583,7 @@ var Act = class {
|
|
|
3463
3583
|
try {
|
|
3464
3584
|
this.emit("notified", notification);
|
|
3465
3585
|
if (this._drain) {
|
|
3466
|
-
const armed = this.
|
|
3586
|
+
const armed = this._arm_for_event_names(
|
|
3467
3587
|
notification.events.map((e) => e.name)
|
|
3468
3588
|
);
|
|
3469
3589
|
if (armed) this._settle.schedule({ debounceMs: 0 });
|
|
@@ -3569,14 +3689,14 @@ var Act = class {
|
|
|
3569
3689
|
skipValidation
|
|
3570
3690
|
);
|
|
3571
3691
|
if (this._reactive_events.size > 0)
|
|
3572
|
-
this.
|
|
3692
|
+
this._arm_for_event_names(
|
|
3573
3693
|
snapshots.map((s) => s.event.name)
|
|
3574
3694
|
);
|
|
3575
3695
|
this.emit("committed", snapshots);
|
|
3576
3696
|
return snapshots;
|
|
3577
3697
|
});
|
|
3578
3698
|
}
|
|
3579
|
-
async load(stateOrName, stream, callback, asOf) {
|
|
3699
|
+
async load(stateOrName, stream, callback, asOf, actor) {
|
|
3580
3700
|
return this._scoped(async () => {
|
|
3581
3701
|
let merged;
|
|
3582
3702
|
if (typeof stateOrName === "string") {
|
|
@@ -3586,7 +3706,7 @@ var Act = class {
|
|
|
3586
3706
|
} else {
|
|
3587
3707
|
merged = this._states.get(stateOrName.name) || stateOrName;
|
|
3588
3708
|
}
|
|
3589
|
-
return await this._es.load(merged, stream, callback, asOf);
|
|
3709
|
+
return await this._es.load(merged, stream, callback, asOf, actor);
|
|
3590
3710
|
});
|
|
3591
3711
|
}
|
|
3592
3712
|
/**
|
|
@@ -3680,6 +3800,34 @@ var Act = class {
|
|
|
3680
3800
|
return events;
|
|
3681
3801
|
});
|
|
3682
3802
|
}
|
|
3803
|
+
/**
|
|
3804
|
+
* Wipe the sensitive-data payload for every event on the stream — see
|
|
3805
|
+
* {@link IAct.forget}. Application-level half of #566.
|
|
3806
|
+
*
|
|
3807
|
+
* Throws on adapters without `Store.forget_pii`, invalidates the cache
|
|
3808
|
+
* entry for the stream, emits the `forgotten` lifecycle event with the
|
|
3809
|
+
* row count. Idempotent: a second call returns `{eventCount: 0}` and
|
|
3810
|
+
* does NOT re-emit.
|
|
3811
|
+
*
|
|
3812
|
+
* @param stream - Target stream.
|
|
3813
|
+
* @returns `{eventCount}` — number of events whose PII column was wiped.
|
|
3814
|
+
*/
|
|
3815
|
+
async forget(stream) {
|
|
3816
|
+
return this._scoped(async () => {
|
|
3817
|
+
const s = store2();
|
|
3818
|
+
if (!s.forget_pii) {
|
|
3819
|
+
throw new Error(
|
|
3820
|
+
`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).`
|
|
3821
|
+
);
|
|
3822
|
+
}
|
|
3823
|
+
const eventCount = await s.forget_pii(stream);
|
|
3824
|
+
await cache2().invalidate(stream);
|
|
3825
|
+
if (eventCount > 0) {
|
|
3826
|
+
this.emit("forgotten", { stream, at: /* @__PURE__ */ new Date(), eventCount });
|
|
3827
|
+
}
|
|
3828
|
+
return { eventCount };
|
|
3829
|
+
});
|
|
3830
|
+
}
|
|
3683
3831
|
/**
|
|
3684
3832
|
* Processes pending reactions by draining uncommitted events from the event store.
|
|
3685
3833
|
*
|
|
@@ -3720,28 +3868,28 @@ var Act = class {
|
|
|
3720
3868
|
async drain(options = {}) {
|
|
3721
3869
|
if (!this._drain)
|
|
3722
3870
|
return { fetched: [], leased: [], acked: [], blocked: [] };
|
|
3723
|
-
return this._scoped(() => this.
|
|
3871
|
+
return this._scoped(() => this._drain_all(options));
|
|
3724
3872
|
}
|
|
3725
3873
|
/** Arm every active lane controller (ACT-1103). */
|
|
3726
|
-
|
|
3874
|
+
_arm_all() {
|
|
3727
3875
|
for (const c of this._drain_controllers.values()) c.arm();
|
|
3728
3876
|
}
|
|
3729
3877
|
/**
|
|
3730
3878
|
* Arm only the lane controllers whose reactions match the supplied
|
|
3731
3879
|
* event names (ACT-1103 selective arming). Events with any dynamic
|
|
3732
|
-
* resolver fall back to `
|
|
3880
|
+
* resolver fall back to `_arm_all()` via the `"all"` sentinel — the
|
|
3733
3881
|
* resolver's lane isn't known until correlate runs the function.
|
|
3734
3882
|
* Events with no reactions are skipped; `_event_to_lanes` doesn't
|
|
3735
3883
|
* carry them. Returns true when any controller was armed (used by
|
|
3736
3884
|
* the notify handler to decide whether to schedule a settle).
|
|
3737
3885
|
*/
|
|
3738
|
-
|
|
3886
|
+
_arm_for_event_names(names) {
|
|
3739
3887
|
const to_arm = /* @__PURE__ */ new Set();
|
|
3740
3888
|
for (const name of names) {
|
|
3741
3889
|
const set = this._event_to_lanes.get(name);
|
|
3742
3890
|
if (set === void 0) continue;
|
|
3743
3891
|
if (set === ALL_LANES) {
|
|
3744
|
-
this.
|
|
3892
|
+
this._arm_all();
|
|
3745
3893
|
return true;
|
|
3746
3894
|
}
|
|
3747
3895
|
for (const lane of set) to_arm.add(lane);
|
|
@@ -3758,7 +3906,7 @@ var Act = class {
|
|
|
3758
3906
|
* `SKIP LOCKED` keeps cross-controller races safe. Lifecycle events
|
|
3759
3907
|
* (`acked`, `blocked`) may interleave by lane — listeners filter via
|
|
3760
3908
|
* `lease.lane`. */
|
|
3761
|
-
async
|
|
3909
|
+
async _drain_all(options) {
|
|
3762
3910
|
const results = await Promise.all(
|
|
3763
3911
|
[...this._drain_controllers.values()].map((c) => c.drain(options))
|
|
3764
3912
|
);
|
|
@@ -3878,7 +4026,7 @@ var Act = class {
|
|
|
3878
4026
|
* @see {@link stop_correlations} to stop the worker
|
|
3879
4027
|
*/
|
|
3880
4028
|
start_correlations(query = {}, frequency = 1e4, callback) {
|
|
3881
|
-
return this._correlate.
|
|
4029
|
+
return this._correlate.start_polling(query, frequency, callback);
|
|
3882
4030
|
}
|
|
3883
4031
|
/**
|
|
3884
4032
|
* Stops the automatic correlation worker.
|
|
@@ -3898,7 +4046,7 @@ var Act = class {
|
|
|
3898
4046
|
* @see {@link start_correlations}
|
|
3899
4047
|
*/
|
|
3900
4048
|
stop_correlations() {
|
|
3901
|
-
this._correlate.
|
|
4049
|
+
this._correlate.stop_polling();
|
|
3902
4050
|
}
|
|
3903
4051
|
/**
|
|
3904
4052
|
* Cancels any pending or active settle cycle.
|
|
@@ -3944,7 +4092,7 @@ var Act = class {
|
|
|
3944
4092
|
async reset(input) {
|
|
3945
4093
|
return this._scoped(async () => {
|
|
3946
4094
|
const count = await store2().reset(input);
|
|
3947
|
-
if (count > 0 && this._reactive_events.size > 0) this.
|
|
4095
|
+
if (count > 0 && this._reactive_events.size > 0) this._arm_all();
|
|
3948
4096
|
return count;
|
|
3949
4097
|
});
|
|
3950
4098
|
}
|
|
@@ -3978,7 +4126,7 @@ var Act = class {
|
|
|
3978
4126
|
async unblock(input) {
|
|
3979
4127
|
return this._scoped(async () => {
|
|
3980
4128
|
const count = await store2().unblock(input);
|
|
3981
|
-
if (count > 0 && this._reactive_events.size > 0) this.
|
|
4129
|
+
if (count > 0 && this._reactive_events.size > 0) this._arm_all();
|
|
3982
4130
|
return count;
|
|
3983
4131
|
});
|
|
3984
4132
|
}
|
|
@@ -4204,14 +4352,14 @@ var Act = class {
|
|
|
4204
4352
|
if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
|
|
4205
4353
|
return this._scoped(async () => {
|
|
4206
4354
|
await this.correlate({ limit: 1e3 });
|
|
4207
|
-
const
|
|
4208
|
-
const result = await
|
|
4209
|
-
|
|
4210
|
-
|
|
4355
|
+
const close_actor = { id: "$close", name: "close" };
|
|
4356
|
+
const result = await run_close_cycle(targets, {
|
|
4357
|
+
reactive_events_size: this._reactive_events.size,
|
|
4358
|
+
event_to_state: this._event_to_state,
|
|
4211
4359
|
load: this._es.load,
|
|
4212
4360
|
tombstone: this._es.tombstone,
|
|
4213
4361
|
logger: this._logger,
|
|
4214
|
-
correlation:
|
|
4362
|
+
correlation: close_correlation(this._correlator, close_actor)
|
|
4215
4363
|
});
|
|
4216
4364
|
this.emit("closed", result);
|
|
4217
4365
|
return result;
|
|
@@ -4254,17 +4402,17 @@ var Act = class {
|
|
|
4254
4402
|
};
|
|
4255
4403
|
|
|
4256
4404
|
// src/builders/act-builder.ts
|
|
4257
|
-
function
|
|
4405
|
+
function register_batch_handler(proj, batch_handlers) {
|
|
4258
4406
|
if (!proj.batchHandler || !proj.target) return;
|
|
4259
|
-
const existing =
|
|
4407
|
+
const existing = batch_handlers.get(proj.target);
|
|
4260
4408
|
if (existing && existing !== proj.batchHandler) {
|
|
4261
4409
|
throw new Error(`Duplicate batch handler for target "${proj.target}"`);
|
|
4262
4410
|
}
|
|
4263
|
-
|
|
4411
|
+
batch_handlers.set(proj.target, proj.batchHandler);
|
|
4264
4412
|
}
|
|
4265
|
-
function
|
|
4413
|
+
function validate_lane_references(registry, lanes) {
|
|
4266
4414
|
const declared = /* @__PURE__ */ new Set([DEFAULT_LANE, ...lanes.map((l) => l.name)]);
|
|
4267
|
-
for (const [
|
|
4415
|
+
for (const [event_name, def] of Object.entries(registry.events)) {
|
|
4268
4416
|
const entry = def;
|
|
4269
4417
|
for (const [handlerName, reaction] of entry.reactions) {
|
|
4270
4418
|
const resolver = reaction.resolver;
|
|
@@ -4272,7 +4420,7 @@ function validateLaneReferences(registry, lanes) {
|
|
|
4272
4420
|
const lane = resolver.lane;
|
|
4273
4421
|
if (lane && !declared.has(lane)) {
|
|
4274
4422
|
throw new Error(
|
|
4275
|
-
`Reaction "${handlerName}" on "${
|
|
4423
|
+
`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.`
|
|
4276
4424
|
);
|
|
4277
4425
|
}
|
|
4278
4426
|
}
|
|
@@ -4280,76 +4428,80 @@ function validateLaneReferences(registry, lanes) {
|
|
|
4280
4428
|
}
|
|
4281
4429
|
function act() {
|
|
4282
4430
|
const states = /* @__PURE__ */ new Map();
|
|
4431
|
+
const _sf = /* @__PURE__ */ new Map();
|
|
4432
|
+
const _dp = /* @__PURE__ */ new Map();
|
|
4283
4433
|
const registry = {
|
|
4284
4434
|
actions: {},
|
|
4285
|
-
events: {}
|
|
4435
|
+
events: {},
|
|
4436
|
+
sensitive_fields: (event_name) => _sf.get(event_name) ?? [],
|
|
4437
|
+
disclosure_predicate: (state_name) => _dp.get(state_name) ?? null
|
|
4286
4438
|
};
|
|
4287
|
-
const
|
|
4288
|
-
const
|
|
4439
|
+
const pending_projections = [];
|
|
4440
|
+
const batch_handlers = /* @__PURE__ */ new Map();
|
|
4289
4441
|
const lanes = [];
|
|
4290
4442
|
let _built = false;
|
|
4291
|
-
const
|
|
4292
|
-
const
|
|
4443
|
+
const finalize_deprecations = () => {
|
|
4444
|
+
const deprecation_summary = [];
|
|
4293
4445
|
for (const state2 of states.values()) {
|
|
4294
|
-
const
|
|
4295
|
-
const deprecated =
|
|
4446
|
+
const event_names = Object.keys(state2.events);
|
|
4447
|
+
const deprecated = deprecated_event_names(event_names);
|
|
4296
4448
|
if (deprecated.size === 0) continue;
|
|
4297
4449
|
state2._deprecated = deprecated;
|
|
4298
4450
|
for (const name of deprecated) {
|
|
4299
|
-
const current =
|
|
4300
|
-
|
|
4301
|
-
|
|
4451
|
+
const current = current_version_of(name, event_names);
|
|
4452
|
+
deprecation_summary.push({
|
|
4453
|
+
state_name: state2.name,
|
|
4302
4454
|
deprecated: name,
|
|
4303
4455
|
current
|
|
4304
4456
|
});
|
|
4305
4457
|
}
|
|
4306
|
-
for (const [
|
|
4307
|
-
const
|
|
4308
|
-
if (
|
|
4309
|
-
const current =
|
|
4458
|
+
for (const [action_name, handler] of Object.entries(state2.on)) {
|
|
4459
|
+
const static_target = handler?._static_emit;
|
|
4460
|
+
if (static_target && deprecated.has(static_target)) {
|
|
4461
|
+
const current = current_version_of(static_target, event_names);
|
|
4310
4462
|
throw new Error(
|
|
4311
|
-
`Action "${
|
|
4463
|
+
`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.`
|
|
4312
4464
|
);
|
|
4313
4465
|
}
|
|
4314
4466
|
}
|
|
4315
4467
|
}
|
|
4316
|
-
if (
|
|
4317
|
-
const list =
|
|
4318
|
-
(d) => `"${d.deprecated}" (current: "${d.current}", state: "${d.
|
|
4468
|
+
if (deprecation_summary.length > 0) {
|
|
4469
|
+
const list = deprecation_summary.map(
|
|
4470
|
+
(d) => `"${d.deprecated}" (current: "${d.current}", state: "${d.state_name}")`
|
|
4319
4471
|
).join(", ");
|
|
4320
4472
|
log().info(
|
|
4321
|
-
`Act registered ${
|
|
4473
|
+
`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.`
|
|
4322
4474
|
);
|
|
4323
4475
|
}
|
|
4324
4476
|
};
|
|
4325
4477
|
const builder = {
|
|
4326
4478
|
withState: (state2) => {
|
|
4327
|
-
|
|
4479
|
+
register_state(state2, states, registry.actions, registry.events);
|
|
4328
4480
|
return builder;
|
|
4329
4481
|
},
|
|
4330
4482
|
withSlice: (input) => {
|
|
4331
4483
|
for (const s of input.states.values()) {
|
|
4332
|
-
|
|
4484
|
+
register_state(s, states, registry.actions, registry.events);
|
|
4333
4485
|
}
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
for (const
|
|
4337
|
-
const existing = lanes.find((l) => l.name ===
|
|
4486
|
+
merge_event_register(registry.events, input.events);
|
|
4487
|
+
pending_projections.push(...input.projections);
|
|
4488
|
+
for (const slice_lane of input.lanes) {
|
|
4489
|
+
const existing = lanes.find((l) => l.name === slice_lane.name);
|
|
4338
4490
|
if (!existing) {
|
|
4339
|
-
lanes.push(
|
|
4491
|
+
lanes.push(slice_lane);
|
|
4340
4492
|
continue;
|
|
4341
4493
|
}
|
|
4342
|
-
if (existing.leaseMillis !==
|
|
4494
|
+
if (existing.leaseMillis !== slice_lane.leaseMillis || existing.streamLimit !== slice_lane.streamLimit || existing.cycleMs !== slice_lane.cycleMs) {
|
|
4343
4495
|
throw new Error(
|
|
4344
|
-
`Lane "${
|
|
4496
|
+
`Lane "${slice_lane.name}" was already declared with a different config`
|
|
4345
4497
|
);
|
|
4346
4498
|
}
|
|
4347
4499
|
}
|
|
4348
4500
|
return builder;
|
|
4349
4501
|
},
|
|
4350
4502
|
withProjection: (proj) => {
|
|
4351
|
-
|
|
4352
|
-
|
|
4503
|
+
merge_projection(proj, registry.events);
|
|
4504
|
+
register_batch_handler(proj, batch_handlers);
|
|
4353
4505
|
return builder;
|
|
4354
4506
|
},
|
|
4355
4507
|
withActor: () => builder,
|
|
@@ -4387,18 +4539,70 @@ function act() {
|
|
|
4387
4539
|
}),
|
|
4388
4540
|
build: (options) => {
|
|
4389
4541
|
if (!_built) {
|
|
4390
|
-
for (const proj of
|
|
4391
|
-
|
|
4392
|
-
|
|
4542
|
+
for (const proj of pending_projections) {
|
|
4543
|
+
merge_projection(proj, registry.events);
|
|
4544
|
+
register_batch_handler(proj, batch_handlers);
|
|
4545
|
+
}
|
|
4546
|
+
finalize_deprecations();
|
|
4547
|
+
validate_lane_references(registry, lanes);
|
|
4548
|
+
for (const [event_name, reg] of Object.entries(
|
|
4549
|
+
registry.events
|
|
4550
|
+
)) {
|
|
4551
|
+
const fields = pii_fields(reg.schema);
|
|
4552
|
+
if (fields.length === 0) continue;
|
|
4553
|
+
_sf.set(event_name, fields);
|
|
4554
|
+
for (const [name, reaction] of reg.reactions) {
|
|
4555
|
+
const inner = reaction.handler;
|
|
4556
|
+
const wrapped = (event, stream, app) => inner(pii_strip(event, fields), stream, app);
|
|
4557
|
+
Object.defineProperty(wrapped, "name", { value: inner.name });
|
|
4558
|
+
reaction.handler = wrapped;
|
|
4559
|
+
reg.reactions.set(name, reaction);
|
|
4560
|
+
}
|
|
4561
|
+
}
|
|
4562
|
+
for (const state2 of states.values()) {
|
|
4563
|
+
if (state2.disclose) _dp.set(state2.name, state2.disclose);
|
|
4564
|
+
const fields_by_event = /* @__PURE__ */ new Map();
|
|
4565
|
+
for (const event_name of Object.keys(state2.events)) {
|
|
4566
|
+
const fields = _sf.get(event_name);
|
|
4567
|
+
if (fields) fields_by_event.set(event_name, fields);
|
|
4568
|
+
}
|
|
4569
|
+
if (fields_by_event.size === 0) continue;
|
|
4570
|
+
if (state2.snap) {
|
|
4571
|
+
const offending = [...fields_by_event.keys()];
|
|
4572
|
+
throw new Error(
|
|
4573
|
+
`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.`
|
|
4574
|
+
);
|
|
4575
|
+
}
|
|
4576
|
+
const disclose = state2.disclose ?? null;
|
|
4577
|
+
state2.view = (event, actor) => {
|
|
4578
|
+
const fields = fields_by_event.get(event.name);
|
|
4579
|
+
return fields ? pii_gate(event, fields, disclose, actor) : event;
|
|
4580
|
+
};
|
|
4581
|
+
state2.message = (validated) => {
|
|
4582
|
+
const fields = fields_by_event.get(validated.name);
|
|
4583
|
+
return fields ? pii_split(validated, fields) : validated;
|
|
4584
|
+
};
|
|
4585
|
+
for (const [event_name, fields] of fields_by_event) {
|
|
4586
|
+
const original = state2.patch[event_name];
|
|
4587
|
+
state2.patch[event_name] = (event, s) => original(pii_merge(event, fields), s);
|
|
4588
|
+
}
|
|
4589
|
+
}
|
|
4590
|
+
for (const [target, original] of batch_handlers) {
|
|
4591
|
+
const wrapped = async (events, stream) => {
|
|
4592
|
+
const stripped = events.map((e) => {
|
|
4593
|
+
const f = _sf.get(e.name);
|
|
4594
|
+
return f ? pii_strip(e, f) : e;
|
|
4595
|
+
});
|
|
4596
|
+
return original(stripped, stream);
|
|
4597
|
+
};
|
|
4598
|
+
batch_handlers.set(target, wrapped);
|
|
4393
4599
|
}
|
|
4394
|
-
finalizeDeprecations();
|
|
4395
|
-
validateLaneReferences(registry, lanes);
|
|
4396
4600
|
_built = true;
|
|
4397
4601
|
}
|
|
4398
4602
|
return new Act(
|
|
4399
4603
|
registry,
|
|
4400
4604
|
states,
|
|
4401
|
-
|
|
4605
|
+
batch_handlers,
|
|
4402
4606
|
options,
|
|
4403
4607
|
lanes
|
|
4404
4608
|
);
|
|
@@ -4411,7 +4615,7 @@ function act() {
|
|
|
4411
4615
|
// src/builders/projection-builder.ts
|
|
4412
4616
|
function _projection(target) {
|
|
4413
4617
|
const events = {};
|
|
4414
|
-
const
|
|
4618
|
+
const default_resolver = typeof target === "string" ? { target } : void 0;
|
|
4415
4619
|
const base = {
|
|
4416
4620
|
on: (entry) => {
|
|
4417
4621
|
const keys = Object.keys(entry);
|
|
@@ -4428,7 +4632,7 @@ function _projection(target) {
|
|
|
4428
4632
|
do: (handler) => {
|
|
4429
4633
|
const reaction = {
|
|
4430
4634
|
handler,
|
|
4431
|
-
resolver:
|
|
4635
|
+
resolver: default_resolver ?? _this_,
|
|
4432
4636
|
options: {
|
|
4433
4637
|
blockOnError: true,
|
|
4434
4638
|
maxRetries: 3
|
|
@@ -4484,7 +4688,7 @@ function slice() {
|
|
|
4484
4688
|
const lanes = [];
|
|
4485
4689
|
const builder = {
|
|
4486
4690
|
withState: (state2) => {
|
|
4487
|
-
|
|
4691
|
+
register_state(state2, states, actions, events);
|
|
4488
4692
|
return builder;
|
|
4489
4693
|
},
|
|
4490
4694
|
withProjection: (proj) => {
|
|
@@ -4540,12 +4744,12 @@ function state(entry) {
|
|
|
4540
4744
|
const keys = Object.keys(entry);
|
|
4541
4745
|
if (keys.length !== 1) throw new Error("state() requires exactly one key");
|
|
4542
4746
|
const name = keys[0];
|
|
4543
|
-
const
|
|
4747
|
+
const state_schema = entry[name];
|
|
4544
4748
|
return {
|
|
4545
4749
|
init(init) {
|
|
4546
4750
|
return {
|
|
4547
4751
|
emits(events) {
|
|
4548
|
-
const
|
|
4752
|
+
const default_patch = Object.fromEntries(
|
|
4549
4753
|
Object.keys(events).map((k) => {
|
|
4550
4754
|
const fn = Object.assign(({ data }) => data, {
|
|
4551
4755
|
_passthrough: true
|
|
@@ -4556,11 +4760,16 @@ function state(entry) {
|
|
|
4556
4760
|
const internal = {
|
|
4557
4761
|
events,
|
|
4558
4762
|
actions: {},
|
|
4559
|
-
state:
|
|
4763
|
+
state: state_schema,
|
|
4560
4764
|
name,
|
|
4561
4765
|
init,
|
|
4562
|
-
patch:
|
|
4563
|
-
on: {}
|
|
4766
|
+
patch: default_patch,
|
|
4767
|
+
on: {},
|
|
4768
|
+
// Step delegates initialized as identity. `act().build()`
|
|
4769
|
+
// overrides on states with `sensitive(...)` events to bake in
|
|
4770
|
+
// the gate / split.
|
|
4771
|
+
view: (event) => event,
|
|
4772
|
+
message: (validated) => validated
|
|
4564
4773
|
};
|
|
4565
4774
|
const builder = action_builder(internal);
|
|
4566
4775
|
return Object.assign(builder, {
|
|
@@ -4596,11 +4805,14 @@ function action_builder(state2) {
|
|
|
4596
4805
|
}
|
|
4597
4806
|
function emit(handler) {
|
|
4598
4807
|
if (typeof handler === "string") {
|
|
4599
|
-
const
|
|
4600
|
-
const
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4808
|
+
const event_name = handler;
|
|
4809
|
+
const emit_fn = Object.assign(
|
|
4810
|
+
(payload) => [event_name, payload],
|
|
4811
|
+
{
|
|
4812
|
+
_static_emit: event_name
|
|
4813
|
+
}
|
|
4814
|
+
);
|
|
4815
|
+
internal.on[action2] = emit_fn;
|
|
4604
4816
|
} else {
|
|
4605
4817
|
internal.on[action2] = handler;
|
|
4606
4818
|
}
|
|
@@ -4612,6 +4824,10 @@ function action_builder(state2) {
|
|
|
4612
4824
|
internal.snap = snap2;
|
|
4613
4825
|
return builder;
|
|
4614
4826
|
},
|
|
4827
|
+
discloses(disclose) {
|
|
4828
|
+
internal.disclose = disclose;
|
|
4829
|
+
return builder;
|
|
4830
|
+
},
|
|
4615
4831
|
build() {
|
|
4616
4832
|
return internal;
|
|
4617
4833
|
}
|
|
@@ -4649,7 +4865,7 @@ var CsvFile = class {
|
|
|
4649
4865
|
let header = null;
|
|
4650
4866
|
for await (const line of lines) {
|
|
4651
4867
|
if (!line.trim()) continue;
|
|
4652
|
-
const fields =
|
|
4868
|
+
const fields = parse_csv_line(line);
|
|
4653
4869
|
if (!header) {
|
|
4654
4870
|
header = fields;
|
|
4655
4871
|
const expected = CSV_COLUMNS.join(",");
|
|
@@ -4686,21 +4902,21 @@ var CsvFile = class {
|
|
|
4686
4902
|
flags: "w",
|
|
4687
4903
|
encoding: "utf8"
|
|
4688
4904
|
});
|
|
4689
|
-
let
|
|
4905
|
+
let next_id = 1;
|
|
4690
4906
|
try {
|
|
4691
|
-
await
|
|
4907
|
+
await write_line(writer, CSV_COLUMNS.join(","));
|
|
4692
4908
|
await driver(async (event) => {
|
|
4693
|
-
const id =
|
|
4909
|
+
const id = next_id++;
|
|
4694
4910
|
const row = [
|
|
4695
4911
|
String(id),
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4912
|
+
csv_escape(event.name),
|
|
4913
|
+
csv_escape(JSON.stringify(event.data)),
|
|
4914
|
+
csv_escape(event.stream),
|
|
4699
4915
|
String(event.version),
|
|
4700
4916
|
event.created.toISOString(),
|
|
4701
|
-
|
|
4917
|
+
csv_escape(JSON.stringify(event.meta))
|
|
4702
4918
|
].join(",");
|
|
4703
|
-
await
|
|
4919
|
+
await write_line(writer, row);
|
|
4704
4920
|
return id;
|
|
4705
4921
|
});
|
|
4706
4922
|
} finally {
|
|
@@ -4733,7 +4949,7 @@ async function* linesFromBlob(blob) {
|
|
|
4733
4949
|
await Promise.resolve();
|
|
4734
4950
|
}
|
|
4735
4951
|
}
|
|
4736
|
-
function
|
|
4952
|
+
function parse_csv_line(line) {
|
|
4737
4953
|
const fields = [];
|
|
4738
4954
|
let i = 0;
|
|
4739
4955
|
while (i < line.length) {
|
|
@@ -4766,11 +4982,11 @@ function parseCsvLine(line) {
|
|
|
4766
4982
|
}
|
|
4767
4983
|
return fields;
|
|
4768
4984
|
}
|
|
4769
|
-
function
|
|
4985
|
+
function csv_escape(value) {
|
|
4770
4986
|
if (/[",\n\r]/.test(value)) return `"${value.replace(/"/g, '""')}"`;
|
|
4771
4987
|
return value;
|
|
4772
4988
|
}
|
|
4773
|
-
function
|
|
4989
|
+
function write_line(writer, line) {
|
|
4774
4990
|
return new Promise((resolve, reject) => {
|
|
4775
4991
|
writer.write(`${line}
|
|
4776
4992
|
`, (err) => {
|
|
@@ -4802,6 +5018,8 @@ function writeLine(writer, line) {
|
|
|
4802
5018
|
NonRetryableError,
|
|
4803
5019
|
PackageSchema,
|
|
4804
5020
|
QuerySchema,
|
|
5021
|
+
REDACTED,
|
|
5022
|
+
SHREDDED,
|
|
4805
5023
|
SNAP_EVENT,
|
|
4806
5024
|
StreamClosedError,
|
|
4807
5025
|
TOMBSTONE_EVENT,
|
|
@@ -4818,6 +5036,7 @@ function writeLine(writer, line) {
|
|
|
4818
5036
|
port,
|
|
4819
5037
|
projection,
|
|
4820
5038
|
scoped,
|
|
5039
|
+
sensitive,
|
|
4821
5040
|
sleep,
|
|
4822
5041
|
slice,
|
|
4823
5042
|
state,
|