Use UserCache.Instance to get the singleton instance.
StaticChangeHow long a burst of user changes is collected before one reload runs.
StaticMaxUpper bound on remembered misses, so a probing attacker cannot grow the map without limit.
StaticMaxHow many TransactionWaitMs windows a pending reload waits for an open transaction before giving up. At the defaults that is 15 s; a unit of work still open after that is stuck, and the periodic staleness check will catch whatever it eventually commits.
StaticMissHow long FindUser remembers that a lookup found nothing, before it asks again.
StaticPeerUpper bound of the random extra delay before reloading after another server's notice, so a fleet does not query the database in lockstep.
StaticTransactionHow long to wait before re-checking whether the writing transaction has settled.
StaticInstanceStaticUsersStops the timers and event subscriptions this cache owns. For tests and clean shutdown.
Finds a user by ID or email. Prefer this over Users.find(...) on any path that decides
whether a request is allowed. A plain find renders "this process has not loaded that user"
as "that user does not exist", which is how a newly created account is rejected as invalid
until the server restarts.
A miss here is checked against the database (one indexed row read) and, when the user is really there, added to the cache so later lookups are free. A user that the database does not have either is remembered as absent for MissRetryIntervalMs, so a bad credential cannot turn into a query per request.
The user, or undefined when neither the cache nor the database has it.
The Global Object Store is a place to store global objects that need to be shared across the application. Depending on the execution environment, this could be the window object in a browser, or the global object in a node environment, or something else in other contexts. The key here is that in some cases static variables are not truly shared because it is possible that a given class might have copies of its code in multiple paths in a deployed application. This approach ensures that no matter how many code copies might exist, there is only one instance of the object in question by using the Global Object Store.
Returns the system user, or undefined when the cache has not been refreshed or the row is
absent — every caller already guards for that, and with _users defaulting to [] this can
no longer throw. The declared return type stays UserInfo because widening it to
UserInfo | undefined is a read-surface change, and Phase 1 of the provider refactor moves
this class without touching its read surface. The new home compiles under strict, hence the
explicit assertion where the old one silently inferred the same lie.
ProtectedLoadReads the users and user-roles base views through the provider and stitches each user's roles onto it.
Why not RunView? DatabaseProviderBase is an IRunViewProvider, so this could go
through the standard read path and inherit its shared-cache invalidation. It deliberately does
not, for three reasons:
RunView requires a contextUser and throws without one
(CheckUserReadPermissions). This cache is what produces the system user MJAPI then passes
to everything else, so at first load there is no UserInfo to read as.RunView applies the reading user's entity permission, row-level
security and field-level projection (buildFieldSecuritySelectList). This cache is
process-global and answers "who is this?" for every request, so a set scoped to whichever
user happened to bootstrap — or missing Email because a deployment denied that field to
their role — would silently break identity for everyone else.MJ: Users is cacheable, so a
RunView load would publish every user row (names, emails, employee links) into Redis,
where today nothing about users is stored.The freshness that RunView would have brought is provided instead by the subscriptions on
this class — see the class comment.
OptionalwhereClause: stringoptional predicate on vwUsers (already quoted and escaped), for
FindUser's single-row read.
This method will refresh the cache with the latest data from the database, and subscribes this process to the changes that make it stale (see the class comment).
the configured database provider to read through. Works on any dialect — the SQL is built with the provider's own quoting and core-schema name.
OptionalautoRefreshIntervalMS: numberoptional; when set, the whole cache is also reloaded on this
interval. The subscriptions make that unnecessary for most hosts;
StartStalenessChecks is the cheaper periodic safety net. Omit it to
keep whatever schedule an earlier call set — Refresh is also called from
paths that know nothing about scheduling, and those must not end it. Pass 0
to turn the schedule off.
Compares the database with what this cache was built from — two counts and the newest
__mj_UpdatedAt — and reloads only when they differ. Two small aggregate queries, so it is
cheap enough to run on an interval. A deleted role row changes a count; a deactivation, or any
other edit, moves __mj_UpdatedAt.
It runs in two situations, and is skipped otherwise:
MJ: Users or MJ: User Roles declares TrustServerCacheCompletely = false — rows can
change without an event (raw SQL, another application, a restore), and this is the only thing
that can find them.A single process with a private cache has no peer whose notice it can miss, so it issues no query — which keeps a serverless database free to pause.
true when the cache was reloaded.
Reloads the cache from the provider it was last refreshed through. Concurrent calls share one read. Does nothing when this process never had a provider (a host that only calls SetUsers).
Replace the cached set with users the caller already materialized.
The seam exists for callers that cannot take Refresh's path — the PostgreSQL
bootstrap in testing-cli builds its UserInfo[] before there is a DatabaseProviderBase
to read through, so it holds users but has nothing to hand Refresh.
Without this, that caller reached in and assigned the private field by string index:
(UserCache.Instance as unknown as Record<string, unknown>)['_users'] = userInfos;
which type-checks forever and fails silently: rename _users and the cache is simply never
populated — no compile error, no exception, and the symptom ("no users") surfaces arbitrarily
far from the cause. A public method makes that rename a build error instead.
Deliberately does NOT start the auto-refresh timer: the caller supplied this data, so there is no provider to re-read it through. Use Refresh when there is one.
Runs RefreshIfChangedInDatabase on an interval. intervalMs <= 0 stops it.
Convenience method to get a user by their name
name of the user
OptionalcaseSensitive: booleanoptional, if true, the search will be case sensitive
Protected StaticgetReturns the singleton instance of the class. If the instance does not exist, it is created and stored in the Global Object Store. If className is provided it will be used as part of the key in the Global Object Store, otherwise the actual class name will be used. NOTE: the class name used by default is the lowest level of the object hierarchy, so if you have a class that extends another class, the lowest level class name will be used.
OptionalclassName: string
Server-side cache of users and their roles, shared by every consumer in the process.
Dialect-neutral: UserCache.Refresh takes a DatabaseProviderBase and reads through
ExecuteSQL/QuoteSchemaAndView, so SQL Server and PostgreSQL share one implementation.It keeps itself current by four independent routes, because each can be bypassed by the next:
MJ: Users/MJ: User Rolessave and delete events, so the process making the change does not have to remember to.mj sync push,mj codegen,mj migrate) has the same effect.Extends
BaseSingletonso bundler duplication cannot produce a second instance. The class NAME is load-bearing:BaseSingletonkeys its global store on the constructor name, so renaming it hands every existing holder a second, empty instance.