Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions packages/context/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,39 @@ import { AsyncLocalStorage } from 'async_hooks'

import type { GlobalContext } from './context.js'

let CONTEXT_STORAGE: AsyncLocalStorage<Map<string, GlobalContext>>
// The singleton lives on `globalThis` (keyed by a registered Symbol) so the
// ESM and CJS variants of this package share the same `AsyncLocalStorage`
// instance.
//
// `@cedarjs/context` ships dual ESM+CJS, and Node resolves the variant per
// caller — ESM consumers (e.g. an ESM user-api package) load `dist/index.js`,
// CJS consumers (e.g. cedar's CLI loading `@cedarjs/api-server`'s CJS entry)
// load `dist/cjs/index.js`. Each variant is a separate module instance with
// its own module-scoped state. If we kept the `AsyncLocalStorage` in a
// module-scoped `let`, each variant would have its own store, and the
// `currentUser` written into one would be invisible to the other — a
// classic dual-package hazard. Stashing it on `globalThis` under a
// `Symbol.for` key bridges them: both variants resolve the same key to the
// same object, so they share the store and `context.currentUser` propagates
// between framework code and user code regardless of which variant each
// loaded.
const STORAGE_KEY = Symbol.for('__cedarjs_context_storage__')

type ContextStorageGlobal = typeof globalThis & {
[STORAGE_KEY]?: AsyncLocalStorage<Map<string, GlobalContext>>
}

/**
* This returns a AsyncLocalStorage instance, not the actual store.
* Should not be used by Redwood apps directly. The framework handles
* this.
*/
export const getAsyncStoreInstance = () => {
if (!CONTEXT_STORAGE) {
CONTEXT_STORAGE = new AsyncLocalStorage<Map<string, GlobalContext>>()
const g = globalThis as ContextStorageGlobal

if (!g[STORAGE_KEY]) {
g[STORAGE_KEY] = new AsyncLocalStorage<Map<string, GlobalContext>>()
}
return CONTEXT_STORAGE

return g[STORAGE_KEY]
}
Comment on lines +39 to 40
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 The return expression g[STORAGE_KEY] is typed as AsyncLocalStorage<Map<string, GlobalContext>> | undefined because the ContextStorageGlobal type marks the property optional with ?:. With strict: true (and thus strictNullChecks) in the repo's base tsconfig, TypeScript's control flow analysis does not reliably narrow computed symbol-indexed properties on mutable objects like globalThis across an if-assignment boundary — so the inferred return type of getAsyncStoreInstance likely includes undefined. Every caller in context.ts chains .getStore() directly onto the return value without a null check, which would be a compile-time error under strict null checks. A non-null assertion on the return makes the invariant explicit and preserves the original AsyncLocalStorage (never-undefined) return type.

Suggested change
return g[STORAGE_KEY]
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return g[STORAGE_KEY]!
}

Loading