@rotorsoft/act 1.5.2 → 1.7.0
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.map +1 -1
- package/dist/@types/builders/state-builder.d.ts +23 -2
- package/dist/@types/builders/state-builder.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/event-sourcing.d.ts +11 -1
- package/dist/@types/internal/event-sourcing.d.ts.map +1 -1
- package/dist/@types/types/action.d.ts +137 -10
- package/dist/@types/types/action.d.ts.map +1 -1
- package/dist/@types/types/reaction.d.ts +12 -39
- package/dist/@types/types/reaction.d.ts.map +1 -1
- package/dist/index.cjs +194 -133
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +194 -133
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -2123,6 +2123,29 @@ var subscribe = (streams) => store2().subscribe(streams);
|
|
|
2123
2123
|
|
|
2124
2124
|
// src/internal/event-sourcing.ts
|
|
2125
2125
|
var import_act_patch = require("@rotorsoft/act-patch");
|
|
2126
|
+
|
|
2127
|
+
// src/internal/backoff.ts
|
|
2128
|
+
function computeBackoffDelay(retry, opts) {
|
|
2129
|
+
if (!opts || opts.baseMs <= 0) return 0;
|
|
2130
|
+
const r = Math.max(0, retry);
|
|
2131
|
+
let delay;
|
|
2132
|
+
switch (opts.strategy) {
|
|
2133
|
+
case "fixed":
|
|
2134
|
+
delay = opts.baseMs;
|
|
2135
|
+
break;
|
|
2136
|
+
case "linear":
|
|
2137
|
+
delay = opts.baseMs * (r + 1);
|
|
2138
|
+
break;
|
|
2139
|
+
case "exponential":
|
|
2140
|
+
delay = opts.baseMs * 2 ** r;
|
|
2141
|
+
if (opts.maxMs !== void 0) delay = Math.min(delay, opts.maxMs);
|
|
2142
|
+
break;
|
|
2143
|
+
}
|
|
2144
|
+
if (opts.jitter) delay = delay * (0.5 + Math.random());
|
|
2145
|
+
return Math.max(0, Math.floor(delay));
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
// src/internal/event-sourcing.ts
|
|
2126
2149
|
var DEFAULT_BATCH = 500;
|
|
2127
2150
|
async function snap(snapshot) {
|
|
2128
2151
|
try {
|
|
@@ -2162,13 +2185,30 @@ function is_valid(event) {
|
|
|
2162
2185
|
return true;
|
|
2163
2186
|
}
|
|
2164
2187
|
async function scan(source, opts = {}, callback) {
|
|
2165
|
-
const {
|
|
2188
|
+
const {
|
|
2189
|
+
drop_snapshots = false,
|
|
2190
|
+
drop_closed_streams = false,
|
|
2191
|
+
on_progress,
|
|
2192
|
+
event_migrations,
|
|
2193
|
+
stream_rename
|
|
2194
|
+
} = opts;
|
|
2166
2195
|
const limit = opts.batch_size ?? DEFAULT_BATCH;
|
|
2167
2196
|
const id_map = /* @__PURE__ */ new Map();
|
|
2168
2197
|
let kept = 0;
|
|
2169
2198
|
let dropped_snaps = 0;
|
|
2199
|
+
let dropped_closed = 0;
|
|
2200
|
+
let migrated_count = 0;
|
|
2170
2201
|
let processed = 0;
|
|
2171
2202
|
let at;
|
|
2203
|
+
const closed_streams = /* @__PURE__ */ new Set();
|
|
2204
|
+
if (drop_closed_streams) {
|
|
2205
|
+
await source.query(
|
|
2206
|
+
(e) => {
|
|
2207
|
+
if (e.name === TOMBSTONE_EVENT) closed_streams.add(e.stream);
|
|
2208
|
+
},
|
|
2209
|
+
{ names: [TOMBSTONE_EVENT] }
|
|
2210
|
+
);
|
|
2211
|
+
}
|
|
2172
2212
|
let max_id;
|
|
2173
2213
|
const probed = await source.query(
|
|
2174
2214
|
(e) => {
|
|
@@ -2192,12 +2232,35 @@ async function scan(source, opts = {}, callback) {
|
|
|
2192
2232
|
dropped_snaps++;
|
|
2193
2233
|
return;
|
|
2194
2234
|
}
|
|
2235
|
+
if (closed_streams.has(event.stream) && event.name !== TOMBSTONE_EVENT) {
|
|
2236
|
+
dropped_closed++;
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2239
|
+
let migrated = event;
|
|
2240
|
+
const migration = event_migrations?.[event.name];
|
|
2241
|
+
if (migration) {
|
|
2242
|
+
const old_data = migration.from_schema.parse(event.data);
|
|
2243
|
+
const new_data = migration.migrate(old_data);
|
|
2244
|
+
migration.to_schema.parse(new_data);
|
|
2245
|
+
migrated = {
|
|
2246
|
+
...event,
|
|
2247
|
+
name: migration.to,
|
|
2248
|
+
// biome-ignore lint/suspicious/noExplicitAny: migration target shape is caller-defined
|
|
2249
|
+
data: new_data
|
|
2250
|
+
};
|
|
2251
|
+
migrated_count++;
|
|
2252
|
+
}
|
|
2253
|
+
if (stream_rename) {
|
|
2254
|
+
const renamed = stream_rename(migrated.stream);
|
|
2255
|
+
if (renamed !== migrated.stream)
|
|
2256
|
+
migrated = { ...migrated, stream: renamed };
|
|
2257
|
+
}
|
|
2195
2258
|
if (!callback) {
|
|
2196
2259
|
kept++;
|
|
2197
2260
|
return;
|
|
2198
2261
|
}
|
|
2199
|
-
let remapped =
|
|
2200
|
-
const caused_by =
|
|
2262
|
+
let remapped = migrated;
|
|
2263
|
+
const caused_by = migrated.meta.causation.event?.id;
|
|
2201
2264
|
if (caused_by !== void 0) {
|
|
2202
2265
|
const new_caused_by = id_map.get(caused_by);
|
|
2203
2266
|
if (new_caused_by !== void 0 && new_caused_by !== caused_by) {
|
|
@@ -2224,10 +2287,10 @@ async function scan(source, opts = {}, callback) {
|
|
|
2224
2287
|
}
|
|
2225
2288
|
return {
|
|
2226
2289
|
kept,
|
|
2290
|
+
migrated: migrated_count,
|
|
2227
2291
|
dropped: {
|
|
2228
|
-
closed_streams:
|
|
2229
|
-
snapshots: dropped_snaps
|
|
2230
|
-
empty_streams: 0
|
|
2292
|
+
closed_streams: dropped_closed,
|
|
2293
|
+
snapshots: dropped_snaps
|
|
2231
2294
|
}
|
|
2232
2295
|
};
|
|
2233
2296
|
}
|
|
@@ -2290,113 +2353,126 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
2290
2353
|
const { stream, expectedVersion, actor } = target;
|
|
2291
2354
|
if (!stream) throw new Error("Missing target stream");
|
|
2292
2355
|
const validated = skipValidation ? payload : validate(action2, payload, me.actions[action2]);
|
|
2293
|
-
const
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
warned.
|
|
2324
|
-
|
|
2325
|
-
|
|
2356
|
+
const opts = me.options?.[action2];
|
|
2357
|
+
const maxRetries = opts?.maxRetries ?? 0;
|
|
2358
|
+
for (let attempt = 0; ; attempt++) {
|
|
2359
|
+
try {
|
|
2360
|
+
const snapshot = await load(me, stream);
|
|
2361
|
+
if (snapshot.event?.name === TOMBSTONE_EVENT)
|
|
2362
|
+
throw new StreamClosedError(stream);
|
|
2363
|
+
const expected = expectedVersion ?? snapshot.event?.version;
|
|
2364
|
+
if (me.given) {
|
|
2365
|
+
const invariants = me.given[action2] || [];
|
|
2366
|
+
invariants.forEach(({ valid, description }) => {
|
|
2367
|
+
if (!valid(snapshot.state, actor))
|
|
2368
|
+
throw new InvariantError(
|
|
2369
|
+
action2,
|
|
2370
|
+
validated,
|
|
2371
|
+
target,
|
|
2372
|
+
snapshot,
|
|
2373
|
+
description
|
|
2374
|
+
);
|
|
2375
|
+
});
|
|
2376
|
+
}
|
|
2377
|
+
const result = me.on[action2](validated, snapshot, target);
|
|
2378
|
+
if (!result) return [snapshot];
|
|
2379
|
+
if (Array.isArray(result) && result.length === 0) {
|
|
2380
|
+
return [snapshot];
|
|
2381
|
+
}
|
|
2382
|
+
const tuples = Array.isArray(result[0]) ? result : [result];
|
|
2383
|
+
const deprecated = me._deprecated;
|
|
2384
|
+
if (deprecated && deprecated.size > 0) {
|
|
2385
|
+
const me_ = me;
|
|
2386
|
+
const warned = me_._warned ?? (me_._warned = /* @__PURE__ */ new Set());
|
|
2387
|
+
for (const [name] of tuples) {
|
|
2388
|
+
const evt = name;
|
|
2389
|
+
if (deprecated.has(evt) && !warned.has(evt)) {
|
|
2390
|
+
warned.add(evt);
|
|
2391
|
+
log().warn(
|
|
2392
|
+
`Action "${String(action2)}" emitted deprecated event "${evt}". A newer version exists in the registry \u2014 update the action's .emit() to target the current version. (warned once per process)`
|
|
2393
|
+
);
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
const emitted = tuples.map(([name, data]) => ({
|
|
2398
|
+
name,
|
|
2399
|
+
data: skipValidation ? data : validate(name, data, me.events[name])
|
|
2400
|
+
}));
|
|
2401
|
+
const meta = {
|
|
2402
|
+
correlation: reactingTo?.meta.correlation || correlator({
|
|
2403
|
+
action: action2,
|
|
2404
|
+
state: me.name,
|
|
2405
|
+
stream,
|
|
2406
|
+
actor: target.actor
|
|
2407
|
+
}),
|
|
2408
|
+
causation: {
|
|
2409
|
+
action: {
|
|
2410
|
+
name: action2,
|
|
2411
|
+
...target
|
|
2412
|
+
// payload intentionally omitted: it can be large or contain PII,
|
|
2413
|
+
// and callers correlate via the correlation id when they need it.
|
|
2414
|
+
},
|
|
2415
|
+
event: reactingTo ? {
|
|
2416
|
+
id: reactingTo.id,
|
|
2417
|
+
name: reactingTo.name,
|
|
2418
|
+
stream: reactingTo.stream
|
|
2419
|
+
} : void 0
|
|
2420
|
+
}
|
|
2421
|
+
};
|
|
2422
|
+
let committed;
|
|
2423
|
+
try {
|
|
2424
|
+
committed = await store2().commit(
|
|
2425
|
+
stream,
|
|
2426
|
+
emitted,
|
|
2427
|
+
meta,
|
|
2428
|
+
// Reactions skip optimistic concurrency: they always append against the
|
|
2429
|
+
// current head. Stream leasing already serializes concurrent reactions,
|
|
2430
|
+
// and forcing version checks here would turn ordinary catch-up into
|
|
2431
|
+
// spurious retries.
|
|
2432
|
+
reactingTo ? void 0 : expected
|
|
2326
2433
|
);
|
|
2434
|
+
} catch (error) {
|
|
2435
|
+
if (error instanceof ConcurrencyError) {
|
|
2436
|
+
await cache2().invalidate(stream);
|
|
2437
|
+
}
|
|
2438
|
+
throw error;
|
|
2439
|
+
}
|
|
2440
|
+
let { state: state2, patches } = snapshot;
|
|
2441
|
+
const snapshots = committed.map((event) => {
|
|
2442
|
+
const p = me.patch[event.name](event, state2);
|
|
2443
|
+
state2 = (0, import_act_patch.patch)(state2, p);
|
|
2444
|
+
patches++;
|
|
2445
|
+
return {
|
|
2446
|
+
event,
|
|
2447
|
+
state: state2,
|
|
2448
|
+
version: event.version,
|
|
2449
|
+
patches,
|
|
2450
|
+
snaps: snapshot.snaps,
|
|
2451
|
+
patch: p,
|
|
2452
|
+
cache_hit: snapshot.cache_hit,
|
|
2453
|
+
replayed: snapshot.replayed
|
|
2454
|
+
};
|
|
2455
|
+
});
|
|
2456
|
+
const last = snapshots.at(-1);
|
|
2457
|
+
const snapped = me.snap?.(last);
|
|
2458
|
+
cache2().set(stream, {
|
|
2459
|
+
state: last.state,
|
|
2460
|
+
version: last.event.version,
|
|
2461
|
+
event_id: last.event.id,
|
|
2462
|
+
patches: snapped ? 0 : last.patches,
|
|
2463
|
+
snaps: snapped ? last.snaps + 1 : last.snaps
|
|
2464
|
+
}).catch((err) => log().error(err));
|
|
2465
|
+
if (snapped) void snap(last);
|
|
2466
|
+
return snapshots;
|
|
2467
|
+
} catch (error) {
|
|
2468
|
+
if (!(error instanceof ConcurrencyError)) throw error;
|
|
2469
|
+
if (attempt >= maxRetries) throw error;
|
|
2470
|
+
if (opts?.backoff) {
|
|
2471
|
+
const delayMs = computeBackoffDelay(attempt, opts.backoff);
|
|
2472
|
+
if (delayMs > 0) await sleep(delayMs);
|
|
2327
2473
|
}
|
|
2328
2474
|
}
|
|
2329
2475
|
}
|
|
2330
|
-
const emitted = tuples.map(([name, data]) => ({
|
|
2331
|
-
name,
|
|
2332
|
-
data: skipValidation ? data : validate(name, data, me.events[name])
|
|
2333
|
-
}));
|
|
2334
|
-
const meta = {
|
|
2335
|
-
correlation: reactingTo?.meta.correlation || correlator({
|
|
2336
|
-
action: action2,
|
|
2337
|
-
state: me.name,
|
|
2338
|
-
stream,
|
|
2339
|
-
actor: target.actor
|
|
2340
|
-
}),
|
|
2341
|
-
causation: {
|
|
2342
|
-
action: {
|
|
2343
|
-
name: action2,
|
|
2344
|
-
...target
|
|
2345
|
-
// payload intentionally omitted: it can be large or contain PII,
|
|
2346
|
-
// and callers correlate via the correlation id when they need it.
|
|
2347
|
-
},
|
|
2348
|
-
event: reactingTo ? {
|
|
2349
|
-
id: reactingTo.id,
|
|
2350
|
-
name: reactingTo.name,
|
|
2351
|
-
stream: reactingTo.stream
|
|
2352
|
-
} : void 0
|
|
2353
|
-
}
|
|
2354
|
-
};
|
|
2355
|
-
let committed;
|
|
2356
|
-
try {
|
|
2357
|
-
committed = await store2().commit(
|
|
2358
|
-
stream,
|
|
2359
|
-
emitted,
|
|
2360
|
-
meta,
|
|
2361
|
-
// Reactions skip optimistic concurrency: they always append against the
|
|
2362
|
-
// current head. Stream leasing already serializes concurrent reactions,
|
|
2363
|
-
// and forcing version checks here would turn ordinary catch-up into
|
|
2364
|
-
// spurious retries.
|
|
2365
|
-
reactingTo ? void 0 : expected
|
|
2366
|
-
);
|
|
2367
|
-
} catch (error) {
|
|
2368
|
-
if (error instanceof ConcurrencyError) {
|
|
2369
|
-
await cache2().invalidate(stream);
|
|
2370
|
-
}
|
|
2371
|
-
throw error;
|
|
2372
|
-
}
|
|
2373
|
-
let { state: state2, patches } = snapshot;
|
|
2374
|
-
const snapshots = committed.map((event) => {
|
|
2375
|
-
const p = me.patch[event.name](event, state2);
|
|
2376
|
-
state2 = (0, import_act_patch.patch)(state2, p);
|
|
2377
|
-
patches++;
|
|
2378
|
-
return {
|
|
2379
|
-
event,
|
|
2380
|
-
state: state2,
|
|
2381
|
-
version: event.version,
|
|
2382
|
-
patches,
|
|
2383
|
-
snaps: snapshot.snaps,
|
|
2384
|
-
patch: p,
|
|
2385
|
-
cache_hit: snapshot.cache_hit,
|
|
2386
|
-
replayed: snapshot.replayed
|
|
2387
|
-
};
|
|
2388
|
-
});
|
|
2389
|
-
const last = snapshots.at(-1);
|
|
2390
|
-
const snapped = me.snap?.(last);
|
|
2391
|
-
cache2().set(stream, {
|
|
2392
|
-
state: last.state,
|
|
2393
|
-
version: last.event.version,
|
|
2394
|
-
event_id: last.event.id,
|
|
2395
|
-
patches: snapped ? 0 : last.patches,
|
|
2396
|
-
snaps: snapped ? last.snaps + 1 : last.snaps
|
|
2397
|
-
}).catch((err) => log().error(err));
|
|
2398
|
-
if (snapped) void snap(last);
|
|
2399
|
-
return snapshots;
|
|
2400
2476
|
}
|
|
2401
2477
|
|
|
2402
2478
|
// src/internal/tracing.ts
|
|
@@ -2930,27 +3006,6 @@ var _this_ = ({ stream }) => ({
|
|
|
2930
3006
|
target: stream
|
|
2931
3007
|
});
|
|
2932
3008
|
|
|
2933
|
-
// src/internal/backoff.ts
|
|
2934
|
-
function computeBackoffDelay(retry, opts) {
|
|
2935
|
-
if (!opts || opts.baseMs <= 0) return 0;
|
|
2936
|
-
const r = Math.max(0, retry);
|
|
2937
|
-
let delay;
|
|
2938
|
-
switch (opts.strategy) {
|
|
2939
|
-
case "fixed":
|
|
2940
|
-
delay = opts.baseMs;
|
|
2941
|
-
break;
|
|
2942
|
-
case "linear":
|
|
2943
|
-
delay = opts.baseMs * (r + 1);
|
|
2944
|
-
break;
|
|
2945
|
-
case "exponential":
|
|
2946
|
-
delay = opts.baseMs * 2 ** r;
|
|
2947
|
-
if (opts.maxMs !== void 0) delay = Math.min(delay, opts.maxMs);
|
|
2948
|
-
break;
|
|
2949
|
-
}
|
|
2950
|
-
if (opts.jitter) delay = delay * (0.5 + Math.random());
|
|
2951
|
-
return Math.max(0, Math.floor(delay));
|
|
2952
|
-
}
|
|
2953
|
-
|
|
2954
3009
|
// src/internal/reactions.ts
|
|
2955
3010
|
function finalize(lease, handled, at, error, options, logger, failed_at) {
|
|
2956
3011
|
if (!error) return { lease, handled, acked_at: at };
|
|
@@ -3942,13 +3997,15 @@ var Act = class {
|
|
|
3942
3997
|
return s;
|
|
3943
3998
|
})();
|
|
3944
3999
|
let kept = 0;
|
|
3945
|
-
let
|
|
4000
|
+
let migrated = 0;
|
|
4001
|
+
let dropped = { closed_streams: 0, snapshots: 0 };
|
|
3946
4002
|
await target.restore(async (callback) => {
|
|
3947
4003
|
const partial = await scan(source, opts, callback);
|
|
3948
4004
|
kept = partial.kept;
|
|
4005
|
+
migrated = partial.migrated;
|
|
3949
4006
|
dropped = partial.dropped;
|
|
3950
4007
|
});
|
|
3951
|
-
return { kept, dropped, duration_ms: Date.now() - started };
|
|
4008
|
+
return { kept, migrated, dropped, duration_ms: Date.now() - started };
|
|
3952
4009
|
});
|
|
3953
4010
|
}
|
|
3954
4011
|
/**
|
|
@@ -4480,7 +4537,7 @@ function state(entry) {
|
|
|
4480
4537
|
function action_builder(state2) {
|
|
4481
4538
|
const internal = state2;
|
|
4482
4539
|
const builder = {
|
|
4483
|
-
on(entry) {
|
|
4540
|
+
on(entry, options) {
|
|
4484
4541
|
const keys = Object.keys(entry);
|
|
4485
4542
|
if (keys.length !== 1) throw new Error(".on() requires exactly one key");
|
|
4486
4543
|
const action2 = keys[0];
|
|
@@ -4488,6 +4545,10 @@ function action_builder(state2) {
|
|
|
4488
4545
|
if (action2 in internal.actions)
|
|
4489
4546
|
throw new Error(`Duplicate action "${action2}"`);
|
|
4490
4547
|
internal.actions[action2] = schema;
|
|
4548
|
+
if (options) {
|
|
4549
|
+
internal.options ??= {};
|
|
4550
|
+
internal.options[action2] = options;
|
|
4551
|
+
}
|
|
4491
4552
|
function given(rules) {
|
|
4492
4553
|
internal.given ??= {};
|
|
4493
4554
|
internal.given[action2] = rules;
|