DataStoreCmdrService
Cmdr commands for inspecting and stress-testing player datastores, for customer service and debugging.
Targets come in as playerIds, so the same command reaches a player in this server (., *, a
name) and one who is not (a name Cmdr resolves through GetUserIdFromNameAsync, or #userId for
an account whose name is unknown or since changed).
There are two families here, and they reach the key differently.
The lock commands (datastore-lock-info, datastore-lock, datastore-unlock) write the key
directly without opening a session, so they act on a lock left behind by a server that died --
the usual reason to reach for them.
The data commands (datastore-read-json, datastore-write-json, datastore-delete,
datastore-copy) go through a real DataStore, which means they steal the session from
whichever server holds it, including this one. That is deliberate: these exist to stress-test the
session-locking system, and a read that cannot be starved of a current write is exactly what is
being tested. A player whose session is stolen mid-play is disrupted -- they are kicked when their
server notices -- so treat these as debug tooling rather than customer-service tooling.
WARNING
The session lock is soft. A loading session steals it once its retry ladder is exhausted, so
datastore-lock parks a key for that long and no longer. Unlocking is the durable half.
INFO
TODO: give the read paths a read-only DataStore so they stop stealing. The load path already
uses a plain GetAsync and takes no lock, so the store itself is a thin flag -- but a read that
is guaranteed to see the current write needs MessagingService to ask the holding session to
flush first, plus the edge cases around a session that never answers. Deferred rather than
half-built.
Functions
Init
Initializes the service. Should be done via ServiceBag.Init.
SetReplyConfig
DataStoreCmdrService.SetReplyConfig() → ()Sets how long a command may run before it tells the executor it is still working, and how that line is colored.
Start
Registers the commands. Should be done via ServiceBag.Start.
_promiseWithDataStore
DataStoreCmdrService._promiseWithDataStore(userId: number,doesWrite: boolean,--
whether to flush before handing a live store back
) → Promise<T>Opens the store for userId, runs handler against it, and puts it back.
PlayerDataStoreManager.PromiseDataStore opens a real session, which takes the lock from whoever
holds it -- kicking that player when their server notices the theft. Releasing what was taken is
therefore the part that matters: an orphaned lock is exactly what datastore-unlock exists to
clear, and until it is dropped the player cannot rejoin.
So a store opened for an absent player is closed again, and the promise waits for that flush to land rather than reporting success while the lock is still held. A store belonging to a player in this server is left alone instead: no session was stolen, and removing it would strand a live one.
_executeForUserIds
DataStoreCmdrService._executeForUserIds(context: CommandContext,userIds: {number},) → stringRuns handler once per target userId and renders the results back into the console.
Runs sequentially, to keep a batch from firing concurrent writes at the datastore. A target that fails reports on its own line rather than throwing into Cmdr or costing the rest of the batch its result.
Nothing reaches the console until every target is done, and a single target can sit in a load
retry ladder for a long time, so a target still going after the reply config's
slowReplySeconds says so.