Skip to content

feat(OUT-3696): cache getClients for allow-listed workspaces#1210

Open
priosshrsth wants to merge 8 commits intofeature/c1-optimizationfrom
anit/out-3696-add-caching-for-clients-requests
Open

feat(OUT-3696): cache getClients for allow-listed workspaces#1210
priosshrsth wants to merge 8 commits intofeature/c1-optimizationfrom
anit/out-3696-add-caching-for-clients-requests

Conversation

@priosshrsth
Copy link
Copy Markdown
Collaborator

Summary

  • Adds server-side caching for getAllClients via Next.js Cache Components ('use cache' directive) with per-workspace cacheTag for targeted invalidation.
  • Enables cacheComponents and bumps cacheMaxMemorySize to 500MB.
  • Removes force-no-store / noStore() directives that conflicted with Cache Components.
  • Restricted to an allow-list of workspace IDs (CACHED_CLIENT_WORKSPACES) for safe rollout.

Impact

  • /users endpoint: ~18s → ~2s for allow-listed workspaces.

Test plan

  • Verify /users response time on an allow-listed workspace is dramatically reduced
  • Verify non-allow-listed workspaces fall through to the existing uncached path
  • Verify cache invalidates correctly on client list changes (manual: revalidateTag / updateTag path)
  • Verify removal of force-no-store doesn't surface stale data on dynamic routes

🤖 Generated with Claude Code

priosshrsth and others added 5 commits May 6, 2026 15:02
Add a layout-level WorkspaceFetcher that uses SWR to populate
authDetails.workspace from /api/workspace on the client. SSR pages
((home), detail, client, manage-templates, manage-templates/[id]) no
longer block on copilot.getWorkspace() per request — they let the
Redux store hydrate from the cached client fetch (60s deduping).

Server-side route uses React cache() via getMemoizedWorkspace for
per-request memoization. Workspace consumers (Sidebar, AppBridges,
HeaderBreadcrumbs, TaskBoard) now read portalUrl from the store via
selectAuthDetails instead of taking it as a prop.

Add tsc script (tsc --noEmit) to package.json for type-only checks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@linear-code
Copy link
Copy Markdown

linear-code Bot commented May 8, 2026

OUT-3696

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 8, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
tasks-app Ready Ready Preview, Comment May 8, 2026 9:03am

Request Review

@priosshrsth priosshrsth force-pushed the anit/out-3696-add-caching-for-clients-requests branch from 949d9a0 to bbee878 Compare May 8, 2026 07:27
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 8, 2026

Deployment failed with the following error:

Deploying Serverless Functions to multiple regions is restricted to the Pro and Enterprise plans.

Learn More: https://vercel.link/multiple-function-regions

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 8, 2026

Greptile Summary

This PR introduces server-side caching for getAllClients using Next.js Cache Components ('use cache'), gated behind a hardcoded allow-list of workspace IDs, and removes the force-no-store/noStore() directives that previously prevented all caching. The performance improvement on allow-listed workspaces is substantial (~18s → ~2s on /users).

  • Cache fast-path in _getClients: allow-listed workspaces bypass the normal SDK pagination and serve from a tagged, hourly-refreshed 'use cache' function; companyId-filtered calls fall through to the original path.
  • Global config change: cacheMaxMemorySize raised to 500 MB, cacheComponents enabled, and staleTimes/webpack overrides removed; force-no-store directives stripped from six page/fetcher files.
  • get-all-clients.ts: new file handles paginated fetch with cacheLife('hours') and tag-based invalidation; the error handler calls updateTag but that API is restricted to Server Actions — Route Handlers must use revalidateTag instead.

Confidence Score: 3/5

Two defects in the new caching layer need to be fixed before merging: the limit argument is silently dropped for allow-listed workspaces, and the error-path cache invalidation uses the wrong API for its execution context.

The _getClients cache fast-path discards args.limit, so every caller that passes an explicit limit (several services across the codebase) will receive the full client list instead of the bounded set it expects. Separately, updateTag is only valid inside a Server Action; getAllClients is called from Route Handlers, so on a fetch error the intended cache invalidation silently fails or throws.

src/utils/get-all-clients.ts and src/utils/CopilotAPI.ts both need fixes before this is safe to roll out.

Important Files Changed

Filename Overview
src/utils/get-all-clients.ts New file implementing the 'use cache' cached client fetcher; contains two bugs — updateTag is used where revalidateTag is required (wrong API for Route Handler context), and import needs to be updated accordingly.
src/utils/CopilotAPI.ts Cache fast-path added to _getClients; silently drops args.limit for allow-listed workspaces, and hardcodes a single workspace ID in source.
src/app/api/users/users.controller.ts Removes noStore() from both user endpoints; the first call is left as a dead comment rather than fully removed.
next.config.js Enables cacheComponents, bumps cacheMaxMemorySize to 500 MB, and removes staleTimes/webpack overrides; webpack-only SVG handling is gone but turbopack rule remains.
src/app/manage-templates/page.tsx Removes fetchCache = 'force-no-store' and cleans up unused AppMargin/SizeofAppMargin imports.

Sequence Diagram

sequenceDiagram
    participant RH as Route Handler
    participant CA as CopilotAPI#_getClients
    participant GAC as getAllClients
    participant NC as Next.js Cache
    participant API as /v1/clients

    RH->>CA: getClients(listArgs with limit)
    alt workspace in CACHED_CLIENT_WORKSPACES and no companyId
        CA->>GAC: getAllClients(workspaceId)
        GAC->>NC: fetchAllClientsCached(workspaceId)
        alt cache HIT
            NC-->>GAC: cached ClientResponse[]
        else cache MISS
            NC->>API: GET paginated
            API-->>NC: ClientResponse[]
        end
        GAC-->>CA: full list, limit ignored
        CA-->>RH: all clients returned
    else normal path
        CA->>API: listClients with limit applied
        API-->>CA: bounded ClientsResponse
        CA-->>RH: bounded result
    end
Loading

Reviews (1): Last reviewed commit: "feat(OUT-3696): cache getClients via 'us..." | Re-trigger Greptile

Comment thread src/utils/CopilotAPI.ts
Comment on lines +146 to +152
if (!args.companyId) {
const payload = await this._getTokenPayload()
if (payload && CACHED_CLIENT_WORKSPACES.has(payload.workspaceId)) {
let cached = await getAllClients(payload.workspaceId)
return ClientsResponseSchema.parse({ data: cached })
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 args.limit is silently ignored for allow-listed workspaces

The cache fast-path returns the full unfiltered client list (cached) and wraps it directly in ClientsResponseSchema.parse({ data: cached }), completely discarding args.limit. Any caller that passes a limit — e.g. getClients({ limit: MAX_FETCH_ASSIGNEE_COUNT }) in users.service.ts, subtasks.service.ts, activity-log.service.ts, and others — will silently receive the entire workspace client list instead of the bounded result set it expects, which can break pagination, downstream processing, and memory assumptions in those callers.

Copy link
Copy Markdown
Collaborator Author

@priosshrsth priosshrsth May 8, 2026

Choose a reason for hiding this comment

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

This is not an issue. The api is intended to fetch all user it seems. The limit usage is just confusing.

Comment thread src/utils/get-all-clients.ts Outdated
@@ -0,0 +1,50 @@
import { cacheLife, cacheTag, updateTag } from 'next/cache'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 updateTag is documented as exclusively callable from a Server Action. getAllClients is invoked from CopilotAPI._getClients, which runs inside Route Handlers (/api/users). Calling updateTag outside a Server Action will either throw or silently no-op, meaning the cache is never invalidated on error. The correct invalidation primitive for Route Handlers is revalidateTag.

Suggested change
import { cacheLife, cacheTag, updateTag } from 'next/cache'
import { cacheLife, cacheTag, revalidateTag } from 'next/cache'

Comment thread src/utils/get-all-clients.ts Outdated
Comment on lines +46 to +49
} catch (err) {
updateTag(tagFor(workspaceId))
throw err
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Replace the updateTag call in the error handler with revalidateTag, which is the correct API for cache invalidation outside Server Actions.

Suggested change
} catch (err) {
updateTag(tagFor(workspaceId))
throw err
}
} catch (err) {
revalidateTag(tagFor(workspaceId))
throw err
}

Comment on lines 5 to 7
export const getUsers = async (req: NextRequest) => {
noStore()
// noStore()
const user = await authenticate(req)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 The noStore() call was left as a comment rather than removed. This dead code should be cleaned up to avoid confusion about the intent — it reads as if the opt-out is still active.

Suggested change
export const getUsers = async (req: NextRequest) => {
noStore()
// noStore()
const user = await authenticate(req)
export const getUsers = async (req: NextRequest) => {
const user = await authenticate(req)

Comment thread src/utils/CopilotAPI.ts Outdated

// Workspaces that route _getClients through the durable Next.js cache.
// All other workspaces continue to use the SDK pagination path.
const CACHED_CLIENT_WORKSPACES = new Set<string>(['us-west-2_lg5zB-Utp'])
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 The workspace ID is hardcoded in source, making it harder to onboard additional workspaces without a code change and deployment. An environment variable such as CACHED_CLIENT_WORKSPACES (comma-separated) would let the allow-list be updated at runtime.

Suggested change
const CACHED_CLIENT_WORKSPACES = new Set<string>(['us-west-2_lg5zB-Utp'])
const CACHED_CLIENT_WORKSPACES = new Set<string>(
(process.env.CACHED_CLIENT_WORKSPACES ?? 'us-west-2_lg5zB-Utp').split(',').filter(Boolean),
)

…kspaces

- Cache getAllClients with 'use cache' directive + per-workspace cacheTag
- Enable cacheComponents and bump cacheMaxMemorySize to 500MB
- Remove force-no-store / noStore() directives across app
- Restrict caching to CACHED_CLIENT_WORKSPACES allow-list
…it/out-3696-add-caching-for-clients-requests

# Conflicts:
#	src/app/layout.tsx
@priosshrsth priosshrsth changed the base branch from workspace-fetch-optimization to feature/c1-optimization May 8, 2026 08:11
- apply args.limit post-fetch on cached client list
- env-configurable CACHED_CLIENT_WORKSPACES allow-list
- drop broken updateTag/revalidateTag (both Server-Action-only in Next 16); cacheLife handles freshness
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant