@rotorsoft/act 1.1.0 → 1.3.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 +44 -1
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/adapters/in-memory-store.d.ts +9 -12
- package/dist/@types/adapters/in-memory-store.d.ts.map +1 -1
- package/dist/@types/csv.d.ts +41 -0
- package/dist/@types/csv.d.ts.map +1 -0
- package/dist/@types/index.d.ts +1 -0
- package/dist/@types/index.d.ts.map +1 -1
- package/dist/@types/internal/event-sourcing.d.ts +36 -1
- package/dist/@types/internal/event-sourcing.d.ts.map +1 -1
- package/dist/@types/internal/index.d.ts +1 -0
- package/dist/@types/internal/index.d.ts.map +1 -1
- package/dist/@types/types/action.d.ts +115 -1
- package/dist/@types/types/action.d.ts.map +1 -1
- package/dist/@types/types/audit.d.ts +2 -1
- package/dist/@types/types/audit.d.ts.map +1 -1
- package/dist/@types/types/ports.d.ts +45 -124
- package/dist/@types/types/ports.d.ts.map +1 -1
- package/dist/{chunk-TN4XS7WE.js → chunk-XKCTGUW2.js} +18 -54
- package/dist/chunk-XKCTGUW2.js.map +1 -0
- package/dist/index.cjs +343 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +326 -1
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs +17 -53
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-TN4XS7WE.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
sleep,
|
|
20
20
|
store,
|
|
21
21
|
validate
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-XKCTGUW2.js";
|
|
23
23
|
import {
|
|
24
24
|
ActorSchema,
|
|
25
25
|
CausationEventSchema,
|
|
@@ -868,6 +868,51 @@ var subscribe = (streams) => store().subscribe(streams);
|
|
|
868
868
|
|
|
869
869
|
// src/internal/event-sourcing.ts
|
|
870
870
|
import { patch } from "@rotorsoft/act-patch";
|
|
871
|
+
async function* iterate(source, filter) {
|
|
872
|
+
const state2 = {
|
|
873
|
+
slot: null,
|
|
874
|
+
onProduce: null,
|
|
875
|
+
onConsume: null,
|
|
876
|
+
done: false,
|
|
877
|
+
error: void 0
|
|
878
|
+
};
|
|
879
|
+
const wakeProduce = () => {
|
|
880
|
+
const fn = state2.onProduce;
|
|
881
|
+
state2.onProduce = null;
|
|
882
|
+
if (fn) fn();
|
|
883
|
+
};
|
|
884
|
+
void source.query((event) => {
|
|
885
|
+
state2.slot = event;
|
|
886
|
+
wakeProduce();
|
|
887
|
+
return new Promise((resolve) => {
|
|
888
|
+
state2.onConsume = () => resolve();
|
|
889
|
+
});
|
|
890
|
+
}, filter).then(
|
|
891
|
+
() => {
|
|
892
|
+
state2.done = true;
|
|
893
|
+
wakeProduce();
|
|
894
|
+
},
|
|
895
|
+
(err) => {
|
|
896
|
+
state2.error = err;
|
|
897
|
+
state2.done = true;
|
|
898
|
+
wakeProduce();
|
|
899
|
+
}
|
|
900
|
+
);
|
|
901
|
+
while (true) {
|
|
902
|
+
if (state2.slot === null && !state2.done)
|
|
903
|
+
await new Promise((resolve) => {
|
|
904
|
+
state2.onProduce = resolve;
|
|
905
|
+
});
|
|
906
|
+
if (state2.error) throw state2.error;
|
|
907
|
+
if (state2.slot === null) return;
|
|
908
|
+
const event = state2.slot;
|
|
909
|
+
state2.slot = null;
|
|
910
|
+
const fn = state2.onConsume;
|
|
911
|
+
state2.onConsume = null;
|
|
912
|
+
fn();
|
|
913
|
+
yield event;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
871
916
|
async function snap(snapshot) {
|
|
872
917
|
try {
|
|
873
918
|
const { id, stream, name, meta, version } = snapshot.event;
|
|
@@ -899,6 +944,60 @@ async function tombstone(stream, expectedVersion, correlation) {
|
|
|
899
944
|
throw error;
|
|
900
945
|
}
|
|
901
946
|
}
|
|
947
|
+
function isValid(event) {
|
|
948
|
+
if (event.version < 0) return false;
|
|
949
|
+
if (!(event.created instanceof Date) || Number.isNaN(event.created.getTime()))
|
|
950
|
+
return false;
|
|
951
|
+
return true;
|
|
952
|
+
}
|
|
953
|
+
async function scan(source, opts = {}, callback) {
|
|
954
|
+
const { drop_snapshots = false, on_progress } = opts;
|
|
955
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
956
|
+
let kept = 0;
|
|
957
|
+
let droppedSnapshots = 0;
|
|
958
|
+
let processed = 0;
|
|
959
|
+
for await (const event of iterate(source)) {
|
|
960
|
+
processed++;
|
|
961
|
+
if (!isValid(event)) throw new Error(`Invalid event at index ${processed}`);
|
|
962
|
+
if (on_progress) on_progress({ processed });
|
|
963
|
+
if (drop_snapshots && event.name === SNAP_EVENT) {
|
|
964
|
+
droppedSnapshots++;
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
if (!callback) {
|
|
968
|
+
kept++;
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
let remapped = event;
|
|
972
|
+
const causedBy = event.meta.causation.event?.id;
|
|
973
|
+
if (causedBy !== void 0) {
|
|
974
|
+
const newCausedBy = idMap.get(causedBy);
|
|
975
|
+
if (newCausedBy !== void 0 && newCausedBy !== causedBy) {
|
|
976
|
+
remapped = {
|
|
977
|
+
...event,
|
|
978
|
+
meta: {
|
|
979
|
+
...event.meta,
|
|
980
|
+
causation: {
|
|
981
|
+
...event.meta.causation,
|
|
982
|
+
event: { ...event.meta.causation.event, id: newCausedBy }
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
const newId = await callback(remapped);
|
|
989
|
+
idMap.set(event.id, newId);
|
|
990
|
+
kept++;
|
|
991
|
+
}
|
|
992
|
+
return {
|
|
993
|
+
kept,
|
|
994
|
+
dropped: {
|
|
995
|
+
closed_streams: 0,
|
|
996
|
+
snapshots: droppedSnapshots,
|
|
997
|
+
empty_streams: 0
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
902
1001
|
async function load(me, stream, callback, asOf) {
|
|
903
1002
|
const timeTravel = !!asOf && Object.values(asOf).some((v) => v !== void 0);
|
|
904
1003
|
const cached = timeTravel ? void 0 : await cache().get(stream);
|
|
@@ -2533,6 +2632,70 @@ var Act = class {
|
|
|
2533
2632
|
return count;
|
|
2534
2633
|
});
|
|
2535
2634
|
}
|
|
2635
|
+
/**
|
|
2636
|
+
* Atomically wipe the store and rebuild it from an async stream of
|
|
2637
|
+
* committed events. The framework owns iteration, validation,
|
|
2638
|
+
* `drop_snapshots` filtering, `on_progress`, and the per-call
|
|
2639
|
+
* `old → new` causation remap; the adapter's {@link Store.restore}
|
|
2640
|
+
* driver supplies the transaction lifecycle and per-event insert.
|
|
2641
|
+
*
|
|
2642
|
+
* Throws if the adapter has no restore capability. Throws on the
|
|
2643
|
+
* first invalid event (negative version, malformed `created`) with
|
|
2644
|
+
* the running index in the message; atomic transaction rollback in
|
|
2645
|
+
* the adapter means a failing restore leaves the store byte-for-byte
|
|
2646
|
+
* unchanged.
|
|
2647
|
+
*
|
|
2648
|
+
* @param source - Async stream of events in target order. Streamed
|
|
2649
|
+
* rather than buffered so multi-million-event backups don't OOM.
|
|
2650
|
+
* Each event's original `id` is used as a causation lookup key but
|
|
2651
|
+
* never written through — adapters renumber densely.
|
|
2652
|
+
* @param opts - {@link ScanOptions}. `drop_snapshots` skips
|
|
2653
|
+
* `__snapshot__` events (counted in the result); `on_progress`
|
|
2654
|
+
* fires once per event.
|
|
2655
|
+
* @returns {@link ScanResult} with `kept`, `duration_ms`, and
|
|
2656
|
+
* `dropped` per-category counters.
|
|
2657
|
+
*
|
|
2658
|
+
* @example Round-trip a CSV backup
|
|
2659
|
+
* ```typescript
|
|
2660
|
+
* async function* parseCsv(blob: string) {
|
|
2661
|
+
* for (const line of blob.split("\n").slice(1)) {
|
|
2662
|
+
* const [id, name, data, stream, version, created, meta] = parse(line);
|
|
2663
|
+
* yield {
|
|
2664
|
+
* id: +id, name, data: JSON.parse(data), stream,
|
|
2665
|
+
* version: +version, created: new Date(created),
|
|
2666
|
+
* meta: JSON.parse(meta),
|
|
2667
|
+
* };
|
|
2668
|
+
* }
|
|
2669
|
+
* }
|
|
2670
|
+
* const result = await app.restore(parseCsv(csvBlob), {});
|
|
2671
|
+
* console.log(`Restored ${result.kept} events in ${result.duration_ms}ms`);
|
|
2672
|
+
* await cache().clear(); // operator's responsibility
|
|
2673
|
+
* ```
|
|
2674
|
+
*
|
|
2675
|
+
* @see {@link Store.restore} for the underlying driver-pattern primitive.
|
|
2676
|
+
*/
|
|
2677
|
+
async restore(source, opts = {}, sink) {
|
|
2678
|
+
return this._scoped(async () => {
|
|
2679
|
+
const started = Date.now();
|
|
2680
|
+
if (opts.dry_run) {
|
|
2681
|
+
const partial = await scan(source, opts);
|
|
2682
|
+
return { ...partial, duration_ms: Date.now() - started };
|
|
2683
|
+
}
|
|
2684
|
+
const target = sink ?? (() => {
|
|
2685
|
+
const s = store();
|
|
2686
|
+
if (!s.restore) throw new Error("adapter has no restore capability");
|
|
2687
|
+
return s;
|
|
2688
|
+
})();
|
|
2689
|
+
let kept = 0;
|
|
2690
|
+
let dropped = { closed_streams: 0, snapshots: 0, empty_streams: 0 };
|
|
2691
|
+
await target.restore(async (callback) => {
|
|
2692
|
+
const partial = await scan(source, opts, callback);
|
|
2693
|
+
kept = partial.kept;
|
|
2694
|
+
dropped = partial.dropped;
|
|
2695
|
+
});
|
|
2696
|
+
return { kept, dropped, duration_ms: Date.now() - started };
|
|
2697
|
+
});
|
|
2698
|
+
}
|
|
2536
2699
|
/**
|
|
2537
2700
|
* Return every currently-blocked stream position. Convenience wrapper
|
|
2538
2701
|
* around `store().query_streams(cb, { blocked: true })` for the common
|
|
@@ -3098,6 +3261,167 @@ function action_builder(state2) {
|
|
|
3098
3261
|
};
|
|
3099
3262
|
return builder;
|
|
3100
3263
|
}
|
|
3264
|
+
|
|
3265
|
+
// src/csv.ts
|
|
3266
|
+
import { createReadStream, createWriteStream } from "fs";
|
|
3267
|
+
import { createInterface } from "readline";
|
|
3268
|
+
var CSV_COLUMNS = [
|
|
3269
|
+
"id",
|
|
3270
|
+
"name",
|
|
3271
|
+
"data",
|
|
3272
|
+
"stream",
|
|
3273
|
+
"version",
|
|
3274
|
+
"created",
|
|
3275
|
+
"meta"
|
|
3276
|
+
];
|
|
3277
|
+
var CsvFile = class {
|
|
3278
|
+
path;
|
|
3279
|
+
blob;
|
|
3280
|
+
constructor(options) {
|
|
3281
|
+
if ("path" in options) {
|
|
3282
|
+
this.path = options.path;
|
|
3283
|
+
this.blob = null;
|
|
3284
|
+
} else {
|
|
3285
|
+
this.path = null;
|
|
3286
|
+
this.blob = options.blob;
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
async query(callback, _filter) {
|
|
3290
|
+
const lines = this.blob !== null ? linesFromBlob(this.blob) : linesFromFile(this.path);
|
|
3291
|
+
let count = 0;
|
|
3292
|
+
let header = null;
|
|
3293
|
+
for await (const line of lines) {
|
|
3294
|
+
if (!line.trim()) continue;
|
|
3295
|
+
const fields = parseCsvLine(line);
|
|
3296
|
+
if (!header) {
|
|
3297
|
+
header = fields;
|
|
3298
|
+
const expected = CSV_COLUMNS.join(",");
|
|
3299
|
+
if (header.join(",") !== expected)
|
|
3300
|
+
throw new Error(`Invalid CSV header. Expected: ${expected}`);
|
|
3301
|
+
continue;
|
|
3302
|
+
}
|
|
3303
|
+
if (fields.length !== CSV_COLUMNS.length)
|
|
3304
|
+
throw new Error(
|
|
3305
|
+
`Row ${count + 1}: expected ${CSV_COLUMNS.length} fields, got ${fields.length}`
|
|
3306
|
+
);
|
|
3307
|
+
const event = {
|
|
3308
|
+
id: Number.parseInt(fields[0], 10),
|
|
3309
|
+
name: fields[1],
|
|
3310
|
+
data: JSON.parse(fields[2]),
|
|
3311
|
+
stream: fields[3],
|
|
3312
|
+
version: Number.parseInt(fields[4], 10),
|
|
3313
|
+
created: new Date(fields[5]),
|
|
3314
|
+
meta: JSON.parse(fields[6])
|
|
3315
|
+
};
|
|
3316
|
+
await Promise.resolve(callback(event));
|
|
3317
|
+
count++;
|
|
3318
|
+
}
|
|
3319
|
+
if (header === null)
|
|
3320
|
+
throw new Error("CSV must have a header and at least one row");
|
|
3321
|
+
return count;
|
|
3322
|
+
}
|
|
3323
|
+
async restore(driver) {
|
|
3324
|
+
if (this.path === null)
|
|
3325
|
+
throw new Error(
|
|
3326
|
+
"CsvFile in blob mode is read-only \u2014 provide `path` to write"
|
|
3327
|
+
);
|
|
3328
|
+
const writer = createWriteStream(this.path, {
|
|
3329
|
+
flags: "w",
|
|
3330
|
+
encoding: "utf8"
|
|
3331
|
+
});
|
|
3332
|
+
let nextId = 1;
|
|
3333
|
+
try {
|
|
3334
|
+
await writeLine(writer, CSV_COLUMNS.join(","));
|
|
3335
|
+
await driver(async (event) => {
|
|
3336
|
+
const id = nextId++;
|
|
3337
|
+
const row = [
|
|
3338
|
+
String(id),
|
|
3339
|
+
csvEscape(event.name),
|
|
3340
|
+
csvEscape(JSON.stringify(event.data)),
|
|
3341
|
+
csvEscape(event.stream),
|
|
3342
|
+
String(event.version),
|
|
3343
|
+
event.created.toISOString(),
|
|
3344
|
+
csvEscape(JSON.stringify(event.meta))
|
|
3345
|
+
].join(",");
|
|
3346
|
+
await writeLine(writer, row);
|
|
3347
|
+
return id;
|
|
3348
|
+
});
|
|
3349
|
+
} finally {
|
|
3350
|
+
await new Promise((resolve) => writer.end(resolve));
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
async dispose() {
|
|
3354
|
+
}
|
|
3355
|
+
};
|
|
3356
|
+
async function* linesFromFile(path) {
|
|
3357
|
+
const stream = createReadStream(path, { encoding: "utf8" });
|
|
3358
|
+
const rl = createInterface({
|
|
3359
|
+
input: stream,
|
|
3360
|
+
crlfDelay: Number.POSITIVE_INFINITY
|
|
3361
|
+
});
|
|
3362
|
+
try {
|
|
3363
|
+
for await (const line of rl) yield line;
|
|
3364
|
+
} finally {
|
|
3365
|
+
rl.close();
|
|
3366
|
+
stream.close();
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
async function* linesFromBlob(blob) {
|
|
3370
|
+
let start = 0;
|
|
3371
|
+
while (start < blob.length) {
|
|
3372
|
+
const nl = blob.indexOf("\n", start);
|
|
3373
|
+
const end = nl === -1 ? blob.length : nl;
|
|
3374
|
+
yield blob.slice(start, end);
|
|
3375
|
+
start = nl === -1 ? blob.length : nl + 1;
|
|
3376
|
+
await Promise.resolve();
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
function parseCsvLine(line) {
|
|
3380
|
+
const fields = [];
|
|
3381
|
+
let i = 0;
|
|
3382
|
+
while (i < line.length) {
|
|
3383
|
+
if (line[i] === '"') {
|
|
3384
|
+
let value = "";
|
|
3385
|
+
i++;
|
|
3386
|
+
while (i < line.length) {
|
|
3387
|
+
if (line[i] === '"' && line[i + 1] === '"') {
|
|
3388
|
+
value += '"';
|
|
3389
|
+
i += 2;
|
|
3390
|
+
} else if (line[i] === '"') {
|
|
3391
|
+
i++;
|
|
3392
|
+
break;
|
|
3393
|
+
} else {
|
|
3394
|
+
value += line[i++];
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
fields.push(value);
|
|
3398
|
+
if (line[i] === ",") i++;
|
|
3399
|
+
} else {
|
|
3400
|
+
const next = line.indexOf(",", i);
|
|
3401
|
+
if (next === -1) {
|
|
3402
|
+
fields.push(line.slice(i));
|
|
3403
|
+
i = line.length;
|
|
3404
|
+
} else {
|
|
3405
|
+
fields.push(line.slice(i, next));
|
|
3406
|
+
i = next + 1;
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
return fields;
|
|
3411
|
+
}
|
|
3412
|
+
function csvEscape(value) {
|
|
3413
|
+
if (/[",\n\r]/.test(value)) return `"${value.replace(/"/g, '""')}"`;
|
|
3414
|
+
return value;
|
|
3415
|
+
}
|
|
3416
|
+
function writeLine(writer, line) {
|
|
3417
|
+
return new Promise((resolve, reject) => {
|
|
3418
|
+
writer.write(`${line}
|
|
3419
|
+
`, (err) => {
|
|
3420
|
+
if (err) reject(err);
|
|
3421
|
+
else resolve();
|
|
3422
|
+
});
|
|
3423
|
+
});
|
|
3424
|
+
}
|
|
3101
3425
|
export {
|
|
3102
3426
|
Act,
|
|
3103
3427
|
ActorSchema,
|
|
@@ -3105,6 +3429,7 @@ export {
|
|
|
3105
3429
|
CommittedMetaSchema,
|
|
3106
3430
|
ConcurrencyError,
|
|
3107
3431
|
ConsoleLogger,
|
|
3432
|
+
CsvFile,
|
|
3108
3433
|
DEFAULT_LANE,
|
|
3109
3434
|
DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
3110
3435
|
DEFAULT_SETTLE_DEBOUNCE_MS,
|