@arbitro/client 0.2.0 → 0.5.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.mjs CHANGED
@@ -1,3 +1,18 @@
1
+ import {
2
+ frame,
3
+ packDisconnect,
4
+ packHello,
5
+ packPublish,
6
+ packPublishBatch,
7
+ packPublishWithReply
8
+ } from "./chunk-GW36GP2C.mjs";
9
+ import {
10
+ HEADER_SIZE,
11
+ OFF_ACTION,
12
+ OFF_MSG_LEN,
13
+ OFF_SEQ
14
+ } from "./chunk-C2QLJBAC.mjs";
15
+
1
16
  // src/types/config.ts
2
17
  var DeliverPolicy = /* @__PURE__ */ ((DeliverPolicy2) => {
3
18
  DeliverPolicy2["All"] = "All";
@@ -177,101 +192,6 @@ function makeLazyMessage(raw, codec, fields, onAck, onNack, onNackDelay) {
177
192
  return msg;
178
193
  }
179
194
 
180
- // src/proto/constants.ts
181
- var MAGIC_V2 = 843207233;
182
- var HELLO_SIZE = 8;
183
- var CURRENT_VERSION = 2;
184
- var HEADER_SIZE = 16;
185
- var OFF_ACTION = 0;
186
- var OFF_FLAGS = 2;
187
- var OFF_ENTRY_FLAGS = 3;
188
- var OFF_MSG_LEN = 4;
189
- var OFF_SEQ = 8;
190
-
191
- // src/proto/frame.ts
192
- function frame(action, seq, bodyLen, flags = 0, entryFlags = 0) {
193
- const buf = Buffer.allocUnsafe(HEADER_SIZE + bodyLen);
194
- buf.writeUInt16LE(action, OFF_ACTION);
195
- buf[OFF_FLAGS] = flags;
196
- buf[OFF_ENTRY_FLAGS] = entryFlags;
197
- buf.writeUInt32LE(bodyLen, OFF_MSG_LEN);
198
- buf.writeBigUInt64LE(seq, OFF_SEQ);
199
- return buf;
200
- }
201
- function packHello(caps = 2 /* Reply */) {
202
- const buf = Buffer.allocUnsafe(HELLO_SIZE);
203
- buf.writeUInt32LE(MAGIC_V2, 0);
204
- buf[4] = CURRENT_VERSION;
205
- buf[5] = 0 /* Client */;
206
- buf.writeUInt16LE(caps, 6);
207
- return buf;
208
- }
209
- function packDisconnect(seq) {
210
- return frame(1541 /* Disconnect */, seq, 0);
211
- }
212
-
213
- // src/proto/publish.ts
214
- var EMPTY = Buffer.alloc(0);
215
- function packPublish(seq, streamId2, subject, payload, flags = 0, entryFlags = 0, msgId = EMPTY) {
216
- const buf = frame(
217
- 257 /* Publish */,
218
- seq,
219
- 8 + subject.length + msgId.length + payload.length,
220
- flags,
221
- entryFlags
222
- );
223
- buf.writeUInt32LE(streamId2, HEADER_SIZE);
224
- buf.writeUInt16LE(subject.length, HEADER_SIZE + 4);
225
- buf.writeUInt16LE(msgId.length, HEADER_SIZE + 6);
226
- let off = HEADER_SIZE + 8;
227
- subject.copy(buf, off);
228
- off += subject.length;
229
- msgId.copy(buf, off);
230
- off += msgId.length;
231
- payload.copy(buf, off);
232
- return buf;
233
- }
234
- function packPublishWithReply(seq, streamId2, subject, replyTo, payload, flags = 0, entryFlags = 0) {
235
- const tail = subject.length + replyTo.length + payload.length;
236
- const buf = frame(260 /* PublishWithReply */, seq, 12 + tail, flags, entryFlags);
237
- buf.writeUInt32LE(streamId2, HEADER_SIZE);
238
- buf.writeUInt16LE(subject.length, HEADER_SIZE + 4);
239
- buf.writeUInt16LE(replyTo.length, HEADER_SIZE + 6);
240
- buf.writeUInt32LE(0, HEADER_SIZE + 8);
241
- let off = HEADER_SIZE + 12;
242
- subject.copy(buf, off);
243
- off += subject.length;
244
- replyTo.copy(buf, off);
245
- off += replyTo.length;
246
- payload.copy(buf, off);
247
- return buf;
248
- }
249
- function packPublishBatch(seq, streamId2, entries, flags = 0, entryFlags = 0) {
250
- let tail = 0;
251
- for (const e of entries) {
252
- const midLen = e.msgId ? e.msgId.length : 0;
253
- tail += 8 + e.subject.length + midLen + e.payload.length;
254
- }
255
- const buf = frame(259 /* PublishBatch */, seq, 8 + tail, flags, entryFlags);
256
- buf.writeUInt32LE(streamId2, HEADER_SIZE);
257
- buf.writeUInt32LE(entries.length, HEADER_SIZE + 4);
258
- let off = HEADER_SIZE + 8;
259
- for (const e of entries) {
260
- const mid = e.msgId ?? EMPTY;
261
- buf.writeUInt16LE(e.subject.length, off);
262
- buf.writeUInt16LE(mid.length, off + 2);
263
- buf.writeUInt32LE(e.payload.length, off + 4);
264
- off += 8;
265
- buf.write(e.subject, off);
266
- off += e.subject.length;
267
- mid.copy(buf, off);
268
- off += mid.length;
269
- e.payload.copy(buf, off);
270
- off += e.payload.length;
271
- }
272
- return buf;
273
- }
274
-
275
195
  // src/proto/delivery.ts
276
196
  function packCold(action, seq, body) {
277
197
  const utf8 = Buffer.from(JSON.stringify(body), "utf8");
@@ -572,6 +492,37 @@ function resolveLogger(logger) {
572
492
  return logger ?? NOOP;
573
493
  }
574
494
 
495
+ // src/cron/cron-frame.ts
496
+ function packCreateCron(seq, body) {
497
+ const json = Buffer.from(JSON.stringify(body), "utf8");
498
+ const buf = frame(1793 /* CreateCron */, seq, json.length);
499
+ json.copy(buf, HEADER_SIZE);
500
+ return buf;
501
+ }
502
+ function packDeleteCron(seq, name) {
503
+ const buf = frame(1794 /* DeleteCron */, seq, name.length);
504
+ name.copy(buf, HEADER_SIZE);
505
+ return buf;
506
+ }
507
+ var CRON_FIRE_FIXED = 2 + 8 + 8;
508
+ function decodeCronFire(body) {
509
+ if (body.length < CRON_FIRE_FIXED) return void 0;
510
+ const nameLen = body.readUInt16LE(0);
511
+ if (body.length < CRON_FIRE_FIXED + nameLen) return void 0;
512
+ const fireTimeMs = body.readBigUInt64LE(2);
513
+ const fireCount = body.readBigUInt64LE(10);
514
+ const name = body.subarray(18, 18 + nameLen).toString();
515
+ return { name, fireTimeMs, fireCount };
516
+ }
517
+ function packCronAck(seq, name, ok) {
518
+ const bodyLen = 3 + name.length;
519
+ const buf = frame(1797 /* CronAck */, seq, bodyLen);
520
+ buf.writeUInt16LE(name.length, HEADER_SIZE);
521
+ buf[HEADER_SIZE + 2] = ok ? 0 : 1;
522
+ name.copy(buf, HEADER_SIZE + 3);
523
+ return buf;
524
+ }
525
+
575
526
  // src/net/connection.ts
576
527
  function parseAddr(addr) {
577
528
  const i = addr.lastIndexOf(":");
@@ -595,6 +546,7 @@ var Connection = class _Connection {
595
546
  log;
596
547
  activeSubs = /* @__PURE__ */ new Map();
597
548
  metrics;
549
+ cronState;
598
550
  attachSocket(socket) {
599
551
  socket.setNoDelay(true);
600
552
  socket.on("data", (chunk) => this.framer.push(chunk, (f) => this.onFrame(f)));
@@ -649,6 +601,10 @@ var Connection = class _Connection {
649
601
  setMetrics(m) {
650
602
  this.metrics = m;
651
603
  }
604
+ /** Attach cron state so the connection can dispatch CronFire frames. */
605
+ setCronState(s) {
606
+ this.cronState = s;
607
+ }
652
608
  // ── Frame routing ─────────────────────────────────────────────────────────
653
609
  // Seq-based dispatch: match reply.header.seq → pending request. O(1).
654
610
  resolvePending(frame2) {
@@ -700,6 +656,10 @@ var Connection = class _Connection {
700
656
  this.handleBatchDeliver(frame2);
701
657
  return;
702
658
  }
659
+ case 1796 /* CronFire */: {
660
+ this.dispatchCronFire(frame2);
661
+ return;
662
+ }
703
663
  case 1538 /* Pong */:
704
664
  return;
705
665
  default: {
@@ -749,6 +709,19 @@ var Connection = class _Connection {
749
709
  off = tailEnd;
750
710
  }
751
711
  }
712
+ // ── Cron dispatch ──────────────────────────────────────────────────────────
713
+ dispatchCronFire(frame2) {
714
+ const body = frame2.subarray(HEADER_SIZE);
715
+ const view = decodeCronFire(body);
716
+ if (!view) return;
717
+ const handler = this.cronState?.getHandler(view.name);
718
+ const nameBuf = Buffer.from(view.name);
719
+ if (!handler) {
720
+ this.send(packCronAck(this.nextSeq(), nameBuf, false));
721
+ return;
722
+ }
723
+ handler({ name: view.name, fireTime: view.fireTimeMs, fireCount: view.fireCount }).then(() => this.send(packCronAck(this.nextSeq(), nameBuf, true))).catch(() => this.send(packCronAck(this.nextSeq(), nameBuf, false)));
724
+ }
752
725
  // ── Subscriptions ─────────────────────────────────────────────────────────
753
726
  async sendSubscribeV2(consumerId, filter, handler, onRenew) {
754
727
  return new Promise((resolve, reject) => {
@@ -791,6 +764,14 @@ var Connection = class _Connection {
791
764
  }).catch(() => {
792
765
  });
793
766
  }
767
+ this.replayCrons();
768
+ }
769
+ replayCrons() {
770
+ if (!this.cronState) return;
771
+ for (const { config } of this.cronState.allConfigs()) {
772
+ const seq = this.nextSeq();
773
+ this.socket.write(packCreateCron(seq, config));
774
+ }
794
775
  }
795
776
  // ── Routes (internal use) ─────────────────────────────────────────────────
796
777
  registerRoute(consumerId, handler) {
@@ -1074,9 +1055,9 @@ var ClientMetrics = class {
1074
1055
  };
1075
1056
 
1076
1057
  // src/stream/publish.ts
1077
- var EMPTY2 = Buffer.alloc(0);
1058
+ var EMPTY = Buffer.alloc(0);
1078
1059
  function toMsgIdBuf(id) {
1079
- if (id == null) return EMPTY2;
1060
+ if (id == null) return EMPTY;
1080
1061
  return typeof id === "string" ? Buffer.from(id) : id;
1081
1062
  }
1082
1063
  async function streamPublishAck(conn, sid, subject, data, opts) {
@@ -1105,6 +1086,86 @@ async function streamRequest(conn, sid, subject, data, timeoutMs) {
1105
1086
  return Buffer.alloc(0);
1106
1087
  }
1107
1088
 
1089
+ // src/cron/cron-builder.ts
1090
+ var CronBuilder = class {
1091
+ constructor(conn, cronState, cronName) {
1092
+ this.conn = conn;
1093
+ this.cronState = cronState;
1094
+ this.cronName = cronName;
1095
+ }
1096
+ expr;
1097
+ timezone;
1098
+ timeoutMs = 3e4;
1099
+ allowOverlap = false;
1100
+ every(expression) {
1101
+ this.expr = expression;
1102
+ return this;
1103
+ }
1104
+ tz(timezone) {
1105
+ this.timezone = timezone;
1106
+ return this;
1107
+ }
1108
+ timeout(ms) {
1109
+ this.timeoutMs = ms;
1110
+ return this;
1111
+ }
1112
+ overlap(allow) {
1113
+ this.allowOverlap = allow;
1114
+ return this;
1115
+ }
1116
+ async run(handler) {
1117
+ if (!this.expr) throw new Error("cron expression required \u2014 call .every()");
1118
+ const body = {
1119
+ name: this.cronName,
1120
+ every: this.expr,
1121
+ tz: this.timezone,
1122
+ timeout_ms: this.timeoutMs,
1123
+ overlap: this.allowOverlap
1124
+ };
1125
+ const seq = this.conn.nextSeq();
1126
+ await this.conn.sendExpectReply(packCreateCron(seq, body));
1127
+ this.cronState.register(this.cronName, body, handler);
1128
+ return new CronHandle(this.conn, this.cronState, this.cronName);
1129
+ }
1130
+ };
1131
+ var CronHandle = class {
1132
+ constructor(conn, cronState, cronName) {
1133
+ this.conn = conn;
1134
+ this.cronState = cronState;
1135
+ this.cronName = cronName;
1136
+ }
1137
+ get name() {
1138
+ return this.cronName;
1139
+ }
1140
+ async stop() {
1141
+ const nameBuf = Buffer.from(this.cronName);
1142
+ const seq = this.conn.nextSeq();
1143
+ await this.conn.sendExpectReply(packDeleteCron(seq, nameBuf));
1144
+ this.cronState.remove(this.cronName);
1145
+ }
1146
+ };
1147
+
1148
+ // src/cron/cron-state.ts
1149
+ var CronState = class {
1150
+ handlers = /* @__PURE__ */ new Map();
1151
+ register(name, config, handler) {
1152
+ this.handlers.set(name, { handler, config });
1153
+ }
1154
+ remove(name) {
1155
+ this.handlers.delete(name);
1156
+ }
1157
+ getHandler(name) {
1158
+ return this.handlers.get(name)?.handler;
1159
+ }
1160
+ allConfigs() {
1161
+ const out = [];
1162
+ for (const [name, entry] of this.handlers) {
1163
+ out.push({ name, config: entry.config });
1164
+ }
1165
+ return out;
1166
+ }
1167
+ };
1168
+
1108
1169
  // src/client/client.ts
1109
1170
  var DEFAULT_CONFIG = {
1110
1171
  servers: ["127.0.0.1:9898"],
@@ -1119,6 +1180,7 @@ var ArbitroClient = class {
1119
1180
  logger;
1120
1181
  sidCache = /* @__PURE__ */ new Map();
1121
1182
  _metrics = new ClientMetrics();
1183
+ _cronState = new CronState();
1122
1184
  constructor(config) {
1123
1185
  this.cfg = { ...DEFAULT_CONFIG, ...config };
1124
1186
  this.tls = config.tls;
@@ -1135,6 +1197,7 @@ var ArbitroClient = class {
1135
1197
  this.logger
1136
1198
  );
1137
1199
  this.conn.setMetrics(this._metrics);
1200
+ this.conn.setCronState(this._cronState);
1138
1201
  return this;
1139
1202
  }
1140
1203
  /**
@@ -1220,6 +1283,22 @@ var ArbitroClient = class {
1220
1283
  const sid = await this.resolveStreamId(streamName);
1221
1284
  return streamRequest(this.conn, sid, subject, data, timeoutMs);
1222
1285
  }
1286
+ /**
1287
+ * Publish a message with a delivery delay. The broker parks the message
1288
+ * in its delayed journal and delivers it to consumers after `delayMs`
1289
+ * milliseconds. Returns a Promise that resolves once the broker confirms
1290
+ * receipt.
1291
+ */
1292
+ async publishDelayed(streamName, subject, data, delayMs) {
1293
+ const sid = await this.resolveStreamId(streamName);
1294
+ const { packPublishDelayed: packPublishDelayed2 } = await import("./publish-BSVUMN7T.mjs");
1295
+ const { Flag: Flag3 } = await import("./constants-57DO6N3H.mjs");
1296
+ const subj = Buffer.from(this.prefixed(subject));
1297
+ await this.conn.sendExpectReply(
1298
+ packPublishDelayed2(this.conn.nextSeq(), sid, subj, data, BigInt(delayMs), Flag3.AckReq)
1299
+ );
1300
+ this._metrics.publishesSent++;
1301
+ }
1223
1302
  async subscribe(streamName, configOrCb, callbackOrOpts, opts) {
1224
1303
  let config;
1225
1304
  let callback;
@@ -1328,7 +1407,7 @@ var ArbitroClient = class {
1328
1407
  async createConsumerRaw(streamName, config) {
1329
1408
  const sid = await this.resolveStreamId(streamName);
1330
1409
  const name = Buffer.from(config.name ?? streamName);
1331
- const group = Buffer.from(config.name ?? streamName);
1410
+ const group = Buffer.from(config.group ?? config.name ?? streamName);
1332
1411
  const filter = Buffer.from(config.filter ?? "");
1333
1412
  const ackPolicyByte = config.ackPolicy === "none" /* None */ ? 0 : 1;
1334
1413
  const opts = {
@@ -1339,7 +1418,7 @@ var ArbitroClient = class {
1339
1418
  maxInflight: config.maxAckPending ?? 0,
1340
1419
  ackPolicy: ackPolicyByte,
1341
1420
  deliverPolicy: deliverPolicyToU8(config.deliverPolicy),
1342
- deliverMode: config.fanout ? 1 : 0,
1421
+ deliverMode: config.fanout ? 0 : 1,
1343
1422
  ackWaitMs: config.ackWaitMs ?? 0,
1344
1423
  startSeq: BigInt(config.startSeq ?? 0)
1345
1424
  };
@@ -1454,6 +1533,11 @@ var ArbitroClient = class {
1454
1533
  stream(name, config) {
1455
1534
  return new Stream(this, name, config);
1456
1535
  }
1536
+ // ── Cron ──────────────────────────────────────────────────────────────────
1537
+ /** Start building a cron job. Call `.every()` then `.run()` to register. */
1538
+ cron(name) {
1539
+ return new CronBuilder(this.conn, this._cronState, name);
1540
+ }
1457
1541
  // ── Lifecycle ─────────────────────────────────────────────────────────────
1458
1542
  async close() {
1459
1543
  await this.conn.close();
@@ -1533,6 +1617,257 @@ function streamId(name) {
1533
1617
  return h >>> 0;
1534
1618
  }
1535
1619
 
1620
+ // src/workflow/task.ts
1621
+ var TASK_HEADER = 7;
1622
+ var COMPENSATION_BIT = 32768;
1623
+ function encodeTask(instanceId, stepIndex, attempt, context) {
1624
+ const buf = Buffer.allocUnsafe(TASK_HEADER + context.length);
1625
+ buf.writeUInt32LE(instanceId, 0);
1626
+ buf.writeUInt16LE(stepIndex, 4);
1627
+ buf[6] = attempt;
1628
+ context.copy(buf, TASK_HEADER);
1629
+ return buf;
1630
+ }
1631
+ function decodeTask(payload) {
1632
+ if (payload.length < TASK_HEADER) return void 0;
1633
+ return {
1634
+ instanceId: payload.readUInt32LE(0),
1635
+ stepIndex: payload.readUInt16LE(4),
1636
+ attempt: payload[6],
1637
+ context: payload.subarray(TASK_HEADER)
1638
+ };
1639
+ }
1640
+
1641
+ // src/workflow/handle.ts
1642
+ var nextInstanceId = 1;
1643
+ function allocInstanceId() {
1644
+ return nextInstanceId++;
1645
+ }
1646
+ var WorkflowHandle = class {
1647
+ constructor(workflowName, taskStreamName, dlqStreamName, sub, triggerSub) {
1648
+ this.workflowName = workflowName;
1649
+ this.taskStreamName = taskStreamName;
1650
+ this.dlqStreamName = dlqStreamName;
1651
+ this.sub = sub;
1652
+ this.triggerSub = triggerSub;
1653
+ }
1654
+ get name() {
1655
+ return this.workflowName;
1656
+ }
1657
+ get taskStream() {
1658
+ return this.taskStreamName;
1659
+ }
1660
+ get dlqStream() {
1661
+ return this.dlqStreamName;
1662
+ }
1663
+ async trigger(client, context) {
1664
+ const instanceId = allocInstanceId();
1665
+ const msgId = `wf:${instanceId}:0:0`;
1666
+ const subject = `_wf.${this.workflowName}.step.0`;
1667
+ const task = encodeTask(instanceId, 0, 0, context);
1668
+ await client.publish(this.taskStreamName, subject, task, { msgId });
1669
+ return instanceId;
1670
+ }
1671
+ };
1672
+
1673
+ // src/workflow/processor.ts
1674
+ async function processMessage(cfg, msg) {
1675
+ const task = decodeTask(msg.data());
1676
+ if (!task) {
1677
+ msg.ack();
1678
+ return;
1679
+ }
1680
+ if (task.context.length > cfg.maxContextSize) {
1681
+ msg.ack();
1682
+ return;
1683
+ }
1684
+ const isCompensation = (task.stepIndex & COMPENSATION_BIT) !== 0;
1685
+ if (isCompensation) {
1686
+ await runCompensation(cfg, msg, task);
1687
+ return;
1688
+ }
1689
+ if (task.stepIndex >= cfg.steps.length) {
1690
+ msg.ack();
1691
+ return;
1692
+ }
1693
+ await runStep(cfg, msg, task);
1694
+ }
1695
+ async function runCompensation(cfg, msg, task) {
1696
+ const idx = task.stepIndex & ~COMPENSATION_BIT;
1697
+ const comp = cfg.steps[idx]?.compensation;
1698
+ if (comp) {
1699
+ try {
1700
+ await comp({ name: cfg.name, instanceId: task.instanceId, stepIndex: idx, attempt: task.attempt, context: task.context });
1701
+ } catch {
1702
+ }
1703
+ }
1704
+ msg.ack();
1705
+ }
1706
+ async function runStep(cfg, msg, task) {
1707
+ const handler = cfg.steps[task.stepIndex].handler;
1708
+ try {
1709
+ const result = await handler({
1710
+ name: cfg.name,
1711
+ instanceId: task.instanceId,
1712
+ stepIndex: task.stepIndex,
1713
+ attempt: task.attempt,
1714
+ context: task.context
1715
+ });
1716
+ if (result.context.length > cfg.maxContextSize) {
1717
+ msg.nack();
1718
+ return;
1719
+ }
1720
+ await advance(cfg, msg, task, result);
1721
+ } catch (err) {
1722
+ await onFailure(cfg, msg, task, err);
1723
+ }
1724
+ }
1725
+ async function advance(cfg, msg, task, result) {
1726
+ const nextStep = task.stepIndex + 1;
1727
+ if (nextStep < cfg.steps.length) {
1728
+ const msgId = `wf:${task.instanceId}:${nextStep}:0`;
1729
+ const subject = `_wf.${cfg.name}.step.${nextStep}`;
1730
+ const buf = encodeTask(task.instanceId, nextStep, 0, result.context);
1731
+ await cfg.client.publish(cfg.taskStreamName, subject, buf, { msgId });
1732
+ }
1733
+ msg.ack();
1734
+ }
1735
+ async function onFailure(cfg, msg, task, err) {
1736
+ if (task.attempt >= cfg.maxRetries) {
1737
+ await publishDlq(cfg, task, err);
1738
+ await publishCompensations(cfg, task);
1739
+ msg.ack();
1740
+ } else {
1741
+ msg.nack();
1742
+ }
1743
+ }
1744
+ async function publishDlq(cfg, task, err) {
1745
+ const dlqSubject = `_wf.${cfg.name}.dlq.${task.stepIndex}`;
1746
+ const errBytes = Buffer.from(String(err));
1747
+ const buf = Buffer.allocUnsafe(7 + 4 + errBytes.length + task.context.length);
1748
+ buf.writeUInt32LE(task.instanceId, 0);
1749
+ buf.writeUInt16LE(task.stepIndex, 4);
1750
+ buf[6] = task.attempt;
1751
+ buf.writeUInt32LE(errBytes.length, 7);
1752
+ errBytes.copy(buf, 11);
1753
+ task.context.copy(buf, 11 + errBytes.length);
1754
+ const msgId = `wf:${task.instanceId}:dlq:${task.stepIndex}`;
1755
+ await cfg.client.publish(cfg.dlqStreamName, dlqSubject, buf, { msgId }).catch(() => {
1756
+ });
1757
+ }
1758
+ async function publishCompensations(cfg, task) {
1759
+ for (let i = task.stepIndex - 1; i >= 0; i--) {
1760
+ const compStep = COMPENSATION_BIT | i;
1761
+ const subject = `_wf.${cfg.name}.compensate.${i}`;
1762
+ const buf = encodeTask(task.instanceId, compStep, 0, task.context);
1763
+ const msgId = `wf:${task.instanceId}:comp:${i}`;
1764
+ await cfg.client.publish(cfg.taskStreamName, subject, buf, { msgId }).catch(() => {
1765
+ });
1766
+ }
1767
+ }
1768
+
1769
+ // src/workflow/workflow.ts
1770
+ var nextWorkerUid = 1;
1771
+ var WorkflowBuilder = class {
1772
+ constructor(client, workflowName) {
1773
+ this.client = client;
1774
+ this.workflowName = workflowName;
1775
+ }
1776
+ triggerSubject;
1777
+ triggerStreamName;
1778
+ steps = [];
1779
+ ackWaitMs = 3e4;
1780
+ maxInflightVal = 10;
1781
+ maxRetriesVal = 3;
1782
+ maxContextSizeVal = 256 * 1024;
1783
+ trigger(subject) {
1784
+ this.triggerSubject = subject;
1785
+ return this;
1786
+ }
1787
+ triggerStream(streamName) {
1788
+ this.triggerStreamName = streamName;
1789
+ return this;
1790
+ }
1791
+ step(name, handler) {
1792
+ this.steps.push({ name, handler, compensation: void 0 });
1793
+ return this;
1794
+ }
1795
+ /** Compensation handler for the most recently added step. */
1796
+ compensate(_stepName, handler) {
1797
+ const last = this.steps[this.steps.length - 1];
1798
+ if (last) last.compensation = handler;
1799
+ return this;
1800
+ }
1801
+ ackWait(ms) {
1802
+ this.ackWaitMs = ms;
1803
+ return this;
1804
+ }
1805
+ inflight(n) {
1806
+ this.maxInflightVal = n;
1807
+ return this;
1808
+ }
1809
+ maxRetries(n) {
1810
+ this.maxRetriesVal = n;
1811
+ return this;
1812
+ }
1813
+ maxContextSize(bytes) {
1814
+ this.maxContextSizeVal = bytes;
1815
+ return this;
1816
+ }
1817
+ async start() {
1818
+ if (!this.triggerSubject) throw new Error("trigger subject required");
1819
+ if (this.steps.length === 0) throw new Error("at least one step required");
1820
+ const name = this.workflowName;
1821
+ const taskStream = `_wf.${name}.tasks`;
1822
+ const taskSubject = `_wf.${name}.>`;
1823
+ const dlqStream = `_wf.${name}.dlq`;
1824
+ const dlqSubject = `_wf.${name}.dlq.>`;
1825
+ await this.client.upsertStream(taskStream, { subjectFilter: taskSubject, idempotencyWindowMs: 3e5 });
1826
+ await this.client.upsertStream(dlqStream, { subjectFilter: dlqSubject });
1827
+ const cfg = {
1828
+ client: this.client,
1829
+ name,
1830
+ taskStreamName: taskStream,
1831
+ dlqStreamName: dlqStream,
1832
+ steps: this.steps,
1833
+ maxContextSize: this.maxContextSizeVal,
1834
+ maxRetries: this.maxRetriesVal
1835
+ };
1836
+ const sub = await this.subscribeWorker(cfg, taskStream, taskSubject);
1837
+ const triggerSub = await this.subscribeTrigger(taskStream, name);
1838
+ return new WorkflowHandle(name, taskStream, dlqStream, sub, triggerSub);
1839
+ }
1840
+ async subscribeWorker(cfg, taskStream, taskSubject) {
1841
+ const uid = nextWorkerUid++;
1842
+ return this.client.subscribe(taskStream, {
1843
+ name: `_wf_${cfg.name}_w${uid}`,
1844
+ group: `_wf_${cfg.name}_workers`,
1845
+ filter: taskSubject,
1846
+ ackPolicy: "explicit" /* Explicit */,
1847
+ ackWaitMs: this.ackWaitMs,
1848
+ maxAckPending: this.maxInflightVal
1849
+ }, (msg) => {
1850
+ void processMessage(cfg, msg);
1851
+ });
1852
+ }
1853
+ async subscribeTrigger(taskStream, name) {
1854
+ if (!this.triggerSubject || !this.triggerStreamName) return void 0;
1855
+ const subject = this.triggerSubject;
1856
+ return this.client.subscribe(this.triggerStreamName, {
1857
+ name: `_wf_${name}_trigger`,
1858
+ filter: subject,
1859
+ ackPolicy: "explicit" /* Explicit */,
1860
+ ackWaitMs: this.ackWaitMs,
1861
+ maxAckPending: 1
1862
+ }, async (msg) => {
1863
+ const id = allocInstanceId();
1864
+ const taskBuf = encodeTask(id, 0, 0, msg.data());
1865
+ await this.client.publish(taskStream, `_wf.${name}.step.0`, taskBuf, { msgId: `wf:${id}:0:0` });
1866
+ msg.ack();
1867
+ });
1868
+ }
1869
+ };
1870
+
1536
1871
  // src/utils/zod.ts
1537
1872
  import { Packr as Packr2, Unpackr as Unpackr2 } from "msgpackr";
1538
1873
  var packr = new Packr2({ structuredClone: false, useRecords: false });
@@ -1553,8 +1888,11 @@ export {
1553
1888
  AckPolicy,
1554
1889
  ArbitroClient,
1555
1890
  ArbitroError,
1891
+ COMPENSATION_BIT,
1556
1892
  Codec,
1557
1893
  Consumer,
1894
+ CronBuilder,
1895
+ CronHandle,
1558
1896
  DeliverPolicy,
1559
1897
  ErrorCode,
1560
1898
  JournalType,
@@ -1564,6 +1902,8 @@ export {
1564
1902
  StringCodec,
1565
1903
  Subscription,
1566
1904
  Topic,
1905
+ WorkflowBuilder,
1906
+ WorkflowHandle,
1567
1907
  decodeJson,
1568
1908
  decodeString,
1569
1909
  encodeJson,