@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.cjs CHANGED
@@ -36,6 +36,7 @@ __export(index_exports, {
36
36
  CommittedMetaSchema: () => CommittedMetaSchema,
37
37
  ConcurrencyError: () => ConcurrencyError,
38
38
  ConsoleLogger: () => ConsoleLogger,
39
+ CsvFile: () => CsvFile,
39
40
  DEFAULT_LANE: () => DEFAULT_LANE,
40
41
  DEFAULT_MAX_SUBSCRIBED_STREAMS: () => DEFAULT_MAX_SUBSCRIBED_STREAMS,
41
42
  DEFAULT_SETTLE_DEBOUNCE_MS: () => DEFAULT_SETTLE_DEBOUNCE_MS,
@@ -717,7 +718,7 @@ var InMemoryStore = class {
717
718
  continue;
718
719
  if (query.after && e.id <= query.after) break;
719
720
  if (query.created_after && e.created <= query.created_after) break;
720
- callback(e);
721
+ await Promise.resolve(callback(e));
721
722
  count++;
722
723
  if (query?.limit && count >= query.limit) break;
723
724
  }
@@ -729,7 +730,7 @@ var InMemoryStore = class {
729
730
  if (query?.created_after && e.created <= query.created_after) continue;
730
731
  if (query?.before && e.id >= query.before) break;
731
732
  if (query?.created_before && e.created >= query.created_before) break;
732
- callback(e);
733
+ await Promise.resolve(callback(e));
733
734
  count++;
734
735
  if (query?.limit && count >= query.limit) break;
735
736
  }
@@ -2088,6 +2089,7 @@ var subscribe = (streams) => store2().subscribe(streams);
2088
2089
 
2089
2090
  // src/internal/event-sourcing.ts
2090
2091
  var import_act_patch = require("@rotorsoft/act-patch");
2092
+ var DEFAULT_BATCH = 500;
2091
2093
  async function snap(snapshot) {
2092
2094
  try {
2093
2095
  const { id, stream, name, meta, version } = snapshot.event;
@@ -2119,7 +2121,7 @@ async function tombstone(stream, expectedVersion, correlation) {
2119
2121
  throw error;
2120
2122
  }
2121
2123
  }
2122
- function isValid(event) {
2124
+ function is_valid(event) {
2123
2125
  if (event.version < 0) return false;
2124
2126
  if (!(event.created instanceof Date) || Number.isNaN(event.created.getTime()))
2125
2127
  return false;
@@ -2127,48 +2129,70 @@ function isValid(event) {
2127
2129
  }
2128
2130
  async function scan(source, opts = {}, callback) {
2129
2131
  const { drop_snapshots = false, on_progress } = opts;
2130
- const idMap = /* @__PURE__ */ new Map();
2132
+ const limit = opts.batch_size ?? DEFAULT_BATCH;
2133
+ const id_map = /* @__PURE__ */ new Map();
2131
2134
  let kept = 0;
2132
- let droppedSnapshots = 0;
2135
+ let dropped_snaps = 0;
2133
2136
  let processed = 0;
2134
- for await (const event of source) {
2135
- processed++;
2136
- if (!isValid(event)) throw new Error(`Invalid event at index ${processed}`);
2137
- if (on_progress) on_progress({ processed });
2138
- if (drop_snapshots && event.name === SNAP_EVENT) {
2139
- droppedSnapshots++;
2140
- continue;
2141
- }
2142
- if (!callback) {
2143
- kept++;
2144
- continue;
2145
- }
2146
- let remapped = event;
2147
- const causedBy = event.meta.causation.event?.id;
2148
- if (causedBy !== void 0) {
2149
- const newCausedBy = idMap.get(causedBy);
2150
- if (newCausedBy !== void 0 && newCausedBy !== causedBy) {
2151
- remapped = {
2152
- ...event,
2153
- meta: {
2154
- ...event.meta,
2155
- causation: {
2156
- ...event.meta.causation,
2157
- event: { ...event.meta.causation.event, id: newCausedBy }
2158
- }
2137
+ let at;
2138
+ let max_id;
2139
+ const probed = await source.query(
2140
+ (e) => {
2141
+ max_id = e.id;
2142
+ },
2143
+ { backward: true, limit: 1 }
2144
+ );
2145
+ if (probed !== 1) max_id = void 0;
2146
+ while (true) {
2147
+ let got = 0;
2148
+ let id;
2149
+ await source.query(
2150
+ async (event) => {
2151
+ got++;
2152
+ id = event.id;
2153
+ processed++;
2154
+ if (!is_valid(event))
2155
+ throw new Error(`Invalid event at index ${processed}`);
2156
+ if (on_progress) on_progress({ processed, id: event.id, max_id });
2157
+ if (drop_snapshots && event.name === SNAP_EVENT) {
2158
+ dropped_snaps++;
2159
+ return;
2160
+ }
2161
+ if (!callback) {
2162
+ kept++;
2163
+ return;
2164
+ }
2165
+ let remapped = event;
2166
+ const caused_by = event.meta.causation.event?.id;
2167
+ if (caused_by !== void 0) {
2168
+ const new_caused_by = id_map.get(caused_by);
2169
+ if (new_caused_by !== void 0 && new_caused_by !== caused_by) {
2170
+ remapped = {
2171
+ ...event,
2172
+ meta: {
2173
+ ...event.meta,
2174
+ causation: {
2175
+ ...event.meta.causation,
2176
+ event: { ...event.meta.causation.event, id: new_caused_by }
2177
+ }
2178
+ }
2179
+ };
2159
2180
  }
2160
- };
2161
- }
2162
- }
2163
- const newId = await callback(remapped);
2164
- idMap.set(event.id, newId);
2165
- kept++;
2181
+ }
2182
+ const new_id = await callback(remapped);
2183
+ id_map.set(event.id, new_id);
2184
+ kept++;
2185
+ },
2186
+ { after: at, limit }
2187
+ );
2188
+ if (got !== limit) break;
2189
+ at = id;
2166
2190
  }
2167
2191
  return {
2168
2192
  kept,
2169
2193
  dropped: {
2170
2194
  closed_streams: 0,
2171
- snapshots: droppedSnapshots,
2195
+ snapshots: dropped_snaps,
2172
2196
  empty_streams: 0
2173
2197
  }
2174
2198
  };
@@ -3849,18 +3873,21 @@ var Act = class {
3849
3873
  *
3850
3874
  * @see {@link Store.restore} for the underlying driver-pattern primitive.
3851
3875
  */
3852
- async restore(source, opts = {}) {
3876
+ async restore(source, opts = {}, sink) {
3853
3877
  return this._scoped(async () => {
3854
3878
  const started = Date.now();
3855
3879
  if (opts.dry_run) {
3856
3880
  const partial = await scan(source, opts);
3857
3881
  return { ...partial, duration_ms: Date.now() - started };
3858
3882
  }
3859
- const s = store2();
3860
- if (!s.restore) throw new Error("adapter has no restore capability");
3883
+ const target = sink ?? (() => {
3884
+ const s = store2();
3885
+ if (!s.restore) throw new Error("adapter has no restore capability");
3886
+ return s;
3887
+ })();
3861
3888
  let kept = 0;
3862
3889
  let dropped = { closed_streams: 0, snapshots: 0, empty_streams: 0 };
3863
- await s.restore(async (callback) => {
3890
+ await target.restore(async (callback) => {
3864
3891
  const partial = await scan(source, opts, callback);
3865
3892
  kept = partial.kept;
3866
3893
  dropped = partial.dropped;
@@ -4433,6 +4460,167 @@ function action_builder(state2) {
4433
4460
  };
4434
4461
  return builder;
4435
4462
  }
4463
+
4464
+ // src/csv.ts
4465
+ var import_node_fs = require("fs");
4466
+ var import_node_readline = require("readline");
4467
+ var CSV_COLUMNS = [
4468
+ "id",
4469
+ "name",
4470
+ "data",
4471
+ "stream",
4472
+ "version",
4473
+ "created",
4474
+ "meta"
4475
+ ];
4476
+ var CsvFile = class {
4477
+ path;
4478
+ blob;
4479
+ constructor(options) {
4480
+ if ("path" in options) {
4481
+ this.path = options.path;
4482
+ this.blob = null;
4483
+ } else {
4484
+ this.path = null;
4485
+ this.blob = options.blob;
4486
+ }
4487
+ }
4488
+ async query(callback, _filter) {
4489
+ const lines = this.blob !== null ? linesFromBlob(this.blob) : linesFromFile(this.path);
4490
+ let count = 0;
4491
+ let header = null;
4492
+ for await (const line of lines) {
4493
+ if (!line.trim()) continue;
4494
+ const fields = parseCsvLine(line);
4495
+ if (!header) {
4496
+ header = fields;
4497
+ const expected = CSV_COLUMNS.join(",");
4498
+ if (header.join(",") !== expected)
4499
+ throw new Error(`Invalid CSV header. Expected: ${expected}`);
4500
+ continue;
4501
+ }
4502
+ if (fields.length !== CSV_COLUMNS.length)
4503
+ throw new Error(
4504
+ `Row ${count + 1}: expected ${CSV_COLUMNS.length} fields, got ${fields.length}`
4505
+ );
4506
+ const event = {
4507
+ id: Number.parseInt(fields[0], 10),
4508
+ name: fields[1],
4509
+ data: JSON.parse(fields[2]),
4510
+ stream: fields[3],
4511
+ version: Number.parseInt(fields[4], 10),
4512
+ created: new Date(fields[5]),
4513
+ meta: JSON.parse(fields[6])
4514
+ };
4515
+ await Promise.resolve(callback(event));
4516
+ count++;
4517
+ }
4518
+ if (header === null)
4519
+ throw new Error("CSV must have a header and at least one row");
4520
+ return count;
4521
+ }
4522
+ async restore(driver) {
4523
+ if (this.path === null)
4524
+ throw new Error(
4525
+ "CsvFile in blob mode is read-only \u2014 provide `path` to write"
4526
+ );
4527
+ const writer = (0, import_node_fs.createWriteStream)(this.path, {
4528
+ flags: "w",
4529
+ encoding: "utf8"
4530
+ });
4531
+ let nextId = 1;
4532
+ try {
4533
+ await writeLine(writer, CSV_COLUMNS.join(","));
4534
+ await driver(async (event) => {
4535
+ const id = nextId++;
4536
+ const row = [
4537
+ String(id),
4538
+ csvEscape(event.name),
4539
+ csvEscape(JSON.stringify(event.data)),
4540
+ csvEscape(event.stream),
4541
+ String(event.version),
4542
+ event.created.toISOString(),
4543
+ csvEscape(JSON.stringify(event.meta))
4544
+ ].join(",");
4545
+ await writeLine(writer, row);
4546
+ return id;
4547
+ });
4548
+ } finally {
4549
+ await new Promise((resolve) => writer.end(resolve));
4550
+ }
4551
+ }
4552
+ async dispose() {
4553
+ }
4554
+ };
4555
+ async function* linesFromFile(path) {
4556
+ const stream = (0, import_node_fs.createReadStream)(path, { encoding: "utf8" });
4557
+ const rl = (0, import_node_readline.createInterface)({
4558
+ input: stream,
4559
+ crlfDelay: Number.POSITIVE_INFINITY
4560
+ });
4561
+ try {
4562
+ for await (const line of rl) yield line;
4563
+ } finally {
4564
+ rl.close();
4565
+ stream.close();
4566
+ }
4567
+ }
4568
+ async function* linesFromBlob(blob) {
4569
+ let start = 0;
4570
+ while (start < blob.length) {
4571
+ const nl = blob.indexOf("\n", start);
4572
+ const end = nl === -1 ? blob.length : nl;
4573
+ yield blob.slice(start, end);
4574
+ start = nl === -1 ? blob.length : nl + 1;
4575
+ await Promise.resolve();
4576
+ }
4577
+ }
4578
+ function parseCsvLine(line) {
4579
+ const fields = [];
4580
+ let i = 0;
4581
+ while (i < line.length) {
4582
+ if (line[i] === '"') {
4583
+ let value = "";
4584
+ i++;
4585
+ while (i < line.length) {
4586
+ if (line[i] === '"' && line[i + 1] === '"') {
4587
+ value += '"';
4588
+ i += 2;
4589
+ } else if (line[i] === '"') {
4590
+ i++;
4591
+ break;
4592
+ } else {
4593
+ value += line[i++];
4594
+ }
4595
+ }
4596
+ fields.push(value);
4597
+ if (line[i] === ",") i++;
4598
+ } else {
4599
+ const next = line.indexOf(",", i);
4600
+ if (next === -1) {
4601
+ fields.push(line.slice(i));
4602
+ i = line.length;
4603
+ } else {
4604
+ fields.push(line.slice(i, next));
4605
+ i = next + 1;
4606
+ }
4607
+ }
4608
+ }
4609
+ return fields;
4610
+ }
4611
+ function csvEscape(value) {
4612
+ if (/[",\n\r]/.test(value)) return `"${value.replace(/"/g, '""')}"`;
4613
+ return value;
4614
+ }
4615
+ function writeLine(writer, line) {
4616
+ return new Promise((resolve, reject) => {
4617
+ writer.write(`${line}
4618
+ `, (err) => {
4619
+ if (err) reject(err);
4620
+ else resolve();
4621
+ });
4622
+ });
4623
+ }
4436
4624
  // Annotate the CommonJS export names for ESM import in node:
4437
4625
  0 && (module.exports = {
4438
4626
  Act,
@@ -4441,6 +4629,7 @@ function action_builder(state2) {
4441
4629
  CommittedMetaSchema,
4442
4630
  ConcurrencyError,
4443
4631
  ConsoleLogger,
4632
+ CsvFile,
4444
4633
  DEFAULT_LANE,
4445
4634
  DEFAULT_MAX_SUBSCRIBED_STREAMS,
4446
4635
  DEFAULT_SETTLE_DEBOUNCE_MS,