@arbitro/client 0.4.1 → 0.5.2

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-BSPQZJHF.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-3EHQPPLU.mjs";
15
15
 
16
16
  // src/types/config.ts
17
17
  var DeliverPolicy = /* @__PURE__ */ ((DeliverPolicy2) => {
@@ -270,6 +270,7 @@ var packDrainSubject = (seq, name, subject) => packCold2(1030 /* DrainSubject */
270
270
  name: bytesArr(name),
271
271
  subject: bytesArr(subject)
272
272
  });
273
+ var packDeleteMessage = (seq, name, msgSeq) => packCold2(1031 /* DeleteMessage */, seq, { name: bytesArr(name), seq: Number(msgSeq) });
273
274
  var packListStreams = (seq, offset = 0, limit = 1e3) => packCold2(1028 /* ListStreams */, seq, { offset: offset >>> 0, limit: limit >>> 0 });
274
275
  function packCreateConsumer(seq, opts) {
275
276
  const limits = (opts.subjectLimits ?? []).map((l) => ({
@@ -927,6 +928,10 @@ var Consumer = class {
927
928
  }
928
929
  return this.client.getPending(this.streamName, this.name);
929
930
  }
931
+ /** Tombstone a single message by seq. Returns true if found. */
932
+ deleteMessage(seq) {
933
+ return this.client.deleteMessage(this.streamName, seq);
934
+ }
930
935
  subscribe(codecOrCb, cbOrOpts, opts) {
931
936
  if (!codecOrCb || typeof codecOrCb === "function") {
932
937
  return this.client.subscribe(
@@ -1016,6 +1021,10 @@ var Stream = class {
1016
1021
  request(subject, data, timeoutMs) {
1017
1022
  return this.client.request(this.name, subject, data, timeoutMs);
1018
1023
  }
1024
+ /** Tombstone a single message by seq. Returns true if found. */
1025
+ deleteMessage(seq) {
1026
+ return this.client.deleteMessage(this.name, seq);
1027
+ }
1019
1028
  // ── Context factories ───────────────────────────────────────────────────
1020
1029
  consumer(overrides) {
1021
1030
  const config = {
@@ -1291,8 +1300,8 @@ var ArbitroClient = class {
1291
1300
  */
1292
1301
  async publishDelayed(streamName, subject, data, delayMs) {
1293
1302
  const sid = await this.resolveStreamId(streamName);
1294
- const { packPublishDelayed: packPublishDelayed2 } = await import("./publish-UA3YZGMK.mjs");
1295
- const { Flag: Flag3 } = await import("./constants-KF57DJ2L.mjs");
1303
+ const { packPublishDelayed: packPublishDelayed2 } = await import("./publish-NKAK5BOA.mjs");
1304
+ const { Flag: Flag3 } = await import("./constants-LWTWKOBZ.mjs");
1296
1305
  const subj = Buffer.from(this.prefixed(subject));
1297
1306
  await this.conn.sendExpectReply(
1298
1307
  packPublishDelayed2(this.conn.nextSeq(), sid, subj, data, BigInt(delayMs), Flag3.AckReq)
@@ -1399,6 +1408,19 @@ var ArbitroClient = class {
1399
1408
  );
1400
1409
  return Number(refSeq);
1401
1410
  }
1411
+ /**
1412
+ * Tombstone a single message by sequence number.
1413
+ *
1414
+ * The broker marks the entry as deleted — it will never be delivered
1415
+ * to any consumer. Returns `true` if the message was found and
1416
+ * tombstoned, `false` if not found or already tombstoned.
1417
+ */
1418
+ async deleteMessage(streamName, seq) {
1419
+ const refSeq = await this.conn.sendExpectReply(
1420
+ packDeleteMessage(this.conn.nextSeq(), Buffer.from(streamName), seq)
1421
+ );
1422
+ return refSeq > 0n;
1423
+ }
1402
1424
  // ── Consumer management ───────────────────────────────────────────────────
1403
1425
  async createConsumer(streamName, config) {
1404
1426
  const consumerId = await this.createConsumerRaw(streamName, config);
@@ -1407,7 +1429,7 @@ var ArbitroClient = class {
1407
1429
  async createConsumerRaw(streamName, config) {
1408
1430
  const sid = await this.resolveStreamId(streamName);
1409
1431
  const name = Buffer.from(config.name ?? streamName);
1410
- const group = Buffer.from(config.name ?? streamName);
1432
+ const group = Buffer.from(config.group ?? config.name ?? streamName);
1411
1433
  const filter = Buffer.from(config.filter ?? "");
1412
1434
  const ackPolicyByte = config.ackPolicy === "none" /* None */ ? 0 : 1;
1413
1435
  const opts = {
@@ -1617,6 +1639,257 @@ function streamId(name) {
1617
1639
  return h >>> 0;
1618
1640
  }
1619
1641
 
1642
+ // src/workflow/task.ts
1643
+ var TASK_HEADER = 7;
1644
+ var COMPENSATION_BIT = 32768;
1645
+ function encodeTask(instanceId, stepIndex, attempt, context) {
1646
+ const buf = Buffer.allocUnsafe(TASK_HEADER + context.length);
1647
+ buf.writeUInt32LE(instanceId, 0);
1648
+ buf.writeUInt16LE(stepIndex, 4);
1649
+ buf[6] = attempt;
1650
+ context.copy(buf, TASK_HEADER);
1651
+ return buf;
1652
+ }
1653
+ function decodeTask(payload) {
1654
+ if (payload.length < TASK_HEADER) return void 0;
1655
+ return {
1656
+ instanceId: payload.readUInt32LE(0),
1657
+ stepIndex: payload.readUInt16LE(4),
1658
+ attempt: payload[6],
1659
+ context: payload.subarray(TASK_HEADER)
1660
+ };
1661
+ }
1662
+
1663
+ // src/workflow/handle.ts
1664
+ var nextInstanceId = 1;
1665
+ function allocInstanceId() {
1666
+ return nextInstanceId++;
1667
+ }
1668
+ var WorkflowHandle = class {
1669
+ constructor(workflowName, taskStreamName, dlqStreamName, sub, triggerSub) {
1670
+ this.workflowName = workflowName;
1671
+ this.taskStreamName = taskStreamName;
1672
+ this.dlqStreamName = dlqStreamName;
1673
+ this.sub = sub;
1674
+ this.triggerSub = triggerSub;
1675
+ }
1676
+ get name() {
1677
+ return this.workflowName;
1678
+ }
1679
+ get taskStream() {
1680
+ return this.taskStreamName;
1681
+ }
1682
+ get dlqStream() {
1683
+ return this.dlqStreamName;
1684
+ }
1685
+ async trigger(client, context) {
1686
+ const instanceId = allocInstanceId();
1687
+ const msgId = `wf:${instanceId}:0:0`;
1688
+ const subject = `_wf.${this.workflowName}.step.0`;
1689
+ const task = encodeTask(instanceId, 0, 0, context);
1690
+ await client.publish(this.taskStreamName, subject, task, { msgId });
1691
+ return instanceId;
1692
+ }
1693
+ };
1694
+
1695
+ // src/workflow/processor.ts
1696
+ async function processMessage(cfg, msg) {
1697
+ const task = decodeTask(msg.data());
1698
+ if (!task) {
1699
+ msg.ack();
1700
+ return;
1701
+ }
1702
+ if (task.context.length > cfg.maxContextSize) {
1703
+ msg.ack();
1704
+ return;
1705
+ }
1706
+ const isCompensation = (task.stepIndex & COMPENSATION_BIT) !== 0;
1707
+ if (isCompensation) {
1708
+ await runCompensation(cfg, msg, task);
1709
+ return;
1710
+ }
1711
+ if (task.stepIndex >= cfg.steps.length) {
1712
+ msg.ack();
1713
+ return;
1714
+ }
1715
+ await runStep(cfg, msg, task);
1716
+ }
1717
+ async function runCompensation(cfg, msg, task) {
1718
+ const idx = task.stepIndex & ~COMPENSATION_BIT;
1719
+ const comp = cfg.steps[idx]?.compensation;
1720
+ if (comp) {
1721
+ try {
1722
+ await comp({ name: cfg.name, instanceId: task.instanceId, stepIndex: idx, attempt: task.attempt, context: task.context });
1723
+ } catch {
1724
+ }
1725
+ }
1726
+ msg.ack();
1727
+ }
1728
+ async function runStep(cfg, msg, task) {
1729
+ const handler = cfg.steps[task.stepIndex].handler;
1730
+ try {
1731
+ const result = await handler({
1732
+ name: cfg.name,
1733
+ instanceId: task.instanceId,
1734
+ stepIndex: task.stepIndex,
1735
+ attempt: task.attempt,
1736
+ context: task.context
1737
+ });
1738
+ if (result.context.length > cfg.maxContextSize) {
1739
+ msg.nack();
1740
+ return;
1741
+ }
1742
+ await advance(cfg, msg, task, result);
1743
+ } catch (err) {
1744
+ await onFailure(cfg, msg, task, err);
1745
+ }
1746
+ }
1747
+ async function advance(cfg, msg, task, result) {
1748
+ const nextStep = task.stepIndex + 1;
1749
+ if (nextStep < cfg.steps.length) {
1750
+ const msgId = `wf:${task.instanceId}:${nextStep}:0`;
1751
+ const subject = `_wf.${cfg.name}.step.${nextStep}`;
1752
+ const buf = encodeTask(task.instanceId, nextStep, 0, result.context);
1753
+ await cfg.client.publish(cfg.taskStreamName, subject, buf, { msgId });
1754
+ }
1755
+ msg.ack();
1756
+ }
1757
+ async function onFailure(cfg, msg, task, err) {
1758
+ if (task.attempt >= cfg.maxRetries) {
1759
+ await publishDlq(cfg, task, err);
1760
+ await publishCompensations(cfg, task);
1761
+ msg.ack();
1762
+ } else {
1763
+ msg.nack();
1764
+ }
1765
+ }
1766
+ async function publishDlq(cfg, task, err) {
1767
+ const dlqSubject = `_wf.${cfg.name}.dlq.${task.stepIndex}`;
1768
+ const errBytes = Buffer.from(String(err));
1769
+ const buf = Buffer.allocUnsafe(7 + 4 + errBytes.length + task.context.length);
1770
+ buf.writeUInt32LE(task.instanceId, 0);
1771
+ buf.writeUInt16LE(task.stepIndex, 4);
1772
+ buf[6] = task.attempt;
1773
+ buf.writeUInt32LE(errBytes.length, 7);
1774
+ errBytes.copy(buf, 11);
1775
+ task.context.copy(buf, 11 + errBytes.length);
1776
+ const msgId = `wf:${task.instanceId}:dlq:${task.stepIndex}`;
1777
+ await cfg.client.publish(cfg.dlqStreamName, dlqSubject, buf, { msgId }).catch(() => {
1778
+ });
1779
+ }
1780
+ async function publishCompensations(cfg, task) {
1781
+ for (let i = task.stepIndex - 1; i >= 0; i--) {
1782
+ const compStep = COMPENSATION_BIT | i;
1783
+ const subject = `_wf.${cfg.name}.compensate.${i}`;
1784
+ const buf = encodeTask(task.instanceId, compStep, 0, task.context);
1785
+ const msgId = `wf:${task.instanceId}:comp:${i}`;
1786
+ await cfg.client.publish(cfg.taskStreamName, subject, buf, { msgId }).catch(() => {
1787
+ });
1788
+ }
1789
+ }
1790
+
1791
+ // src/workflow/workflow.ts
1792
+ var nextWorkerUid = 1;
1793
+ var WorkflowBuilder = class {
1794
+ constructor(client, workflowName) {
1795
+ this.client = client;
1796
+ this.workflowName = workflowName;
1797
+ }
1798
+ triggerSubject;
1799
+ triggerStreamName;
1800
+ steps = [];
1801
+ ackWaitMs = 3e4;
1802
+ maxInflightVal = 10;
1803
+ maxRetriesVal = 3;
1804
+ maxContextSizeVal = 256 * 1024;
1805
+ trigger(subject) {
1806
+ this.triggerSubject = subject;
1807
+ return this;
1808
+ }
1809
+ triggerStream(streamName) {
1810
+ this.triggerStreamName = streamName;
1811
+ return this;
1812
+ }
1813
+ step(name, handler) {
1814
+ this.steps.push({ name, handler, compensation: void 0 });
1815
+ return this;
1816
+ }
1817
+ /** Compensation handler for the most recently added step. */
1818
+ compensate(_stepName, handler) {
1819
+ const last = this.steps[this.steps.length - 1];
1820
+ if (last) last.compensation = handler;
1821
+ return this;
1822
+ }
1823
+ ackWait(ms) {
1824
+ this.ackWaitMs = ms;
1825
+ return this;
1826
+ }
1827
+ inflight(n) {
1828
+ this.maxInflightVal = n;
1829
+ return this;
1830
+ }
1831
+ maxRetries(n) {
1832
+ this.maxRetriesVal = n;
1833
+ return this;
1834
+ }
1835
+ maxContextSize(bytes) {
1836
+ this.maxContextSizeVal = bytes;
1837
+ return this;
1838
+ }
1839
+ async start() {
1840
+ if (!this.triggerSubject) throw new Error("trigger subject required");
1841
+ if (this.steps.length === 0) throw new Error("at least one step required");
1842
+ const name = this.workflowName;
1843
+ const taskStream = `_wf.${name}.tasks`;
1844
+ const taskSubject = `_wf.${name}.>`;
1845
+ const dlqStream = `_wf.${name}.dlq`;
1846
+ const dlqSubject = `_wf.${name}.dlq.>`;
1847
+ await this.client.upsertStream(taskStream, { subjectFilter: taskSubject, idempotencyWindowMs: 3e5 });
1848
+ await this.client.upsertStream(dlqStream, { subjectFilter: dlqSubject });
1849
+ const cfg = {
1850
+ client: this.client,
1851
+ name,
1852
+ taskStreamName: taskStream,
1853
+ dlqStreamName: dlqStream,
1854
+ steps: this.steps,
1855
+ maxContextSize: this.maxContextSizeVal,
1856
+ maxRetries: this.maxRetriesVal
1857
+ };
1858
+ const sub = await this.subscribeWorker(cfg, taskStream, taskSubject);
1859
+ const triggerSub = await this.subscribeTrigger(taskStream, name);
1860
+ return new WorkflowHandle(name, taskStream, dlqStream, sub, triggerSub);
1861
+ }
1862
+ async subscribeWorker(cfg, taskStream, taskSubject) {
1863
+ const uid = nextWorkerUid++;
1864
+ return this.client.subscribe(taskStream, {
1865
+ name: `_wf_${cfg.name}_w${uid}`,
1866
+ group: `_wf_${cfg.name}_workers`,
1867
+ filter: taskSubject,
1868
+ ackPolicy: "explicit" /* Explicit */,
1869
+ ackWaitMs: this.ackWaitMs,
1870
+ maxAckPending: this.maxInflightVal
1871
+ }, (msg) => {
1872
+ void processMessage(cfg, msg);
1873
+ });
1874
+ }
1875
+ async subscribeTrigger(taskStream, name) {
1876
+ if (!this.triggerSubject || !this.triggerStreamName) return void 0;
1877
+ const subject = this.triggerSubject;
1878
+ return this.client.subscribe(this.triggerStreamName, {
1879
+ name: `_wf_${name}_trigger`,
1880
+ filter: subject,
1881
+ ackPolicy: "explicit" /* Explicit */,
1882
+ ackWaitMs: this.ackWaitMs,
1883
+ maxAckPending: 1
1884
+ }, async (msg) => {
1885
+ const id = allocInstanceId();
1886
+ const taskBuf = encodeTask(id, 0, 0, msg.data());
1887
+ await this.client.publish(taskStream, `_wf.${name}.step.0`, taskBuf, { msgId: `wf:${id}:0:0` });
1888
+ msg.ack();
1889
+ });
1890
+ }
1891
+ };
1892
+
1620
1893
  // src/utils/zod.ts
1621
1894
  import { Packr as Packr2, Unpackr as Unpackr2 } from "msgpackr";
1622
1895
  var packr = new Packr2({ structuredClone: false, useRecords: false });
@@ -1637,6 +1910,7 @@ export {
1637
1910
  AckPolicy,
1638
1911
  ArbitroClient,
1639
1912
  ArbitroError,
1913
+ COMPENSATION_BIT,
1640
1914
  Codec,
1641
1915
  Consumer,
1642
1916
  CronBuilder,
@@ -1650,6 +1924,8 @@ export {
1650
1924
  StringCodec,
1651
1925
  Subscription,
1652
1926
  Topic,
1927
+ WorkflowBuilder,
1928
+ WorkflowHandle,
1653
1929
  decodeJson,
1654
1930
  decodeString,
1655
1931
  encodeJson,