Skip to content

Releases: ericplane/Scribe

Scribe v2.0.0 - The Integrity Update

Choose a tag to compare

@ericplane ericplane released this 24 Aug 21:49

This release closes a long list of defects in the money, persistence and replication paths, and adds
derived fields, idempotent commands, narrowed float replication and a schema check for stored data.
It also changes a number of behaviours that existing games depend on, including the replication wire
format, so read the behaviour changes below before upgrading. A game that uses neither monetization
nor offline writes will find most of its risk in the wire format change and the template compile
rules.

Behaviour changes

  • The replication protocol version moved from 1 to 6, so a server and a client built from different
    Scribe versions now refuse each other and log PROTOCOL_MISMATCH instead of mis-decoding frames.
    Deploy the server and the client together, because a mixed deploy leaves players unable to load.

  • Scribe now ships its own patched copy of ProfileStore inside the package, so the ProfileStore
    Wally dependency is no longer required. Remove it from your wally.toml when you upgrade.

  • Game code can no longer write anywhere inside Scribe's reserved _Scribe root, and every mutator
    on such a path now raises an error naming the path and the API that owns that state. Reading a
    table inside that root also hands back a detached copy rather than the live stored table.

  • A transaction can no longer touch a second player's data. Opening a transaction on another player,
    or writing to one from inside an open transaction, now raises and rolls the transaction back, and
    the error points at the durable outbox as the way to move value between players.

  • A cross-server message is no longer acknowledged when nothing is connected to Data.OnMessage or
    when a handler raises. It stays on the key and is offered again on the player's next load, so a
    handler must now tolerate seeing the same message twice.

  • Data.RestoreVersion no longer rolls the reserved _Scribe root back with the game data. Granted
    receipts, paid gifts, perks, the purchase log and running cooldowns are carried across from the
    live profile, and the new RollBackReserved option restores the old behaviour when that root is
    itself what needs repairing.

  • A migration step that changes the reserved _Scribe root now has that change discarded and the
    stored root kept, reported as MIGRATION_RESERVED_DISCARDED. A migration that rebuilt the profile
    from its own key list used to destroy receipt idempotency and paid gifts in silence.

  • Data.SendMessage now returns false and logs MESSAGE_QUEUE_FULL when the recipient's offline
    inbox is at its cap. It used to report success after throwing a message away.

  • A template that declares a non-finite Min or Max on Scribe.Number, or a MaxLength that is
    not a non-negative integer, now fails to compile. This fails at startup rather than in production,
    and a negative MaxLength previously deleted the end of every value it was applied to.

  • Data.UpdateOffline now commits as a compare-and-set, so the session check and the write are a
    single DataStore call. It gained one refusal reason, that the profile changed while the update was
    being prepared, and a refusal now writes nothing at all.

  • Data.WaitForData can now answer still-loading where it used to answer timeout. A load that
    is merely slow is worth retrying, so code branching on timeout should handle both.

  • Cooldown and claim keys passed to the public timed API are now refused if they contain invalid
    UTF-8 or begin with @, which is reserved for Scribe's own idempotency claims. Rename any key of
    yours that starts with that character.

  • A write that would leave a container holding both array indices and string keys is now refused,
    whether it arrives as a keyed write or as an Insert. That shape loses half its contents on save.

  • A product grant that yields and then fails part way is now settled as granted, logged as
    GRANT_PARTIAL and counted in ReceiptsPartial. It used to be retried, which compounded the
    writes it had already made.

  • Two pass names sharing one gamepass Id now fail to boot, matching the refusal products have
    always had for a duplicate Id. Give each pass its own gamepass or register it once, because an
    in-experience purchase reports only the Id and used to credit whichever of the two names Scribe
    registered last.

  • tostring on a Scribe.Big now keeps the fractional part instead of rounding to a whole number,
    so a third of ten prints as 3.33333333333333. The numbers inside bounds error messages change
    with it.

  • Dividing a Scribe.Big by zero now raises instead of returning nil.

  • A Set that writes the value a field already holds no longer fires Changed or queues a
    replication op on a Scribe.Big, a flags field or a datatype field. Those three used to fire
    where the identical no-op on an integer cost nothing.

  • SchemaPolicy now defaults to Warn under DevMode and stays off on live servers, so a Studio
    session reports stored data that no longer matches the template. An explicit setting still wins in
    both directions.

  • Data.Request now returns Scribe.RequestFailed as a third value whenever the refusal is
    Scribe's rather than your handler's. Only a caller that forwards the results of Data.Request
    straight into another call needs to change.

  • In edit mode, meaning a storybook or the command bar, a bundle now builds the client half instead
    of the server half. Building the server half used to create the transport folder and RemoteEvents
    in ReplicatedStorage and leave the client stub raising.

Added

  • Scribe.Derived declares a field that Scribe computes from other declared fields instead of
    accepting writes. It is never persisted or migrated, it recomputes when an input changes, and
    every mutator is absent from its type and raises at runtime.

  • Client.RequestOnce sends a command tagged with a caller-supplied idempotency key, so the server
    runs the handler at most once per key and answers repeats with the original reply. Keys must be
    non-empty, valid UTF-8 and at most 64 bytes.

  • A command spec now accepts Idempotent = true, which makes the command require a key sent through
    RequestOnce. The requirement is symmetric, so a key sent to a plain command and a keyless call
    to an idempotent one are both refused.

  • PurchaseSpec gained an optional IdempotencyKey, and a repeat under the same key returns
    exactly what the first call returned and spends nothing. The new PurchaseClaimTTL and
    MaxPurchaseClaims options govern how long a claim is kept and how many may be live on one
    profile.

  • Data.Stop releases everything a bundle holds on the process, including the background loops, the
    Players and MarketplaceService listeners and the transport channel claim. A game never needs it,
    but a test suite or a simulation that builds many bundles does.

  • Scribe.Number gained a Precision option that narrows a replicated field to four, two or one
    bytes. The server keeps the full double it was given and only the client copy is quantized, so do
    not compare the two for equality.

  • Scribe.CFrame gained Precision = "exact", which packs every component bit for bit at 49 bytes
    instead of the default 13 or 29. Scribe.Datatypes.Pack takes the same value as an optional third
    argument.

  • A Scribe.Big value now supports Pow for a non-negative integer exponent and Log10. Both are
    reads that return a new value, and Pow refuses a fractional, negative or non-finite exponent.

  • The new SchemaPolicy option checks stored data against the template when a profile loads. Only a
    table mixing array indices with string keys ends the session under Reject, and under that
    setting a bounded Scribe.Big outside its bounds also refuses the load.

  • An outbound frame larger than the outbound budget is now split into fragments and reassembled by
    the client, where it previously could not be sent at all. A frame needing more than sixteen
    fragments logs OUTBOUND_OVERSIZE once per server.

  • A custom transport may now declare MaxFrameBytes, and Scribe keeps every frame under it. An
    adapter whose own framing inflates the buffer can carry its ceiling with it instead of having to
    be paired with a matching MaxOutboundBytes setting.

  • Scribe.GetPercentiles returns the P50, P90 and P99 of each recorded metric, which GetMetrics
    could not report. It is computed over the most recent 256 samples per name, so it does not agree
    with the all-time count.

  • Scribe.GetBudgetSnapshot reports the DataStore request allowance the engine currently gives, by
    request type. Its Available field is false when the engine could not be asked at all.

  • Scribe.AddLogSink now returns a function that removes the sink again, so a sink with a lifetime
    no longer stays registered for the life of the server.

  • Scribe.RequestReason, Scribe.PurchaseReason and Scribe.GiftReason name the fixed refusals of
    Data.Request, Data.Purchase and Data.PromptGift, each with a matching exported type.

  • The new ImportLegacyData option adopts data from another library once, before Scribe has ever
    saved for that player. The adopted profile then runs the full migration chain.

  • The new LoadTimeout option bounds how long a profile load is given, defaulting to 120 seconds
    with a floor of 60.

  • The new LogRingSize option sets how many recent entries GetRecentLogs retains, which used to
    be fixed at 512.

  • The new MaxOutboundBytes option caps the bytes in one outbound frame before fragmentation,
    defaulting to 65536 with a floor of 256.

  • The new MaxInboundRetainedBytes option caps how much memory one inbound client frame may cause
    the server to retain, defaulting to sixteen times MaxInboundBytes.

  • Th...

Read more

Scribe v1.3.2 - Security Hardening

Choose a tag to compare

@ericplane ericplane released this 09 Aug 15:35
  • A hardening pass on the client boundary: command arguments decode only after the rate limit and every other gate, a pre-Ready session answers not-ready for every name so commands can't be enumerated, and new MaxInboundFrameRate caps raw inbound frames per player
  • Receipt de-duplication evicts by age (PurchaseIdTTL, MaxProcessedPurchaseIds) rather than count, warning PURCHASE_ID_EVICTED when a still-retryable id is dropped
  • Command Args accept Scribe declarators and nested shapes
  • New Security guide

Full Changelog: v1.3.1...v1.3.2

Scribe v1.3.1 - Visibility Hotfix

Choose a tag to compare

@ericplane ericplane released this 07 Aug 08:22
  • Scribe.ServerOnly(Scribe.Session(v)) resolved to Session and replicated that field to its owner, leaking one wrapped as a secret. Saving and replication are now independent: pair Scribe.Session with Scribe.ServerOnly or Scribe.Shared in either order, while combining those two is a startup error.
  • Mode = "Mock" also never reached leaderboards, so Studio play-tests read and wrote real OrderedDataStores.
  • Upgrade note: a leaderboard Stat that can never rank (a non-numeric field, a whole container, or a Scribe.Session field) is now a boot error rather than a board that stays silently empty, so check your board configs. Plus a clearer PROFILE_UNPERSISTABLE for a raw datatype written around the accessor, a new Commands & Requests guide, and 77 documentation corrections.

Full Changelog: v1.3.0...v1.3.1

Scribe v1.3.0 - Big, SetOf, MapOf, Flags Declerators

Choose a tag to compare

@ericplane ericplane released this 04 Aug 07:33
  • Added Scribe.SetOf (unique membership)
  • Added Scribe.MapOf (declared key type, so integer keys survive the DataStore round trip)
  • Added Scribe.Flags (up to 32 named booleans in one field)
  • Added Scribe.Big (idle-game numbers past 2^53, rankable on a leaderboard, with a per-board SigFigs trading exponent range for displayed resolution)
  • Added Evict on ArrayOf for a self-trimming history
  • Added OnPlayerLeaving which runs before the final save, so a playtime tally written there persists
  • OnCooldown takes { IncludeOfflineTime = false } for a cooldown that only ticks while the player is online
  • Batch now delivers the "one Changed pass" it always documented: a container fires once per batch rather than once per write, on both realms, Insert/Remove/Clear included.
  • Upgrade notes: a container Changed takes (new, old) and errors at connect on a third key parameter, so move that logic to the new OnChildChanged(key, new, old). Leaf listeners are unchanged. A Scribe.Big board is server-only, and its entry.Score is a big rather than a number.

Full Changelog: v1.2.1...v1.3.0

Scribe v1.2.1 - .Get() & .Clone() Fixes

Choose a tag to compare

@ericplane ericplane released this 01 Aug 13:21
  • Root whole-data reads (Get, Clone, Observe, Changed) now return exactly your declared roots: no internal _Scribe, and Scribe.Session roots are finally included.
  • Get() is frozen, so a stray Get().Coins = 1 raises instead of writing silently.
  • Scribe.ServerOnly fields left the client type, so reading one there is a build error, not a nil.
  • Use Clone() to edit, Data.Export for internals.

Full Changelog: v1.2.0...v1.2.1

Scribe v1.2.0 - Leaderboard Improvements

Choose a tag to compare

@ericplane ericplane released this 31 Jul 16:39
  • Leaderboards gain a per-board RefreshInterval (default 60s, floored at 60) so a board can read less often
  • A server-side OnLeaderboard signal has been added that fires with (boardName, entries) when a board actually changes, including server-only boards, so you no longer need a polling loop

Full Changelog: v1.1.0...v1.2.0

Scribe v1.1.0 - OnCooldownEnded & Bug Fixes

Choose a tag to compare

@ericplane ericplane released this 29 Jul 20:18
  • New OnCooldownEnded signal and a Scribe.PlayerData<T> type for annotating one player's accessor tree.
  • A whole-container Set/Clear now fires Changed/Observe on the fields beneath it
  • A receipt Grant that throws no longer leaves partial writes that compound on retry
  • Mode = "NoSave" dry runs actually run your migrations
  • Returning players whose data is still all defaults are no longer mistaken for new ones and skipped.
  • Value.Remove(i) with an out-of-range index is now a no-op instead of deleting a real element.
  • A product Grant that yields still works but loses rollback and logs an error; move async work outside it
  • New opt-in MigrationShadow re-runs your migrations against the raw stored data in Studio and warns when a nil-guarded step silently no-ops because Reconcile already filled the field.
  • Offline writes and RestoreVersion no longer report success for a save the DataStore silently dropped, which could bank a Robux grant that never persisted.

Full Changelog: v1.0.12...v1.1.0

Scribe v1.0.12 - Timeouts for WaitForData & Flush

Choose a tag to compare

@ericplane ericplane released this 27 Jul 22:20
  • WaitForData and Flush now take a timeout (60s and 15s by default), thanks to @ryancundiff in #9
  • ProfileKeyPrefix now accepts "" for games adopting a database whose keys were bare user ids
  • Fixes RestoreVersion reporting success when the profile had been erased
  • FixesScribe.Configure wrongly refusing to run after a failed Scribe() call

Full Changelog: v1.0.11...v1.0.12

Scribe v1.0.11 - Mode and strict HandleReciept

Choose a tag to compare

@ericplane ericplane released this 23 Jul 16:31
  • New Mode option (Live, Mock, NoSave) that replaces the four separate persistence flags
  • Scribe.Configure for the process-wide autosave interval
  • TryHandleReceipt so an external ProcessReceipt router can fall through on products Scribe does not own.
  • Lifecycle failure reasons are now one typed set (Scribe.LifecycleReason), which renames session-end to session-ended and left to player-left. (Changes required)
  • Fixes read-only profile views, which never finished loading.

Full Changelog: v1.0.10...v1.0.11

Scribe v1.0.10 - DictOf & ArrayOf Additions

Choose a tag to compare

@ericplane ericplane released this 21 Jul 22:37
  • New Scribe.ArrayOf, Scribe.DictOf, and Scribe.Optional: arrays and dictionaries whose elements have a real schema, so Roblox datatypes finally work inside containers with no manual Pack/Unpack, and elements get typing, bounds, and MaxItems/MaxKeys caps.
  • Upgrade notes: element records are closed, so an undeclared field is a write error; Set(nil) on a middle array index is refused (use Remove); a table can no longer mix array indices and string keys.

Full Changelog: v1.0.9...v1.0.10