@jarenjs/db 0.34.0 → 0.34.2

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.
@@ -25,7 +25,10 @@
25
25
  * - **Shutdown is bounded.** Handlers receive an `AbortSignal` and
26
26
  * `stop()` takes a deadline, so a handler that never settles cannot
27
27
  * hold `stop()` — and therefore `store.close()`, and therefore the
28
- * database file — open forever.
28
+ * database file — open forever. A loop the deadline could not drain
29
+ * is CANCELLED, not merely left behind: when its handler finally
30
+ * settles it exits without another claim, store write or poll
31
+ * timer, and its abandoned job recovers by lease expiry (§5).
29
32
  */
30
33
  export declare const JOBS_TABLE = "_jaren_jobs";
31
34
  export declare const JOB_CHECKPOINTS_TABLE = "_jaren_job_checkpoints";
@@ -117,6 +120,9 @@ export declare function createJobEngine(options: {
117
120
  * the loops — but only up to `graceMs`. A handler that ignores its
118
121
  * signal cannot hold the process open; the resolved record says so
119
122
  * instead, and the lease expiry (§5) lets another worker re-claim.
123
+ * A loop the grace period could not drain is cancelled outright:
124
+ * when its handler finally settles it exits without another
125
+ * claim, store write or poll timer.
120
126
  * @param {{ graceMs?: number }} [stopOptions]
121
127
  * @returns {Promise<{ drained: boolean, inFlight: number }>}
122
128
  */
@@ -125,7 +125,8 @@ stats() }`:
125
125
  plainly (§8);
126
126
  - `stop({ graceMs })` aborts in-flight handlers and resolves once they
127
127
  settle **or** the grace period expires (default 5 s), answering
128
- `{ drained, inFlight }` see §6.1;
128
+ `{ drained, inFlight }`; a loop the grace period could not drain is
129
+ **cancelled**, not left running — see §6.1;
129
130
  - `stats()` reports claims, completions, failures, wakes, polls and the
130
131
  in-flight handler count.
131
132
 
@@ -162,6 +163,16 @@ handle really is released. A handler that ignores its signal therefore
162
163
  cannot hold the database file open for the life of the process, and the
163
164
  lease expiry (§5) lets another worker re-claim its job.
164
165
 
166
+ A loop the grace period could not drain is **cancelled**, not merely
167
+ uncounted: when its wedged handler finally settles, the loop exits
168
+ without writing the completion or the failure, without claiming again,
169
+ and without re-arming its poll timer — the store it would touch is the
170
+ one the caller is closing, and the job it abandons recovers by lease
171
+ expiry (§5). A later `start()` builds fresh loops in a new session (and
172
+ re-registers the worker's wake-on-enqueue hook and its place in
173
+ `stopAll`), so a cancelled loop can never be revived as an extra
174
+ claimer.
175
+
165
176
  ## 7. The DAG composition
166
177
 
167
178
  ```js
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jarenjs/db",
3
3
  "private": false,
4
- "version": "0.34.0",
4
+ "version": "0.34.2",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "types": "./types/index.d.ts",
@@ -71,9 +71,9 @@
71
71
  "prepack": "npm run build:types"
72
72
  },
73
73
  "dependencies": {
74
- "@jarenjs/core": "^0.34.0",
75
- "@jarenjs/json": "^0.34.0",
76
- "@jarenjs/validate": "^0.34.0"
74
+ "@jarenjs/core": "^0.34.2",
75
+ "@jarenjs/json": "^0.34.2",
76
+ "@jarenjs/validate": "^0.34.2"
77
77
  },
78
78
  "bin": {
79
79
  "jaren-db": "./src/cli.js"
package/src/jobs.js CHANGED
@@ -26,7 +26,10 @@
26
26
  * - **Shutdown is bounded.** Handlers receive an `AbortSignal` and
27
27
  * `stop()` takes a deadline, so a handler that never settles cannot
28
28
  * hold `stop()` — and therefore `store.close()`, and therefore the
29
- * database file — open forever.
29
+ * database file — open forever. A loop the deadline could not drain
30
+ * is CANCELLED, not merely left behind: when its handler finally
31
+ * settles it exits without another claim, store write or poll
32
+ * timer, and its abandoned job recovers by lease expiry (§5).
30
33
  */
31
34
 
32
35
  import { chain } from './driver.js';
@@ -370,6 +373,17 @@ export function createJobEngine(options) {
370
373
 
371
374
  /** Aborted when `stop()` is called: the handler's cue to wind up. */
372
375
  let shutdown = new AbortController();
376
+ /**
377
+ * The per-`start()` loop session. When a `stop()` grace deadline
378
+ * expires the session is cancelled: a loop that could not be
379
+ * drained must abandon its job — no completion or failure write,
380
+ * no further claim, no re-armed poll timer — because the store it
381
+ * would touch is the one the caller is about to close. The
382
+ * abandoned lease expires and the next claim re-runs the job (§5).
383
+ * A later `start()` opens a NEW session, so a cancelled loop can
384
+ * never be revived.
385
+ */
386
+ let session = { cancelled: false };
373
387
  /** In-flight handler count, so `stop()` can report what it left. */
374
388
  let inFlight = 0;
375
389
 
@@ -389,7 +403,8 @@ export function createJobEngine(options) {
389
403
  }
390
404
  };
391
405
 
392
- const runOne = async (job) => {
406
+ /** @param {{ cancelled: boolean }} loopSession */
407
+ const runOne = async (job, loopSession) => {
393
408
  stats.claims += 1;
394
409
  inFlight += 1;
395
410
  try {
@@ -399,9 +414,13 @@ export function createJobEngine(options) {
399
414
  { job, checkpointsFor, signal: shutdown.signal });
400
415
  }
401
416
  catch (error) {
417
+ // past cancellation the store is closing: leave the leased
418
+ // row to expiry-based recovery (§5) instead of racing it
419
+ if (loopSession.cancelled) return;
402
420
  await recordFailure(job, error);
403
421
  return;
404
422
  }
423
+ if (loopSession.cancelled) return;
405
424
  try {
406
425
  // a §7 handler may have completed transactionally already; the
407
426
  // guarded update makes this a no-op then
@@ -420,8 +439,9 @@ export function createJobEngine(options) {
420
439
  }
421
440
  };
422
441
 
423
- const loop = async () => {
424
- while (running) {
442
+ /** @param {{ cancelled: boolean }} loopSession */
443
+ const loop = async (loopSession) => {
444
+ while (running && !loopSession.cancelled) {
425
445
  let job;
426
446
  try {
427
447
  job = await Promise.resolve(claim({
@@ -430,13 +450,13 @@ export function createJobEngine(options) {
430
450
  catch {
431
451
  job = undefined; // a transient storage failure: back off to the poll
432
452
  }
433
- if (!running) return;
453
+ if (!running || loopSession.cancelled) return;
434
454
  if (job === undefined) {
435
455
  await sleep();
436
456
  continue;
437
457
  }
438
458
  try {
439
- await runOne(job);
459
+ await runOne(job, loopSession);
440
460
  }
441
461
  catch {
442
462
  // `runOne` normalizes every handler outcome, so reaching here
@@ -453,7 +473,12 @@ export function createJobEngine(options) {
453
473
  if (running) throw new TypeError('the worker is already started');
454
474
  running = true;
455
475
  shutdown = new AbortController();
456
- loops = Array.from({ length: concurrency }, () => loop());
476
+ session = { cancelled: false };
477
+ loops = Array.from({ length: concurrency }, () => loop(session));
478
+ // a restart re-registers what stop() removed: the
479
+ // wake-on-enqueue hook and the stopAll membership
480
+ wakers.add(onWake);
481
+ workers.add(worker);
457
482
  return worker;
458
483
  },
459
484
  /**
@@ -461,6 +486,9 @@ export function createJobEngine(options) {
461
486
  * the loops — but only up to `graceMs`. A handler that ignores its
462
487
  * signal cannot hold the process open; the resolved record says so
463
488
  * instead, and the lease expiry (§5) lets another worker re-claim.
489
+ * A loop the grace period could not drain is cancelled outright:
490
+ * when its handler finally settles it exits without another
491
+ * claim, store write or poll timer.
464
492
  * @param {{ graceMs?: number }} [stopOptions]
465
493
  * @returns {Promise<{ drained: boolean, inFlight: number }>}
466
494
  */
@@ -478,6 +506,7 @@ export function createJobEngine(options) {
478
506
  ]);
479
507
  clearTimeout(timer);
480
508
  if (drained) loops = [];
509
+ else session.cancelled = true; // cancel what could not be drained
481
510
  wakers.delete(onWake);
482
511
  workers.delete(worker);
483
512
  return { drained: drained === true, inFlight };