@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/README.md +99 -21
- package/dist/chunk-C2QLJBAC.mjs +102 -0
- package/dist/chunk-C2QLJBAC.mjs.map +1 -0
- package/dist/chunk-GW36GP2C.mjs +124 -0
- package/dist/chunk-GW36GP2C.mjs.map +1 -0
- package/dist/constants-57DO6N3H.mjs +33 -0
- package/dist/constants-57DO6N3H.mjs.map +1 -0
- package/dist/index.d.mts +129 -1
- package/dist/index.d.ts +129 -1
- package/dist/index.js +680 -95
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +439 -99
- package/dist/index.mjs.map +1 -1
- package/dist/publish-BSVUMN7T.mjs +14 -0
- package/dist/publish-BSVUMN7T.mjs.map +1 -0
- package/package.json +1 -1
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
|
|
1058
|
+
var EMPTY = Buffer.alloc(0);
|
|
1078
1059
|
function toMsgIdBuf(id) {
|
|
1079
|
-
if (id == null) return
|
|
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 ?
|
|
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,
|