Skip to content

Conversation

@therockstorm
Copy link
Member

@therockstorm therockstorm commented Dec 4, 2025

Summary

After the API changes in #215, people are using the API as expected, with one exception: they're including data that is already included by default (e.g. workflow key, recipient ID) in idempotencyKey.resourceId. For example:

{
  workflowKey: 'shift-created',
  recipients: ['user-123'],
  idempotencyKey: {
    resourceId: `shift-created-user-123-${shiftId}`,
  },
};

We explain this in the comments, but people don't reliably read comments. This PR attempts to address it in two ways:

  1. Make it clear you're only providing part of the idempotency key by renaming idempotencyKey to idempotencyKeyParts.
  2. Make it "look" wrong if you provide more than just the resource ID by including the type of resource.
{
  workflowKey: 'shift-created',
  recipients: ['user-123'],
  idempotencyKeyParts: { // <- Renamed
    resource: { // <- Object instead of a string that includes the resource type, which we ignore in the library.
      type: "shift",
      id: shiftId
    }
  },
};

Question to reviewers: Do you think this is sufficient to change behavior?

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 4, 2025

📝 Walkthrough

Walkthrough

This PR refactors the idempotency key structure in the notifications system, replacing a flat idempotencyKey with resourceId with a new nested idempotencyKeyParts containing resource metadata (type and id). The change introduces a union type supporting both event-time and resource-based idempotency while maintaining backward compatibility, updating hashing logic to resolve resourceId from both legacy and new formats.

Changes

Cohort / File(s) Summary
Documentation & Examples
packages/ai-rules/.ruler/backend/notifications.md, packages/notifications/README.md, packages/notifications/examples/usage.md, packages/notifications/examples/enqueueNotificationJob.ts
Updated enqueue examples and documentation to use idempotencyKeyParts with nested resource object instead of flat idempotencyKey with resourceId.
Idempotency Hashing Logic
packages/notifications/src/lib/internal/triggerIdempotencyKeyParamsToHash.ts, packages/notifications/src/lib/internal/triggerIdempotencyKeyParamsToHash.spec.ts
Added conditional resolution logic to extract resourceId from either legacy format (params.resourceId) or new nested format (params.resource.id). New test suite validates hash determinism, format equivalence, and proper differentiation across variants.
Type Definitions & Enqueue Core
packages/notifications/src/lib/notificationJobEnqueuer.ts, packages/notifications/src/lib/triggerIdempotencyKey.ts
Introduced new IdempotencyKeyParts union type with mutually exclusive branches (eventOccurredAt vs nested resource), deprecated IdempotencyKey interface, updated NotificationEnqueueData to support both legacy and new idempotency key shapes, and expanded TriggerIdempotencyKeyParams to accept IdempotencyKeyParts.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~35 minutes

Key areas requiring attention:

  • Type system changes: The new IdempotencyKeyParts union type and its mutual exclusivity constraints need careful validation to ensure consumers can only provide one of the two formats
  • Backward compatibility logic: The dual-format support in triggerIdempotencyKeyParamsToHash.ts conditionally resolves resourceId; verify fallback behavior is correct and covers all edge cases
  • Enqueue logic: The merging of idempotencyKey and idempotencyKeyParts in NotificationJobEnqueuer.enqueueOneOrMore must be reviewed to ensure no conflicts or silent failures when both are provided
  • Deprecation handling: Confirm that deprecated IdempotencyKey interface doesn't break existing callers and that migration path is clear

Possibly related PRs

Suggested reviewers

  • stefanchrobot
  • piotrj

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main objective of the PR: updating the idempotencyKey API to clarify that developers should only provide a portion of the idempotency key.
Description check ✅ Passed The PR description clearly explains the motivation (API misuse from PR #215), provides concrete before/after examples, and outlines the two API changes made.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch idempotency-key-api-update

Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link

nx-cloud bot commented Dec 4, 2025

View your CI Pipeline Execution ↗ for commit 550d684

Command Status Duration Result
nx affected --configuration ci --parallel 3 --t... ✅ Succeeded 12s View ↗
nx build ai-rules ✅ Succeeded 5s View ↗

☁️ Nx Cloud last updated this comment at 2025-12-04 19:19:18 UTC

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/notifications/src/lib/internal/triggerIdempotencyKeyParamsToHash.spec.ts (1)

33-47: Test confirms design: resource.type excluded from hash.

This test verifies that resourceId: "same-id" and resource: { type: "account", id: "same-id" } produce identical hashes. Consider adding a test case that explicitly documents the behavior when different types share the same ID:

it("produces same hash for different resource types with same id (type is not hashed)", () => {
  const accountInput = {
    ...baseParams,
    resource: { type: "account", id: "shared-id" },
  };
  const shiftInput = {
    ...baseParams,
    resource: { type: "shift", id: "shared-id" },
  };

  const actualAccount = triggerIdempotencyKeyParamsToHash(accountInput);
  const actualShift = triggerIdempotencyKeyParamsToHash(shiftInput);

  // Type is intentionally excluded from hash - IDs must be globally unique
  expect(actualAccount).toBe(actualShift);
});
packages/notifications/src/lib/notificationJobEnqueuer.ts (1)

235-241: Clarify behavior when both idempotencyKey and idempotencyKeyParts are provided.

The spread order means idempotencyKeyParts properties override idempotencyKey properties (e.g., eventOccurredAt). This is reasonable for migration, but consider either:

  1. Adding runtime validation to warn/error if both are provided, or
  2. Documenting the precedence behavior in the JSDoc
const idempotencyKeyParams: TriggerIdempotencyKeyParams = {
  // idempotencyKeyParts takes precedence over deprecated idempotencyKey
  ...data.idempotencyKey,
  ...data.idempotencyKeyParts,
  chunk: number,
  recipients,
  workflowKey: data.workflowKey,
};
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 612315a and 550d684.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (8)
  • packages/ai-rules/.ruler/backend/notifications.md (1 hunks)
  • packages/notifications/README.md (1 hunks)
  • packages/notifications/examples/enqueueNotificationJob.ts (1 hunks)
  • packages/notifications/examples/usage.md (1 hunks)
  • packages/notifications/src/lib/internal/triggerIdempotencyKeyParamsToHash.spec.ts (1 hunks)
  • packages/notifications/src/lib/internal/triggerIdempotencyKeyParamsToHash.ts (2 hunks)
  • packages/notifications/src/lib/notificationJobEnqueuer.ts (3 hunks)
  • packages/notifications/src/lib/triggerIdempotencyKey.ts (3 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Write concise, technical TypeScript code with accurate examples
Use functional and declarative programming patterns
Prefer iteration and modularization over code duplication
Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError)
Structure files: constants, types, exported functions, non-exported functions
Avoid magic strings and numbers; define constants
When declaring functions, use the function keyword, not const
Files should read from top to bottom: exported items live on top and the internal functions and methods they call go below them
Prefer data immutability
Sanitize user input
Handle errors and edge cases at the beginning of functions
Use early returns for error conditions to avoid deeply nested if statements
Place the happy path last in the function for improved readability
Avoid unnecessary else statements; use the if-return pattern instead
Use guard clauses to handle preconditions and invalid states early
Implement proper error logging and user-friendly error messages
Favor @clipboard-health/util-ts's Either type for expected errors instead of try/catch
Use strict-mode TypeScript for all code; prefer interfaces over types
Avoid enums; use const maps instead
Strive for precise types. Look for type definitions in the codebase and create your own if none exist
Avoid using type assertions like as or ! unless absolutely necessary
Use the unknown type instead of any when the type is truly unknown
Use an object to pass multiple function params and to return results
Leverage union types, intersection types, and conditional types for complex type definitions
Use mapped types and utility types (e.g., Partial<T>, Pick<T>, Omit<T>) to transform existing types
Implement generic types to create reusable, flexible type definitions
Utilize the keyof operator and index access types for dynamic property access
Implement discriminated unions for type-safe handling of different object shapes where approp...

Files:

  • packages/notifications/src/lib/triggerIdempotencyKey.ts
  • packages/notifications/src/lib/internal/triggerIdempotencyKeyParamsToHash.ts
  • packages/notifications/examples/enqueueNotificationJob.ts
  • packages/notifications/src/lib/internal/triggerIdempotencyKeyParamsToHash.spec.ts
  • packages/notifications/src/lib/notificationJobEnqueuer.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use camelCase for files and directories (e.g., modules/shiftOffers.ts)

Files:

  • packages/notifications/src/lib/triggerIdempotencyKey.ts
  • packages/notifications/src/lib/internal/triggerIdempotencyKeyParamsToHash.ts
  • packages/notifications/examples/enqueueNotificationJob.ts
  • packages/notifications/src/lib/internal/triggerIdempotencyKeyParamsToHash.spec.ts
  • packages/notifications/src/lib/notificationJobEnqueuer.ts
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{test,spec}.{ts,tsx}: Follow the Arrange-Act-Assert convention for tests with newlines between each section
Name test variables using the mockX, input, expected, actual convention
Aim for high test coverage, writing both positive and negative test cases
Prefer it.each for multiple test cases
Avoid conditional logic in tests

Files:

  • packages/notifications/src/lib/internal/triggerIdempotencyKeyParamsToHash.spec.ts
🧬 Code graph analysis (3)
packages/notifications/src/lib/triggerIdempotencyKey.ts (1)
packages/notifications/src/lib/notificationJobEnqueuer.ts (2)
  • IdempotencyKey (28-31)
  • IdempotencyKeyParts (33-61)
packages/notifications/src/lib/internal/triggerIdempotencyKeyParamsToHash.ts (1)
packages/notifications/src/lib/triggerIdempotencyKey.ts (1)
  • TriggerIdempotencyKeyParams (22-37)
packages/notifications/src/lib/internal/triggerIdempotencyKeyParamsToHash.spec.ts (1)
packages/notifications/src/lib/internal/triggerIdempotencyKeyParamsToHash.ts (1)
  • triggerIdempotencyKeyParamsToHash (8-10)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: pull-request
  • GitHub Check: semgrep-cloud-platform/scan
🔇 Additional comments (9)
packages/notifications/src/lib/internal/triggerIdempotencyKeyParamsToHash.ts (1)

17-22: Consider including resource.type in the hash for collision prevention.

The resource.type field is discarded during hash computation. If two different resource types share the same id, they will produce identical hashes. While this may be intentional (the type exists only for API clarity), it could lead to unintended idempotency collisions.

If this is by design (e.g., id values are globally unique across types), consider adding a comment clarifying this assumption. Otherwise, include type in the hash:

     resourceId:
       "resourceId" in params
         ? params.resourceId
         : "resource" in params && params.resource && "id" in params.resource
-          ? params.resource.id
+          ? `${params.resource.type}:${params.resource.id}`
           : undefined,
packages/notifications/examples/usage.md (1)

95-101: Documentation update looks good.

The example correctly demonstrates the new idempotencyKeyParts structure with the nested resource object, aligning with the API changes.

packages/notifications/README.md (1)

89-95: README example correctly updated.

The embedded example properly reflects the new idempotencyKeyParts API with the nested resource structure.

packages/notifications/src/lib/triggerIdempotencyKey.ts (1)

22-37: Type definition correctly updated to support both formats.

The union type (IdempotencyKey | IdempotencyKeyParts) properly supports both the deprecated legacy format and the new structured format, maintaining backward compatibility.

packages/notifications/examples/enqueueNotificationJob.ts (1)

19-25: Example correctly demonstrates the new API.

The update properly shows the idempotencyKeyParts structure with the nested resource object, which should help prevent the misuse described in the PR objectives.

packages/ai-rules/.ruler/backend/notifications.md (1)

87-93: AI rules documentation properly updated.

The embedded example correctly reflects the new idempotencyKeyParts API structure.

packages/notifications/src/lib/internal/triggerIdempotencyKeyParamsToHash.spec.ts (1)

1-127: Comprehensive test coverage for the new hashing logic.

The tests properly cover resourceId extraction from both formats, deterministic hashing, recipient order invariance, and workplaceId inclusion. Good adherence to Arrange-Act-Assert pattern and naming conventions.

packages/notifications/src/lib/notificationJobEnqueuer.ts (2)

33-61: Well-designed discriminated union type.

The IdempotencyKeyParts union with mutually exclusive branches (eventOccurredAt vs resource) is properly structured. The JSDoc documentation clearly explains the intended usage patterns.


25-31: Deprecation path is well-documented.

The @deprecated annotations on IdempotencyKey and idempotencyKey field, along with the comprehensive JSDoc on idempotencyKeyParts, provide clear migration guidance.

Also applies to: 63-87

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants