@rotorsoft/act 1.2.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/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,51 @@ 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
+ async function* iterate(source, filter) {
2093
+ const state2 = {
2094
+ slot: null,
2095
+ onProduce: null,
2096
+ onConsume: null,
2097
+ done: false,
2098
+ error: void 0
2099
+ };
2100
+ const wakeProduce = () => {
2101
+ const fn = state2.onProduce;
2102
+ state2.onProduce = null;
2103
+ if (fn) fn();
2104
+ };
2105
+ void source.query((event) => {
2106
+ state2.slot = event;
2107
+ wakeProduce();
2108
+ return new Promise((resolve) => {
2109
+ state2.onConsume = () => resolve();
2110
+ });
2111
+ }, filter).then(
2112
+ () => {
2113
+ state2.done = true;
2114
+ wakeProduce();
2115
+ },
2116
+ (err) => {
2117
+ state2.error = err;
2118
+ state2.done = true;
2119
+ wakeProduce();
2120
+ }
2121
+ );
2122
+ while (true) {
2123
+ if (state2.slot === null && !state2.done)
2124
+ await new Promise((resolve) => {
2125
+ state2.onProduce = resolve;
2126
+ });
2127
+ if (state2.error) throw state2.error;
2128
+ if (state2.slot === null) return;
2129
+ const event = state2.slot;
2130
+ state2.slot = null;
2131
+ const fn = state2.onConsume;
2132
+ state2.onConsume = null;
2133
+ fn();
2134
+ yield event;
2135
+ }
2136
+ }
2091
2137
  async function snap(snapshot) {
2092
2138
  try {
2093
2139
  const { id, stream, name, meta, version } = snapshot.event;
@@ -2131,7 +2177,7 @@ async function scan(source, opts = {}, callback) {
2131
2177
  let kept = 0;
2132
2178
  let droppedSnapshots = 0;
2133
2179
  let processed = 0;
2134
- for await (const event of source) {
2180
+ for await (const event of iterate(source)) {
2135
2181
  processed++;
2136
2182
  if (!isValid(event)) throw new Error(`Invalid event at index ${processed}`);
2137
2183
  if (on_progress) on_progress({ processed });
@@ -3849,18 +3895,21 @@ var Act = class {
3849
3895
  *
3850
3896
  * @see {@link Store.restore} for the underlying driver-pattern primitive.
3851
3897
  */
3852
- async restore(source, opts = {}) {
3898
+ async restore(source, opts = {}, sink) {
3853
3899
  return this._scoped(async () => {
3854
3900
  const started = Date.now();
3855
3901
  if (opts.dry_run) {
3856
3902
  const partial = await scan(source, opts);
3857
3903
  return { ...partial, duration_ms: Date.now() - started };
3858
3904
  }
3859
- const s = store2();
3860
- if (!s.restore) throw new Error("adapter has no restore capability");
3905
+ const target = sink ?? (() => {
3906
+ const s = store2();
3907
+ if (!s.restore) throw new Error("adapter has no restore capability");
3908
+ return s;
3909
+ })();
3861
3910
  let kept = 0;
3862
3911
  let dropped = { closed_streams: 0, snapshots: 0, empty_streams: 0 };
3863
- await s.restore(async (callback) => {
3912
+ await target.restore(async (callback) => {
3864
3913
  const partial = await scan(source, opts, callback);
3865
3914
  kept = partial.kept;
3866
3915
  dropped = partial.dropped;
@@ -4433,6 +4482,167 @@ function action_builder(state2) {
4433
4482
  };
4434
4483
  return builder;
4435
4484
  }
4485
+
4486
+ // src/csv.ts
4487
+ var import_node_fs = require("fs");
4488
+ var import_node_readline = require("readline");
4489
+ var CSV_COLUMNS = [
4490
+ "id",
4491
+ "name",
4492
+ "data",
4493
+ "stream",
4494
+ "version",
4495
+ "created",
4496
+ "meta"
4497
+ ];
4498
+ var CsvFile = class {
4499
+ path;
4500
+ blob;
4501
+ constructor(options) {
4502
+ if ("path" in options) {
4503
+ this.path = options.path;
4504
+ this.blob = null;
4505
+ } else {
4506
+ this.path = null;
4507
+ this.blob = options.blob;
4508
+ }
4509
+ }
4510
+ async query(callback, _filter) {
4511
+ const lines = this.blob !== null ? linesFromBlob(this.blob) : linesFromFile(this.path);
4512
+ let count = 0;
4513
+ let header = null;
4514
+ for await (const line of lines) {
4515
+ if (!line.trim()) continue;
4516
+ const fields = parseCsvLine(line);
4517
+ if (!header) {
4518
+ header = fields;
4519
+ const expected = CSV_COLUMNS.join(",");
4520
+ if (header.join(",") !== expected)
4521
+ throw new Error(`Invalid CSV header. Expected: ${expected}`);
4522
+ continue;
4523
+ }
4524
+ if (fields.length !== CSV_COLUMNS.length)
4525
+ throw new Error(
4526
+ `Row ${count + 1}: expected ${CSV_COLUMNS.length} fields, got ${fields.length}`
4527
+ );
4528
+ const event = {
4529
+ id: Number.parseInt(fields[0], 10),
4530
+ name: fields[1],
4531
+ data: JSON.parse(fields[2]),
4532
+ stream: fields[3],
4533
+ version: Number.parseInt(fields[4], 10),
4534
+ created: new Date(fields[5]),
4535
+ meta: JSON.parse(fields[6])
4536
+ };
4537
+ await Promise.resolve(callback(event));
4538
+ count++;
4539
+ }
4540
+ if (header === null)
4541
+ throw new Error("CSV must have a header and at least one row");
4542
+ return count;
4543
+ }
4544
+ async restore(driver) {
4545
+ if (this.path === null)
4546
+ throw new Error(
4547
+ "CsvFile in blob mode is read-only \u2014 provide `path` to write"
4548
+ );
4549
+ const writer = (0, import_node_fs.createWriteStream)(this.path, {
4550
+ flags: "w",
4551
+ encoding: "utf8"
4552
+ });
4553
+ let nextId = 1;
4554
+ try {
4555
+ await writeLine(writer, CSV_COLUMNS.join(","));
4556
+ await driver(async (event) => {
4557
+ const id = nextId++;
4558
+ const row = [
4559
+ String(id),
4560
+ csvEscape(event.name),
4561
+ csvEscape(JSON.stringify(event.data)),
4562
+ csvEscape(event.stream),
4563
+ String(event.version),
4564
+ event.created.toISOString(),
4565
+ csvEscape(JSON.stringify(event.meta))
4566
+ ].join(",");
4567
+ await writeLine(writer, row);
4568
+ return id;
4569
+ });
4570
+ } finally {
4571
+ await new Promise((resolve) => writer.end(resolve));
4572
+ }
4573
+ }
4574
+ async dispose() {
4575
+ }
4576
+ };
4577
+ async function* linesFromFile(path) {
4578
+ const stream = (0, import_node_fs.createReadStream)(path, { encoding: "utf8" });
4579
+ const rl = (0, import_node_readline.createInterface)({
4580
+ input: stream,
4581
+ crlfDelay: Number.POSITIVE_INFINITY
4582
+ });
4583
+ try {
4584
+ for await (const line of rl) yield line;
4585
+ } finally {
4586
+ rl.close();
4587
+ stream.close();
4588
+ }
4589
+ }
4590
+ async function* linesFromBlob(blob) {
4591
+ let start = 0;
4592
+ while (start < blob.length) {
4593
+ const nl = blob.indexOf("\n", start);
4594
+ const end = nl === -1 ? blob.length : nl;
4595
+ yield blob.slice(start, end);
4596
+ start = nl === -1 ? blob.length : nl + 1;
4597
+ await Promise.resolve();
4598
+ }
4599
+ }
4600
+ function parseCsvLine(line) {
4601
+ const fields = [];
4602
+ let i = 0;
4603
+ while (i < line.length) {
4604
+ if (line[i] === '"') {
4605
+ let value = "";
4606
+ i++;
4607
+ while (i < line.length) {
4608
+ if (line[i] === '"' && line[i + 1] === '"') {
4609
+ value += '"';
4610
+ i += 2;
4611
+ } else if (line[i] === '"') {
4612
+ i++;
4613
+ break;
4614
+ } else {
4615
+ value += line[i++];
4616
+ }
4617
+ }
4618
+ fields.push(value);
4619
+ if (line[i] === ",") i++;
4620
+ } else {
4621
+ const next = line.indexOf(",", i);
4622
+ if (next === -1) {
4623
+ fields.push(line.slice(i));
4624
+ i = line.length;
4625
+ } else {
4626
+ fields.push(line.slice(i, next));
4627
+ i = next + 1;
4628
+ }
4629
+ }
4630
+ }
4631
+ return fields;
4632
+ }
4633
+ function csvEscape(value) {
4634
+ if (/[",\n\r]/.test(value)) return `"${value.replace(/"/g, '""')}"`;
4635
+ return value;
4636
+ }
4637
+ function writeLine(writer, line) {
4638
+ return new Promise((resolve, reject) => {
4639
+ writer.write(`${line}
4640
+ `, (err) => {
4641
+ if (err) reject(err);
4642
+ else resolve();
4643
+ });
4644
+ });
4645
+ }
4436
4646
  // Annotate the CommonJS export names for ESM import in node:
4437
4647
  0 && (module.exports = {
4438
4648
  Act,
@@ -4441,6 +4651,7 @@ function action_builder(state2) {
4441
4651
  CommittedMetaSchema,
4442
4652
  ConcurrencyError,
4443
4653
  ConsoleLogger,
4654
+ CsvFile,
4444
4655
  DEFAULT_LANE,
4445
4656
  DEFAULT_MAX_SUBSCRIBED_STREAMS,
4446
4657
  DEFAULT_SETTLE_DEBOUNCE_MS,