@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/README.md CHANGED
@@ -248,6 +248,74 @@ await client.publishDelayed("ORDERS", "orders.reminder", payload, 5000); // 5s d
248
248
 
249
249
  Messages are persisted immediately — survives broker restart.
250
250
 
251
+ ## Workflow Orchestration
252
+
253
+ Client-side linear pipelines over Arbitro streams. The broker has no workflow-specific code -- everything uses streams, consumer groups, and idempotent publish.
254
+
255
+ ### WorkflowBuilder API
256
+
257
+ | Method | Signature | Description |
258
+ |--------|-----------|-------------|
259
+ | `trigger` | `(subject: string) => this` | Subject pattern that triggers new instances. Required. |
260
+ | `triggerStream` | *(not yet implemented)* | Planned: auto-subscribe to an external stream for trigger. |
261
+ | `step` | `(name: string, handler: StepHandler) => this` | Append a processing step. |
262
+ | `compensate` | *(not yet implemented)* | Planned: rollback handler per step (saga pattern). |
263
+ | `maxRetries` | *(not yet implemented)* | Planned: attempts before DLQ. |
264
+ | `maxContextSize` | *(not yet implemented)* | Planned: max context payload in bytes. |
265
+ | `ackWait` | `(ms: number) => this` | Ack timeout for failover (default: 30000). |
266
+ | `inflight` | `(n: number) => this` | Concurrent tasks per worker (default: 10). |
267
+ | `start` | `() => Promise<WorkflowHandle>` | Register streams, consumer, and start processing. |
268
+
269
+ ### WorkflowHandle API
270
+
271
+ | Method | Signature | Description |
272
+ |--------|-----------|-------------|
273
+ | `trigger` | `(client: ArbitroClient, context: Buffer) => Promise<number>` | Trigger a new workflow instance. Returns the instance ID. |
274
+ | `name` | `string` (getter) | Workflow name. |
275
+
276
+ ### Complete Example
277
+
278
+ ```typescript
279
+ import { ArbitroClient, WorkflowBuilder } from '@arbitro/client'
280
+ import type { StepContext, StepResult } from '@arbitro/client'
281
+
282
+ const client = new ArbitroClient({ servers: ['127.0.0.1:9898'] })
283
+ await client.connect()
284
+
285
+ const wf = await new WorkflowBuilder(client, 'order-process')
286
+ .trigger('orders.created')
287
+ // Step 1: validate
288
+ .step('validate', async (ctx: StepContext): Promise<StepResult> => {
289
+ const validated = await validateOrder(ctx.context)
290
+ return { context: validated }
291
+ })
292
+ // Step 2: charge
293
+ .step('charge', async (ctx: StepContext): Promise<StepResult> => {
294
+ const receipt = await chargePayment(ctx.context)
295
+ return { context: receipt }
296
+ })
297
+ // Step 3: ship
298
+ .step('ship', async (ctx: StepContext): Promise<StepResult> => {
299
+ const tracking = await createShipment(ctx.context)
300
+ return { context: tracking }
301
+ })
302
+ .ackWait(30_000)
303
+ .inflight(10)
304
+ .start()
305
+
306
+ // Manual trigger
307
+ const instanceId = await wf.trigger(client, Buffer.from('order-123-payload'))
308
+ console.log(`started instance ${instanceId}`)
309
+ ```
310
+
311
+ ### Internals
312
+
313
+ - Tasks flow through `_wf.{name}.tasks` stream with a consumer `_wf.{name}.workers`.
314
+ - Each step transition publishes with `msgId` format `wf:{instance}:{step}:{attempt}` for deduplication.
315
+ - `ackWait` enables failover: if a worker dies, the broker redelivers to another subscriber.
316
+
317
+ > **Note:** The TypeScript workflow module currently implements the core step pipeline. Compensation (saga), DLQ, `triggerStream`, `maxRetries`, and `maxContextSize` are available in the Rust client and planned for the TS client.
318
+
251
319
  ## Reconnect behavior
252
320
 
253
321
  The TS client reconnects transport automatically and reattaches active subscriptions and cron jobs after reconnect. That behavior lives in the client, not in the benchmarks. This matters for:
@@ -99,4 +99,4 @@ export {
99
99
  EntryFlag,
100
100
  Action
101
101
  };
102
- //# sourceMappingURL=chunk-6BCX2E2R.mjs.map
102
+ //# sourceMappingURL=chunk-C2QLJBAC.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/proto/constants.ts"],"sourcesContent":["// V2 wire protocol constants — must match arbitro-proto/src/action.rs exactly.\n\n// ── Hello handshake (only frame without a Header) ───────────────────────\nexport const MAGIC_V2 = 0x32425241 // \"ARB2\" as u32 LE\nexport const HELLO_SIZE = 8\nexport const CURRENT_VERSION = 2\n\nexport const enum Role {\n Client = 0,\n Server = 1,\n}\n\nexport const enum Cap {\n Headers = 1 << 0,\n Reply = 1 << 1,\n BatchHeaders = 1 << 2,\n CompressedPayload = 1 << 3,\n}\n\n// ── Header (16 bytes, every frame after Hello) ──────────────────────────\nexport const HEADER_SIZE = 16\n\n// Byte offsets within the 16-byte header\nexport const OFF_ACTION = 0 // u16 LE\nexport const OFF_FLAGS = 2 // u8\nexport const OFF_ENTRY_FLAGS = 3 // u8\nexport const OFF_MSG_LEN = 4 // u32 LE\nexport const OFF_SEQ = 8 // u64 LE\n\n// ── Transport flags (header.flags, offset 2) ────────────────────────────\nexport const enum Flag {\n None = 0x00,\n AckReq = 0x01,\n Dup = 0x02,\n PriorityHigh = 0x04,\n}\n\n// ── Per-message flags (header.entry_flags, offset 3) ────────────────────\nexport const enum EntryFlag {\n None = 0x00,\n Retain = 0x01,\n Compressed = 0x02,\n NoBackpressure = 0x04,\n}\n\n// ── Action codes (0xFFGG: FF=family, GG=variant) ────────────────────────\nexport const enum Action {\n // 0x00xx — Handshake / control\n Hello = 0x0001,\n Auth = 0x0002,\n\n // 0x01xx — Publish family\n Publish = 0x0101,\n PublishAccumulate = 0x0102,\n PublishBatch = 0x0103,\n PublishWithReply = 0x0104,\n PublishWithHeaders = 0x0105,\n PublishBatchWithHeaders = 0x0106,\n\n // 0x02xx — Delivery / Ack\n Deliver = 0x0200,\n Ack = 0x0201,\n Nack = 0x0202,\n RepOk = 0x0203,\n RepError = 0x0204,\n RepBatch = 0x0205,\n BatchAck = 0x0206,\n FanoutBatch = 0x0207,\n AckSync = 0x0208,\n BatchAckSync = 0x0209,\n BatchNack = 0x020A,\n\n // 0x03xx — Subscription\n Subscribe = 0x0301,\n Unsubscribe = 0x0302,\n\n // 0x04xx — Stream management\n CreateStream = 0x0401,\n DeleteStream = 0x0402,\n GetStream = 0x0403,\n ListStreams = 0x0404,\n PurgeStream = 0x0405,\n DrainSubject = 0x0406,\n\n // 0x05xx — Consumer management\n CreateConsumer = 0x0501,\n DeleteConsumer = 0x0502,\n GetConsumer = 0x0503,\n ListConsumers = 0x0504,\n ConsumerStats = 0x0505,\n PauseConsumer = 0x0506,\n ResumeConsumer = 0x0507,\n\n // 0x08xx — Delayed publish\n PublishDelayed = 0x0801,\n\n // 0x06xx — System\n Ping = 0x0601,\n Pong = 0x0602,\n Connect = 0x0603,\n Connected = 0x0604,\n Disconnect = 0x0605,\n\n // 0x07xx — Cron scheduling\n CreateCron = 0x0701,\n DeleteCron = 0x0702,\n ListCrons = 0x0703,\n CronFire = 0x0704,\n CronAck = 0x0705,\n}\n"],"mappings":"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","names":["Role","Cap","Flag","EntryFlag","Action"]}
1
+ {"version":3,"sources":["../src/proto/constants.ts"],"sourcesContent":["// V2 wire protocol constants — must match arbitro-proto/src/action.rs exactly.\n\n// ── Hello handshake (only frame without a Header) ───────────────────────\nexport const MAGIC_V2 = 0x32425241 // \"ARB2\" as u32 LE\nexport const HELLO_SIZE = 8\nexport const CURRENT_VERSION = 2\n\nexport const enum Role {\n Client = 0,\n Server = 1,\n}\n\nexport const enum Cap {\n Headers = 1 << 0,\n Reply = 1 << 1,\n BatchHeaders = 1 << 2,\n CompressedPayload = 1 << 3,\n}\n\n// ── Header (16 bytes, every frame after Hello) ──────────────────────────\nexport const HEADER_SIZE = 16\n\n// Byte offsets within the 16-byte header\nexport const OFF_ACTION = 0 // u16 LE\nexport const OFF_FLAGS = 2 // u8\nexport const OFF_ENTRY_FLAGS = 3 // u8\nexport const OFF_MSG_LEN = 4 // u32 LE\nexport const OFF_SEQ = 8 // u64 LE\n\n// ── Transport flags (header.flags, offset 2) ────────────────────────────\nexport const enum Flag {\n None = 0x00,\n AckReq = 0x01,\n Dup = 0x02,\n PriorityHigh = 0x04,\n}\n\n// ── Per-message flags (header.entry_flags, offset 3) ────────────────────\nexport const enum EntryFlag {\n None = 0x00,\n Retain = 0x01,\n Compressed = 0x02,\n NoBackpressure = 0x04,\n}\n\n// ── Action codes (0xFFGG: FF=family, GG=variant) ────────────────────────\nexport const enum Action {\n // 0x00xx — Handshake / control\n Hello = 0x0001,\n Auth = 0x0002,\n\n // 0x01xx — Publish family\n Publish = 0x0101,\n PublishAccumulate = 0x0102,\n PublishBatch = 0x0103,\n PublishWithReply = 0x0104,\n PublishWithHeaders = 0x0105,\n PublishBatchWithHeaders = 0x0106,\n\n // 0x02xx — Delivery / Ack\n Deliver = 0x0200,\n Ack = 0x0201,\n Nack = 0x0202,\n RepOk = 0x0203,\n RepError = 0x0204,\n RepBatch = 0x0205,\n BatchAck = 0x0206,\n FanoutBatch = 0x0207,\n AckSync = 0x0208,\n BatchAckSync = 0x0209,\n BatchNack = 0x020A,\n\n // 0x03xx — Subscription\n Subscribe = 0x0301,\n Unsubscribe = 0x0302,\n\n // 0x04xx — Stream management\n CreateStream = 0x0401,\n DeleteStream = 0x0402,\n GetStream = 0x0403,\n ListStreams = 0x0404,\n PurgeStream = 0x0405,\n DrainSubject = 0x0406,\n\n // 0x05xx — Consumer management\n CreateConsumer = 0x0501,\n DeleteConsumer = 0x0502,\n GetConsumer = 0x0503,\n ListConsumers = 0x0504,\n ConsumerStats = 0x0505,\n PauseConsumer = 0x0506,\n ResumeConsumer = 0x0507,\n\n // 0x08xx — Delayed publish\n PublishDelayed = 0x0801,\n\n // 0x06xx — System\n Ping = 0x0601,\n Pong = 0x0602,\n Connect = 0x0603,\n Connected = 0x0604,\n Disconnect = 0x0605,\n\n // 0x07xx — Cron scheduling\n CreateCron = 0x0701,\n DeleteCron = 0x0702,\n ListCrons = 0x0703,\n CronFire = 0x0704,\n CronAck = 0x0705,\n\n // 0x09xx reserved (workflow removed — now a client-side library).\n}\n"],"mappings":"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","names":["Role","Cap","Flag","EntryFlag","Action"]}
@@ -8,7 +8,7 @@ import {
8
8
  OFF_FLAGS,
9
9
  OFF_MSG_LEN,
10
10
  OFF_SEQ
11
- } from "./chunk-6BCX2E2R.mjs";
11
+ } from "./chunk-C2QLJBAC.mjs";
12
12
 
13
13
  // src/proto/frame.ts
14
14
  function frame(action, seq, bodyLen, flags = 0, entryFlags = 0) {
@@ -121,4 +121,4 @@ export {
121
121
  packPublishDelayed,
122
122
  packPublishBatch
123
123
  };
124
- //# sourceMappingURL=chunk-SKCXQO7R.mjs.map
124
+ //# sourceMappingURL=chunk-GW36GP2C.mjs.map
@@ -13,7 +13,7 @@ import {
13
13
  OFF_MSG_LEN,
14
14
  OFF_SEQ,
15
15
  Role
16
- } from "./chunk-6BCX2E2R.mjs";
16
+ } from "./chunk-C2QLJBAC.mjs";
17
17
  export {
18
18
  Action,
19
19
  CURRENT_VERSION,
@@ -30,4 +30,4 @@ export {
30
30
  OFF_SEQ,
31
31
  Role
32
32
  };
33
- //# sourceMappingURL=constants-KF57DJ2L.mjs.map
33
+ //# sourceMappingURL=constants-57DO6N3H.mjs.map
package/dist/index.d.mts CHANGED
@@ -86,6 +86,8 @@ interface SubjectInflightLimit {
86
86
  }
87
87
  interface ConsumerConfig {
88
88
  name?: string;
89
+ /** Shared consumer group name for round-robin delivery. Defaults to `name`. */
90
+ group?: string;
89
91
  filter?: string;
90
92
  fanout?: boolean;
91
93
  /** Consumer-side ACK policy. None = fire-and-forget delivery, Explicit = consumer must ACK. */
@@ -654,6 +656,64 @@ declare class Topic<T extends Record<string, unknown>> {
654
656
 
655
657
  declare function streamId(name: Buffer | string): number;
656
658
 
659
+ declare class WorkflowHandle {
660
+ private readonly workflowName;
661
+ private readonly taskStreamName;
662
+ private readonly dlqStreamName;
663
+ private readonly sub;
664
+ private readonly triggerSub;
665
+ constructor(workflowName: string, taskStreamName: string, dlqStreamName: string, sub: unknown, triggerSub: unknown | undefined);
666
+ get name(): string;
667
+ get taskStream(): string;
668
+ get dlqStream(): string;
669
+ trigger(client: ArbitroClient, context: Buffer): Promise<number>;
670
+ }
671
+
672
+ interface StepContext {
673
+ readonly name: string;
674
+ readonly instanceId: number;
675
+ readonly stepIndex: number;
676
+ readonly attempt: number;
677
+ readonly context: Buffer;
678
+ }
679
+ interface StepResult {
680
+ readonly context: Buffer;
681
+ }
682
+ type StepHandler = (ctx: StepContext) => Promise<StepResult>;
683
+ declare class WorkflowBuilder {
684
+ private readonly client;
685
+ private readonly workflowName;
686
+ private triggerSubject;
687
+ private triggerStreamName;
688
+ private readonly steps;
689
+ private ackWaitMs;
690
+ private maxInflightVal;
691
+ private maxRetriesVal;
692
+ private maxContextSizeVal;
693
+ constructor(client: ArbitroClient, workflowName: string);
694
+ trigger(subject: string): this;
695
+ triggerStream(streamName: string): this;
696
+ step(name: string, handler: StepHandler): this;
697
+ /** Compensation handler for the most recently added step. */
698
+ compensate(_stepName: string, handler: StepHandler): this;
699
+ ackWait(ms: number): this;
700
+ inflight(n: number): this;
701
+ maxRetries(n: number): this;
702
+ maxContextSize(bytes: number): this;
703
+ start(): Promise<WorkflowHandle>;
704
+ private subscribeWorker;
705
+ private subscribeTrigger;
706
+ }
707
+
708
+ /** Bit flag set on stepIndex to mark compensation tasks. */
709
+ declare const COMPENSATION_BIT = 32768;
710
+ interface DecodedTask {
711
+ readonly instanceId: number;
712
+ readonly stepIndex: number;
713
+ readonly attempt: number;
714
+ readonly context: Buffer;
715
+ }
716
+
657
717
  /** Wraps a ZodObject schema as an Encoding<T>.
658
718
  *
659
719
  * - encode: msgpack (no Zod overhead — bytes out)
@@ -667,4 +727,4 @@ declare function zodCodec<S extends ZodRawShape>(zodSchema: ZodObject<S>): Encod
667
727
  readonly fields: string[];
668
728
  };
669
729
 
670
- export { AckPolicy, ArbitroClient, ArbitroError, type BatchPublishEntry, type ClientConfig, type ClientMetricsSnapshot, Codec, Consumer, type ConsumerConfig, type ConsumerInfo, CronBuilder, type CronContext, CronHandle, type CronHandler, type DeleteStreamOpts, DeliverPolicy, type Encoding, ErrorCode, type ErrorCodeValue, type FieldType, type FieldTypeMap, type FlushConfig, type InferSchema, type JournalConfig, JournalType, JsonCodec, type LazyMessage, type LogFn, type Logger, Message, type PublishOpts, type ReconnectConfig, type Schema, Stream, type StreamConfig, type StreamInfo, StringCodec, type SubjectInflightLimit, type SubscribeOptions, Subscription, type TlsConfig, Topic, decodeJson, decodeString, encodeJson, encodeString, makeLazyMessage, schema, streamId, zodCodec };
730
+ export { AckPolicy, ArbitroClient, ArbitroError, type BatchPublishEntry, COMPENSATION_BIT, type ClientConfig, type ClientMetricsSnapshot, Codec, Consumer, type ConsumerConfig, type ConsumerInfo, CronBuilder, type CronContext, CronHandle, type CronHandler, type DecodedTask, type DeleteStreamOpts, DeliverPolicy, type Encoding, ErrorCode, type ErrorCodeValue, type FieldType, type FieldTypeMap, type FlushConfig, type InferSchema, type JournalConfig, JournalType, JsonCodec, type LazyMessage, type LogFn, type Logger, Message, type PublishOpts, type ReconnectConfig, type Schema, type StepContext, type StepHandler, type StepResult, Stream, type StreamConfig, type StreamInfo, StringCodec, type SubjectInflightLimit, type SubscribeOptions, Subscription, type TlsConfig, Topic, WorkflowBuilder, WorkflowHandle, decodeJson, decodeString, encodeJson, encodeString, makeLazyMessage, schema, streamId, zodCodec };
package/dist/index.d.ts CHANGED
@@ -86,6 +86,8 @@ interface SubjectInflightLimit {
86
86
  }
87
87
  interface ConsumerConfig {
88
88
  name?: string;
89
+ /** Shared consumer group name for round-robin delivery. Defaults to `name`. */
90
+ group?: string;
89
91
  filter?: string;
90
92
  fanout?: boolean;
91
93
  /** Consumer-side ACK policy. None = fire-and-forget delivery, Explicit = consumer must ACK. */
@@ -654,6 +656,64 @@ declare class Topic<T extends Record<string, unknown>> {
654
656
 
655
657
  declare function streamId(name: Buffer | string): number;
656
658
 
659
+ declare class WorkflowHandle {
660
+ private readonly workflowName;
661
+ private readonly taskStreamName;
662
+ private readonly dlqStreamName;
663
+ private readonly sub;
664
+ private readonly triggerSub;
665
+ constructor(workflowName: string, taskStreamName: string, dlqStreamName: string, sub: unknown, triggerSub: unknown | undefined);
666
+ get name(): string;
667
+ get taskStream(): string;
668
+ get dlqStream(): string;
669
+ trigger(client: ArbitroClient, context: Buffer): Promise<number>;
670
+ }
671
+
672
+ interface StepContext {
673
+ readonly name: string;
674
+ readonly instanceId: number;
675
+ readonly stepIndex: number;
676
+ readonly attempt: number;
677
+ readonly context: Buffer;
678
+ }
679
+ interface StepResult {
680
+ readonly context: Buffer;
681
+ }
682
+ type StepHandler = (ctx: StepContext) => Promise<StepResult>;
683
+ declare class WorkflowBuilder {
684
+ private readonly client;
685
+ private readonly workflowName;
686
+ private triggerSubject;
687
+ private triggerStreamName;
688
+ private readonly steps;
689
+ private ackWaitMs;
690
+ private maxInflightVal;
691
+ private maxRetriesVal;
692
+ private maxContextSizeVal;
693
+ constructor(client: ArbitroClient, workflowName: string);
694
+ trigger(subject: string): this;
695
+ triggerStream(streamName: string): this;
696
+ step(name: string, handler: StepHandler): this;
697
+ /** Compensation handler for the most recently added step. */
698
+ compensate(_stepName: string, handler: StepHandler): this;
699
+ ackWait(ms: number): this;
700
+ inflight(n: number): this;
701
+ maxRetries(n: number): this;
702
+ maxContextSize(bytes: number): this;
703
+ start(): Promise<WorkflowHandle>;
704
+ private subscribeWorker;
705
+ private subscribeTrigger;
706
+ }
707
+
708
+ /** Bit flag set on stepIndex to mark compensation tasks. */
709
+ declare const COMPENSATION_BIT = 32768;
710
+ interface DecodedTask {
711
+ readonly instanceId: number;
712
+ readonly stepIndex: number;
713
+ readonly attempt: number;
714
+ readonly context: Buffer;
715
+ }
716
+
657
717
  /** Wraps a ZodObject schema as an Encoding<T>.
658
718
  *
659
719
  * - encode: msgpack (no Zod overhead — bytes out)
@@ -667,4 +727,4 @@ declare function zodCodec<S extends ZodRawShape>(zodSchema: ZodObject<S>): Encod
667
727
  readonly fields: string[];
668
728
  };
669
729
 
670
- export { AckPolicy, ArbitroClient, ArbitroError, type BatchPublishEntry, type ClientConfig, type ClientMetricsSnapshot, Codec, Consumer, type ConsumerConfig, type ConsumerInfo, CronBuilder, type CronContext, CronHandle, type CronHandler, type DeleteStreamOpts, DeliverPolicy, type Encoding, ErrorCode, type ErrorCodeValue, type FieldType, type FieldTypeMap, type FlushConfig, type InferSchema, type JournalConfig, JournalType, JsonCodec, type LazyMessage, type LogFn, type Logger, Message, type PublishOpts, type ReconnectConfig, type Schema, Stream, type StreamConfig, type StreamInfo, StringCodec, type SubjectInflightLimit, type SubscribeOptions, Subscription, type TlsConfig, Topic, decodeJson, decodeString, encodeJson, encodeString, makeLazyMessage, schema, streamId, zodCodec };
730
+ export { AckPolicy, ArbitroClient, ArbitroError, type BatchPublishEntry, COMPENSATION_BIT, type ClientConfig, type ClientMetricsSnapshot, Codec, Consumer, type ConsumerConfig, type ConsumerInfo, CronBuilder, type CronContext, CronHandle, type CronHandler, type DecodedTask, type DeleteStreamOpts, DeliverPolicy, type Encoding, ErrorCode, type ErrorCodeValue, type FieldType, type FieldTypeMap, type FlushConfig, type InferSchema, type JournalConfig, JournalType, JsonCodec, type LazyMessage, type LogFn, type Logger, Message, type PublishOpts, type ReconnectConfig, type Schema, type StepContext, type StepHandler, type StepResult, Stream, type StreamConfig, type StreamInfo, StringCodec, type SubjectInflightLimit, type SubscribeOptions, Subscription, type TlsConfig, Topic, WorkflowBuilder, WorkflowHandle, decodeJson, decodeString, encodeJson, encodeString, makeLazyMessage, schema, streamId, zodCodec };
package/dist/index.js CHANGED
@@ -267,6 +267,7 @@ __export(index_exports, {
267
267
  AckPolicy: () => AckPolicy,
268
268
  ArbitroClient: () => ArbitroClient,
269
269
  ArbitroError: () => ArbitroError,
270
+ COMPENSATION_BIT: () => COMPENSATION_BIT,
270
271
  Codec: () => Codec,
271
272
  Consumer: () => Consumer,
272
273
  CronBuilder: () => CronBuilder,
@@ -280,6 +281,8 @@ __export(index_exports, {
280
281
  StringCodec: () => StringCodec,
281
282
  Subscription: () => Subscription,
282
283
  Topic: () => Topic,
284
+ WorkflowBuilder: () => WorkflowBuilder,
285
+ WorkflowHandle: () => WorkflowHandle,
283
286
  decodeJson: () => decodeJson,
284
287
  decodeString: () => decodeString,
285
288
  encodeJson: () => encodeJson,
@@ -1706,7 +1709,7 @@ var ArbitroClient = class {
1706
1709
  async createConsumerRaw(streamName, config) {
1707
1710
  const sid = await this.resolveStreamId(streamName);
1708
1711
  const name = Buffer.from(config.name ?? streamName);
1709
- const group = Buffer.from(config.name ?? streamName);
1712
+ const group = Buffer.from(config.group ?? config.name ?? streamName);
1710
1713
  const filter = Buffer.from(config.filter ?? "");
1711
1714
  const ackPolicyByte = config.ackPolicy === "none" /* None */ ? 0 : 1;
1712
1715
  const opts = {
@@ -1916,6 +1919,257 @@ function streamId(name) {
1916
1919
  return h >>> 0;
1917
1920
  }
1918
1921
 
1922
+ // src/workflow/task.ts
1923
+ var TASK_HEADER = 7;
1924
+ var COMPENSATION_BIT = 32768;
1925
+ function encodeTask(instanceId, stepIndex, attempt, context) {
1926
+ const buf = Buffer.allocUnsafe(TASK_HEADER + context.length);
1927
+ buf.writeUInt32LE(instanceId, 0);
1928
+ buf.writeUInt16LE(stepIndex, 4);
1929
+ buf[6] = attempt;
1930
+ context.copy(buf, TASK_HEADER);
1931
+ return buf;
1932
+ }
1933
+ function decodeTask(payload) {
1934
+ if (payload.length < TASK_HEADER) return void 0;
1935
+ return {
1936
+ instanceId: payload.readUInt32LE(0),
1937
+ stepIndex: payload.readUInt16LE(4),
1938
+ attempt: payload[6],
1939
+ context: payload.subarray(TASK_HEADER)
1940
+ };
1941
+ }
1942
+
1943
+ // src/workflow/handle.ts
1944
+ var nextInstanceId = 1;
1945
+ function allocInstanceId() {
1946
+ return nextInstanceId++;
1947
+ }
1948
+ var WorkflowHandle = class {
1949
+ constructor(workflowName, taskStreamName, dlqStreamName, sub, triggerSub) {
1950
+ this.workflowName = workflowName;
1951
+ this.taskStreamName = taskStreamName;
1952
+ this.dlqStreamName = dlqStreamName;
1953
+ this.sub = sub;
1954
+ this.triggerSub = triggerSub;
1955
+ }
1956
+ get name() {
1957
+ return this.workflowName;
1958
+ }
1959
+ get taskStream() {
1960
+ return this.taskStreamName;
1961
+ }
1962
+ get dlqStream() {
1963
+ return this.dlqStreamName;
1964
+ }
1965
+ async trigger(client, context) {
1966
+ const instanceId = allocInstanceId();
1967
+ const msgId = `wf:${instanceId}:0:0`;
1968
+ const subject = `_wf.${this.workflowName}.step.0`;
1969
+ const task = encodeTask(instanceId, 0, 0, context);
1970
+ await client.publish(this.taskStreamName, subject, task, { msgId });
1971
+ return instanceId;
1972
+ }
1973
+ };
1974
+
1975
+ // src/workflow/processor.ts
1976
+ async function processMessage(cfg, msg) {
1977
+ const task = decodeTask(msg.data());
1978
+ if (!task) {
1979
+ msg.ack();
1980
+ return;
1981
+ }
1982
+ if (task.context.length > cfg.maxContextSize) {
1983
+ msg.ack();
1984
+ return;
1985
+ }
1986
+ const isCompensation = (task.stepIndex & COMPENSATION_BIT) !== 0;
1987
+ if (isCompensation) {
1988
+ await runCompensation(cfg, msg, task);
1989
+ return;
1990
+ }
1991
+ if (task.stepIndex >= cfg.steps.length) {
1992
+ msg.ack();
1993
+ return;
1994
+ }
1995
+ await runStep(cfg, msg, task);
1996
+ }
1997
+ async function runCompensation(cfg, msg, task) {
1998
+ const idx = task.stepIndex & ~COMPENSATION_BIT;
1999
+ const comp = cfg.steps[idx]?.compensation;
2000
+ if (comp) {
2001
+ try {
2002
+ await comp({ name: cfg.name, instanceId: task.instanceId, stepIndex: idx, attempt: task.attempt, context: task.context });
2003
+ } catch {
2004
+ }
2005
+ }
2006
+ msg.ack();
2007
+ }
2008
+ async function runStep(cfg, msg, task) {
2009
+ const handler = cfg.steps[task.stepIndex].handler;
2010
+ try {
2011
+ const result = await handler({
2012
+ name: cfg.name,
2013
+ instanceId: task.instanceId,
2014
+ stepIndex: task.stepIndex,
2015
+ attempt: task.attempt,
2016
+ context: task.context
2017
+ });
2018
+ if (result.context.length > cfg.maxContextSize) {
2019
+ msg.nack();
2020
+ return;
2021
+ }
2022
+ await advance(cfg, msg, task, result);
2023
+ } catch (err) {
2024
+ await onFailure(cfg, msg, task, err);
2025
+ }
2026
+ }
2027
+ async function advance(cfg, msg, task, result) {
2028
+ const nextStep = task.stepIndex + 1;
2029
+ if (nextStep < cfg.steps.length) {
2030
+ const msgId = `wf:${task.instanceId}:${nextStep}:0`;
2031
+ const subject = `_wf.${cfg.name}.step.${nextStep}`;
2032
+ const buf = encodeTask(task.instanceId, nextStep, 0, result.context);
2033
+ await cfg.client.publish(cfg.taskStreamName, subject, buf, { msgId });
2034
+ }
2035
+ msg.ack();
2036
+ }
2037
+ async function onFailure(cfg, msg, task, err) {
2038
+ if (task.attempt >= cfg.maxRetries) {
2039
+ await publishDlq(cfg, task, err);
2040
+ await publishCompensations(cfg, task);
2041
+ msg.ack();
2042
+ } else {
2043
+ msg.nack();
2044
+ }
2045
+ }
2046
+ async function publishDlq(cfg, task, err) {
2047
+ const dlqSubject = `_wf.${cfg.name}.dlq.${task.stepIndex}`;
2048
+ const errBytes = Buffer.from(String(err));
2049
+ const buf = Buffer.allocUnsafe(7 + 4 + errBytes.length + task.context.length);
2050
+ buf.writeUInt32LE(task.instanceId, 0);
2051
+ buf.writeUInt16LE(task.stepIndex, 4);
2052
+ buf[6] = task.attempt;
2053
+ buf.writeUInt32LE(errBytes.length, 7);
2054
+ errBytes.copy(buf, 11);
2055
+ task.context.copy(buf, 11 + errBytes.length);
2056
+ const msgId = `wf:${task.instanceId}:dlq:${task.stepIndex}`;
2057
+ await cfg.client.publish(cfg.dlqStreamName, dlqSubject, buf, { msgId }).catch(() => {
2058
+ });
2059
+ }
2060
+ async function publishCompensations(cfg, task) {
2061
+ for (let i = task.stepIndex - 1; i >= 0; i--) {
2062
+ const compStep = COMPENSATION_BIT | i;
2063
+ const subject = `_wf.${cfg.name}.compensate.${i}`;
2064
+ const buf = encodeTask(task.instanceId, compStep, 0, task.context);
2065
+ const msgId = `wf:${task.instanceId}:comp:${i}`;
2066
+ await cfg.client.publish(cfg.taskStreamName, subject, buf, { msgId }).catch(() => {
2067
+ });
2068
+ }
2069
+ }
2070
+
2071
+ // src/workflow/workflow.ts
2072
+ var nextWorkerUid = 1;
2073
+ var WorkflowBuilder = class {
2074
+ constructor(client, workflowName) {
2075
+ this.client = client;
2076
+ this.workflowName = workflowName;
2077
+ }
2078
+ triggerSubject;
2079
+ triggerStreamName;
2080
+ steps = [];
2081
+ ackWaitMs = 3e4;
2082
+ maxInflightVal = 10;
2083
+ maxRetriesVal = 3;
2084
+ maxContextSizeVal = 256 * 1024;
2085
+ trigger(subject) {
2086
+ this.triggerSubject = subject;
2087
+ return this;
2088
+ }
2089
+ triggerStream(streamName) {
2090
+ this.triggerStreamName = streamName;
2091
+ return this;
2092
+ }
2093
+ step(name, handler) {
2094
+ this.steps.push({ name, handler, compensation: void 0 });
2095
+ return this;
2096
+ }
2097
+ /** Compensation handler for the most recently added step. */
2098
+ compensate(_stepName, handler) {
2099
+ const last = this.steps[this.steps.length - 1];
2100
+ if (last) last.compensation = handler;
2101
+ return this;
2102
+ }
2103
+ ackWait(ms) {
2104
+ this.ackWaitMs = ms;
2105
+ return this;
2106
+ }
2107
+ inflight(n) {
2108
+ this.maxInflightVal = n;
2109
+ return this;
2110
+ }
2111
+ maxRetries(n) {
2112
+ this.maxRetriesVal = n;
2113
+ return this;
2114
+ }
2115
+ maxContextSize(bytes) {
2116
+ this.maxContextSizeVal = bytes;
2117
+ return this;
2118
+ }
2119
+ async start() {
2120
+ if (!this.triggerSubject) throw new Error("trigger subject required");
2121
+ if (this.steps.length === 0) throw new Error("at least one step required");
2122
+ const name = this.workflowName;
2123
+ const taskStream = `_wf.${name}.tasks`;
2124
+ const taskSubject = `_wf.${name}.>`;
2125
+ const dlqStream = `_wf.${name}.dlq`;
2126
+ const dlqSubject = `_wf.${name}.dlq.>`;
2127
+ await this.client.upsertStream(taskStream, { subjectFilter: taskSubject, idempotencyWindowMs: 3e5 });
2128
+ await this.client.upsertStream(dlqStream, { subjectFilter: dlqSubject });
2129
+ const cfg = {
2130
+ client: this.client,
2131
+ name,
2132
+ taskStreamName: taskStream,
2133
+ dlqStreamName: dlqStream,
2134
+ steps: this.steps,
2135
+ maxContextSize: this.maxContextSizeVal,
2136
+ maxRetries: this.maxRetriesVal
2137
+ };
2138
+ const sub = await this.subscribeWorker(cfg, taskStream, taskSubject);
2139
+ const triggerSub = await this.subscribeTrigger(taskStream, name);
2140
+ return new WorkflowHandle(name, taskStream, dlqStream, sub, triggerSub);
2141
+ }
2142
+ async subscribeWorker(cfg, taskStream, taskSubject) {
2143
+ const uid = nextWorkerUid++;
2144
+ return this.client.subscribe(taskStream, {
2145
+ name: `_wf_${cfg.name}_w${uid}`,
2146
+ group: `_wf_${cfg.name}_workers`,
2147
+ filter: taskSubject,
2148
+ ackPolicy: "explicit" /* Explicit */,
2149
+ ackWaitMs: this.ackWaitMs,
2150
+ maxAckPending: this.maxInflightVal
2151
+ }, (msg) => {
2152
+ void processMessage(cfg, msg);
2153
+ });
2154
+ }
2155
+ async subscribeTrigger(taskStream, name) {
2156
+ if (!this.triggerSubject || !this.triggerStreamName) return void 0;
2157
+ const subject = this.triggerSubject;
2158
+ return this.client.subscribe(this.triggerStreamName, {
2159
+ name: `_wf_${name}_trigger`,
2160
+ filter: subject,
2161
+ ackPolicy: "explicit" /* Explicit */,
2162
+ ackWaitMs: this.ackWaitMs,
2163
+ maxAckPending: 1
2164
+ }, async (msg) => {
2165
+ const id = allocInstanceId();
2166
+ const taskBuf = encodeTask(id, 0, 0, msg.data());
2167
+ await this.client.publish(taskStream, `_wf.${name}.step.0`, taskBuf, { msgId: `wf:${id}:0:0` });
2168
+ msg.ack();
2169
+ });
2170
+ }
2171
+ };
2172
+
1919
2173
  // src/utils/zod.ts
1920
2174
  var import_msgpackr2 = require("msgpackr");
1921
2175
  var packr = new import_msgpackr2.Packr({ structuredClone: false, useRecords: false });
@@ -1937,6 +2191,7 @@ function zodCodec(zodSchema) {
1937
2191
  AckPolicy,
1938
2192
  ArbitroClient,
1939
2193
  ArbitroError,
2194
+ COMPENSATION_BIT,
1940
2195
  Codec,
1941
2196
  Consumer,
1942
2197
  CronBuilder,
@@ -1950,6 +2205,8 @@ function zodCodec(zodSchema) {
1950
2205
  StringCodec,
1951
2206
  Subscription,
1952
2207
  Topic,
2208
+ WorkflowBuilder,
2209
+ WorkflowHandle,
1953
2210
  decodeJson,
1954
2211
  decodeString,
1955
2212
  encodeJson,