Member Junction
    Preparing search index...

    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:

    1. Local writes — refreshes on MJ: Users / MJ: User Roles save and delete events, so the process making the change does not have to remember to.
    2. Other processes — a refresh for a local write stamps USER_CACHE_STAMP_KEY in the shared store, and peers refresh on that notice. Clearing the shared cache (mj sync push, mj codegen, mj migrate) has the same effect.
    3. Writers that bypass both (raw SQL, another application) — RefreshIfChangedInDatabase compares two counts and a timestamp against the database and reloads only on a difference. MJAPI runs it on the engine-sweep interval.
    4. A miss is not a negative fact — FindUser falls back to an authoritative single-row read, so a user this process has not heard about is looked up rather than denied.

    Extends BaseSingleton so bundler duplication cannot produce a second instance. The class NAME is load-bearing: BaseSingleton keys its global store on the constructor name, so renaming it hands every existing holder a second, empty instance.

    Hierarchy (View Summary)

    Index

    Constructors

    Properties

    ChangeDebounceMs: number

    How long a burst of user changes is collected before one reload runs.

    MaxRememberedMisses: number

    Upper bound on remembered misses, so a probing attacker cannot grow the map without limit.

    MaxTransactionWaits: number

    How 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.

    MissRetryIntervalMs: number

    How long FindUser remembers that a lookup found nothing, before it asks again.

    PeerNoticeJitterMs: number

    Upper bound of the random extra delay before reloading after another server's notice, so a fleet does not query the database in lockstep.

    TransactionWaitMs: number

    How long to wait before re-checking whether the writing transaction has settled.

    Accessors

    • get GlobalKey(): string

      Returns string

    • get SYSTEM_USER_ID(): string

      Returns string

    • get Users(): UserInfo[]

      Returns UserInfo[]

    • get Instance(): UserCache

      Returns UserCache

    • get Users(): UserInfo[]

      Returns UserInfo[]

    Methods

    • Stops the timers and event subscriptions this cache owns. For tests and clean shutdown.

      Returns void

    • 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.

      Parameters

      • criteria: { Email?: string; ID?: string }

      Returns Promise<UserInfo>

      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 GlobalObjectStore

    • 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.

      Returns UserInfo

    • Reads 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:

      1. Bootstrap. Server-side 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.
      2. It must be unfiltered. 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.
      3. It would put the user table in the shared cache. 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.

      Parameters

      • provider: DatabaseProviderBase
      • OptionalwhereClause: string

        optional predicate on vwUsers (already quoted and escaped), for FindUser's single-row read.

      Returns Promise<UserInfo[]>

    • 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).

      Parameters

      • provider: DatabaseProviderBase

        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: number

        optional; 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.

      Returns Promise<void>

    • 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.
      • This process shares its cache with other processes. Then a change made on a peer reaches this one only as a pub/sub notice, and pub/sub delivers at most once with no replay after a reconnect. A lost notice is harmless for a GRANT — a new user or role is a cache miss, and a miss falls back to an authoritative read in FindUser. It is not harmless for a REVOCATION: a removed role or a deactivated user is a cache HIT that still carries the old state, and nothing re-reads it. Without this check a dropped notice would leave a revoked user authorised on that peer until the next user write or a restart. Revocation is a security control, so the cost — one cheap query per process per interval — is paid wherever peers exist.

      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.

      Returns Promise<boolean>

      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).

      Returns Promise<void>

    • 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.

      Parameters

      Returns void

    • Runs RefreshIfChangedInDatabase on an interval. intervalMs <= 0 stops it.

      Parameters

      • intervalMs: number

      Returns void

    • Returns void

    • Convenience method to get a user by their name

      Parameters

      • name: string

        name of the user

      • OptionalcaseSensitive: boolean

        optional, if true, the search will be case sensitive

      Returns UserInfo

    • Returns 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.

      Type Parameters

      Parameters

      • this: new () => T
      • OptionalclassName: string

      Returns T