@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.d.ts CHANGED
@@ -290,7 +290,22 @@ interface JobQueue {
290
290
  destroy(): void;
291
291
  createJob(job: AnyJob): Promise<void>;
292
292
  getJob(jobId: JobId): Promise<AnyJob | null>;
293
- updateJob(job: AnyJob, oldStatus?: JobStatus): Promise<void>;
293
+ /**
294
+ * Atomically claim the NEXT pending job matching one of `types` — the ONE
295
+ * transition that makes this a queue rather than a state store
296
+ * (JOB-QUEUE-DRIVER P0; reshaped claim-by-TYPE in P2, while every worker
297
+ * is still ours). pending → running, `startedAt` stamped, progress empty.
298
+ * An announcement is a WAKE-UP, not a reservation: the claimed job may
299
+ * differ from any announced one, no ordering among matching pending jobs
300
+ * is promised, and an empty `types` accepts any type. Simultaneous claims
301
+ * admit one winner PER pending job; a claim that finds nothing is
302
+ * DECLINED (`none-available`), never an error.
303
+ */
304
+ claimNextJob(types: string[]): Promise<{
305
+ job: AnyJob;
306
+ } | {
307
+ declined: 'none-available';
308
+ }>;
294
309
  /** Move a running job to `complete`. Returns false if the job isn't running. */
295
310
  completeJob(jobId: JobId, result: Record<string, unknown>): Promise<boolean>;
296
311
  /**
@@ -398,10 +413,30 @@ declare class FsJobQueue implements JobQueue {
398
413
  * Get a job by ID (searches all status directories)
399
414
  */
400
415
  getJob(jobId: JobId): Promise<AnyJob | null>;
416
+ /**
417
+ * Serializes claims. This driver's atomicity is single-process by design,
418
+ * but `claimJob`'s contract must hold even against interleaved awaits in
419
+ * that one process — a read-check-write with await points in it can admit
420
+ * two winners without this chain (the TOCTOU the operation exists to kill).
421
+ */
422
+ private claimChain;
423
+ claimNextJob(types: string[]): Promise<{
424
+ job: AnyJob;
425
+ } | {
426
+ declined: 'none-available';
427
+ }>;
428
+ private doClaimNext;
401
429
  /**
402
430
  * Update a job (atomic: delete old, write new)
403
431
  */
404
- updateJob(job: AnyJob, oldStatus?: JobStatus): Promise<void>;
432
+ /**
433
+ * Cross-status move (or in-place rewrite): delete old slot, write new.
434
+ * Private — the interface exposes named transitions only (JOB-QUEUE-DRIVER
435
+ * P0); a generic public patch is trivial on files and impossible on an
436
+ * in-flight message, so no driver may offer one. Re-entering `pending`
437
+ * announces, which is how failJob's retry reaches a listening worker.
438
+ */
439
+ private transition;
405
440
  /**
406
441
  * Move a running job to `complete`. Returns false (and changes
407
442
  * nothing) if the job is missing or not running — which also makes
@@ -478,6 +513,136 @@ declare class FsJobQueue implements JobQueue {
478
513
  }>;
479
514
  }
480
515
 
516
+ /**
517
+ * JetStreamJobQueue — the JetStream driver behind the `JobQueue` interface
518
+ * (JOB-QUEUE-DRIVER P1, topology ruling M: gateway-mediated).
519
+ *
520
+ * Two primitives, one authority each:
521
+ *
522
+ * - **KV bucket `jobs`** is the AUTHORITATIVE operational state: one entry
523
+ * per job (`{ job, lastProgressAt }`), every transition a revision-CAS —
524
+ * which is what makes `claimJob` atomic: simultaneous claims race on one
525
+ * revision and exactly one update wins.
526
+ * - **Stream `JOBS`** (subjects `jobs.<category>.<type>`, work-queue
527
+ * retention) is the DELIVERY vehicle and redelivery timer. A delivered
528
+ * message is the lease this process holds for a job; `working()`
529
+ * heartbeats extend it, and a gateway that dies stops heartbeating, so
530
+ * `AckWait` redelivers the job to a live instance — gateway-death
531
+ * recovery, protocol-native.
532
+ *
533
+ * Worker-death recovery is NOT AckWait's job under the mediated topology
534
+ * (the gateway holding the lease is alive; the worker vanished): it is the
535
+ * `lastProgressAt` sweep (`recoverStaleRunningJobs`), the same contract the
536
+ * fs driver's mtime janitor implemented. The redelivery handler checks KV
537
+ * state, so the two recovery paths can never double-apply.
538
+ *
539
+ * Battery ownership (JOB-QUEUE-DRIVER P0): the retry decision belongs to
540
+ * `will-retry.ts` — `max_deliver` is unlimited and a deterministic failure
541
+ * is `term()`ed, so the backend's retry engine never becomes a second
542
+ * authority. The cursor merge belongs to `checkpoint-merge.ts`, shared with
543
+ * every driver.
544
+ *
545
+ * The boundary is the point: no NATS type, subject string, or delivery
546
+ * handle escapes this file.
547
+ */
548
+
549
+ interface JetStreamJobQueueOptions {
550
+ /** NATS server address(es), e.g. "192.168.64.42:4222". */
551
+ servers: string | string[];
552
+ /** Worker presumed dead after this long without progress (default 30 min). */
553
+ staleRunningMs?: number;
554
+ /** Lease redelivery window when THIS process stops heartbeating (default 30 s). */
555
+ ackWaitMs?: number;
556
+ /** Re-announce + worker-death-sweep cadence (default 30 s; tests shrink it). */
557
+ tickMs?: number;
558
+ /** Reconnect on connection loss (default true; tests turn it off). */
559
+ reconnect?: boolean;
560
+ }
561
+ declare class JetStreamJobQueue implements JobQueue {
562
+ private readonly options;
563
+ private readonly logger;
564
+ private readonly eventBus?;
565
+ private nc;
566
+ private js;
567
+ private jsm;
568
+ private kv;
569
+ private iter;
570
+ /** Delivered messages this process holds — the lease, keyed by jobId,
571
+ * with the job's type so a by-type claim can walk them without a read. */
572
+ private readonly held;
573
+ private heartbeat;
574
+ private reconciling;
575
+ private tick;
576
+ private ticking;
577
+ private readonly lastProgressWrite;
578
+ private readonly staleRunningMs;
579
+ private readonly ackWaitMs;
580
+ private readonly tickMs;
581
+ constructor(options: JetStreamJobQueueOptions, logger: Logger, eventBus?: EventBus | undefined);
582
+ initialize(): Promise<void>;
583
+ destroy(): void;
584
+ /**
585
+ * A delivery is a job arriving at this gateway. What it means depends on
586
+ * the job's authoritative (KV) state:
587
+ * - pending → hold the lease, announce for a worker to claim;
588
+ * - running → hold silently (a claim raced ahead of the delivery, or a
589
+ * redelivery reached a fresh instance after a gateway death);
590
+ * - terminal → the work already concluded elsewhere; consume the message;
591
+ * - unknown → not ours to run; terminate it.
592
+ */
593
+ private onDelivery;
594
+ /** Same wire shape as the fs driver: only jobs with a resourceId announce. */
595
+ private announce;
596
+ private read;
597
+ private write;
598
+ /**
599
+ * Read-transform-CAS with bounded retries: the transform sees the current
600
+ * job and returns the replacement (or null to stop). Every state
601
+ * transition goes through here, which is what makes each one atomic.
602
+ */
603
+ private cas;
604
+ createJob(job: AnyJob): Promise<void>;
605
+ getJob(jobIdArg: JobId): Promise<AnyJob | null>;
606
+ claimNextJob(types: string[]): Promise<{
607
+ job: AnyJob;
608
+ } | {
609
+ declined: 'none-available';
610
+ }>;
611
+ /** CAS one pending job to running; null when someone else won it. */
612
+ private tryClaim;
613
+ completeJob(jobIdArg: JobId, result: Record<string, unknown>): Promise<boolean>;
614
+ failJob(jobIdArg: JobId, error: string, completedUnits?: string[], failureClass?: 'transient' | 'deterministic', unitCursors?: Record<string, UnitCursor>): Promise<'retried' | 'failed' | null>;
615
+ checkpointUnits(jobIdArg: JobId, completedUnits: string[], unitCursors?: Record<string, UnitCursor>): Promise<void>;
616
+ recordProgress(jobIdArg: JobId, progress: Record<string, unknown>): Promise<void>;
617
+ cancelPendingJobs(category: 'annotation' | 'generation'): Promise<number>;
618
+ cancelJob(jobIdArg: JobId): Promise<boolean>;
619
+ getStats(): Promise<{
620
+ pending: number;
621
+ running: number;
622
+ complete: number;
623
+ failed: number;
624
+ cancelled: number;
625
+ }>;
626
+ /**
627
+ * Worker-death recovery: the same contract the fs driver's mtime janitor
628
+ * implemented — a running job with no progress inside the stale window is
629
+ * retried or failed, checkpoint intact. Gateway-death recovery never comes
630
+ * through here; that is AckWait redelivery.
631
+ */
632
+ recoverStaleRunningJobs(): Promise<number>;
633
+ /** The heartbeat body — extend live leases, settle concluded ones. */
634
+ private reconcileHeld;
635
+ /**
636
+ * Materialize the KV key list BEFORE touching any entry: interleaving
637
+ * `get`/`update` with an open `keys()` iterator on the same connection
638
+ * makes the iterator drop entries (measured: the second pending job
639
+ * vanished from every sweep until this was split into two passes).
640
+ */
641
+ private allKeys;
642
+ /** Conclude this process's lease on a job, if it holds one. */
643
+ private settleLease;
644
+ }
645
+
481
646
  /**
482
647
  * Citation-token resolver (INLINE-CITATIONS P1).
483
648
  *
@@ -865,5 +1030,5 @@ declare function generateResourceFromTopic(topic: string, entityTypes: string[],
865
1030
  */
866
1031
  declare const STALL_THRESHOLD_MS: number;
867
1032
 
868
- export { AnnotationDetection, FsJobQueue, STALL_THRESHOLD_MS, generateResourceFromTopic, isCancelledJob, isCompleteJob, isFailedJob, isPendingJob, isRunningJob, processAssessmentJob, processCommentJob, processGenerationJob, processHighlightJob, processReferenceJob, processTagJob };
869
- export type { AnyJob, AssessmentDetectionJob, AssessmentDetectionParams, AssessmentDetectionProgress, CancelledJob, CommentDetectionJob, CommentDetectionParams, CommentDetectionProgress, CompleteJob, DetectionJob, DetectionParams, DetectionProgress, FailedJob, GenerationJob, GenerationResult, HighlightDetectionJob, HighlightDetectionParams, HighlightDetectionProgress, JobMetadata, JobQueryFilters, JobQueue, JobStatus, JobType, OnProgress, PendingJob, ProcessorResult, RunningJob, TagDetectionJob, TagDetectionParams, TagDetectionProgress, YieldProgress };
1033
+ export { AnnotationDetection, FsJobQueue, JetStreamJobQueue, STALL_THRESHOLD_MS, generateResourceFromTopic, isCancelledJob, isCompleteJob, isFailedJob, isPendingJob, isRunningJob, processAssessmentJob, processCommentJob, processGenerationJob, processHighlightJob, processReferenceJob, processTagJob };
1034
+ export type { AnyJob, AssessmentDetectionJob, AssessmentDetectionParams, AssessmentDetectionProgress, CancelledJob, CommentDetectionJob, CommentDetectionParams, CommentDetectionProgress, CompleteJob, DetectionJob, DetectionParams, DetectionProgress, FailedJob, GenerationJob, GenerationResult, HighlightDetectionJob, HighlightDetectionParams, HighlightDetectionProgress, JetStreamJobQueueOptions, JobMetadata, JobQueryFilters, JobQueue, JobStatus, JobType, OnProgress, PendingJob, ProcessorResult, RunningJob, TagDetectionJob, TagDetectionParams, TagDetectionProgress, YieldProgress };