@crewhaus/idempotency-keys 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -7,11 +7,18 @@ export interface IdempotencyStore<TValue = unknown> {
7
7
  size(): number;
8
8
  }
9
9
  /**
10
- * Compose a stable key from a job id + attempt number. We hash so the
11
- * key fits the typical kv-store key-length limits regardless of job-id
12
- * shape.
10
+ * Compose a stable key from a job id. We hash so the key fits the typical
11
+ * kv-store key-length limits regardless of job-id shape.
12
+ *
13
+ * `discriminator` partitions the key space for callers whose dedupe unit is
14
+ * NARROWER than the job id — a graph node that must re-run once per attempt,
15
+ * say. It defaults to `0`, which is what queue redelivery wants and what the
16
+ * content-derived callers (`target-managed`, `target-browser-driver`) already
17
+ * pass explicitly, so their keys are unchanged. Passing a per-delivery
18
+ * counter here defeats redelivery dedupe: don't, unless re-running is the
19
+ * point.
13
20
  */
14
- export declare function idempotencyKey(jobId: string, attempt: number): IdempotencyKey;
21
+ export declare function idempotencyKey(jobId: string, discriminator?: number): IdempotencyKey;
15
22
  export type InMemoryStoreOptions = {
16
23
  readonly now?: () => number;
17
24
  };
package/dist/index.js CHANGED
@@ -2,15 +2,22 @@
2
2
  * Catalog R7 `idempotency-keys` — Section 23 BATCH.
3
3
  *
4
4
  * Per-key cached-result store the BATCH consumer wraps the user's
5
- * handler with so a job that's already been processed (e.g. after a
6
- * crash → retry, or after a transient nack that produced a partial
7
- * result) returns the cached value without re-invoking the model.
5
+ * handler with, so a job that ALREADY COMPLETED and comes back anyway
6
+ * (a crash or a lost ack between the handler returning and the queue
7
+ * being told, an expired visibility lease) returns the cached value
8
+ * without re-invoking the model.
8
9
  *
9
- * Key shape: `idempotencyKey(jobId, attempt)` — the consumer derives
10
- * the key from the job's id + attempt counter on the queue. Same job
11
- * + same attempt → same key, so two consumers grabbing the same job
12
- * (the "double-pull" race when visibility leases collide) get the
13
- * same cached result.
10
+ * Key shape: `idempotencyKey(jobId)` — the key is the job's IDENTITY and
11
+ * nothing else. It deliberately does NOT include the attempt counter: a
12
+ * redelivery (crash → retry, a swallowed ack, a visibility lease that
13
+ * collided with another consumer) is precisely the case the cache exists
14
+ * to serve, and every queue adapter that tracks attempts hands the
15
+ * redelivery a *higher* attempt number. Folding the attempt into the key
16
+ * therefore guaranteed a miss on exactly the path that matters and made
17
+ * the configured window unreachable.
18
+ *
19
+ * Only SUCCESSFUL results are cached (see `withIdempotency`), so a
20
+ * failed attempt never suppresses its own retry.
14
21
  *
15
22
  * The default `createInMemoryIdempotencyStore` is a Map with a per-key
16
23
  * TTL. Eviction happens lazily on next get/set; tests can override the
@@ -18,12 +25,19 @@
18
25
  */
19
26
  import { createHash } from "node:crypto";
20
27
  /**
21
- * Compose a stable key from a job id + attempt number. We hash so the
22
- * key fits the typical kv-store key-length limits regardless of job-id
23
- * shape.
28
+ * Compose a stable key from a job id. We hash so the key fits the typical
29
+ * kv-store key-length limits regardless of job-id shape.
30
+ *
31
+ * `discriminator` partitions the key space for callers whose dedupe unit is
32
+ * NARROWER than the job id — a graph node that must re-run once per attempt,
33
+ * say. It defaults to `0`, which is what queue redelivery wants and what the
34
+ * content-derived callers (`target-managed`, `target-browser-driver`) already
35
+ * pass explicitly, so their keys are unchanged. Passing a per-delivery
36
+ * counter here defeats redelivery dedupe: don't, unless re-running is the
37
+ * point.
24
38
  */
25
- export function idempotencyKey(jobId, attempt) {
26
- return createHash("sha256").update(`${jobId}:${attempt}`).digest("hex").slice(0, 24);
39
+ export function idempotencyKey(jobId, discriminator = 0) {
40
+ return createHash("sha256").update(`${jobId}:${discriminator}`).digest("hex").slice(0, 24);
27
41
  }
28
42
  export function createInMemoryIdempotencyStore(opts = {}) {
29
43
  const now = opts.now ?? Date.now;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crewhaus/idempotency-keys",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "Idempotency-key dedup store for the BATCH consumer (Section 23 BATCH)",
6
6
  "main": "dist/index.js",
@@ -15,7 +15,7 @@
15
15
  "test": "bun test src"
16
16
  },
17
17
  "dependencies": {
18
- "@crewhaus/errors": "0.4.0"
18
+ "@crewhaus/errors": "0.5.0"
19
19
  },
20
20
  "license": "Apache-2.0",
21
21
  "author": {