@arbitro/client 0.4.1 → 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
@@ -5,13 +5,13 @@ import {
5
5
  packPublish,
6
6
  packPublishBatch,
7
7
  packPublishWithReply
8
- } from "./chunk-SKCXQO7R.mjs";
8
+ } from "./chunk-GW36GP2C.mjs";
9
9
  import {
10
10
  HEADER_SIZE,
11
11
  OFF_ACTION,
12
12
  OFF_MSG_LEN,
13
13
  OFF_SEQ
14
- } from "./chunk-6BCX2E2R.mjs";
14
+ } from "./chunk-C2QLJBAC.mjs";
15
15
 
16
16
  // src/types/config.ts
17
17
  var DeliverPolicy = /* @__PURE__ */ ((DeliverPolicy2) => {
@@ -1291,8 +1291,8 @@ var ArbitroClient = class {
1291
1291
  */
1292
1292
  async publishDelayed(streamName, subject, data, delayMs) {
1293
1293
  const sid = await this.resolveStreamId(streamName);
1294
- const { packPublishDelayed: packPublishDelayed2 } = await import("./publish-UA3YZGMK.mjs");
1295
- const { Flag: Flag3 } = await import("./constants-KF57DJ2L.mjs");
1294
+ const { packPublishDelayed: packPublishDelayed2 } = await import("./publish-BSVUMN7T.mjs");
1295
+ const { Flag: Flag3 } = await import("./constants-57DO6N3H.mjs");
1296
1296
  const subj = Buffer.from(this.prefixed(subject));
1297
1297
  await this.conn.sendExpectReply(
1298
1298
  packPublishDelayed2(this.conn.nextSeq(), sid, subj, data, BigInt(delayMs), Flag3.AckReq)
@@ -1407,7 +1407,7 @@ var ArbitroClient = class {
1407
1407
  async createConsumerRaw(streamName, config) {
1408
1408
  const sid = await this.resolveStreamId(streamName);
1409
1409
  const name = Buffer.from(config.name ?? streamName);
1410
- const group = Buffer.from(config.name ?? streamName);
1410
+ const group = Buffer.from(config.group ?? config.name ?? streamName);
1411
1411
  const filter = Buffer.from(config.filter ?? "");
1412
1412
  const ackPolicyByte = config.ackPolicy === "none" /* None */ ? 0 : 1;
1413
1413
  const opts = {
@@ -1617,6 +1617,257 @@ function streamId(name) {
1617
1617
  return h >>> 0;
1618
1618
  }
1619
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
+
1620
1871
  // src/utils/zod.ts
1621
1872
  import { Packr as Packr2, Unpackr as Unpackr2 } from "msgpackr";
1622
1873
  var packr = new Packr2({ structuredClone: false, useRecords: false });
@@ -1637,6 +1888,7 @@ export {
1637
1888
  AckPolicy,
1638
1889
  ArbitroClient,
1639
1890
  ArbitroError,
1891
+ COMPENSATION_BIT,
1640
1892
  Codec,
1641
1893
  Consumer,
1642
1894
  CronBuilder,
@@ -1650,6 +1902,8 @@ export {
1650
1902
  StringCodec,
1651
1903
  Subscription,
1652
1904
  Topic,
1905
+ WorkflowBuilder,
1906
+ WorkflowHandle,
1653
1907
  decodeJson,
1654
1908
  decodeString,
1655
1909
  encodeJson,