@ddd-ts-kit/redis 0.10.0 → 0.12.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.
Files changed (42) hide show
  1. package/README.md +33 -2
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +2 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/jobs/claimed-job-of-record.d.ts +5 -0
  7. package/dist/jobs/claimed-job-of-record.d.ts.map +1 -0
  8. package/dist/jobs/claimed-job-of-record.js +23 -0
  9. package/dist/jobs/claimed-job-of-record.js.map +1 -0
  10. package/dist/jobs/job-scripts.d.ts +13 -0
  11. package/dist/jobs/job-scripts.d.ts.map +1 -0
  12. package/dist/jobs/job-scripts.js +84 -0
  13. package/dist/jobs/job-scripts.js.map +1 -0
  14. package/dist/jobs/parse-enqueued-by.d.ts +4 -0
  15. package/dist/jobs/parse-enqueued-by.d.ts.map +1 -0
  16. package/dist/jobs/parse-enqueued-by.js +23 -0
  17. package/dist/jobs/parse-enqueued-by.js.map +1 -0
  18. package/dist/jobs/parse-job-record.d.ts +4 -0
  19. package/dist/jobs/parse-job-record.d.ts.map +1 -0
  20. package/dist/jobs/parse-job-record.js +17 -0
  21. package/dist/jobs/parse-job-record.js.map +1 -0
  22. package/dist/jobs/queued-job-of-record.d.ts +5 -0
  23. package/dist/jobs/queued-job-of-record.d.ts.map +1 -0
  24. package/dist/jobs/queued-job-of-record.js +18 -0
  25. package/dist/jobs/queued-job-of-record.js.map +1 -0
  26. package/dist/jobs/redis-job-record.d.ts +25 -0
  27. package/dist/jobs/redis-job-record.d.ts.map +1 -0
  28. package/dist/jobs/redis-job-record.js +2 -0
  29. package/dist/jobs/redis-job-record.js.map +1 -0
  30. package/dist/jobs/redis-job-store-options.d.ts +11 -0
  31. package/dist/jobs/redis-job-store-options.d.ts.map +1 -0
  32. package/dist/jobs/redis-job-store-options.js +2 -0
  33. package/dist/jobs/redis-job-store-options.js.map +1 -0
  34. package/dist/jobs/redis-job-store.d.ts +48 -0
  35. package/dist/jobs/redis-job-store.d.ts.map +1 -0
  36. package/dist/jobs/redis-job-store.js +178 -0
  37. package/dist/jobs/redis-job-store.js.map +1 -0
  38. package/dist/jobs/report-lost-jobs.d.ts +10 -0
  39. package/dist/jobs/report-lost-jobs.d.ts.map +1 -0
  40. package/dist/jobs/report-lost-jobs.js +18 -0
  41. package/dist/jobs/report-lost-jobs.js.map +1 -0
  42. package/package.json +7 -6
package/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # @ddd-ts-kit/redis
2
2
 
3
3
  Adapters of kernel ports on the Redis protocol: the `Cache`, `Lock` and `RateLimiter` ports,
4
- the wake-up signal of the outbox relay, and an integration event provider on Redis Streams.
4
+ the wake-up signal of the outbox relay, a store of background jobs, and an integration event
5
+ provider on Redis Streams.
5
6
  They use a common subset of the protocol, so the same code runs on Redis, Dragonfly and
6
7
  Valkey: the choice of server is an operational one, made in the composition root by a
7
8
  connection URL.
@@ -12,7 +13,7 @@ npm i @ddd-ts-kit/redis ioredis
12
13
 
13
14
  | Entry | Holds | Imports |
14
15
  |---|---|---|
15
- | `@ddd-ts-kit/redis` | `connectRedis`, `redisHealth`, `RedisCache`, `RedisLock`, `RedisRateLimiter`, `RedisRelaySignal`, `relayWakeUpMiddleware`; re-exported from the kernel: the `Lock` and `RateLimiter` ports, `InMemoryLock`, `InMemoryRateLimiter` | the kernel, `ioredis` (peer) |
16
+ | `@ddd-ts-kit/redis` | `connectRedis`, `redisHealth`, `RedisCache`, `RedisLock`, `RedisRateLimiter`, `RedisRelaySignal`, `relayWakeUpMiddleware`, `RedisJobStore`, `reportLostJobs`; re-exported from the kernel: the `Lock` and `RateLimiter` ports, `InMemoryLock`, `InMemoryRateLimiter` | the kernel, `@ddd-ts-kit/scheduling`, `ioredis` (peer) |
16
17
  | `@ddd-ts-kit/redis/event-bus` | `RedisStreamsEventBus`, its options, topology and error types, the names of the entry fields | the kernel, `ioredis` (peer) |
17
18
  | `@ddd-ts-kit/redis/testing` | `cacheContract`, `lockContract`, `rateLimiterContract`, re-exported from `@ddd-ts-kit/testing/contracts` | `@ddd-ts-kit/testing` (optional peer), the runner you pass |
18
19
 
@@ -665,6 +666,36 @@ timeout, groups lost and created again, reconnection through a TCP relay the tes
665
666
  graceful close) and the whole chain from the outbox to an idempotent consumer. Each test works
666
667
  under a prefix of its own and deletes its keys.
667
668
 
669
+ ## Background job store
670
+
671
+ `RedisJobStore` is the `JobStore` of `@ddd-ts-kit/scheduling` on Redis, Dragonfly or Valkey,
672
+ for a project without a database or a queue it wants out of it: a few Lua scripts on the
673
+ client of the package, every key under `{<namespace>}` so that a cluster keeps them in one slot
674
+ and Dragonfly sees every key a script touches declared.
675
+
676
+ ```ts
677
+ const jobStore = new RedisJobStore(redis, { namespace: 'jobs', onLostJob: reportLostJobs(logger, errors) });
678
+
679
+ kernelModule({ jobStore });
680
+ ```
681
+
682
+ `reportLostJobs(logger, errors?)` logs a write lost after its unit of work committed (the id of
683
+ the job, the class and code of the error) and hands the error to the `ErrorReporter`, origin
684
+ `queue`; a project initialised with `--queue redis` has it bound by its platform module.
685
+
686
+ Redis cannot join the transaction of a database. Inside a unit of work, `add`, `cancel` and
687
+ `retryDead` answer from what Redis holds and write once it committed (`whenCommitted` of the
688
+ kernel): a command that rolls back leaves no job and cancels none, but a process that dies
689
+ between the commit and the write loses that write, and `onLostJob` is told when it fails. With
690
+ a database, prefer the jobs table of the ORM package. The idempotency key, scoped by the tenant
691
+ of the job, is checked before the job is staged and again when it is written: two jobs of one
692
+ key staged before either committed both answer their own id, and only the first is kept.
693
+
694
+ Not BullMQ: it would bring a second worker model that the store contract (claim, lease,
695
+ heartbeat, reports by attempt) does not map onto, and emulated cluster mode on Dragonfly. The
696
+ store passes `jobStoreContract` on Dragonfly in the integration job of CI; it was also run by
697
+ hand on Redis 8 (last run: Dragonfly 2.0.2, Redis 8.2, 2026-10-13). What the jobs guarantee: [Background jobs](../../docs/concepts/background-jobs.md).
698
+
668
699
  ## Contract suites
669
700
 
670
701
  Each port has a suite that its in-memory implementation and its adapter both pass. An adapter
package/dist/index.d.ts CHANGED
@@ -14,4 +14,7 @@ export { RedisRelaySignal, type RedisRelaySignalOptions } from './relay/redis-re
14
14
  export { relayWakeUpMiddleware } from './relay/relay-wake-up-middleware.js';
15
15
  export { InMemoryLock, Lock, type AcquireOptions, type LockHandle, type WaitingOptions } from '@ddd-ts-kit/kernel';
16
16
  export { InMemoryRateLimiter, RateLimiter, type RateLimit, type RateLimitDecision } from '@ddd-ts-kit/kernel';
17
+ export { RedisJobStore } from './jobs/redis-job-store.js';
18
+ export type { RedisJobStoreOptions } from './jobs/redis-job-store-options.js';
19
+ export { reportLostJobs } from './jobs/report-lost-jobs.js';
17
20
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,WAAW,EAAE,KAAK,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC9F,YAAY,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,YAAY,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,KAAK,eAAe,EAAE,MAAM,+BAA+B,CAAC;AACnF,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACtD,YAAY,EAAE,sBAAsB,EAAE,MAAM,0CAA0C,CAAC;AACvF,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,KAAK,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AACtG,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxE,OAAO,EAAE,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,sCAAsC,CAAC;AACtG,OAAO,EAAE,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAC/F,OAAO,EAAE,qBAAqB,EAAE,MAAM,qCAAqC,CAAC;AAC5E,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,KAAK,cAAc,EAAE,KAAK,UAAU,EAAE,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACnH,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,iBAAiB,EAAE,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,WAAW,EAAE,KAAK,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC9F,YAAY,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,YAAY,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,KAAK,eAAe,EAAE,MAAM,+BAA+B,CAAC;AACnF,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACtD,YAAY,EAAE,sBAAsB,EAAE,MAAM,0CAA0C,CAAC;AACvF,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,KAAK,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AACtG,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxE,OAAO,EAAE,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,sCAAsC,CAAC;AACtG,OAAO,EAAE,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAC/F,OAAO,EAAE,qBAAqB,EAAE,MAAM,qCAAqC,CAAC;AAC5E,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,KAAK,cAAc,EAAE,KAAK,UAAU,EAAE,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACnH,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC9G,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,YAAY,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC"}
package/dist/index.js CHANGED
@@ -10,4 +10,6 @@ export { RedisRelaySignal } from './relay/redis-relay-signal.js';
10
10
  export { relayWakeUpMiddleware } from './relay/relay-wake-up-middleware.js';
11
11
  export { InMemoryLock, Lock } from '@ddd-ts-kit/kernel';
12
12
  export { InMemoryRateLimiter, RateLimiter } from '@ddd-ts-kit/kernel';
13
+ export { RedisJobStore } from './jobs/redis-job-store.js';
14
+ export { reportLostJobs } from './jobs/report-lost-jobs.js';
13
15
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAA4C,MAAM,wBAAwB,CAAC;AAG9F,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,YAAY,EAAwB,MAAM,+BAA+B,CAAC;AACnF,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAEtD,OAAO,EAAE,WAAW,EAA6C,MAAM,8BAA8B,CAAC;AAEtG,OAAO,EAAE,SAAS,EAAyB,MAAM,sBAAsB,CAAC;AACxE,OAAO,EAAE,gBAAgB,EAAgC,MAAM,sCAAsC,CAAC;AACtG,OAAO,EAAE,gBAAgB,EAAgC,MAAM,+BAA+B,CAAC;AAC/F,OAAO,EAAE,qBAAqB,EAAE,MAAM,qCAAqC,CAAC;AAC5E,OAAO,EAAE,YAAY,EAAE,IAAI,EAA6D,MAAM,oBAAoB,CAAC;AACnH,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAA0C,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAA4C,MAAM,wBAAwB,CAAC;AAG9F,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,YAAY,EAAwB,MAAM,+BAA+B,CAAC;AACnF,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAEtD,OAAO,EAAE,WAAW,EAA6C,MAAM,8BAA8B,CAAC;AAEtG,OAAO,EAAE,SAAS,EAAyB,MAAM,sBAAsB,CAAC;AACxE,OAAO,EAAE,gBAAgB,EAAgC,MAAM,sCAAsC,CAAC;AACtG,OAAO,EAAE,gBAAgB,EAAgC,MAAM,+BAA+B,CAAC;AAC/F,OAAO,EAAE,qBAAqB,EAAE,MAAM,qCAAqC,CAAC;AAC5E,OAAO,EAAE,YAAY,EAAE,IAAI,EAA6D,MAAM,oBAAoB,CAAC;AACnH,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAA0C,MAAM,oBAAoB,CAAC;AAC9G,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE1D,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { ClaimedJob } from '@ddd-ts-kit/scheduling';
2
+ import type { RedisJobRecord } from './redis-job-record.js';
3
+ /** A record as the worker receives it. */
4
+ export declare function claimedJobOfRecord(record: RedisJobRecord): ClaimedJob;
5
+ //# sourceMappingURL=claimed-job-of-record.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claimed-job-of-record.d.ts","sourceRoot":"","sources":["../../src/jobs/claimed-job-of-record.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,0CAA0C;AAC1C,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,cAAc,GAAG,UAAU,CAarE"}
@@ -0,0 +1,23 @@
1
+ import { parseEnqueuedBy } from './parse-enqueued-by.js';
2
+ /** A record as the worker receives it. */
3
+ export function claimedJobOfRecord(record) {
4
+ const payload = JSON.parse(record.payload);
5
+ return {
6
+ id: record.id,
7
+ name: record.name,
8
+ queue: record.queue,
9
+ payload,
10
+ enqueuedBy: parseEnqueuedBy(record.enqueuedBy),
11
+ enqueuedAt: new Date(record.enqueuedAt),
12
+ attempt: record.attempts,
13
+ ...checkpointOf(record),
14
+ };
15
+ }
16
+ function checkpointOf(record) {
17
+ if (record.checkpoint === undefined) {
18
+ return {};
19
+ }
20
+ const checkpoint = JSON.parse(record.checkpoint);
21
+ return { checkpoint };
22
+ }
23
+ //# sourceMappingURL=claimed-job-of-record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claimed-job-of-record.js","sourceRoot":"","sources":["../../src/jobs/claimed-job-of-record.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGzD,0CAA0C;AAC1C,MAAM,UAAU,kBAAkB,CAAC,MAAsB;IACvD,MAAM,OAAO,GAAY,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAEpD,OAAO;QACL,EAAE,EAAE,MAAM,CAAC,EAAE;QACb,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,OAAO;QACP,UAAU,EAAE,eAAe,CAAC,MAAM,CAAC,UAAU,CAAC;QAC9C,UAAU,EAAE,IAAI,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC;QACvC,OAAO,EAAE,MAAM,CAAC,QAAQ;QACxB,GAAG,YAAY,CAAC,MAAM,CAAC;KACxB,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CAAC,MAAsB;IAC1C,IAAI,MAAM,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;QACpC,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,UAAU,GAAY,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IAE1D,OAAO,EAAE,UAAU,EAAE,CAAC;AACxB,CAAC"}
@@ -0,0 +1,13 @@
1
+ /** KEYS: records, waiting, keys. ARGV: id, record, idempotency field or '', run at. Answers { id, duplicate }. */
2
+ export declare const ADD_JOB = "\nif ARGV[3] ~= '' then\n local kept = redis.call('HGET', KEYS[3], ARGV[3])\n if kept then return {kept, 1} end\nend\nif redis.call('HEXISTS', KEYS[1], ARGV[1]) == 1 then return {ARGV[1], 0} end\nlocal record = cjson.decode(ARGV[2])\nrecord.seq = redis.call('HINCRBY', KEYS[1], '#sequence', 1)\nredis.call('HSET', KEYS[1], ARGV[1], cjson.encode(record))\nredis.call('ZADD', KEYS[2], ARGV[4], ARGV[1])\nif ARGV[3] ~= '' then redis.call('HSET', KEYS[3], ARGV[3], ARGV[1]) end\nreturn {ARGV[1], 0}";
3
+ /** KEYS: records, waiting, running. ARGV: now, limit, lease until. Answers the claimed records, earliest first. */
4
+ export declare const CLAIM_JOBS = "\nlocal limit = tonumber(ARGV[2])\nlocal records = {}\nfor _, set in ipairs({KEYS[2], KEYS[3]}) do\n for _, id in ipairs(redis.call('ZRANGEBYSCORE', set, '-inf', ARGV[1], 'LIMIT', 0, limit)) do\n local raw = redis.call('HGET', KEYS[1], id)\n if raw then table.insert(records, cjson.decode(raw)) end\n end\nend\ntable.sort(records, function(a, b)\n if a.runAt == b.runAt then return a.seq < b.seq end\n return a.runAt < b.runAt\nend)\nlocal claimed = {}\nfor index = 1, math.min(#records, limit) do\n local record = records[index]\n record.status = 'running'\n record.attempts = record.attempts + 1\n record.leaseUntil = tonumber(ARGV[3])\n local raw = cjson.encode(record)\n redis.call('HSET', KEYS[1], record.id, raw)\n redis.call('ZREM', KEYS[2], record.id)\n redis.call('ZADD', KEYS[3], ARGV[3], record.id)\n table.insert(claimed, raw)\nend\nreturn claimed";
5
+ /**
6
+ * KEYS: records, waiting, running, dead, finished. ARGV: id, statuses allowed (space separated),
7
+ * attempt or '', change. A change sets fields, removes those it sets to false, and adds
8
+ * `attemptsDelta` to the attempts. Answers 1 when it applied, 0 otherwise.
9
+ */
10
+ export declare const TRANSITION_JOB = "\nlocal raw = redis.call('HGET', KEYS[1], ARGV[1])\nif not raw then return 0 end\nlocal record = cjson.decode(raw)\nif not string.find(' ' .. ARGV[2] .. ' ', ' ' .. record.status .. ' ', 1, true) then return 0 end\nif ARGV[3] ~= '' and record.attempts ~= tonumber(ARGV[3]) then return 0 end\nfor field, value in pairs(cjson.decode(ARGV[4])) do\n if field == 'attemptsDelta' then record.attempts = record.attempts + value\n elseif value == false then record[field] = nil\n else record[field] = value end\nend\nredis.call('HSET', KEYS[1], ARGV[1], cjson.encode(record))\nfor index = 2, 5 do redis.call('ZREM', KEYS[index], ARGV[1]) end\nif record.status == 'waiting' then redis.call('ZADD', KEYS[2], record.runAt, ARGV[1])\nelseif record.status == 'running' then redis.call('ZADD', KEYS[3], record.leaseUntil, ARGV[1])\nelseif record.status == 'dead' then redis.call('ZADD', KEYS[4], record.finishedAt, ARGV[1])\nelse redis.call('ZADD', KEYS[5], record.finishedAt, ARGV[1]) end\nreturn 1";
11
+ /** KEYS: records, finished, keys. ARGV: before. Answers how many records it dropped. */
12
+ export declare const PURGE_JOBS = "\nlocal ids = redis.call('ZRANGEBYSCORE', KEYS[2], '-inf', '(' .. ARGV[1])\nfor _, id in ipairs(ids) do\n local raw = redis.call('HGET', KEYS[1], id)\n if raw then\n local record = cjson.decode(raw)\n if record.key then redis.call('HDEL', KEYS[3], record.key) end\n redis.call('HDEL', KEYS[1], id)\n end\n redis.call('ZREM', KEYS[2], id)\nend\nreturn #ids";
13
+ //# sourceMappingURL=job-scripts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job-scripts.d.ts","sourceRoot":"","sources":["../../src/jobs/job-scripts.ts"],"names":[],"mappings":"AAQA,kHAAkH;AAClH,eAAO,MAAM,OAAO,qfAWA,CAAC;AAErB,mHAAmH;AACnH,eAAO,MAAM,UAAU,m3BAyBR,CAAC;AAEhB;;;;GAIG;AACH,eAAO,MAAM,cAAc,i+BAiBlB,CAAC;AAEV,wFAAwF;AACxF,eAAO,MAAM,UAAU,qXAWX,CAAC"}
@@ -0,0 +1,84 @@
1
+ /*
2
+ * The scripts of the job store. Each one runs atomically on the server and touches only the keys
3
+ * it is given, which Dragonfly requires: the records hash, the sorted sets of one queue
4
+ * (`waiting` by due instant, `running` by end of lease, `dead`), the finished set and the
5
+ * idempotency keys. A record is JSON whose payload, checkpoint and progress are JSON texts the
6
+ * scripts never open.
7
+ */
8
+ /** KEYS: records, waiting, keys. ARGV: id, record, idempotency field or '', run at. Answers { id, duplicate }. */
9
+ export const ADD_JOB = `
10
+ if ARGV[3] ~= '' then
11
+ local kept = redis.call('HGET', KEYS[3], ARGV[3])
12
+ if kept then return {kept, 1} end
13
+ end
14
+ if redis.call('HEXISTS', KEYS[1], ARGV[1]) == 1 then return {ARGV[1], 0} end
15
+ local record = cjson.decode(ARGV[2])
16
+ record.seq = redis.call('HINCRBY', KEYS[1], '#sequence', 1)
17
+ redis.call('HSET', KEYS[1], ARGV[1], cjson.encode(record))
18
+ redis.call('ZADD', KEYS[2], ARGV[4], ARGV[1])
19
+ if ARGV[3] ~= '' then redis.call('HSET', KEYS[3], ARGV[3], ARGV[1]) end
20
+ return {ARGV[1], 0}`;
21
+ /** KEYS: records, waiting, running. ARGV: now, limit, lease until. Answers the claimed records, earliest first. */
22
+ export const CLAIM_JOBS = `
23
+ local limit = tonumber(ARGV[2])
24
+ local records = {}
25
+ for _, set in ipairs({KEYS[2], KEYS[3]}) do
26
+ for _, id in ipairs(redis.call('ZRANGEBYSCORE', set, '-inf', ARGV[1], 'LIMIT', 0, limit)) do
27
+ local raw = redis.call('HGET', KEYS[1], id)
28
+ if raw then table.insert(records, cjson.decode(raw)) end
29
+ end
30
+ end
31
+ table.sort(records, function(a, b)
32
+ if a.runAt == b.runAt then return a.seq < b.seq end
33
+ return a.runAt < b.runAt
34
+ end)
35
+ local claimed = {}
36
+ for index = 1, math.min(#records, limit) do
37
+ local record = records[index]
38
+ record.status = 'running'
39
+ record.attempts = record.attempts + 1
40
+ record.leaseUntil = tonumber(ARGV[3])
41
+ local raw = cjson.encode(record)
42
+ redis.call('HSET', KEYS[1], record.id, raw)
43
+ redis.call('ZREM', KEYS[2], record.id)
44
+ redis.call('ZADD', KEYS[3], ARGV[3], record.id)
45
+ table.insert(claimed, raw)
46
+ end
47
+ return claimed`;
48
+ /**
49
+ * KEYS: records, waiting, running, dead, finished. ARGV: id, statuses allowed (space separated),
50
+ * attempt or '', change. A change sets fields, removes those it sets to false, and adds
51
+ * `attemptsDelta` to the attempts. Answers 1 when it applied, 0 otherwise.
52
+ */
53
+ export const TRANSITION_JOB = `
54
+ local raw = redis.call('HGET', KEYS[1], ARGV[1])
55
+ if not raw then return 0 end
56
+ local record = cjson.decode(raw)
57
+ if not string.find(' ' .. ARGV[2] .. ' ', ' ' .. record.status .. ' ', 1, true) then return 0 end
58
+ if ARGV[3] ~= '' and record.attempts ~= tonumber(ARGV[3]) then return 0 end
59
+ for field, value in pairs(cjson.decode(ARGV[4])) do
60
+ if field == 'attemptsDelta' then record.attempts = record.attempts + value
61
+ elseif value == false then record[field] = nil
62
+ else record[field] = value end
63
+ end
64
+ redis.call('HSET', KEYS[1], ARGV[1], cjson.encode(record))
65
+ for index = 2, 5 do redis.call('ZREM', KEYS[index], ARGV[1]) end
66
+ if record.status == 'waiting' then redis.call('ZADD', KEYS[2], record.runAt, ARGV[1])
67
+ elseif record.status == 'running' then redis.call('ZADD', KEYS[3], record.leaseUntil, ARGV[1])
68
+ elseif record.status == 'dead' then redis.call('ZADD', KEYS[4], record.finishedAt, ARGV[1])
69
+ else redis.call('ZADD', KEYS[5], record.finishedAt, ARGV[1]) end
70
+ return 1`;
71
+ /** KEYS: records, finished, keys. ARGV: before. Answers how many records it dropped. */
72
+ export const PURGE_JOBS = `
73
+ local ids = redis.call('ZRANGEBYSCORE', KEYS[2], '-inf', '(' .. ARGV[1])
74
+ for _, id in ipairs(ids) do
75
+ local raw = redis.call('HGET', KEYS[1], id)
76
+ if raw then
77
+ local record = cjson.decode(raw)
78
+ if record.key then redis.call('HDEL', KEYS[3], record.key) end
79
+ redis.call('HDEL', KEYS[1], id)
80
+ end
81
+ redis.call('ZREM', KEYS[2], id)
82
+ end
83
+ return #ids`;
84
+ //# sourceMappingURL=job-scripts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job-scripts.js","sourceRoot":"","sources":["../../src/jobs/job-scripts.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,kHAAkH;AAClH,MAAM,CAAC,MAAM,OAAO,GAAG;;;;;;;;;;;oBAWH,CAAC;AAErB,mHAAmH;AACnH,MAAM,CAAC,MAAM,UAAU,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;eAyBX,CAAC;AAEhB;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;;;;;;;;;;;;;;;;;SAiBrB,CAAC;AAEV,wFAAwF;AACxF,MAAM,CAAC,MAAM,UAAU,GAAG;;;;;;;;;;;YAWd,CAAC"}
@@ -0,0 +1,4 @@
1
+ import { type EnqueuedBy } from '@ddd-ts-kit/kernel';
2
+ /** Reads who enqueued a job, as the store wrote it. */
3
+ export declare function parseEnqueuedBy(raw: string): EnqueuedBy;
4
+ //# sourceMappingURL=parse-enqueued-by.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-enqueued-by.d.ts","sourceRoot":"","sources":["../../src/jobs/parse-enqueued-by.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAE9D,uDAAuD;AACvD,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAoBvD"}
@@ -0,0 +1,23 @@
1
+ import { isActor } from '@ddd-ts-kit/kernel';
2
+ /** Reads who enqueued a job, as the store wrote it. */
3
+ export function parseEnqueuedBy(raw) {
4
+ const parsed = JSON.parse(raw);
5
+ const actor = typeof parsed === 'object' && parsed !== null ? Reflect.get(parsed, 'actor') : undefined;
6
+ const correlationId = typeof parsed === 'object' && parsed !== null ? Reflect.get(parsed, 'correlationId') : undefined;
7
+ if (!isActor(actor) || typeof correlationId !== 'string') {
8
+ throw new TypeError('Who enqueued a background job of Redis is not written the way the store writes it.');
9
+ }
10
+ const tenantId = textOf(parsed, 'tenantId');
11
+ const traceparent = textOf(parsed, 'traceparent');
12
+ return {
13
+ correlationId,
14
+ actor,
15
+ ...(tenantId === undefined ? {} : { tenantId }),
16
+ ...(traceparent === undefined ? {} : { traceparent }),
17
+ };
18
+ }
19
+ function textOf(value, field) {
20
+ const text = typeof value === 'object' && value !== null ? Reflect.get(value, field) : undefined;
21
+ return typeof text === 'string' ? text : undefined;
22
+ }
23
+ //# sourceMappingURL=parse-enqueued-by.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-enqueued-by.js","sourceRoot":"","sources":["../../src/jobs/parse-enqueued-by.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAmB,MAAM,oBAAoB,CAAC;AAE9D,uDAAuD;AACvD,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACxC,MAAM,KAAK,GAAY,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAEhH,MAAM,aAAa,GACjB,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAEnG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,OAAO,aAAa,KAAK,QAAQ,EAAE,CAAC;QACzD,MAAM,IAAI,SAAS,CAAC,oFAAoF,CAAC,CAAC;IAC5G,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAC5C,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAElD,OAAO;QACL,aAAa;QACb,KAAK;QACL,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC;QAC/C,GAAG,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC;KACtD,CAAC;AACJ,CAAC;AAED,SAAS,MAAM,CAAC,KAAc,EAAE,KAAa;IAC3C,MAAM,IAAI,GAAY,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAE1G,OAAO,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;AACrD,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { RedisJobRecord } from './redis-job-record.js';
2
+ /** Reads a record the scripts wrote. Throws on anything else: the keys of the namespace belong to the store. */
3
+ export declare function parseJobRecord(raw: string): RedisJobRecord;
4
+ //# sourceMappingURL=parse-job-record.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-job-record.d.ts","sourceRoot":"","sources":["../../src/jobs/parse-job-record.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,gHAAgH;AAChH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,CAQ1D"}
@@ -0,0 +1,17 @@
1
+ /** Reads a record the scripts wrote. Throws on anything else: the keys of the namespace belong to the store. */
2
+ export function parseJobRecord(raw) {
3
+ const parsed = JSON.parse(raw);
4
+ if (!isJobRecord(parsed)) {
5
+ throw new TypeError('A background job record of Redis does not have the shape the store writes.');
6
+ }
7
+ return parsed;
8
+ }
9
+ function isJobRecord(value) {
10
+ if (typeof value !== 'object' || value === null) {
11
+ return false;
12
+ }
13
+ const fields = ['id', 'name', 'queue', 'payload', 'enqueuedBy', 'status'].map((field) => Reflect.get(value, field));
14
+ const instants = ['attempts', 'runAt', 'enqueuedAt', 'seq'].map((field) => Reflect.get(value, field));
15
+ return fields.every((field) => typeof field === 'string') && instants.every((field) => typeof field === 'number');
16
+ }
17
+ //# sourceMappingURL=parse-job-record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-job-record.js","sourceRoot":"","sources":["../../src/jobs/parse-job-record.ts"],"names":[],"mappings":"AAEA,gHAAgH;AAChH,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAExC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,SAAS,CAAC,4EAA4E,CAAC,CAAC;IACpG,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,WAAW,CAAC,KAAc;IACjC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QAChD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAW,EAAE,CAC/F,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAC1B,CAAC;IAEF,MAAM,QAAQ,GAAG,CAAC,UAAU,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAW,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;IAE/G,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC;AACpH,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { QueuedJob } from '@ddd-ts-kit/kernel';
2
+ import type { RedisJobRecord } from './redis-job-record.js';
3
+ /** What a record shows of its job: never the payload, the checkpoint or the actor. */
4
+ export declare function queuedJobOfRecord(record: RedisJobRecord): QueuedJob;
5
+ //# sourceMappingURL=queued-job-of-record.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queued-job-of-record.d.ts","sourceRoot":"","sources":["../../src/jobs/queued-job-of-record.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,sFAAsF;AACtF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,cAAc,GAAG,SAAS,CAenE"}
@@ -0,0 +1,18 @@
1
+ /** What a record shows of its job: never the payload, the checkpoint or the actor. */
2
+ export function queuedJobOfRecord(record) {
3
+ return {
4
+ id: record.id,
5
+ name: record.name,
6
+ queue: record.queue,
7
+ status: record.status,
8
+ attempts: record.attempts,
9
+ enqueuedAt: new Date(record.enqueuedAt),
10
+ runAt: new Date(record.runAt),
11
+ ...(record.tenantId === undefined ? {} : { tenantId: record.tenantId }),
12
+ ...(record.subject === undefined ? {} : { subject: record.subject }),
13
+ ...(record.progress === undefined ? {} : { progress: record.progress }),
14
+ ...(record.lastErrorCode === undefined ? {} : { lastErrorCode: record.lastErrorCode }),
15
+ ...(record.finishedAt === undefined ? {} : { finishedAt: new Date(record.finishedAt) }),
16
+ };
17
+ }
18
+ //# sourceMappingURL=queued-job-of-record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queued-job-of-record.js","sourceRoot":"","sources":["../../src/jobs/queued-job-of-record.ts"],"names":[],"mappings":"AAGA,sFAAsF;AACtF,MAAM,UAAU,iBAAiB,CAAC,MAAsB;IACtD,OAAO;QACL,EAAE,EAAE,MAAM,CAAC,EAAE;QACb,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,UAAU,EAAE,IAAI,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC;QACvC,KAAK,EAAE,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;QAC7B,GAAG,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC;QACvE,GAAG,CAAC,MAAM,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC;QACpE,GAAG,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC;QACvE,GAAG,CAAC,MAAM,CAAC,aAAa,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,MAAM,CAAC,aAAa,EAAE,CAAC;QACtF,GAAG,CAAC,MAAM,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;KACxF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,25 @@
1
+ import type { JobProgress, QueuedJobStatus } from '@ddd-ts-kit/kernel';
2
+ /** A job as the Redis store keeps it: instants in epoch milliseconds, JSON values as texts. */
3
+ export interface RedisJobRecord {
4
+ readonly id: string;
5
+ readonly name: string;
6
+ readonly queue: string;
7
+ readonly payload: string;
8
+ /** A JSON text: an empty list would come back from the scripts as an empty object. */
9
+ readonly enqueuedBy: string;
10
+ /** The idempotency field: name, tenant and key together. */
11
+ readonly key?: string;
12
+ readonly tenantId?: string;
13
+ readonly subject?: string;
14
+ readonly seq: number;
15
+ readonly status: QueuedJobStatus;
16
+ readonly attempts: number;
17
+ readonly runAt: number;
18
+ readonly leaseUntil?: number;
19
+ readonly progress?: JobProgress;
20
+ readonly checkpoint?: string;
21
+ readonly lastErrorCode?: string;
22
+ readonly enqueuedAt: number;
23
+ readonly finishedAt?: number;
24
+ }
25
+ //# sourceMappingURL=redis-job-record.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis-job-record.d.ts","sourceRoot":"","sources":["../../src/jobs/redis-job-record.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAEvE,+FAA+F;AAC/F,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,sFAAsF;IACtF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,4DAA4D;IAC5D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,WAAW,CAAC;IAChC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=redis-job-record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis-job-record.js","sourceRoot":"","sources":["../../src/jobs/redis-job-record.ts"],"names":[],"mappings":""}
@@ -0,0 +1,11 @@
1
+ export interface RedisJobStoreOptions {
2
+ /** Prefix of the keys, typically the application. Default `jobs`. */
3
+ readonly namespace?: string;
4
+ /**
5
+ * Told when a job staged by a committed unit of work could not be written (that job is lost),
6
+ * or a cancellation or a revival staged the same way could not be applied. The composition
7
+ * root hands it to the error reporter and the logger. Default: nothing is told.
8
+ */
9
+ readonly onLostJob?: (error: unknown, jobId: string) => void;
10
+ }
11
+ //# sourceMappingURL=redis-job-store-options.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis-job-store-options.d.ts","sourceRoot":"","sources":["../../src/jobs/redis-job-store-options.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,oBAAoB;IACnC,qEAAqE;IACrE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9D"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=redis-job-store-options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis-job-store-options.js","sourceRoot":"","sources":["../../src/jobs/redis-job-store-options.ts"],"names":[],"mappings":""}
@@ -0,0 +1,48 @@
1
+ import { inspect } from 'node:util';
2
+ import type { Redis } from 'ioredis';
3
+ import { type EnqueuedJob, type QueuedJob } from '@ddd-ts-kit/kernel';
4
+ import { type ClaimedJob, type ClaimRequest, type JobFailure, type JobLease, type JobStore, type JobStoreStats, type JobUpdate, type NewJob } from '@ddd-ts-kit/scheduling';
5
+ import type { RedisJobStoreOptions } from './redis-job-store-options.js';
6
+ /**
7
+ * The background jobs on Redis, Dragonfly or Valkey, in a few Lua scripts: no dependency but the
8
+ * client of the package. Redis cannot join the transaction of the database: inside a unit of
9
+ * work, `add`, `cancel` and `retryDead` answer from what Redis holds and write once it committed
10
+ * (`whenCommitted`), so a command that rolls back leaves no job and cancels none, and a crash
11
+ * between the commit and the write loses that write. Every key carries the namespace in braces,
12
+ * so that a cluster keeps them in one slot.
13
+ */
14
+ export declare class RedisJobStore implements JobStore {
15
+ private readonly client;
16
+ private readonly prefix;
17
+ private readonly onLostJob;
18
+ constructor(client: Redis, options?: RedisJobStoreOptions);
19
+ add(job: NewJob): Promise<EnqueuedJob>;
20
+ claim({ queue, limit, now, leaseMs }: ClaimRequest): Promise<ClaimedJob[]>;
21
+ heartbeat(lease: JobLease, leaseUntil: Date): Promise<boolean>;
22
+ update(lease: JobLease, update: JobUpdate, leaseUntil: Date): Promise<boolean>;
23
+ complete(lease: JobLease, at: Date): Promise<boolean>;
24
+ fail(lease: JobLease, { errorCode, retryAt, at }: JobFailure): Promise<boolean>;
25
+ release(lease: JobLease, runAt: Date): Promise<boolean>;
26
+ cancel(id: string, at: Date): Promise<boolean>;
27
+ find(id: string): Promise<QueuedJob | undefined>;
28
+ retryDead(ids: readonly string[], now: Date): Promise<number>;
29
+ purgeFinished(before: Date): Promise<number>;
30
+ stats(queue: string): Promise<JobStoreStats>;
31
+ /** What a serialisation shows: a name, never the client, whose options carry the password. */
32
+ toJSON(): string;
33
+ /** What `console.log` and `util.inspect` show: the same. */
34
+ [inspect.custom](): string;
35
+ private write;
36
+ /**
37
+ * Inside a unit of work, `write` waits for the commit and `expected` is the answer; anywhere
38
+ * else, it runs at once and its own answer is.
39
+ */
40
+ private afterCommit;
41
+ private revive;
42
+ private holding;
43
+ /** The queue of a job never changes: it is read first, to name the keys the script may touch. */
44
+ private transition;
45
+ private record;
46
+ private key;
47
+ }
48
+ //# sourceMappingURL=redis-job-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis-job-store.d.ts","sourceRoot":"","sources":["../../src/jobs/redis-job-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAiB,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACrF,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,KAAK,MAAM,EACZ,MAAM,wBAAwB,CAAC;AAQhC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AAgBzE;;;;;;;GAOG;AACH,qBAAa,aAAc,YAAW,QAAQ;IAC5C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAE/B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAEhC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0C;IAEpE,YAAY,MAAM,EAAE,KAAK,EAAE,OAAO,GAAE,oBAAyB,EAK5D;IAEK,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAoB3C;IAEK,KAAK,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,YAAY,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAY/E;IAED,SAAS,CAAC,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAE7D;IAED,MAAM,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAM7E;IAED,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAEpD;IAED,IAAI,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAK9E;IAED,OAAO,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAEtD;IAEK,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAUnD;IAEK,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAIrD;IAEK,SAAS,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAKlE;IAEK,aAAa,CAAC,MAAM,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAIjD;IAEK,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAMjD;IAED,8FAA8F;IAC9F,MAAM,IAAI,MAAM,CAEf;IAED,4DAA4D;IAC5D,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,MAAM,CAEzB;YAEa,KAAK;IAsBnB;;;OAGG;YACW,WAAW;YAiBX,MAAM;IAYpB,OAAO,CAAC,OAAO;IAIf,iGAAiG;YACnF,UAAU;YA4BV,MAAM;IAMpB,OAAO,CAAC,GAAG;CAGZ"}
@@ -0,0 +1,178 @@
1
+ import { inspect } from 'node:util';
2
+ import { whenCommitted } from '@ddd-ts-kit/kernel';
3
+ import { tenantKeyOf, } from '@ddd-ts-kit/scheduling';
4
+ import { listOf } from '../replies/list-of.js';
5
+ import { conceal } from '../secrecy/conceal.js';
6
+ import { claimedJobOfRecord } from './claimed-job-of-record.js';
7
+ import { ADD_JOB, CLAIM_JOBS, PURGE_JOBS, TRANSITION_JOB } from './job-scripts.js';
8
+ import { parseJobRecord } from './parse-job-record.js';
9
+ import { queuedJobOfRecord } from './queued-job-of-record.js';
10
+ const DESCRIPTION = 'RedisJobStore [client concealed]';
11
+ /**
12
+ * The background jobs on Redis, Dragonfly or Valkey, in a few Lua scripts: no dependency but the
13
+ * client of the package. Redis cannot join the transaction of the database: inside a unit of
14
+ * work, `add`, `cancel` and `retryDead` answer from what Redis holds and write once it committed
15
+ * (`whenCommitted`), so a command that rolls back leaves no job and cancels none, and a crash
16
+ * between the commit and the write loses that write. Every key carries the namespace in braces,
17
+ * so that a cluster keeps them in one slot.
18
+ */
19
+ export class RedisJobStore {
20
+ client;
21
+ prefix;
22
+ onLostJob;
23
+ constructor(client, options = {}) {
24
+ this.client = client;
25
+ this.prefix = `{${options.namespace ?? 'jobs'}}`;
26
+ this.onLostJob = options.onLostJob ?? (() => undefined);
27
+ conceal(this);
28
+ }
29
+ async add(job) {
30
+ const field = job.idempotencyKey === undefined
31
+ ? ''
32
+ : JSON.stringify([job.name, tenantKeyOf(job.enqueuedBy), job.idempotencyKey]);
33
+ const kept = field === '' ? null : await this.client.hget(this.key('keys'), field);
34
+ if (kept !== null) {
35
+ return { id: kept, duplicate: true };
36
+ }
37
+ await whenCommitted(() => this.write(job, field), (error) => {
38
+ this.onLostJob(error, job.id);
39
+ });
40
+ return { id: job.id, duplicate: false };
41
+ }
42
+ async claim({ queue, limit, now, leaseMs }) {
43
+ if (limit <= 0) {
44
+ return [];
45
+ }
46
+ const keys = [this.key('records'), this.key('waiting', queue), this.key('running', queue)];
47
+ const leaseUntil = now.getTime() + leaseMs;
48
+ const claimed = await this.client.eval(CLAIM_JOBS, keys.length, ...keys, now.getTime(), limit, leaseUntil);
49
+ return listOf(claimed)
50
+ .filter((raw) => typeof raw === 'string')
51
+ .map((raw) => claimedJobOfRecord(parseJobRecord(raw)));
52
+ }
53
+ heartbeat(lease, leaseUntil) {
54
+ return this.holding(lease, { leaseUntil: leaseUntil.getTime() });
55
+ }
56
+ update(lease, update, leaseUntil) {
57
+ return this.holding(lease, {
58
+ leaseUntil: leaseUntil.getTime(),
59
+ ...(update.progress === undefined ? {} : { progress: update.progress }),
60
+ ...('checkpoint' in update ? { checkpoint: jsonOrFalse(update.checkpoint) } : {}),
61
+ });
62
+ }
63
+ complete(lease, at) {
64
+ return this.holding(lease, { status: 'succeeded', finishedAt: at.getTime(), leaseUntil: false });
65
+ }
66
+ fail(lease, { errorCode, retryAt, at }) {
67
+ const next = retryAt === null ? { status: 'dead', finishedAt: at.getTime() } : { status: 'waiting', runAt: retryAt.getTime() };
68
+ return this.holding(lease, { ...next, leaseUntil: false, lastErrorCode: errorCode });
69
+ }
70
+ release(lease, runAt) {
71
+ return this.holding(lease, { status: 'waiting', runAt: runAt.getTime(), leaseUntil: false, attemptsDelta: -1 });
72
+ }
73
+ async cancel(id, at) {
74
+ const status = (await this.record(id))?.status;
75
+ if (status !== 'waiting' && status !== 'running') {
76
+ return false;
77
+ }
78
+ const change = { status: 'cancelled', finishedAt: at.getTime(), leaseUntil: false };
79
+ return this.afterCommit(true, [id], () => this.transition({ id, from: 'waiting running', change }));
80
+ }
81
+ async find(id) {
82
+ const record = await this.record(id);
83
+ return record === undefined ? undefined : queuedJobOfRecord(record);
84
+ }
85
+ async retryDead(ids, now) {
86
+ const records = await Promise.all(ids.map((id) => this.record(id)));
87
+ const dead = records.flatMap((record) => (record?.status === 'dead' ? [record.id] : []));
88
+ return this.afterCommit(dead.length, dead, () => this.revive(dead, now));
89
+ }
90
+ async purgeFinished(before) {
91
+ const keys = [this.key('records'), this.key('finished'), this.key('keys')];
92
+ return Number(await this.client.eval(PURGE_JOBS, keys.length, ...keys, before.getTime()));
93
+ }
94
+ async stats(queue) {
95
+ const [waiting, running, dead] = await Promise.all(['waiting', 'running', 'dead'].map((set) => this.client.zcard(this.key(set, queue))));
96
+ return { waiting: waiting ?? 0, running: running ?? 0, dead: dead ?? 0 };
97
+ }
98
+ /** What a serialisation shows: a name, never the client, whose options carry the password. */
99
+ toJSON() {
100
+ return DESCRIPTION;
101
+ }
102
+ /** What `console.log` and `util.inspect` show: the same. */
103
+ [inspect.custom]() {
104
+ return DESCRIPTION;
105
+ }
106
+ async write(job, field) {
107
+ const keys = [this.key('records'), this.key('waiting', job.queue), this.key('keys')];
108
+ const runAt = job.runAt.getTime();
109
+ const record = {
110
+ id: job.id,
111
+ name: job.name,
112
+ queue: job.queue,
113
+ payload: jsonOrFalse(job.payload) || 'null',
114
+ enqueuedBy: JSON.stringify(job.enqueuedBy),
115
+ ...(field === '' ? {} : { key: field }),
116
+ ...(job.enqueuedBy.tenantId === undefined ? {} : { tenantId: job.enqueuedBy.tenantId }),
117
+ ...(job.subject === undefined ? {} : { subject: job.subject }),
118
+ status: 'waiting',
119
+ attempts: 0,
120
+ runAt,
121
+ enqueuedAt: job.enqueuedAt.getTime(),
122
+ };
123
+ await this.client.eval(ADD_JOB, keys.length, ...keys, job.id, JSON.stringify(record), field, runAt);
124
+ }
125
+ /**
126
+ * Inside a unit of work, `write` waits for the commit and `expected` is the answer; anywhere
127
+ * else, it runs at once and its own answer is.
128
+ */
129
+ async afterCommit(expected, ids, write) {
130
+ let answer = expected;
131
+ await whenCommitted(async () => {
132
+ answer = await write();
133
+ }, (error) => {
134
+ for (const id of ids) {
135
+ this.onLostJob(error, id);
136
+ }
137
+ });
138
+ return answer;
139
+ }
140
+ async revive(ids, now) {
141
+ let revived = 0;
142
+ for (const id of ids) {
143
+ const change = { status: 'waiting', runAt: now.getTime(), finishedAt: false, attempts: 0 };
144
+ revived += (await this.transition({ id, from: 'dead', change })) ? 1 : 0;
145
+ }
146
+ return revived;
147
+ }
148
+ holding(lease, change) {
149
+ return this.transition({ id: lease.id, from: 'running', attempt: lease.attempt, change });
150
+ }
151
+ /** The queue of a job never changes: it is read first, to name the keys the script may touch. */
152
+ async transition({ id, from, attempt, change }) {
153
+ const record = await this.record(id);
154
+ if (record === undefined) {
155
+ return false;
156
+ }
157
+ const keys = [
158
+ this.key('records'),
159
+ ...['waiting', 'running', 'dead'].map((set) => this.key(set, record.queue)),
160
+ this.key('finished'),
161
+ ];
162
+ const held = attempt === undefined ? '' : String(attempt);
163
+ const applied = await this.client.eval(TRANSITION_JOB, keys.length, ...keys, id, from, held, JSON.stringify(change));
164
+ return Number(applied) === 1;
165
+ }
166
+ async record(id) {
167
+ const raw = await this.client.hget(this.key('records'), id);
168
+ return raw === null ? undefined : parseJobRecord(raw);
169
+ }
170
+ key(...parts) {
171
+ return [this.prefix, ...parts].join(':');
172
+ }
173
+ }
174
+ /** JSON of a value, or `false` (the field is removed) for `undefined`, which JSON cannot write. */
175
+ function jsonOrFalse(value) {
176
+ return value === undefined ? false : JSON.stringify(value);
177
+ }
178
+ //# sourceMappingURL=redis-job-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis-job-store.js","sourceRoot":"","sources":["../../src/jobs/redis-job-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import { type ErrorReporter, type Logger } from '@ddd-ts-kit/kernel';
2
+ /**
3
+ * The `onLostJob` of a `RedisJobStore` a composition root wires: a job a committed command
4
+ * enqueued, or a cancellation it decided, that Redis did not take is logged, and reported when a
5
+ * reporter is given: never dropped in silence.
6
+ *
7
+ * new RedisJobStore(client, { onLostJob: reportLostJobs(logger, errors) });
8
+ */
9
+ export declare function reportLostJobs(logger: Logger, errors?: ErrorReporter): (error: unknown, jobId: string) => void;
10
+ //# sourceMappingURL=report-lost-jobs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report-lost-jobs.d.ts","sourceRoot":"","sources":["../../src/jobs/report-lost-jobs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgC,KAAK,aAAa,EAAE,KAAK,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAEnG;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAY9G"}
@@ -0,0 +1,18 @@
1
+ import { errorReportOf, identifyError } from '@ddd-ts-kit/kernel';
2
+ /**
3
+ * The `onLostJob` of a `RedisJobStore` a composition root wires: a job a committed command
4
+ * enqueued, or a cancellation it decided, that Redis did not take is logged, and reported when a
5
+ * reporter is given: never dropped in silence.
6
+ *
7
+ * new RedisJobStore(client, { onLostJob: reportLostJobs(logger, errors) });
8
+ */
9
+ export function reportLostJobs(logger, errors) {
10
+ return (error, jobId) => {
11
+ logger.error('A background job write was lost after its unit of work committed', {
12
+ jobId,
13
+ ...identifyError(error),
14
+ });
15
+ errors?.report(error, errorReportOf(error, { origin: 'queue', operation: 'background job write', severity: 'error' }));
16
+ };
17
+ }
18
+ //# sourceMappingURL=report-lost-jobs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report-lost-jobs.js","sourceRoot":"","sources":["../../src/jobs/report-lost-jobs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAmC,MAAM,oBAAoB,CAAC;AAEnG;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,MAAc,EAAE,MAAsB;IACnE,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;QACtB,MAAM,CAAC,KAAK,CAAC,kEAAkE,EAAE;YAC/E,KAAK;YACL,GAAG,aAAa,CAAC,KAAK,CAAC;SACxB,CAAC,CAAC;QAEH,MAAM,EAAE,MAAM,CACZ,KAAK,EACL,aAAa,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,sBAAsB,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAChG,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ddd-ts-kit/redis",
3
- "version": "0.10.0",
4
- "description": "Adapters of the kernel ports on the Redis protocol (Redis, Dragonfly, Valkey): cache, distributed lock, rate limiter, relay wake-up signal, and an integration event provider on Redis Streams.",
3
+ "version": "0.12.0",
4
+ "description": "Adapters of the kernel ports on the Redis protocol (Redis, Dragonfly, Valkey): cache, distributed lock, rate limiter, relay wake-up signal, a background job store, and an integration event provider on Redis Streams.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -34,11 +34,12 @@
34
34
  "dist"
35
35
  ],
36
36
  "dependencies": {
37
- "@ddd-ts-kit/kernel": "0.10.0"
37
+ "@ddd-ts-kit/kernel": "0.12.0",
38
+ "@ddd-ts-kit/scheduling": "0.12.0"
38
39
  },
39
40
  "peerDependencies": {
40
41
  "ioredis": "^5.0.0 || ^6.0.0",
41
- "@ddd-ts-kit/testing": "0.10.0"
42
+ "@ddd-ts-kit/testing": "0.12.0"
42
43
  },
43
44
  "peerDependenciesMeta": {
44
45
  "@ddd-ts-kit/testing": {
@@ -47,7 +48,7 @@
47
48
  },
48
49
  "devDependencies": {
49
50
  "ioredis": "^6.0.0",
50
- "@ddd-ts-kit/events": "0.10.0",
51
- "@ddd-ts-kit/testing": "0.10.0"
51
+ "@ddd-ts-kit/events": "0.12.0",
52
+ "@ddd-ts-kit/testing": "0.12.0"
52
53
  }
53
54
  }