@trigger.dev/redis-worker 4.4.6 → 4.5.0-rc.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.cjs CHANGED
@@ -16361,9 +16361,581 @@ end
16361
16361
  }
16362
16362
  }
16363
16363
  };
16364
+ var BufferEntryStatus = zod.z.enum(["QUEUED", "DRAINING", "FAILED"]);
16365
+ var BufferEntryError = zod.z.object({
16366
+ code: zod.z.string(),
16367
+ message: zod.z.string()
16368
+ });
16369
+ var stringToInt = zod.z.string().transform((v, ctx) => {
16370
+ const n = Number(v);
16371
+ if (!Number.isInteger(n) || n < 0) {
16372
+ ctx.addIssue({ code: zod.z.ZodIssueCode.custom, message: "expected non-negative integer string" });
16373
+ return zod.z.NEVER;
16374
+ }
16375
+ return n;
16376
+ });
16377
+ var stringToDate = zod.z.string().transform((v, ctx) => {
16378
+ const d = new Date(v);
16379
+ if (Number.isNaN(d.getTime())) {
16380
+ ctx.addIssue({ code: zod.z.ZodIssueCode.custom, message: "expected ISO date string" });
16381
+ return zod.z.NEVER;
16382
+ }
16383
+ return d;
16384
+ });
16385
+ var stringToError = zod.z.string().transform((v, ctx) => {
16386
+ try {
16387
+ return BufferEntryError.parse(JSON.parse(v));
16388
+ } catch {
16389
+ ctx.addIssue({ code: zod.z.ZodIssueCode.custom, message: "expected JSON-encoded BufferEntryError" });
16390
+ return zod.z.NEVER;
16391
+ }
16392
+ });
16393
+ var BufferEntrySchema = zod.z.object({
16394
+ runId: zod.z.string().min(1),
16395
+ envId: zod.z.string().min(1),
16396
+ orgId: zod.z.string().min(1),
16397
+ payload: zod.z.string(),
16398
+ status: BufferEntryStatus,
16399
+ attempts: stringToInt,
16400
+ createdAt: stringToDate,
16401
+ lastError: stringToError.optional()
16402
+ });
16403
+ function serialiseSnapshot(snapshot) {
16404
+ return JSON.stringify(snapshot);
16405
+ }
16406
+ function deserialiseSnapshot(serialised) {
16407
+ return JSON.parse(serialised);
16408
+ }
16409
+
16410
+ // src/mollifier/buffer.ts
16411
+ var MollifierBuffer = class {
16412
+ redis;
16413
+ entryTtlSeconds;
16414
+ logger;
16415
+ constructor(options) {
16416
+ this.entryTtlSeconds = options.entryTtlSeconds;
16417
+ this.logger = options.logger ?? new logger$1.Logger("MollifierBuffer", "debug");
16418
+ this.redis = createRedisClient(
16419
+ {
16420
+ ...options.redisOptions,
16421
+ retryStrategy(times) {
16422
+ const delay = Math.min(times * 50, 1e3);
16423
+ return delay;
16424
+ },
16425
+ maxRetriesPerRequest: 20
16426
+ },
16427
+ {
16428
+ onError: (error) => {
16429
+ this.logger.error("MollifierBuffer redis client error:", { error });
16430
+ }
16431
+ }
16432
+ );
16433
+ this.#registerCommands();
16434
+ }
16435
+ // Returns true if the entry was newly written; false if a duplicate runId
16436
+ // was already buffered (idempotent no-op). Callers can use the boolean to
16437
+ // record a duplicate-accept metric without affecting buffer state.
16438
+ async accept(input) {
16439
+ const entryKey = `mollifier:entries:${input.runId}`;
16440
+ const queueKey = `mollifier:queue:${input.envId}`;
16441
+ const orgsKey = "mollifier:orgs";
16442
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
16443
+ const result = await this.redis.acceptMollifierEntry(
16444
+ entryKey,
16445
+ queueKey,
16446
+ orgsKey,
16447
+ input.runId,
16448
+ input.envId,
16449
+ input.orgId,
16450
+ input.payload,
16451
+ createdAt,
16452
+ String(this.entryTtlSeconds),
16453
+ "mollifier:org-envs:"
16454
+ );
16455
+ return result === 1;
16456
+ }
16457
+ async pop(envId) {
16458
+ const queueKey = `mollifier:queue:${envId}`;
16459
+ const orgsKey = "mollifier:orgs";
16460
+ const entryPrefix = "mollifier:entries:";
16461
+ const encoded = await this.redis.popAndMarkDraining(
16462
+ queueKey,
16463
+ orgsKey,
16464
+ entryPrefix,
16465
+ envId,
16466
+ "mollifier:org-envs:"
16467
+ );
16468
+ if (!encoded) return null;
16469
+ let raw;
16470
+ try {
16471
+ raw = JSON.parse(encoded);
16472
+ } catch {
16473
+ this.logger.error("MollifierBuffer.pop: failed to parse script result", { envId });
16474
+ return null;
16475
+ }
16476
+ const parsed = BufferEntrySchema.safeParse(raw);
16477
+ if (!parsed.success) {
16478
+ this.logger.error("MollifierBuffer.pop: invalid entry shape", {
16479
+ envId,
16480
+ errors: parsed.error.flatten()
16481
+ });
16482
+ return null;
16483
+ }
16484
+ return parsed.data;
16485
+ }
16486
+ async getEntry(runId) {
16487
+ const raw = await this.redis.hgetall(`mollifier:entries:${runId}`);
16488
+ if (!raw || Object.keys(raw).length === 0) return null;
16489
+ const parsed = BufferEntrySchema.safeParse(raw);
16490
+ if (!parsed.success) {
16491
+ this.logger.error("MollifierBuffer.getEntry: invalid entry shape", {
16492
+ runId,
16493
+ errors: parsed.error.flatten()
16494
+ });
16495
+ return null;
16496
+ }
16497
+ return parsed.data;
16498
+ }
16499
+ // Drainer walks these two methods to schedule pops with org-level
16500
+ // fairness: one env per org per tick. The Lua scripts maintain both
16501
+ // sets atomically with the per-env queues, so an org/env appears here
16502
+ // exactly when at least one of its envs has a queued entry.
16503
+ async listOrgs() {
16504
+ return this.redis.smembers("mollifier:orgs");
16505
+ }
16506
+ async listEnvsForOrg(orgId) {
16507
+ return this.redis.smembers(`mollifier:org-envs:${orgId}`);
16508
+ }
16509
+ async ack(runId) {
16510
+ await this.redis.del(`mollifier:entries:${runId}`);
16511
+ }
16512
+ async requeue(runId) {
16513
+ await this.redis.requeueMollifierEntry(
16514
+ `mollifier:entries:${runId}`,
16515
+ "mollifier:orgs",
16516
+ "mollifier:queue:",
16517
+ runId,
16518
+ "mollifier:org-envs:"
16519
+ );
16520
+ }
16521
+ // Returns true if the entry transitioned to FAILED; false if the entry no
16522
+ // longer exists (TTL expired between pop and fail). Caller can use the
16523
+ // boolean to skip downstream FAILED handling for ghost entries.
16524
+ async fail(runId, error) {
16525
+ const result = await this.redis.failMollifierEntry(
16526
+ `mollifier:entries:${runId}`,
16527
+ JSON.stringify(error)
16528
+ );
16529
+ return result === 1;
16530
+ }
16531
+ async getEntryTtlSeconds(runId) {
16532
+ return this.redis.ttl(`mollifier:entries:${runId}`);
16533
+ }
16534
+ async evaluateTrip(envId, options) {
16535
+ const rateKey = `mollifier:rate:${envId}`;
16536
+ const trippedKey = `mollifier:tripped:${envId}`;
16537
+ const result = await this.redis.mollifierEvaluateTrip(
16538
+ rateKey,
16539
+ trippedKey,
16540
+ String(options.windowMs),
16541
+ String(options.threshold),
16542
+ String(options.holdMs)
16543
+ );
16544
+ return { count: result[0], tripped: result[1] === 1 };
16545
+ }
16546
+ async close() {
16547
+ await this.redis.quit();
16548
+ }
16549
+ #registerCommands() {
16550
+ this.redis.defineCommand("acceptMollifierEntry", {
16551
+ numberOfKeys: 3,
16552
+ lua: `
16553
+ local entryKey = KEYS[1]
16554
+ local queueKey = KEYS[2]
16555
+ local orgsKey = KEYS[3]
16556
+ local runId = ARGV[1]
16557
+ local envId = ARGV[2]
16558
+ local orgId = ARGV[3]
16559
+ local payload = ARGV[4]
16560
+ local createdAt = ARGV[5]
16561
+ local ttlSeconds = tonumber(ARGV[6])
16562
+ local orgEnvsPrefix = ARGV[7]
16563
+
16564
+ -- Idempotent: refuse if an entry for this runId already exists in any
16565
+ -- state. Caller-side dedup is also enforced via API idempotency keys,
16566
+ -- but the buffer must not double-enqueue if a caller retries.
16567
+ if redis.call('EXISTS', entryKey) == 1 then
16568
+ return 0
16569
+ end
16570
+
16571
+ redis.call('HSET', entryKey,
16572
+ 'runId', runId,
16573
+ 'envId', envId,
16574
+ 'orgId', orgId,
16575
+ 'payload', payload,
16576
+ 'status', 'QUEUED',
16577
+ 'attempts', '0',
16578
+ 'createdAt', createdAt)
16579
+ redis.call('EXPIRE', entryKey, ttlSeconds)
16580
+ redis.call('LPUSH', queueKey, runId)
16581
+ -- Org-level membership: maintained atomically with the per-env
16582
+ -- queue so the drainer can walk orgs \u2192 envs-for-org and
16583
+ -- schedule one env per org per tick. SADDs are idempotent if the
16584
+ -- org/env are already tracked.
16585
+ redis.call('SADD', orgsKey, orgId)
16586
+ redis.call('SADD', orgEnvsPrefix .. orgId, envId)
16587
+ return 1
16588
+ `
16589
+ });
16590
+ this.redis.defineCommand("requeueMollifierEntry", {
16591
+ numberOfKeys: 2,
16592
+ lua: `
16593
+ local entryKey = KEYS[1]
16594
+ local orgsKey = KEYS[2]
16595
+ local queuePrefix = ARGV[1]
16596
+ local runId = ARGV[2]
16597
+ local orgEnvsPrefix = ARGV[3]
16598
+
16599
+ local envId = redis.call('HGET', entryKey, 'envId')
16600
+ local orgId = redis.call('HGET', entryKey, 'orgId')
16601
+ if not envId then
16602
+ return 0
16603
+ end
16604
+
16605
+ local currentAttempts = redis.call('HGET', entryKey, 'attempts')
16606
+ local nextAttempts = tonumber(currentAttempts or '0') + 1
16607
+
16608
+ redis.call('HSET', entryKey, 'status', 'QUEUED', 'attempts', tostring(nextAttempts))
16609
+ redis.call('LPUSH', queuePrefix .. envId, runId)
16610
+ -- Re-track the org/env: pop may have SREM'd them when the queue
16611
+ -- last emptied. SADDs are idempotent if the values are still
16612
+ -- present.
16613
+ if orgId then
16614
+ redis.call('SADD', orgsKey, orgId)
16615
+ redis.call('SADD', orgEnvsPrefix .. orgId, envId)
16616
+ end
16617
+ return 1
16618
+ `
16619
+ });
16620
+ this.redis.defineCommand("popAndMarkDraining", {
16621
+ numberOfKeys: 2,
16622
+ lua: `
16623
+ local queueKey = KEYS[1]
16624
+ local orgsKey = KEYS[2]
16625
+ local entryPrefix = ARGV[1]
16626
+ local envId = ARGV[2]
16627
+ local orgEnvsPrefix = ARGV[3]
16628
+
16629
+ -- Helper: prune org-level membership when an env's queue empties.
16630
+ -- Called only from the success branch where we know orgId from the
16631
+ -- popped entry. The no-runId branch below can't reach this because
16632
+ -- it has no entry to read orgId from \u2014 accept any stale org-envs
16633
+ -- entries that result (bounded by env count, recovered next accept).
16634
+ local function pruneOrgMembership(orgId)
16635
+ if not orgId then return end
16636
+ local orgEnvsKey = orgEnvsPrefix .. orgId
16637
+ redis.call('SREM', orgEnvsKey, envId)
16638
+ if redis.call('SCARD', orgEnvsKey) == 0 then
16639
+ redis.call('SREM', orgsKey, orgId)
16640
+ end
16641
+ end
16642
+
16643
+ -- Loop to skip orphan queue references \u2014 runIds whose entry hash has
16644
+ -- expired (TTL hit). HSET on a missing key would CREATE a partial
16645
+ -- hash without a TTL, leaking memory. The loop is bounded by queue
16646
+ -- length; entire Lua script remains atomic.
16647
+ while true do
16648
+ local runId = redis.call('RPOP', queueKey)
16649
+ if not runId then
16650
+ -- Queue is empty AND we have no entry to read orgId from, so
16651
+ -- skip org-level cleanup. Stale org-envs entries are bounded
16652
+ -- by env count and recovered on the next accept.
16653
+ return nil
16654
+ end
16655
+
16656
+ local entryKey = entryPrefix .. runId
16657
+ if redis.call('EXISTS', entryKey) == 1 then
16658
+ redis.call('HSET', entryKey, 'status', 'DRAINING')
16659
+ local raw = redis.call('HGETALL', entryKey)
16660
+ local result = {}
16661
+ for i = 1, #raw, 2 do
16662
+ result[raw[i]] = raw[i + 1]
16663
+ end
16664
+ -- Prune org-level membership if this pop drained the queue.
16665
+ -- Atomic with the RPOP above \u2014 a concurrent accept AFTER this
16666
+ -- script will SADD both back along with its LPUSH.
16667
+ if redis.call('LLEN', queueKey) == 0 then
16668
+ pruneOrgMembership(result['orgId'])
16669
+ end
16670
+ return cjson.encode(result)
16671
+ end
16672
+ -- Orphan queue reference: entry TTL expired while runId was queued.
16673
+ -- Discard the reference and loop to the next.
16674
+ end
16675
+ `
16676
+ });
16677
+ this.redis.defineCommand("failMollifierEntry", {
16678
+ numberOfKeys: 1,
16679
+ lua: `
16680
+ local entryKey = KEYS[1]
16681
+ local errorPayload = ARGV[1]
16682
+
16683
+ -- Guard: never create a partial entry. If the hash expired between
16684
+ -- pop and fail, the run is gone \u2014 nothing to mark FAILED.
16685
+ if redis.call('EXISTS', entryKey) == 0 then
16686
+ return 0
16687
+ end
16688
+
16689
+ redis.call('HSET', entryKey, 'status', 'FAILED', 'lastError', errorPayload)
16690
+ return 1
16691
+ `
16692
+ });
16693
+ this.redis.defineCommand("mollifierEvaluateTrip", {
16694
+ numberOfKeys: 2,
16695
+ lua: `
16696
+ local rateKey = KEYS[1]
16697
+ local trippedKey = KEYS[2]
16698
+ local windowMs = tonumber(ARGV[1])
16699
+ local threshold = tonumber(ARGV[2])
16700
+ local holdMs = tonumber(ARGV[3])
16701
+
16702
+ local count = redis.call('INCR', rateKey)
16703
+ if count == 1 then
16704
+ redis.call('PEXPIRE', rateKey, windowMs)
16705
+ end
16706
+
16707
+ if count > threshold then
16708
+ redis.call('PSETEX', trippedKey, holdMs, '1')
16709
+ end
16710
+
16711
+ local tripped = redis.call('EXISTS', trippedKey)
16712
+ return {count, tripped}
16713
+ `
16714
+ });
16715
+ }
16716
+ };
16717
+ var MollifierDrainer = class {
16718
+ buffer;
16719
+ handler;
16720
+ maxAttempts;
16721
+ isRetryable;
16722
+ pollIntervalMs;
16723
+ maxOrgsPerTick;
16724
+ logger;
16725
+ limit;
16726
+ // Rotation state. `orgCursor` advances through the active-orgs list.
16727
+ // Each org has its own internal cursor in `perOrgEnvCursors` for
16728
+ // cycling through that org's envs. Both reset on `start()`.
16729
+ orgCursor = 0;
16730
+ perOrgEnvCursors = /* @__PURE__ */ new Map();
16731
+ isRunning = false;
16732
+ stopping = false;
16733
+ loopPromise = null;
16734
+ constructor(options) {
16735
+ this.buffer = options.buffer;
16736
+ this.handler = options.handler;
16737
+ this.maxAttempts = options.maxAttempts;
16738
+ this.isRetryable = options.isRetryable;
16739
+ this.pollIntervalMs = options.pollIntervalMs ?? 100;
16740
+ this.maxOrgsPerTick = options.maxOrgsPerTick ?? 500;
16741
+ this.logger = options.logger ?? new logger$1.Logger("MollifierDrainer", "debug");
16742
+ this.limit = pLimit(options.concurrency);
16743
+ }
16744
+ async runOnce() {
16745
+ const orgs = await this.buffer.listOrgs();
16746
+ if (orgs.length === 0) return { drained: 0, failed: 0 };
16747
+ const orgSlice = this.takeOrgSlice(orgs);
16748
+ const envsByOrg = await Promise.all(
16749
+ orgSlice.map((orgId) => this.buffer.listEnvsForOrg(orgId))
16750
+ );
16751
+ const targets = [];
16752
+ for (let i = 0; i < orgSlice.length; i++) {
16753
+ const orgId = orgSlice[i];
16754
+ const envsForOrg = envsByOrg[i];
16755
+ if (envsForOrg.length === 0) continue;
16756
+ const envId = this.pickEnvForOrg(orgId, envsForOrg);
16757
+ targets.push(envId);
16758
+ }
16759
+ const inflight = [];
16760
+ for (const envId of targets) {
16761
+ inflight.push(this.limit(() => this.processOneFromEnv(envId)));
16762
+ }
16763
+ const results = await Promise.all(inflight);
16764
+ return {
16765
+ drained: results.filter((r) => r === "drained").length,
16766
+ failed: results.filter((r) => r === "failed").length
16767
+ };
16768
+ }
16769
+ start() {
16770
+ if (this.isRunning) return;
16771
+ this.isRunning = true;
16772
+ this.stopping = false;
16773
+ this.orgCursor = 0;
16774
+ this.perOrgEnvCursors = /* @__PURE__ */ new Map();
16775
+ this.loopPromise = this.loop();
16776
+ }
16777
+ // Signal the loop to exit (`stopping = true`) and wait for it. With no
16778
+ // timeout, wait indefinitely for the in-flight `runOnce` and its handlers
16779
+ // to settle — same semantic as FairQueue / BatchQueue's `stop()`. With a
16780
+ // timeout, race the loop promise against a deadline so a hung handler
16781
+ // can't wedge the process past its termination grace period.
16782
+ async stop(options = {}) {
16783
+ if (!this.isRunning || !this.loopPromise) return;
16784
+ this.stopping = true;
16785
+ if (options.timeoutMs == null) {
16786
+ await this.loopPromise;
16787
+ return;
16788
+ }
16789
+ const timeoutSentinel = Symbol("mollifier.stop.timeout");
16790
+ let timeoutHandle;
16791
+ const timeoutPromise = new Promise((resolve) => {
16792
+ timeoutHandle = setTimeout(() => resolve(timeoutSentinel), options.timeoutMs);
16793
+ });
16794
+ try {
16795
+ const winner = await Promise.race([
16796
+ this.loopPromise.then(() => "done"),
16797
+ timeoutPromise
16798
+ ]);
16799
+ if (winner === timeoutSentinel) {
16800
+ this.logger.warn(
16801
+ "MollifierDrainer.stop: deadline exceeded; returning while loop iteration is in flight",
16802
+ { timeoutMs: options.timeoutMs }
16803
+ );
16804
+ }
16805
+ } finally {
16806
+ if (timeoutHandle) clearTimeout(timeoutHandle);
16807
+ }
16808
+ }
16809
+ // Transient Redis errors (e.g. a connection blip in `listOrgs` /
16810
+ // `listEnvsForOrg` / `pop`) must not kill the polling loop permanently.
16811
+ // We log each `runOnce` failure, back off so we don't spin tight on a
16812
+ // sustained outage, and resume. The loop only exits when `stop()` flips
16813
+ // `stopping`.
16814
+ async loop() {
16815
+ try {
16816
+ let consecutiveErrors = 0;
16817
+ while (!this.stopping) {
16818
+ try {
16819
+ const result = await this.runOnce();
16820
+ consecutiveErrors = 0;
16821
+ if (result.drained === 0 && result.failed === 0) {
16822
+ await this.delay(this.pollIntervalMs);
16823
+ }
16824
+ } catch (err) {
16825
+ consecutiveErrors += 1;
16826
+ this.logger.error("MollifierDrainer.runOnce failed; backing off", {
16827
+ err,
16828
+ consecutiveErrors
16829
+ });
16830
+ await this.delay(this.backoffMs(consecutiveErrors));
16831
+ }
16832
+ }
16833
+ } finally {
16834
+ this.isRunning = false;
16835
+ }
16836
+ }
16837
+ // Exponential backoff capped at 5s. Keeps the loop responsive after a
16838
+ // brief blip while preventing a tight retry loop during a long Redis
16839
+ // outage. 1 → 200ms, 2 → 400ms, 3 → 800ms, 4 → 1.6s, 5 → 3.2s, 6+ → 5s.
16840
+ backoffMs(consecutiveErrors) {
16841
+ const base = Math.max(this.pollIntervalMs, 100);
16842
+ const capped = Math.min(base * 2 ** (consecutiveErrors - 1), 5e3);
16843
+ return capped;
16844
+ }
16845
+ delay(ms) {
16846
+ return new Promise((resolve) => setTimeout(resolve, ms));
16847
+ }
16848
+ // Take up to `maxOrgsPerTick` orgs starting at the current cursor, with
16849
+ // wrap-around. Cursor advances by 1 each tick so every org reaches
16850
+ // every slot position (0..sliceSize-1) over a full cycle — no
16851
+ // head-of-line bias within the slice. Orgs are sorted before slicing
16852
+ // so rotation is deterministic regardless of Redis SET iteration order.
16853
+ takeOrgSlice(orgs) {
16854
+ const sorted = [...orgs].sort();
16855
+ const n = sorted.length;
16856
+ const sliceSize = Math.min(this.maxOrgsPerTick, n);
16857
+ const start = this.orgCursor % n;
16858
+ this.orgCursor = (this.orgCursor + 1) % Math.max(n, 1);
16859
+ const end = start + sliceSize;
16860
+ if (end <= n) return sorted.slice(start, end);
16861
+ return [...sorted.slice(start), ...sorted.slice(0, end - n)];
16862
+ }
16863
+ // Pick one env from the org's active-envs list, rotating per org via
16864
+ // the per-org cursor. Each org's cursor advances by 1 each visit, so
16865
+ // an org with N envs cycles through them across N visits.
16866
+ pickEnvForOrg(orgId, envsForOrg) {
16867
+ const sorted = [...envsForOrg].sort();
16868
+ const cursor = this.perOrgEnvCursors.get(orgId) ?? 0;
16869
+ const idx = cursor % sorted.length;
16870
+ this.perOrgEnvCursors.set(orgId, (cursor + 1) % sorted.length);
16871
+ return sorted[idx];
16872
+ }
16873
+ // A failure for one env (e.g. a Redis hiccup mid-batch in `pop`, or in
16874
+ // `requeue`/`fail` during error recovery inside `processEntry`) must not
16875
+ // poison the rest of the batch — `Promise.all` would otherwise reject and
16876
+ // bubble all the way to `loop()`. Catch both stages here so the failed env
16877
+ // is just counted as "failed" for this tick and we move on.
16878
+ async processOneFromEnv(envId) {
16879
+ let entry;
16880
+ try {
16881
+ entry = await this.buffer.pop(envId);
16882
+ } catch (err) {
16883
+ this.logger.error("MollifierDrainer.pop failed", { envId, err });
16884
+ return "failed";
16885
+ }
16886
+ if (!entry) return "empty";
16887
+ try {
16888
+ return await this.processEntry(entry);
16889
+ } catch (err) {
16890
+ this.logger.error("MollifierDrainer.processEntry failed", {
16891
+ envId,
16892
+ runId: entry.runId,
16893
+ err
16894
+ });
16895
+ return "failed";
16896
+ }
16897
+ }
16898
+ async processEntry(entry) {
16899
+ try {
16900
+ const payload = deserialiseSnapshot(entry.payload);
16901
+ await this.handler({
16902
+ runId: entry.runId,
16903
+ envId: entry.envId,
16904
+ orgId: entry.orgId,
16905
+ payload,
16906
+ attempts: entry.attempts,
16907
+ createdAt: entry.createdAt
16908
+ });
16909
+ await this.buffer.ack(entry.runId);
16910
+ return "drained";
16911
+ } catch (err) {
16912
+ const nextAttempts = entry.attempts + 1;
16913
+ if (this.isRetryable(err) && nextAttempts < this.maxAttempts) {
16914
+ await this.buffer.requeue(entry.runId);
16915
+ this.logger.warn("MollifierDrainer: retryable error, requeued", {
16916
+ runId: entry.runId,
16917
+ attempts: nextAttempts
16918
+ });
16919
+ return "failed";
16920
+ }
16921
+ const code = err instanceof Error ? err.name : "Unknown";
16922
+ const message = err instanceof Error ? err.message : String(err);
16923
+ await this.buffer.fail(entry.runId, { code, message });
16924
+ this.logger.error("MollifierDrainer: terminal failure", {
16925
+ runId: entry.runId,
16926
+ code,
16927
+ message
16928
+ });
16929
+ return "failed";
16930
+ }
16931
+ }
16932
+ };
16364
16933
 
16365
16934
  exports.BaseScheduler = BaseScheduler;
16366
16935
  exports.BatchedSpanManager = BatchedSpanManager;
16936
+ exports.BufferEntryError = BufferEntryError;
16937
+ exports.BufferEntrySchema = BufferEntrySchema;
16938
+ exports.BufferEntryStatus = BufferEntryStatus;
16367
16939
  exports.CallbackFairQueueKeyProducer = CallbackFairQueueKeyProducer;
16368
16940
  exports.ConcurrencyManager = ConcurrencyManager;
16369
16941
  exports.CronSchema = CronSchema;
@@ -16379,6 +16951,8 @@ exports.ImmediateRetry = ImmediateRetry;
16379
16951
  exports.LinearBackoffRetry = LinearBackoffRetry;
16380
16952
  exports.MasterQueue = MasterQueue;
16381
16953
  exports.MessagingAttributes = MessagingAttributes;
16954
+ exports.MollifierBuffer = MollifierBuffer;
16955
+ exports.MollifierDrainer = MollifierDrainer;
16382
16956
  exports.NoRetry = NoRetry;
16383
16957
  exports.NoopScheduler = NoopScheduler;
16384
16958
  exports.RoundRobinScheduler = RoundRobinScheduler;
@@ -16390,7 +16964,9 @@ exports.Worker = Worker;
16390
16964
  exports.WorkerQueueManager = WorkerQueueManager;
16391
16965
  exports.createDefaultRetryStrategy = createDefaultRetryStrategy;
16392
16966
  exports.defaultRetryOptions = defaultRetryOptions;
16967
+ exports.deserialiseSnapshot = deserialiseSnapshot;
16393
16968
  exports.isAbortError = isAbortError;
16394
16969
  exports.noopTelemetry = noopTelemetry;
16970
+ exports.serialiseSnapshot = serialiseSnapshot;
16395
16971
  //# sourceMappingURL=index.cjs.map
16396
16972
  //# sourceMappingURL=index.cjs.map