Skip to content

fix: normalize busyMap key generation to prevent incorrect interval matching#28936

Open
Akash504-ai wants to merge 2 commits intocalcom:mainfrom
Akash504-ai:fix/interval-limit-key-normalization
Open

fix: normalize busyMap key generation to prevent incorrect interval matching#28936
Akash504-ai wants to merge 2 commits intocalcom:mainfrom
Akash504-ai:fix/interval-limit-key-normalization

Conversation

@Akash504-ai
Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes an inconsistency in LimitManager where busy map keys were generated differently between createKey and addBusyTime.

Previously:

  • createKey normalized timestamps using startOf(unit)
  • addBusyTime used raw timestamps

This mismatch caused:

  • isAlreadyBusy() to return incorrect results
  • Duplicate or missed interval matches for the same logical time period

Example:

Adding a busy time at 2026-04-19T15:30 (day)
Checking for 2026-04-19T10:00 (same day)

Before: false
After: true


Visual Demo (For contributors especially)

N/A - logic-level fix, no UI involved.


Mandatory Tasks (DO NOT REMOVE)

  • I have self-reviewed the code
  • I have updated the developer docs if required (N/A)
  • I confirm automated tests are in place that prove my fix is effective

How should this be tested?

  1. Create a LimitManager instance
  2. Add a busy time:
    • start = 2026-04-19T15:30
    • unit = "day"
  3. Call:
    isAlreadyBusy("2026-04-19T10:00", "day")
  4. Verify:
  • Returns true for same day
  • Returns false for different day

Automated tests added in: packages/lib/__tests__/limitManager.test.ts

@github-actions
Copy link
Copy Markdown
Contributor

Welcome to Cal.diy, @Akash504-ai! Thanks for opening this pull request.

A few things to keep in mind:

  • This is Cal.diy, not Cal.com. Cal.diy is a community-driven, fully open-source fork of Cal.com licensed under MIT. Your changes here will be part of Cal.diy — they will not be deployed to the Cal.com production app.
  • Please review our Contributing Guidelines if you haven't already.
  • Make sure your PR title follows the Conventional Commits format.

A maintainer will review your PR soon. Thanks for contributing!

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 19, 2026

📝 Walkthrough

Walkthrough

A new test suite for LimitManager was added with two test cases verifying the isAlreadyBusy method's behavior across day boundaries. Concurrently, the addBusyTime method in LimitManager was updated to normalize busy interval keys in the busyMap by aligning the start time to the specified unit using startOf(params.unit) before converting to ISO string format. This changes how busy intervals are bucketed for overlap detection lookups.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: fixing key normalization in busyMap generation to resolve interval matching issues.
Description check ✅ Passed The description is directly related to the changeset, clearly explaining the bug (key generation mismatch), its impact, and providing concrete examples and testing guidance.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown
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.

🧹 Nitpick comments (2)
packages/lib/intervalLimits/limitManager.ts (1)

85-91: Reuse createKey to prevent future drift.

The key construction here is now identical to LimitManager.createKey (line 37-40). Inlining it risks reintroducing the same mismatch this PR is fixing if one side is later changed. Prefer calling the existing helper.

♻️ Proposed refactor
-    const tzStart = params.timeZone ? params.start.tz(params.timeZone) : params.start;
-    this.busyMap.set(`${params.unit}-${tzStart.startOf(params.unit).toISOString()}`, {
+    const tzStart = params.timeZone ? params.start.tz(params.timeZone) : params.start;
+    const key = LimitManager.createKey(params.start, params.unit, params.timeZone);
+    this.busyMap.set(key, {
       start: tzStart.toISOString(),
       end: tzStart.endOf(params.unit).toISOString(),
       title: params.title,
       source: params.source,
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/intervalLimits/limitManager.ts` around lines 85 - 91, The
busyMap key is being constructed inline but duplicates the logic in
LimitManager.createKey; replace the inline template string
`${params.unit}-${tzStart.startOf(params.unit).toISOString()}` with a call to
the existing helper (e.g., this.createKey(...)) so the code uses
LimitManager.createKey when calling busyMap.set; keep the same tzStart,
params.unit, and other values (start/end/title/source) but pass the key returned
by createKey instead of building it inline.
packages/lib/__tests__/limitManager.test.ts (1)

7-43: Consider expanding coverage to week/month/year and timezone cases.

The current two tests only exercise unit: "day" without a timeZone. Given that the bug being fixed stems from key normalization across startOf(unit) boundaries, it would be valuable to also assert:

  • Same-week / same-month / same-year bucketing (including the week-spans-two-months branch at limitManager.ts line 52-54).
  • A case where timeZone shifts the calendar day (e.g., a UTC time that falls on a different day in America/Los_Angeles) — this is the primary real-world scenario in packages/trpc/server/routers/viewer/slots/util.ts where timeZone is always passed.

Not blocking for this fix, but would guard against regressions in the ancestor-unit fallthrough logic in isAlreadyBusy.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/__tests__/limitManager.test.ts` around lines 7 - 43, Add tests
that cover ancestor units and timezone behavior for LimitManager: extend the
test suite to include cases calling addBusyTime and isAlreadyBusy with unit
values "week", "month", and "year" (exercising the week-spans-two-months branch)
and assert same- and different-bucket behavior; also add a test using a non-UTC
timeZone (e.g., "America/Los_Angeles") where a UTC timestamp maps to a different
local day to verify isAlreadyBusy respects timeZone normalization. Use the same
helpers (LimitManager, addBusyTime, isAlreadyBusy) and dayjs timestamps as in
existing tests so they clearly target the bug in key normalization across
startOf(unit) boundaries.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/lib/__tests__/limitManager.test.ts`:
- Around line 7-43: Add tests that cover ancestor units and timezone behavior
for LimitManager: extend the test suite to include cases calling addBusyTime and
isAlreadyBusy with unit values "week", "month", and "year" (exercising the
week-spans-two-months branch) and assert same- and different-bucket behavior;
also add a test using a non-UTC timeZone (e.g., "America/Los_Angeles") where a
UTC timestamp maps to a different local day to verify isAlreadyBusy respects
timeZone normalization. Use the same helpers (LimitManager, addBusyTime,
isAlreadyBusy) and dayjs timestamps as in existing tests so they clearly target
the bug in key normalization across startOf(unit) boundaries.

In `@packages/lib/intervalLimits/limitManager.ts`:
- Around line 85-91: The busyMap key is being constructed inline but duplicates
the logic in LimitManager.createKey; replace the inline template string
`${params.unit}-${tzStart.startOf(params.unit).toISOString()}` with a call to
the existing helper (e.g., this.createKey(...)) so the code uses
LimitManager.createKey when calling busyMap.set; keep the same tzStart,
params.unit, and other values (start/end/title/source) but pass the key returned
by createKey instead of building it inline.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1d6acc0a-3840-4b5d-9579-29cd6e08c574

📥 Commits

Reviewing files that changed from the base of the PR and between 9efd0e6 and 9a8c909.

📒 Files selected for processing (2)
  • packages/lib/__tests__/limitManager.test.ts
  • packages/lib/intervalLimits/limitManager.ts

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant