@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.js CHANGED
@@ -115,6 +115,7 @@ var init_constants = __esm({
115
115
  Action2[Action2["ListStreams"] = 1028] = "ListStreams";
116
116
  Action2[Action2["PurgeStream"] = 1029] = "PurgeStream";
117
117
  Action2[Action2["DrainSubject"] = 1030] = "DrainSubject";
118
+ Action2[Action2["DeleteMessage"] = 1031] = "DeleteMessage";
118
119
  Action2[Action2["CreateConsumer"] = 1281] = "CreateConsumer";
119
120
  Action2[Action2["DeleteConsumer"] = 1282] = "DeleteConsumer";
120
121
  Action2[Action2["GetConsumer"] = 1283] = "GetConsumer";
@@ -267,6 +268,7 @@ __export(index_exports, {
267
268
  AckPolicy: () => AckPolicy,
268
269
  ArbitroClient: () => ArbitroClient,
269
270
  ArbitroError: () => ArbitroError,
271
+ COMPENSATION_BIT: () => COMPENSATION_BIT,
270
272
  Codec: () => Codec,
271
273
  Consumer: () => Consumer,
272
274
  CronBuilder: () => CronBuilder,
@@ -280,6 +282,8 @@ __export(index_exports, {
280
282
  StringCodec: () => StringCodec,
281
283
  Subscription: () => Subscription,
282
284
  Topic: () => Topic,
285
+ WorkflowBuilder: () => WorkflowBuilder,
286
+ WorkflowHandle: () => WorkflowHandle,
283
287
  decodeJson: () => decodeJson,
284
288
  decodeString: () => decodeString,
285
289
  encodeJson: () => encodeJson,
@@ -559,6 +563,7 @@ var packDrainSubject = (seq, name, subject) => packCold2(1030 /* DrainSubject */
559
563
  name: bytesArr(name),
560
564
  subject: bytesArr(subject)
561
565
  });
566
+ var packDeleteMessage = (seq, name, msgSeq) => packCold2(1031 /* DeleteMessage */, seq, { name: bytesArr(name), seq: Number(msgSeq) });
562
567
  var packListStreams = (seq, offset = 0, limit = 1e3) => packCold2(1028 /* ListStreams */, seq, { offset: offset >>> 0, limit: limit >>> 0 });
563
568
  function packCreateConsumer(seq, opts) {
564
569
  const limits = (opts.subjectLimits ?? []).map((l) => ({
@@ -1222,6 +1227,10 @@ var Consumer = class {
1222
1227
  }
1223
1228
  return this.client.getPending(this.streamName, this.name);
1224
1229
  }
1230
+ /** Tombstone a single message by seq. Returns true if found. */
1231
+ deleteMessage(seq) {
1232
+ return this.client.deleteMessage(this.streamName, seq);
1233
+ }
1225
1234
  subscribe(codecOrCb, cbOrOpts, opts) {
1226
1235
  if (!codecOrCb || typeof codecOrCb === "function") {
1227
1236
  return this.client.subscribe(
@@ -1311,6 +1320,10 @@ var Stream = class {
1311
1320
  request(subject, data, timeoutMs) {
1312
1321
  return this.client.request(this.name, subject, data, timeoutMs);
1313
1322
  }
1323
+ /** Tombstone a single message by seq. Returns true if found. */
1324
+ deleteMessage(seq) {
1325
+ return this.client.deleteMessage(this.name, seq);
1326
+ }
1314
1327
  // ── Context factories ───────────────────────────────────────────────────
1315
1328
  consumer(overrides) {
1316
1329
  const config = {
@@ -1698,6 +1711,19 @@ var ArbitroClient = class {
1698
1711
  );
1699
1712
  return Number(refSeq);
1700
1713
  }
1714
+ /**
1715
+ * Tombstone a single message by sequence number.
1716
+ *
1717
+ * The broker marks the entry as deleted — it will never be delivered
1718
+ * to any consumer. Returns `true` if the message was found and
1719
+ * tombstoned, `false` if not found or already tombstoned.
1720
+ */
1721
+ async deleteMessage(streamName, seq) {
1722
+ const refSeq = await this.conn.sendExpectReply(
1723
+ packDeleteMessage(this.conn.nextSeq(), Buffer.from(streamName), seq)
1724
+ );
1725
+ return refSeq > 0n;
1726
+ }
1701
1727
  // ── Consumer management ───────────────────────────────────────────────────
1702
1728
  async createConsumer(streamName, config) {
1703
1729
  const consumerId = await this.createConsumerRaw(streamName, config);
@@ -1706,7 +1732,7 @@ var ArbitroClient = class {
1706
1732
  async createConsumerRaw(streamName, config) {
1707
1733
  const sid = await this.resolveStreamId(streamName);
1708
1734
  const name = Buffer.from(config.name ?? streamName);
1709
- const group = Buffer.from(config.name ?? streamName);
1735
+ const group = Buffer.from(config.group ?? config.name ?? streamName);
1710
1736
  const filter = Buffer.from(config.filter ?? "");
1711
1737
  const ackPolicyByte = config.ackPolicy === "none" /* None */ ? 0 : 1;
1712
1738
  const opts = {
@@ -1916,6 +1942,257 @@ function streamId(name) {
1916
1942
  return h >>> 0;
1917
1943
  }
1918
1944
 
1945
+ // src/workflow/task.ts
1946
+ var TASK_HEADER = 7;
1947
+ var COMPENSATION_BIT = 32768;
1948
+ function encodeTask(instanceId, stepIndex, attempt, context) {
1949
+ const buf = Buffer.allocUnsafe(TASK_HEADER + context.length);
1950
+ buf.writeUInt32LE(instanceId, 0);
1951
+ buf.writeUInt16LE(stepIndex, 4);
1952
+ buf[6] = attempt;
1953
+ context.copy(buf, TASK_HEADER);
1954
+ return buf;
1955
+ }
1956
+ function decodeTask(payload) {
1957
+ if (payload.length < TASK_HEADER) return void 0;
1958
+ return {
1959
+ instanceId: payload.readUInt32LE(0),
1960
+ stepIndex: payload.readUInt16LE(4),
1961
+ attempt: payload[6],
1962
+ context: payload.subarray(TASK_HEADER)
1963
+ };
1964
+ }
1965
+
1966
+ // src/workflow/handle.ts
1967
+ var nextInstanceId = 1;
1968
+ function allocInstanceId() {
1969
+ return nextInstanceId++;
1970
+ }
1971
+ var WorkflowHandle = class {
1972
+ constructor(workflowName, taskStreamName, dlqStreamName, sub, triggerSub) {
1973
+ this.workflowName = workflowName;
1974
+ this.taskStreamName = taskStreamName;
1975
+ this.dlqStreamName = dlqStreamName;
1976
+ this.sub = sub;
1977
+ this.triggerSub = triggerSub;
1978
+ }
1979
+ get name() {
1980
+ return this.workflowName;
1981
+ }
1982
+ get taskStream() {
1983
+ return this.taskStreamName;
1984
+ }
1985
+ get dlqStream() {
1986
+ return this.dlqStreamName;
1987
+ }
1988
+ async trigger(client, context) {
1989
+ const instanceId = allocInstanceId();
1990
+ const msgId = `wf:${instanceId}:0:0`;
1991
+ const subject = `_wf.${this.workflowName}.step.0`;
1992
+ const task = encodeTask(instanceId, 0, 0, context);
1993
+ await client.publish(this.taskStreamName, subject, task, { msgId });
1994
+ return instanceId;
1995
+ }
1996
+ };
1997
+
1998
+ // src/workflow/processor.ts
1999
+ async function processMessage(cfg, msg) {
2000
+ const task = decodeTask(msg.data());
2001
+ if (!task) {
2002
+ msg.ack();
2003
+ return;
2004
+ }
2005
+ if (task.context.length > cfg.maxContextSize) {
2006
+ msg.ack();
2007
+ return;
2008
+ }
2009
+ const isCompensation = (task.stepIndex & COMPENSATION_BIT) !== 0;
2010
+ if (isCompensation) {
2011
+ await runCompensation(cfg, msg, task);
2012
+ return;
2013
+ }
2014
+ if (task.stepIndex >= cfg.steps.length) {
2015
+ msg.ack();
2016
+ return;
2017
+ }
2018
+ await runStep(cfg, msg, task);
2019
+ }
2020
+ async function runCompensation(cfg, msg, task) {
2021
+ const idx = task.stepIndex & ~COMPENSATION_BIT;
2022
+ const comp = cfg.steps[idx]?.compensation;
2023
+ if (comp) {
2024
+ try {
2025
+ await comp({ name: cfg.name, instanceId: task.instanceId, stepIndex: idx, attempt: task.attempt, context: task.context });
2026
+ } catch {
2027
+ }
2028
+ }
2029
+ msg.ack();
2030
+ }
2031
+ async function runStep(cfg, msg, task) {
2032
+ const handler = cfg.steps[task.stepIndex].handler;
2033
+ try {
2034
+ const result = await handler({
2035
+ name: cfg.name,
2036
+ instanceId: task.instanceId,
2037
+ stepIndex: task.stepIndex,
2038
+ attempt: task.attempt,
2039
+ context: task.context
2040
+ });
2041
+ if (result.context.length > cfg.maxContextSize) {
2042
+ msg.nack();
2043
+ return;
2044
+ }
2045
+ await advance(cfg, msg, task, result);
2046
+ } catch (err) {
2047
+ await onFailure(cfg, msg, task, err);
2048
+ }
2049
+ }
2050
+ async function advance(cfg, msg, task, result) {
2051
+ const nextStep = task.stepIndex + 1;
2052
+ if (nextStep < cfg.steps.length) {
2053
+ const msgId = `wf:${task.instanceId}:${nextStep}:0`;
2054
+ const subject = `_wf.${cfg.name}.step.${nextStep}`;
2055
+ const buf = encodeTask(task.instanceId, nextStep, 0, result.context);
2056
+ await cfg.client.publish(cfg.taskStreamName, subject, buf, { msgId });
2057
+ }
2058
+ msg.ack();
2059
+ }
2060
+ async function onFailure(cfg, msg, task, err) {
2061
+ if (task.attempt >= cfg.maxRetries) {
2062
+ await publishDlq(cfg, task, err);
2063
+ await publishCompensations(cfg, task);
2064
+ msg.ack();
2065
+ } else {
2066
+ msg.nack();
2067
+ }
2068
+ }
2069
+ async function publishDlq(cfg, task, err) {
2070
+ const dlqSubject = `_wf.${cfg.name}.dlq.${task.stepIndex}`;
2071
+ const errBytes = Buffer.from(String(err));
2072
+ const buf = Buffer.allocUnsafe(7 + 4 + errBytes.length + task.context.length);
2073
+ buf.writeUInt32LE(task.instanceId, 0);
2074
+ buf.writeUInt16LE(task.stepIndex, 4);
2075
+ buf[6] = task.attempt;
2076
+ buf.writeUInt32LE(errBytes.length, 7);
2077
+ errBytes.copy(buf, 11);
2078
+ task.context.copy(buf, 11 + errBytes.length);
2079
+ const msgId = `wf:${task.instanceId}:dlq:${task.stepIndex}`;
2080
+ await cfg.client.publish(cfg.dlqStreamName, dlqSubject, buf, { msgId }).catch(() => {
2081
+ });
2082
+ }
2083
+ async function publishCompensations(cfg, task) {
2084
+ for (let i = task.stepIndex - 1; i >= 0; i--) {
2085
+ const compStep = COMPENSATION_BIT | i;
2086
+ const subject = `_wf.${cfg.name}.compensate.${i}`;
2087
+ const buf = encodeTask(task.instanceId, compStep, 0, task.context);
2088
+ const msgId = `wf:${task.instanceId}:comp:${i}`;
2089
+ await cfg.client.publish(cfg.taskStreamName, subject, buf, { msgId }).catch(() => {
2090
+ });
2091
+ }
2092
+ }
2093
+
2094
+ // src/workflow/workflow.ts
2095
+ var nextWorkerUid = 1;
2096
+ var WorkflowBuilder = class {
2097
+ constructor(client, workflowName) {
2098
+ this.client = client;
2099
+ this.workflowName = workflowName;
2100
+ }
2101
+ triggerSubject;
2102
+ triggerStreamName;
2103
+ steps = [];
2104
+ ackWaitMs = 3e4;
2105
+ maxInflightVal = 10;
2106
+ maxRetriesVal = 3;
2107
+ maxContextSizeVal = 256 * 1024;
2108
+ trigger(subject) {
2109
+ this.triggerSubject = subject;
2110
+ return this;
2111
+ }
2112
+ triggerStream(streamName) {
2113
+ this.triggerStreamName = streamName;
2114
+ return this;
2115
+ }
2116
+ step(name, handler) {
2117
+ this.steps.push({ name, handler, compensation: void 0 });
2118
+ return this;
2119
+ }
2120
+ /** Compensation handler for the most recently added step. */
2121
+ compensate(_stepName, handler) {
2122
+ const last = this.steps[this.steps.length - 1];
2123
+ if (last) last.compensation = handler;
2124
+ return this;
2125
+ }
2126
+ ackWait(ms) {
2127
+ this.ackWaitMs = ms;
2128
+ return this;
2129
+ }
2130
+ inflight(n) {
2131
+ this.maxInflightVal = n;
2132
+ return this;
2133
+ }
2134
+ maxRetries(n) {
2135
+ this.maxRetriesVal = n;
2136
+ return this;
2137
+ }
2138
+ maxContextSize(bytes) {
2139
+ this.maxContextSizeVal = bytes;
2140
+ return this;
2141
+ }
2142
+ async start() {
2143
+ if (!this.triggerSubject) throw new Error("trigger subject required");
2144
+ if (this.steps.length === 0) throw new Error("at least one step required");
2145
+ const name = this.workflowName;
2146
+ const taskStream = `_wf.${name}.tasks`;
2147
+ const taskSubject = `_wf.${name}.>`;
2148
+ const dlqStream = `_wf.${name}.dlq`;
2149
+ const dlqSubject = `_wf.${name}.dlq.>`;
2150
+ await this.client.upsertStream(taskStream, { subjectFilter: taskSubject, idempotencyWindowMs: 3e5 });
2151
+ await this.client.upsertStream(dlqStream, { subjectFilter: dlqSubject });
2152
+ const cfg = {
2153
+ client: this.client,
2154
+ name,
2155
+ taskStreamName: taskStream,
2156
+ dlqStreamName: dlqStream,
2157
+ steps: this.steps,
2158
+ maxContextSize: this.maxContextSizeVal,
2159
+ maxRetries: this.maxRetriesVal
2160
+ };
2161
+ const sub = await this.subscribeWorker(cfg, taskStream, taskSubject);
2162
+ const triggerSub = await this.subscribeTrigger(taskStream, name);
2163
+ return new WorkflowHandle(name, taskStream, dlqStream, sub, triggerSub);
2164
+ }
2165
+ async subscribeWorker(cfg, taskStream, taskSubject) {
2166
+ const uid = nextWorkerUid++;
2167
+ return this.client.subscribe(taskStream, {
2168
+ name: `_wf_${cfg.name}_w${uid}`,
2169
+ group: `_wf_${cfg.name}_workers`,
2170
+ filter: taskSubject,
2171
+ ackPolicy: "explicit" /* Explicit */,
2172
+ ackWaitMs: this.ackWaitMs,
2173
+ maxAckPending: this.maxInflightVal
2174
+ }, (msg) => {
2175
+ void processMessage(cfg, msg);
2176
+ });
2177
+ }
2178
+ async subscribeTrigger(taskStream, name) {
2179
+ if (!this.triggerSubject || !this.triggerStreamName) return void 0;
2180
+ const subject = this.triggerSubject;
2181
+ return this.client.subscribe(this.triggerStreamName, {
2182
+ name: `_wf_${name}_trigger`,
2183
+ filter: subject,
2184
+ ackPolicy: "explicit" /* Explicit */,
2185
+ ackWaitMs: this.ackWaitMs,
2186
+ maxAckPending: 1
2187
+ }, async (msg) => {
2188
+ const id = allocInstanceId();
2189
+ const taskBuf = encodeTask(id, 0, 0, msg.data());
2190
+ await this.client.publish(taskStream, `_wf.${name}.step.0`, taskBuf, { msgId: `wf:${id}:0:0` });
2191
+ msg.ack();
2192
+ });
2193
+ }
2194
+ };
2195
+
1919
2196
  // src/utils/zod.ts
1920
2197
  var import_msgpackr2 = require("msgpackr");
1921
2198
  var packr = new import_msgpackr2.Packr({ structuredClone: false, useRecords: false });
@@ -1937,6 +2214,7 @@ function zodCodec(zodSchema) {
1937
2214
  AckPolicy,
1938
2215
  ArbitroClient,
1939
2216
  ArbitroError,
2217
+ COMPENSATION_BIT,
1940
2218
  Codec,
1941
2219
  Consumer,
1942
2220
  CronBuilder,
@@ -1950,6 +2228,8 @@ function zodCodec(zodSchema) {
1950
2228
  StringCodec,
1951
2229
  Subscription,
1952
2230
  Topic,
2231
+ WorkflowBuilder,
2232
+ WorkflowHandle,
1953
2233
  decodeJson,
1954
2234
  decodeString,
1955
2235
  encodeJson,