Skip to content

Store

Server only. Requiring this from a client fails at the require, naming the module.

Twill.Store is what Data is built on. Reach for it directly only for keys that are not player data: a guild, a shared world, a leaderboard snapshot.

local Store = require("@game/ServerScriptService/Twill/Store")
local guilds = Store.New("Guilds", { Members = {}, Level = 1 })
local keep = guilds:StartSessionAsync("guild-4812")
keep.Data.Level += 1

A key records the server holding it and a load count. A write is accepted only when both still match the handle making it.

The count is what protects a key from a server that stalled. That server still carries the right mark, so the mark alone would let it write over whoever took the key while it was gone. The count moved when the key changed hands, so the write is refused and the handle is told it lost the key.

A handle that lost the key fires OnSessionEnd and answers false to every later Save.

StartSessionAsync does not fail when another server holds the key. It waits, and takes the key if the wait runs out.

StageWhat happens
First attemptRegisters a claim on the key and asks the holder to release it.
While waitingRe-reads the key every LoadRepeat seconds.
After StealAfterTakes the key, if the claim registered first is still standing.
Any timeTakes the key at once if the holder has not written for AssumeDead.
After StartTimeoutGives up and answers nil.

A claim that another server replaced does not take the key. That server waited too, and taking it would mean two servers reading the same wait as their turn.

Steal = true skips all of it and takes the key immediately. It exists for recovery, not for ordinary use, and it can cost the holder unsaved changes.

A server that takes its own key again, while a handle here still held changes that were never written, starts the new handle from a copy of those changes. The older handle ends. A player who leaves during storage trouble and returns to the same server therefore finds what they had.

Storage does not say whether a failed write landed. Every write here is safe to make again, so a failed one is tried again up to Attempts times, waiting longer each time.

What failsWhat the store does
An ordinary saveAnswers false. The data stays held and goes with the next save.
The last write of EndSessionAnswers false, keeps the key held, and tries again every LoadRepeat seconds until it lands or another server takes the key.
A write whose outcome is unknownWrites again. A change sent with MessageAsync is recognised and not added twice.
The caller is stopped while waitingThe call still finishes and frees the key.

A value storage cannot keep, such as a number that is not finite, text that is not valid UTF-8, or an Instance, is found before storage is asked. The write is refused the way a Validate refusal is, and the log names the field.

A handle’s Size holds the length of what its last write stored. The store warns once per handle when that passes 3,000,000 of the 4,194,304 characters a key may hold.

MessageAsync writes a change into the key itself rather than sending it to a server. It lands whether or not any server is holding the key, and the server that next holds it receives it.

The holder receives mail through MessageHandler, and only on a save. Call done inside the handler to mark a letter dealt with, or it arrives again on the next save.

Reading a key that holds a value this store did not write, one written in a newer envelope format than this Twill knows, or one whose data cannot be unpacked, is refused. The key is left exactly as it was, Store.OnOverwrite fires, and StartSessionAsync answers nil without retrying.

[Server]

Opens a store, with what a key holds before anything is written to it.

function Store.New(name: string, template: { [string]: any }?, config: Config?): Store

Parameters

NameTypeDescription
namestringThe DataStore name.
template{ [string]: any }?What a key that was never written starts out holding. Copied, never held.
configConfig?What to change about how this store behaves.

Returns

Store - The store. Store.Mock on it is the same store backed by memory that is forgotten when the server stops.

Throws when the name is empty or is not text, and on a config that could not work: a field of the wrong type, a number that is not above zero, Attempts that is not whole, or AssumeDead no longer than AutoSave.

Every field is optional and falls back to the default.

FieldDefaultMeaning
AutoSave180Seconds between saves of a held key.
LoadRepeat10Seconds between re-reads while waiting for a key.
FirstRepeat5Seconds before the second read.
StealAfter40Seconds of waiting before a standing claim takes the key.
AssumeDead630Seconds of silence after which a holder is assumed gone.
StartTimeout120Seconds before StartSessionAsync gives up.
MaxMail1000Letters a key holds before the oldest are dropped.
Attempts4Tries per storage call before it is reported failed.
CompressfalseWhether the data travels packed.
MessagingtrueWhether a holder listens for a request to release.
ValidatenoneA check (data) -> (boolean, string?) every write must pass.

Packing multiplies what fits under the key size ceiling and makes the stored value unreadable in the DataStore browser and in Open Cloud.

[Server]

Runs the store’s Validate check on some data, without writing anything.

function Store:Check(data: { [string]: any }): (boolean, string?)

Returns

boolean - true when the data may be written, and always when the store has no check.

string? - Why it may not, when it may not.

A check that raises is read as a refusal.

[Server]

Takes a key, waiting out or taking over whoever holds it.

function Store:StartSessionAsync(key: string, params: Params?): Keep?

Parameters

NameTypeDescription
keystringThe key to take. At most 50 characters.
params.Cancel(() -> boolean)?Asked before each attempt. Stops waiting when it answers true.
params.Stealboolean?Takes the key at once, whoever holds it.

Returns

Keep? - The handle to work the key through, or nil when it could not be taken. Yields.

Answers nil at once, with an error, when the data cannot be packed for writing.

Throws when the key is empty, too long, or is not text.

[Server]

Reads a key without taking it.

function Store:GetAsync(key: string): Keep?

Returns

Keep? - A handle over what the key holds, or nil when there was nothing to read. Yields.

Nothing read this way saves on its own. Save refuses on it; SetAsync writes it back and gives up whatever session was on the key.

[Server]

Walks back through what a key held before, newest first.

function Store:VersionQuery(key: string, direction: Enum.SortDirection?, from: DateTime?, until_: DateTime?): Query

Parameters

NameTypeDescription
keystringThe key to walk back through.
directionEnum.SortDirection?Descending when left out.
fromDateTime?The earliest version to include.
until_DateTime?The latest version to include.

Returns

Query - A walk. Query:NextAsync answers one version at a time as a read-only Keep, then nil. Yields.

A stand-in store keeps no versions, so a walk on one answers nil at once.

[Server]

Writes a change into a key, to be applied wherever the server holding it is.

function Store:MessageAsync(key: string, body: { [string]: any }): boolean

Returns

boolean - Whether the change was left waiting on the key. Yields.

Answers false when the key holds nothing, and when the server is closing.

Costs two storage requests and one message. Sending one per player action will exhaust the request budget.

[Server]

Forgets a key. There is no way back from this.

function Store:RemoveAsync(key: string): boolean

Returns

boolean - Whether it was forgotten. Yields.

Ends any handle this server holds on the key first, so nothing writes it back.

[Server]

Reports how far storage can be reached from this server.

function Store.Reach(): "NotReady" | "NoInternet" | "NoAccess" | "Access"

Only Studio has to work this out. A live server answers Access.

[Server]

Waits until how far storage can be reached is known.

function Store.AwaitReach(): "NotReady" | "NoInternet" | "NoAccess" | "Access"

Returns

The same answers as Store.Reach. NotReady only when the probe gave no answer within 20 seconds. Yields.

[Server]

Reports whether storage has been failing often enough to stop leaning on it.

function Store.IsStrained(): boolean

True once five calls have failed within two minutes, and for two minutes after the last of them.

[Server]

Reports whether this server is on its way down and taking no new keys.

function Store.IsClosing(): boolean
SignalFires withWhen
Store.OnTroublemessage, store, keyA storage call failed.
Store.OnStrainedstrainedRepeated failure started or stopped.
Store.OnOverwritestore, keyA key held something this store cannot read.

The handle a taken key is worked through.

FieldTypeMeaning
Data{ [string]: any }The live table. Write to it directly.
LastSaved{ [string]: any }A copy of what last reached storage.
KeystringThe key this handle holds.
Users{ number }Users associated with the key.
Marks{ [string]: any }Key metadata, at most 300 characters in total.
CreatednumberWhen the key was first written.
UpdatednumberWhen the key was last written, or 0.
LoadsnumberThe load count this handle took the key at.
Holder{ Place, Job, Id }?Who held the key when this handle was made.
FreshbooleanWhether the key held nothing before this handle took it.
SignalFires withWhen
OnSavenothingBefore every write.
OnLastSave"Manual", "External" or "Shutdown"Before the write that gives the key up.
OnAfterSaveLastSavedAfter a write lands.
OnSessionEndnothingOnce the key is no longer this handle’s.

Reports whether writing to this key still reaches storage.

function Keep:IsActive(): boolean

The answer is only good until the next yield.

Saves now, rather than waiting for when the key would be saved anyway.

function Keep:Save(): boolean

Answers false when the handle is no longer active, when the write was refused, and when Validate refused the data. Yields. Throws on a key that was only read.

A save asked for while another is still waiting to read the key joins it, so a burst of saves costs one write and every caller gets that write’s answer.

Saves one last time and gives the key up, so the next server does not wait.

function Keep:EndSession(): boolean

A second call answers false rather than writing again. When Validate refuses the data, the key is still given up, with the data it held before, and the call answers false. When storage fails the write, the call answers false, the key stays held, and the store tries again on its own. Yields. Throws on a key that was only read.

Writes back a key that was only read, giving up whatever session was on it.

function Keep:SetAsync(): boolean

Answers false when Validate refuses the data. Yields. Throws on a key that was taken rather than read.

Fills in whatever the template gained since the key was last written.

function Keep:Reconcile()

Existing values are left alone.

Associates a user with the key, which a request to be forgotten matches on.

function Keep:AddUserId(userId: number)

Takes that association away again.

function Keep:RemoveUserId(userId: number)

Sets what deals with changes sent to the key from somewhere else.

function Keep:MessageHandler(handler: (body: { [string]: any }, done: () -> ()) -> ())

Letters waiting on the key are handed over as soon as the handler is set, and again after every save. Call done to mark one dealt with.

LimitValueSet by
Key length50 charactersRoblox
Key size4 MBRoblox
Metadata size300 charactersRoblox
Letters per key1000MaxMail
Tries per call4Attempts
Tries per call while closing, including calls already retrying2Fixed
Wait for writes while closing25 secondsFixed