@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/index.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  sleep,
20
20
  store,
21
21
  validate
22
- } from "./chunk-J6NDEEXC.js";
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 isValid(event) {
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 idMap = /* @__PURE__ */ new Map();
911
+ const limit = opts.batch_size ?? DEFAULT_BATCH;
912
+ const id_map = /* @__PURE__ */ new Map();
911
913
  let kept = 0;
912
- let droppedSnapshots = 0;
914
+ let dropped_snaps = 0;
913
915
  let processed = 0;
914
- for await (const event of source) {
915
- processed++;
916
- if (!isValid(event)) throw new Error(`Invalid event at index ${processed}`);
917
- if (on_progress) on_progress({ processed });
918
- if (drop_snapshots && event.name === SNAP_EVENT) {
919
- droppedSnapshots++;
920
- continue;
921
- }
922
- if (!callback) {
923
- kept++;
924
- continue;
925
- }
926
- let remapped = event;
927
- const causedBy = event.meta.causation.event?.id;
928
- if (causedBy !== void 0) {
929
- const newCausedBy = idMap.get(causedBy);
930
- if (newCausedBy !== void 0 && newCausedBy !== causedBy) {
931
- remapped = {
932
- ...event,
933
- meta: {
934
- ...event.meta,
935
- causation: {
936
- ...event.meta.causation,
937
- event: { ...event.meta.causation.event, id: newCausedBy }
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
- const newId = await callback(remapped);
944
- idMap.set(event.id, newId);
945
- kept++;
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: droppedSnapshots,
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 s = store();
2640
- if (!s.restore) throw new Error("adapter has no restore capability");
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 s.restore(async (callback) => {
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,