@semiont/jobs 0.5.36 → 0.5.37

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
@@ -2,6 +2,7 @@ import { promises, mkdtempSync, writeFileSync, readFileSync, rmSync } from 'fs';
2
2
  import * as path from 'path';
3
3
  import { join } from 'path';
4
4
  import { replyChannelsFor, jobId, deriveViews, GENERATABLE_MEDIA_TYPES, estimateTokens, isObject, isString, reconcileSelector, getLocaleEnglishName, cutChunk, chunkText } from '@semiont/core';
5
+ import { connect, RetentionPolicy, DeliverPolicy, AckPolicy, nanos } from 'nats';
5
6
  import { withSpan, recordAnchorOutcome, recordDetectionCall } from '@semiont/observability';
6
7
  import { StructuredReadError } from '@semiont/inference';
7
8
  import { execFileSync } from 'child_process';
@@ -18,10 +19,7 @@ function willRetryAfter(metadata, failureClass) {
18
19
  return failureClass !== "deterministic" && metadata.retryCount < metadata.maxRetries;
19
20
  }
20
21
 
21
- // src/fs-job-queue.ts
22
- var REANNOUNCE_INTERVAL_MS = 3e4;
23
- var STALE_RUNNING_MS = 30 * 6e4;
24
- var PROGRESS_WRITE_MIN_INTERVAL_MS = 5e3;
22
+ // src/checkpoint-merge.ts
25
23
  function mergeUnitCursors(existing, incoming, completed) {
26
24
  const done = new Set(completed);
27
25
  const merged = {};
@@ -35,6 +33,11 @@ function mergeUnitCursors(existing, incoming, completed) {
35
33
  }
36
34
  return merged;
37
35
  }
36
+
37
+ // src/fs-job-queue.ts
38
+ var REANNOUNCE_INTERVAL_MS = 3e4;
39
+ var STALE_RUNNING_MS = 30 * 6e4;
40
+ var PROGRESS_WRITE_MIN_INTERVAL_MS = 5e3;
38
41
  var RETENTION_HOURS = 24;
39
42
  var CLEANUP_INTERVAL_MS = 36e5;
40
43
  var FsJobQueue = class {
@@ -165,10 +168,43 @@ var FsJobQueue = class {
165
168
  }
166
169
  return null;
167
170
  }
171
+ /**
172
+ * Serializes claims. This driver's atomicity is single-process by design,
173
+ * but `claimJob`'s contract must hold even against interleaved awaits in
174
+ * that one process — a read-check-write with await points in it can admit
175
+ * two winners without this chain (the TOCTOU the operation exists to kill).
176
+ */
177
+ claimChain = Promise.resolve();
178
+ async claimNextJob(types) {
179
+ const claim = this.claimChain.then(() => this.doClaimNext(types));
180
+ this.claimChain = claim.catch(() => void 0);
181
+ return claim;
182
+ }
183
+ async doClaimNext(types) {
184
+ const pending = await this.listJobs({ status: "pending", limit: Number.MAX_SAFE_INTEGER });
185
+ const match = pending.find((j) => types.length === 0 || types.includes(j.metadata.type));
186
+ if (!match) return { declined: "none-available" };
187
+ const running = {
188
+ status: "running",
189
+ metadata: match.metadata,
190
+ params: match.params,
191
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
192
+ progress: {}
193
+ };
194
+ await this.transition(running, "pending");
195
+ return { job: running };
196
+ }
168
197
  /**
169
198
  * Update a job (atomic: delete old, write new)
170
199
  */
171
- async updateJob(job, oldStatus) {
200
+ /**
201
+ * Cross-status move (or in-place rewrite): delete old slot, write new.
202
+ * Private — the interface exposes named transitions only (JOB-QUEUE-DRIVER
203
+ * P0); a generic public patch is trivial on files and impossible on an
204
+ * in-flight message, so no driver may offer one. Re-entering `pending`
205
+ * announces, which is how failJob's retry reaches a listening worker.
206
+ */
207
+ async transition(job, oldStatus) {
172
208
  if (oldStatus && oldStatus !== job.status) {
173
209
  const oldPath = this.getJobPath(job.metadata.id, oldStatus);
174
210
  try {
@@ -206,7 +242,7 @@ var FsJobQueue = class {
206
242
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
207
243
  result
208
244
  };
209
- await this.updateJob(completed, "running");
245
+ await this.transition(completed, "running");
210
246
  return true;
211
247
  }
212
248
  /**
@@ -235,7 +271,7 @@ var FsJobQueue = class {
235
271
  metadata: { ...metadata, retryCount: job.metadata.retryCount + 1 },
236
272
  params: job.params
237
273
  };
238
- await this.updateJob(retried, "running");
274
+ await this.transition(retried, "running");
239
275
  return "retried";
240
276
  }
241
277
  if (failureClass === "deterministic" && job.metadata.retryCount < job.metadata.maxRetries) {
@@ -249,7 +285,7 @@ var FsJobQueue = class {
249
285
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
250
286
  error
251
287
  };
252
- await this.updateJob(failed, "running");
288
+ await this.transition(failed, "running");
253
289
  return "failed";
254
290
  }
255
291
  /**
@@ -352,7 +388,7 @@ var FsJobQueue = class {
352
388
  startedAt: job.status === "running" ? job.startedAt : void 0,
353
389
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
354
390
  };
355
- await this.updateJob(cancelledJob, oldStatus);
391
+ await this.transition(cancelledJob, oldStatus);
356
392
  return true;
357
393
  }
358
394
  /**
@@ -474,6 +510,470 @@ var FsJobQueue = class {
474
510
  return stats;
475
511
  }
476
512
  };
513
+ var STREAM = "JOBS";
514
+ var CONSUMER = "gateway-claims";
515
+ var BUCKET = "jobs";
516
+ var PROGRESS_WRITE_MIN_INTERVAL_MS2 = 5e3;
517
+ var MAX_CAS_ATTEMPTS = 20;
518
+ var enc = new TextEncoder();
519
+ var dec = new TextDecoder();
520
+ function categoryOf(type) {
521
+ return type === "generation" ? "generation" : "annotation";
522
+ }
523
+ function subjectFor(job) {
524
+ return `jobs.${categoryOf(job.metadata.type)}.${job.metadata.type}`;
525
+ }
526
+ var JetStreamJobQueue = class {
527
+ constructor(options, logger, eventBus) {
528
+ this.options = options;
529
+ this.logger = logger;
530
+ this.eventBus = eventBus;
531
+ this.staleRunningMs = options.staleRunningMs ?? 30 * 6e4;
532
+ this.ackWaitMs = options.ackWaitMs ?? 3e4;
533
+ this.tickMs = options.tickMs ?? 3e4;
534
+ }
535
+ options;
536
+ logger;
537
+ eventBus;
538
+ nc;
539
+ js;
540
+ jsm;
541
+ kv;
542
+ iter = null;
543
+ /** Delivered messages this process holds — the lease, keyed by jobId,
544
+ * with the job's type so a by-type claim can walk them without a read. */
545
+ held = /* @__PURE__ */ new Map();
546
+ heartbeat = null;
547
+ reconciling = false;
548
+ tick = null;
549
+ ticking = false;
550
+ lastProgressWrite = /* @__PURE__ */ new Map();
551
+ staleRunningMs;
552
+ ackWaitMs;
553
+ tickMs;
554
+ async initialize() {
555
+ this.nc = await connect({
556
+ servers: this.options.servers,
557
+ reconnect: this.options.reconnect ?? true,
558
+ timeout: 1e4
559
+ });
560
+ this.js = this.nc.jetstream();
561
+ this.jsm = await this.nc.jetstreamManager();
562
+ this.kv = await this.js.views.kv(BUCKET);
563
+ try {
564
+ await this.jsm.streams.info(STREAM);
565
+ } catch {
566
+ await this.jsm.streams.add({
567
+ name: STREAM,
568
+ subjects: ["jobs.>"],
569
+ retention: RetentionPolicy.Workqueue
570
+ });
571
+ }
572
+ try {
573
+ await this.jsm.consumers.info(STREAM, CONSUMER);
574
+ } catch {
575
+ await this.jsm.consumers.add(STREAM, {
576
+ durable_name: CONSUMER,
577
+ ack_policy: AckPolicy.Explicit,
578
+ deliver_policy: DeliverPolicy.All,
579
+ ack_wait: nanos(this.ackWaitMs),
580
+ // ONE retry authority: will-retry.ts decides; the stream never
581
+ // gives up a message on its own count.
582
+ max_deliver: -1
583
+ });
584
+ }
585
+ const consumer = await this.js.consumers.get(STREAM, CONSUMER);
586
+ this.iter = await consumer.consume({
587
+ callback: (m) => {
588
+ void this.onDelivery(m).catch((error) => {
589
+ this.logger.warn("Job delivery handling failed", {
590
+ error: error instanceof Error ? error.message : String(error)
591
+ });
592
+ });
593
+ }
594
+ });
595
+ this.heartbeat = setInterval(() => {
596
+ if (this.reconciling) return;
597
+ this.reconciling = true;
598
+ void this.reconcileHeld().catch((error) => {
599
+ this.logger.warn("Held-lease reconciliation failed", {
600
+ error: error instanceof Error ? error.message : String(error)
601
+ });
602
+ }).finally(() => {
603
+ this.reconciling = false;
604
+ });
605
+ }, Math.max(250, Math.floor(this.ackWaitMs / 4)));
606
+ this.heartbeat.unref?.();
607
+ this.tick = setInterval(() => {
608
+ if (this.ticking) return;
609
+ this.ticking = true;
610
+ void (async () => {
611
+ for (const [id, held] of [...this.held]) {
612
+ const envelope = await this.read(jobId(id));
613
+ if (envelope?.job.status === "pending") this.announce(envelope.job);
614
+ }
615
+ await this.recoverStaleRunningJobs();
616
+ })().catch((error) => {
617
+ this.logger.warn("Job-queue tick failed", {
618
+ error: error instanceof Error ? error.message : String(error)
619
+ });
620
+ }).finally(() => {
621
+ this.ticking = false;
622
+ });
623
+ }, this.tickMs);
624
+ this.tick.unref?.();
625
+ }
626
+ destroy() {
627
+ this.iter?.stop();
628
+ this.iter = null;
629
+ if (this.heartbeat) clearInterval(this.heartbeat);
630
+ this.heartbeat = null;
631
+ if (this.tick) clearInterval(this.tick);
632
+ this.tick = null;
633
+ this.held.clear();
634
+ void this.nc?.close();
635
+ }
636
+ /**
637
+ * A delivery is a job arriving at this gateway. What it means depends on
638
+ * the job's authoritative (KV) state:
639
+ * - pending → hold the lease, announce for a worker to claim;
640
+ * - running → hold silently (a claim raced ahead of the delivery, or a
641
+ * redelivery reached a fresh instance after a gateway death);
642
+ * - terminal → the work already concluded elsewhere; consume the message;
643
+ * - unknown → not ours to run; terminate it.
644
+ */
645
+ async onDelivery(m) {
646
+ let id;
647
+ try {
648
+ id = JSON.parse(dec.decode(m.data)).jobId;
649
+ } catch {
650
+ m.term();
651
+ return;
652
+ }
653
+ const envelope = await this.read(jobId(id));
654
+ if (!envelope) {
655
+ m.term();
656
+ return;
657
+ }
658
+ switch (envelope.job.status) {
659
+ case "pending":
660
+ this.held.set(id, { m, type: envelope.job.metadata.type });
661
+ this.announce(envelope.job);
662
+ break;
663
+ case "running":
664
+ this.held.set(id, { m, type: envelope.job.metadata.type });
665
+ break;
666
+ default:
667
+ m.ack();
668
+ this.held.delete(id);
669
+ }
670
+ }
671
+ /** Same wire shape as the fs driver: only jobs with a resourceId announce. */
672
+ announce(job) {
673
+ if (this.eventBus && "params" in job && "resourceId" in job.params) {
674
+ this.eventBus.get("job:queued").next({
675
+ jobId: job.metadata.id,
676
+ jobType: job.metadata.type,
677
+ resourceId: job.params.resourceId,
678
+ userId: job.metadata.userId
679
+ });
680
+ }
681
+ }
682
+ async read(jobIdArg) {
683
+ const entry = await this.kv.get(jobIdArg);
684
+ if (!entry || entry.operation !== "PUT") return null;
685
+ const envelope = JSON.parse(dec.decode(entry.value));
686
+ return { ...envelope, revision: entry.revision };
687
+ }
688
+ async write(jobIdArg, envelope, revision) {
689
+ const payload = enc.encode(JSON.stringify(envelope));
690
+ if (revision === void 0) return this.kv.put(jobIdArg, payload);
691
+ return this.kv.update(jobIdArg, payload, revision);
692
+ }
693
+ /**
694
+ * Read-transform-CAS with bounded retries: the transform sees the current
695
+ * job and returns the replacement (or null to stop). Every state
696
+ * transition goes through here, which is what makes each one atomic.
697
+ */
698
+ async cas(jobIdArg, transform, onMissing) {
699
+ for (let attempt = 0; attempt < MAX_CAS_ATTEMPTS; attempt++) {
700
+ const current = await this.read(jobIdArg);
701
+ if (!current) return onMissing;
702
+ const out = transform(current.job, current.lastProgressAt);
703
+ if (out === null) return onMissing;
704
+ if (!("envelope" in out)) return out.result;
705
+ try {
706
+ await this.write(jobIdArg, out.envelope, current.revision);
707
+ return out.result;
708
+ } catch {
709
+ }
710
+ }
711
+ throw new Error(`Job ${jobIdArg} transition failed after ${MAX_CAS_ATTEMPTS} CAS attempts`);
712
+ }
713
+ async createJob(job) {
714
+ const now = (/* @__PURE__ */ new Date()).toISOString();
715
+ await this.kv.create(job.metadata.id, enc.encode(JSON.stringify({ job, lastProgressAt: now })));
716
+ if (job.status === "pending") {
717
+ await this.js.publish(subjectFor(job), enc.encode(JSON.stringify({ jobId: job.metadata.id })));
718
+ }
719
+ }
720
+ async getJob(jobIdArg) {
721
+ const envelope = await this.read(jobIdArg);
722
+ return envelope?.job ?? null;
723
+ }
724
+ async claimNextJob(types) {
725
+ const matches = (t) => types.length === 0 || types.includes(t);
726
+ for (const [id, held] of [...this.held]) {
727
+ if (!matches(held.type)) continue;
728
+ const won = await this.tryClaim(jobId(id));
729
+ if (won) return won;
730
+ }
731
+ for (const key of await this.allKeys()) {
732
+ const envelope = await this.read(jobId(key));
733
+ if (!envelope || envelope.job.status !== "pending" || !matches(envelope.job.metadata.type)) continue;
734
+ const won = await this.tryClaim(jobId(key));
735
+ if (won) return won;
736
+ }
737
+ return { declined: "none-available" };
738
+ }
739
+ /** CAS one pending job to running; null when someone else won it. */
740
+ async tryClaim(jobIdArg) {
741
+ return this.cas(
742
+ jobIdArg,
743
+ (job) => {
744
+ if (job.status !== "pending") return { result: null };
745
+ const running = {
746
+ status: "running",
747
+ metadata: job.metadata,
748
+ params: job.params,
749
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
750
+ progress: {}
751
+ };
752
+ return {
753
+ envelope: { job: running, lastProgressAt: (/* @__PURE__ */ new Date()).toISOString() },
754
+ result: { job: running }
755
+ };
756
+ },
757
+ null
758
+ );
759
+ }
760
+ async completeJob(jobIdArg, result) {
761
+ const moved = await this.cas(
762
+ jobIdArg,
763
+ (job) => {
764
+ if (job.status !== "running") return { result: false };
765
+ const completed = {
766
+ status: "complete",
767
+ metadata: job.metadata,
768
+ params: job.params,
769
+ startedAt: job.startedAt,
770
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
771
+ result
772
+ };
773
+ return { envelope: { job: completed, lastProgressAt: (/* @__PURE__ */ new Date()).toISOString() }, result: true };
774
+ },
775
+ false
776
+ );
777
+ if (moved) this.settleLease(jobIdArg, "ack");
778
+ return moved;
779
+ }
780
+ async failJob(jobIdArg, error, completedUnits, failureClass, unitCursors) {
781
+ const outcome = await this.cas(
782
+ jobIdArg,
783
+ (job) => {
784
+ if (job.status !== "running") return { result: null };
785
+ const units = [.../* @__PURE__ */ new Set([...job.metadata.completedUnits ?? [], ...completedUnits ?? []])];
786
+ const cursors = mergeUnitCursors(job.metadata.unitCursors, unitCursors, units);
787
+ const metadata = {
788
+ ...job.metadata,
789
+ ...units.length > 0 ? { completedUnits: units } : {},
790
+ ...Object.keys(cursors).length > 0 ? { unitCursors: cursors } : {}
791
+ };
792
+ if ("unitCursors" in metadata && Object.keys(cursors).length === 0) delete metadata.unitCursors;
793
+ if (willRetryAfter(job.metadata, failureClass)) {
794
+ const retried = {
795
+ status: "pending",
796
+ metadata: { ...metadata, retryCount: job.metadata.retryCount + 1 },
797
+ params: job.params
798
+ };
799
+ return { envelope: { job: retried, lastProgressAt: (/* @__PURE__ */ new Date()).toISOString() }, result: "retried" };
800
+ }
801
+ const failed = {
802
+ status: "failed",
803
+ metadata,
804
+ params: job.params,
805
+ startedAt: job.startedAt,
806
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
807
+ error
808
+ };
809
+ return { envelope: { job: failed, lastProgressAt: (/* @__PURE__ */ new Date()).toISOString() }, result: "failed" };
810
+ },
811
+ null
812
+ );
813
+ if (outcome === "retried") {
814
+ const held = this.held.get(jobIdArg);
815
+ if (held) {
816
+ this.held.delete(jobIdArg);
817
+ held.m.nak();
818
+ } else {
819
+ const envelope = await this.read(jobIdArg);
820
+ if (envelope) await this.js.publish(subjectFor(envelope.job), enc.encode(JSON.stringify({ jobId: jobIdArg })));
821
+ }
822
+ } else if (outcome === "failed") {
823
+ this.settleLease(jobIdArg, "term");
824
+ }
825
+ return outcome;
826
+ }
827
+ async checkpointUnits(jobIdArg, completedUnits, unitCursors) {
828
+ await this.cas(
829
+ jobIdArg,
830
+ (job) => {
831
+ if (job.status !== "running") return { result: void 0 };
832
+ const units = [.../* @__PURE__ */ new Set([...job.metadata.completedUnits ?? [], ...completedUnits])];
833
+ const cursors = mergeUnitCursors(job.metadata.unitCursors, unitCursors, units);
834
+ const metadata = { ...job.metadata, completedUnits: units };
835
+ if (Object.keys(cursors).length > 0) metadata.unitCursors = cursors;
836
+ else delete metadata.unitCursors;
837
+ const updated = { ...job, metadata };
838
+ return { envelope: { job: updated, lastProgressAt: (/* @__PURE__ */ new Date()).toISOString() }, result: void 0 };
839
+ },
840
+ void 0
841
+ );
842
+ }
843
+ async recordProgress(jobIdArg, progress) {
844
+ const last = this.lastProgressWrite.get(jobIdArg) ?? 0;
845
+ if (Date.now() - last < PROGRESS_WRITE_MIN_INTERVAL_MS2) return;
846
+ this.lastProgressWrite.set(jobIdArg, Date.now());
847
+ this.held.get(jobIdArg)?.m.working();
848
+ await this.cas(
849
+ jobIdArg,
850
+ (job) => {
851
+ if (job.status !== "running") return { result: void 0 };
852
+ const updated = {
853
+ status: "running",
854
+ metadata: job.metadata,
855
+ params: job.params,
856
+ startedAt: job.startedAt,
857
+ progress
858
+ };
859
+ return { envelope: { job: updated, lastProgressAt: (/* @__PURE__ */ new Date()).toISOString() }, result: void 0 };
860
+ },
861
+ void 0
862
+ );
863
+ }
864
+ async cancelPendingJobs(category) {
865
+ const matches = category === "generation" ? (type) => type === "generation" : (type) => type.endsWith("-annotation");
866
+ let cancelled = 0;
867
+ for (const key of await this.allKeys()) {
868
+ const envelope = await this.read(jobId(key));
869
+ if (!envelope || envelope.job.status !== "pending" || !matches(envelope.job.metadata.type)) continue;
870
+ const done = await this.cas(
871
+ jobId(key),
872
+ (job) => {
873
+ if (job.status !== "pending" || !matches(job.metadata.type)) return { result: false };
874
+ const record = {
875
+ status: "cancelled",
876
+ metadata: job.metadata,
877
+ params: job.params,
878
+ startedAt: void 0,
879
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
880
+ };
881
+ return { envelope: { job: record, lastProgressAt: (/* @__PURE__ */ new Date()).toISOString() }, result: true };
882
+ },
883
+ false
884
+ );
885
+ if (done) {
886
+ cancelled++;
887
+ this.settleLease(jobId(key), "term");
888
+ }
889
+ }
890
+ await this.jsm.streams.purge(STREAM, { filter: `jobs.${category}.>` });
891
+ return cancelled;
892
+ }
893
+ async cancelJob(jobIdArg) {
894
+ const done = await this.cas(
895
+ jobIdArg,
896
+ (job) => {
897
+ if (job.status !== "pending" && job.status !== "running") return { result: false };
898
+ const record = {
899
+ status: "cancelled",
900
+ metadata: job.metadata,
901
+ params: job.params,
902
+ startedAt: job.status === "running" ? job.startedAt : void 0,
903
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
904
+ };
905
+ return { envelope: { job: record, lastProgressAt: (/* @__PURE__ */ new Date()).toISOString() }, result: true };
906
+ },
907
+ false
908
+ );
909
+ if (done) this.settleLease(jobIdArg, "term");
910
+ return done;
911
+ }
912
+ async getStats() {
913
+ const stats = { pending: 0, running: 0, complete: 0, failed: 0, cancelled: 0 };
914
+ for (const key of await this.allKeys()) {
915
+ const envelope = await this.read(jobId(key));
916
+ if (envelope) stats[envelope.job.status]++;
917
+ }
918
+ return stats;
919
+ }
920
+ /**
921
+ * Worker-death recovery: the same contract the fs driver's mtime janitor
922
+ * implemented — a running job with no progress inside the stale window is
923
+ * retried or failed, checkpoint intact. Gateway-death recovery never comes
924
+ * through here; that is AckWait redelivery.
925
+ */
926
+ async recoverStaleRunningJobs() {
927
+ const now = Date.now();
928
+ let recovered = 0;
929
+ for (const key of await this.allKeys()) {
930
+ const envelope = await this.read(jobId(key));
931
+ if (!envelope || envelope.job.status !== "running") continue;
932
+ if (now - Date.parse(envelope.lastProgressAt) < this.staleRunningMs) continue;
933
+ const outcome = await this.failJob(
934
+ jobId(key),
935
+ `worker presumed dead \u2014 no progress within ${this.staleRunningMs / 6e4} minutes`
936
+ );
937
+ if (outcome) {
938
+ this.logger.warn("Recovered stale running job", { jobId: key, outcome });
939
+ recovered++;
940
+ }
941
+ }
942
+ return recovered;
943
+ }
944
+ /** The heartbeat body — extend live leases, settle concluded ones. */
945
+ async reconcileHeld() {
946
+ for (const [id, held] of [...this.held]) {
947
+ const envelope = await this.read(jobId(id));
948
+ const status = envelope?.job.status;
949
+ if (!envelope || status === "complete" || status === "failed" || status === "cancelled") {
950
+ this.held.delete(id);
951
+ held.m.ack();
952
+ } else {
953
+ held.m.working();
954
+ }
955
+ }
956
+ }
957
+ /**
958
+ * Materialize the KV key list BEFORE touching any entry: interleaving
959
+ * `get`/`update` with an open `keys()` iterator on the same connection
960
+ * makes the iterator drop entries (measured: the second pending job
961
+ * vanished from every sweep until this was split into two passes).
962
+ */
963
+ async allKeys() {
964
+ const keys = [];
965
+ for await (const key of await this.kv.keys()) keys.push(key);
966
+ return keys;
967
+ }
968
+ /** Conclude this process's lease on a job, if it holds one. */
969
+ settleLease(jobIdArg, how) {
970
+ const held = this.held.get(jobIdArg);
971
+ if (!held) return;
972
+ this.held.delete(jobIdArg);
973
+ if (how === "ack") held.m.ack();
974
+ else held.m.term();
975
+ }
976
+ };
477
977
 
478
978
  // src/types.ts
479
979
  function isPendingJob(job) {
@@ -2435,6 +2935,6 @@ var WORKER_AWAITED_OPERATIONS = [
2435
2935
  ];
2436
2936
  replyChannelsFor(WORKER_AWAITED_OPERATIONS);
2437
2937
 
2438
- export { AnnotationDetection, FsJobQueue, STALL_THRESHOLD_MS, generateResourceFromTopic, isCancelledJob, isCompleteJob, isFailedJob, isPendingJob, isRunningJob, processAssessmentJob, processCommentJob, processGenerationJob, processHighlightJob, processReferenceJob, processTagJob };
2938
+ export { AnnotationDetection, FsJobQueue, JetStreamJobQueue, STALL_THRESHOLD_MS, generateResourceFromTopic, isCancelledJob, isCompleteJob, isFailedJob, isPendingJob, isRunningJob, processAssessmentJob, processCommentJob, processGenerationJob, processHighlightJob, processReferenceJob, processTagJob };
2439
2939
  //# sourceMappingURL=index.js.map
2440
2940
  //# sourceMappingURL=index.js.map