@rotorsoft/act 1.2.0 → 1.4.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 +2 -2
- package/dist/@types/act.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 +12 -3
- package/dist/@types/internal/event-sourcing.d.ts.map +1 -1
- package/dist/@types/types/action.d.ts +59 -6
- 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 +2 -2
- package/dist/@types/types/ports.d.ts.map +1 -1
- package/dist/{chunk-J6NDEEXC.js → chunk-XKCTGUW2.js} +3 -3
- package/dist/chunk-XKCTGUW2.js.map +1 -0
- package/dist/index.cjs +230 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +228 -40
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs +2 -2
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-J6NDEEXC.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,7 @@ var subscribe = (streams) => store().subscribe(streams);
|
|
|
868
868
|
|
|
869
869
|
// src/internal/event-sourcing.ts
|
|
870
870
|
import { patch } from "@rotorsoft/act-patch";
|
|
871
|
+
var DEFAULT_BATCH = 500;
|
|
871
872
|
async function snap(snapshot) {
|
|
872
873
|
try {
|
|
873
874
|
const { id, stream, name, meta, version } = snapshot.event;
|
|
@@ -899,7 +900,7 @@ async function tombstone(stream, expectedVersion, correlation) {
|
|
|
899
900
|
throw error;
|
|
900
901
|
}
|
|
901
902
|
}
|
|
902
|
-
function
|
|
903
|
+
function is_valid(event) {
|
|
903
904
|
if (event.version < 0) return false;
|
|
904
905
|
if (!(event.created instanceof Date) || Number.isNaN(event.created.getTime()))
|
|
905
906
|
return false;
|
|
@@ -907,48 +908,70 @@ function isValid(event) {
|
|
|
907
908
|
}
|
|
908
909
|
async function scan(source, opts = {}, callback) {
|
|
909
910
|
const { drop_snapshots = false, on_progress } = opts;
|
|
910
|
-
const
|
|
911
|
+
const limit = opts.batch_size ?? DEFAULT_BATCH;
|
|
912
|
+
const id_map = /* @__PURE__ */ new Map();
|
|
911
913
|
let kept = 0;
|
|
912
|
-
let
|
|
914
|
+
let dropped_snaps = 0;
|
|
913
915
|
let processed = 0;
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
916
|
+
let at;
|
|
917
|
+
let max_id;
|
|
918
|
+
const probed = await source.query(
|
|
919
|
+
(e) => {
|
|
920
|
+
max_id = e.id;
|
|
921
|
+
},
|
|
922
|
+
{ backward: true, limit: 1 }
|
|
923
|
+
);
|
|
924
|
+
if (probed !== 1) max_id = void 0;
|
|
925
|
+
while (true) {
|
|
926
|
+
let got = 0;
|
|
927
|
+
let id;
|
|
928
|
+
await source.query(
|
|
929
|
+
async (event) => {
|
|
930
|
+
got++;
|
|
931
|
+
id = event.id;
|
|
932
|
+
processed++;
|
|
933
|
+
if (!is_valid(event))
|
|
934
|
+
throw new Error(`Invalid event at index ${processed}`);
|
|
935
|
+
if (on_progress) on_progress({ processed, id: event.id, max_id });
|
|
936
|
+
if (drop_snapshots && event.name === SNAP_EVENT) {
|
|
937
|
+
dropped_snaps++;
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
if (!callback) {
|
|
941
|
+
kept++;
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
let remapped = event;
|
|
945
|
+
const caused_by = event.meta.causation.event?.id;
|
|
946
|
+
if (caused_by !== void 0) {
|
|
947
|
+
const new_caused_by = id_map.get(caused_by);
|
|
948
|
+
if (new_caused_by !== void 0 && new_caused_by !== caused_by) {
|
|
949
|
+
remapped = {
|
|
950
|
+
...event,
|
|
951
|
+
meta: {
|
|
952
|
+
...event.meta,
|
|
953
|
+
causation: {
|
|
954
|
+
...event.meta.causation,
|
|
955
|
+
event: { ...event.meta.causation.event, id: new_caused_by }
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
};
|
|
939
959
|
}
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
960
|
+
}
|
|
961
|
+
const new_id = await callback(remapped);
|
|
962
|
+
id_map.set(event.id, new_id);
|
|
963
|
+
kept++;
|
|
964
|
+
},
|
|
965
|
+
{ after: at, limit }
|
|
966
|
+
);
|
|
967
|
+
if (got !== limit) break;
|
|
968
|
+
at = id;
|
|
946
969
|
}
|
|
947
970
|
return {
|
|
948
971
|
kept,
|
|
949
972
|
dropped: {
|
|
950
973
|
closed_streams: 0,
|
|
951
|
-
snapshots:
|
|
974
|
+
snapshots: dropped_snaps,
|
|
952
975
|
empty_streams: 0
|
|
953
976
|
}
|
|
954
977
|
};
|
|
@@ -2629,18 +2652,21 @@ var Act = class {
|
|
|
2629
2652
|
*
|
|
2630
2653
|
* @see {@link Store.restore} for the underlying driver-pattern primitive.
|
|
2631
2654
|
*/
|
|
2632
|
-
async restore(source, opts = {}) {
|
|
2655
|
+
async restore(source, opts = {}, sink) {
|
|
2633
2656
|
return this._scoped(async () => {
|
|
2634
2657
|
const started = Date.now();
|
|
2635
2658
|
if (opts.dry_run) {
|
|
2636
2659
|
const partial = await scan(source, opts);
|
|
2637
2660
|
return { ...partial, duration_ms: Date.now() - started };
|
|
2638
2661
|
}
|
|
2639
|
-
const
|
|
2640
|
-
|
|
2662
|
+
const target = sink ?? (() => {
|
|
2663
|
+
const s = store();
|
|
2664
|
+
if (!s.restore) throw new Error("adapter has no restore capability");
|
|
2665
|
+
return s;
|
|
2666
|
+
})();
|
|
2641
2667
|
let kept = 0;
|
|
2642
2668
|
let dropped = { closed_streams: 0, snapshots: 0, empty_streams: 0 };
|
|
2643
|
-
await
|
|
2669
|
+
await target.restore(async (callback) => {
|
|
2644
2670
|
const partial = await scan(source, opts, callback);
|
|
2645
2671
|
kept = partial.kept;
|
|
2646
2672
|
dropped = partial.dropped;
|
|
@@ -3213,6 +3239,167 @@ function action_builder(state2) {
|
|
|
3213
3239
|
};
|
|
3214
3240
|
return builder;
|
|
3215
3241
|
}
|
|
3242
|
+
|
|
3243
|
+
// src/csv.ts
|
|
3244
|
+
import { createReadStream, createWriteStream } from "fs";
|
|
3245
|
+
import { createInterface } from "readline";
|
|
3246
|
+
var CSV_COLUMNS = [
|
|
3247
|
+
"id",
|
|
3248
|
+
"name",
|
|
3249
|
+
"data",
|
|
3250
|
+
"stream",
|
|
3251
|
+
"version",
|
|
3252
|
+
"created",
|
|
3253
|
+
"meta"
|
|
3254
|
+
];
|
|
3255
|
+
var CsvFile = class {
|
|
3256
|
+
path;
|
|
3257
|
+
blob;
|
|
3258
|
+
constructor(options) {
|
|
3259
|
+
if ("path" in options) {
|
|
3260
|
+
this.path = options.path;
|
|
3261
|
+
this.blob = null;
|
|
3262
|
+
} else {
|
|
3263
|
+
this.path = null;
|
|
3264
|
+
this.blob = options.blob;
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
async query(callback, _filter) {
|
|
3268
|
+
const lines = this.blob !== null ? linesFromBlob(this.blob) : linesFromFile(this.path);
|
|
3269
|
+
let count = 0;
|
|
3270
|
+
let header = null;
|
|
3271
|
+
for await (const line of lines) {
|
|
3272
|
+
if (!line.trim()) continue;
|
|
3273
|
+
const fields = parseCsvLine(line);
|
|
3274
|
+
if (!header) {
|
|
3275
|
+
header = fields;
|
|
3276
|
+
const expected = CSV_COLUMNS.join(",");
|
|
3277
|
+
if (header.join(",") !== expected)
|
|
3278
|
+
throw new Error(`Invalid CSV header. Expected: ${expected}`);
|
|
3279
|
+
continue;
|
|
3280
|
+
}
|
|
3281
|
+
if (fields.length !== CSV_COLUMNS.length)
|
|
3282
|
+
throw new Error(
|
|
3283
|
+
`Row ${count + 1}: expected ${CSV_COLUMNS.length} fields, got ${fields.length}`
|
|
3284
|
+
);
|
|
3285
|
+
const event = {
|
|
3286
|
+
id: Number.parseInt(fields[0], 10),
|
|
3287
|
+
name: fields[1],
|
|
3288
|
+
data: JSON.parse(fields[2]),
|
|
3289
|
+
stream: fields[3],
|
|
3290
|
+
version: Number.parseInt(fields[4], 10),
|
|
3291
|
+
created: new Date(fields[5]),
|
|
3292
|
+
meta: JSON.parse(fields[6])
|
|
3293
|
+
};
|
|
3294
|
+
await Promise.resolve(callback(event));
|
|
3295
|
+
count++;
|
|
3296
|
+
}
|
|
3297
|
+
if (header === null)
|
|
3298
|
+
throw new Error("CSV must have a header and at least one row");
|
|
3299
|
+
return count;
|
|
3300
|
+
}
|
|
3301
|
+
async restore(driver) {
|
|
3302
|
+
if (this.path === null)
|
|
3303
|
+
throw new Error(
|
|
3304
|
+
"CsvFile in blob mode is read-only \u2014 provide `path` to write"
|
|
3305
|
+
);
|
|
3306
|
+
const writer = createWriteStream(this.path, {
|
|
3307
|
+
flags: "w",
|
|
3308
|
+
encoding: "utf8"
|
|
3309
|
+
});
|
|
3310
|
+
let nextId = 1;
|
|
3311
|
+
try {
|
|
3312
|
+
await writeLine(writer, CSV_COLUMNS.join(","));
|
|
3313
|
+
await driver(async (event) => {
|
|
3314
|
+
const id = nextId++;
|
|
3315
|
+
const row = [
|
|
3316
|
+
String(id),
|
|
3317
|
+
csvEscape(event.name),
|
|
3318
|
+
csvEscape(JSON.stringify(event.data)),
|
|
3319
|
+
csvEscape(event.stream),
|
|
3320
|
+
String(event.version),
|
|
3321
|
+
event.created.toISOString(),
|
|
3322
|
+
csvEscape(JSON.stringify(event.meta))
|
|
3323
|
+
].join(",");
|
|
3324
|
+
await writeLine(writer, row);
|
|
3325
|
+
return id;
|
|
3326
|
+
});
|
|
3327
|
+
} finally {
|
|
3328
|
+
await new Promise((resolve) => writer.end(resolve));
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
async dispose() {
|
|
3332
|
+
}
|
|
3333
|
+
};
|
|
3334
|
+
async function* linesFromFile(path) {
|
|
3335
|
+
const stream = createReadStream(path, { encoding: "utf8" });
|
|
3336
|
+
const rl = createInterface({
|
|
3337
|
+
input: stream,
|
|
3338
|
+
crlfDelay: Number.POSITIVE_INFINITY
|
|
3339
|
+
});
|
|
3340
|
+
try {
|
|
3341
|
+
for await (const line of rl) yield line;
|
|
3342
|
+
} finally {
|
|
3343
|
+
rl.close();
|
|
3344
|
+
stream.close();
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
async function* linesFromBlob(blob) {
|
|
3348
|
+
let start = 0;
|
|
3349
|
+
while (start < blob.length) {
|
|
3350
|
+
const nl = blob.indexOf("\n", start);
|
|
3351
|
+
const end = nl === -1 ? blob.length : nl;
|
|
3352
|
+
yield blob.slice(start, end);
|
|
3353
|
+
start = nl === -1 ? blob.length : nl + 1;
|
|
3354
|
+
await Promise.resolve();
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
function parseCsvLine(line) {
|
|
3358
|
+
const fields = [];
|
|
3359
|
+
let i = 0;
|
|
3360
|
+
while (i < line.length) {
|
|
3361
|
+
if (line[i] === '"') {
|
|
3362
|
+
let value = "";
|
|
3363
|
+
i++;
|
|
3364
|
+
while (i < line.length) {
|
|
3365
|
+
if (line[i] === '"' && line[i + 1] === '"') {
|
|
3366
|
+
value += '"';
|
|
3367
|
+
i += 2;
|
|
3368
|
+
} else if (line[i] === '"') {
|
|
3369
|
+
i++;
|
|
3370
|
+
break;
|
|
3371
|
+
} else {
|
|
3372
|
+
value += line[i++];
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
fields.push(value);
|
|
3376
|
+
if (line[i] === ",") i++;
|
|
3377
|
+
} else {
|
|
3378
|
+
const next = line.indexOf(",", i);
|
|
3379
|
+
if (next === -1) {
|
|
3380
|
+
fields.push(line.slice(i));
|
|
3381
|
+
i = line.length;
|
|
3382
|
+
} else {
|
|
3383
|
+
fields.push(line.slice(i, next));
|
|
3384
|
+
i = next + 1;
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
3388
|
+
return fields;
|
|
3389
|
+
}
|
|
3390
|
+
function csvEscape(value) {
|
|
3391
|
+
if (/[",\n\r]/.test(value)) return `"${value.replace(/"/g, '""')}"`;
|
|
3392
|
+
return value;
|
|
3393
|
+
}
|
|
3394
|
+
function writeLine(writer, line) {
|
|
3395
|
+
return new Promise((resolve, reject) => {
|
|
3396
|
+
writer.write(`${line}
|
|
3397
|
+
`, (err) => {
|
|
3398
|
+
if (err) reject(err);
|
|
3399
|
+
else resolve();
|
|
3400
|
+
});
|
|
3401
|
+
});
|
|
3402
|
+
}
|
|
3216
3403
|
export {
|
|
3217
3404
|
Act,
|
|
3218
3405
|
ActorSchema,
|
|
@@ -3220,6 +3407,7 @@ export {
|
|
|
3220
3407
|
CommittedMetaSchema,
|
|
3221
3408
|
ConcurrencyError,
|
|
3222
3409
|
ConsoleLogger,
|
|
3410
|
+
CsvFile,
|
|
3223
3411
|
DEFAULT_LANE,
|
|
3224
3412
|
DEFAULT_MAX_SUBSCRIBED_STREAMS,
|
|
3225
3413
|
DEFAULT_SETTLE_DEBOUNCE_MS,
|