TableManager
TableManager is a wrapper around luau tables that provide easy and automatic change tracking, validation, and listener management. TableManager is designed to handle the bulk of your volatile data management needs, emitting detailed change events and snapshots for any changes made to the managed table or its descendants — all without needing to manually fire events or manage listener connections.
What is TableManager good for?
- Tracking changes to nested tables and arrays.
- Emitting detailed change events for any modifications.
- Providing snapshots of the current state for debugging or synchronization.
- Integrating with ProfileStore for easy management of player data.
What TableManager is Not
- TableManager is not a state management library, and does not include any opinionated features for structuring your data, managing side effects, or integrating with other systems. It is purely a change tracking and notification system for tables.
- TableManager is not intended to be used with tables that are mutated by external code without going through TableManager's API.
- TableManager is not meant for data with a high frequency of updates. It focuses on providing detailed and accurate change information, which can be expensive to generate for large or rapidly changing data.
Usage
local manager = TableManager.new({
Player = { Name = "Alice", Health = 100 },
Inventory = { "Sword", "Shield" },
})
-- Fire when a specific field changes.
manager:OnValueChange("Player.Health", function(health, oldHealth)
print(`health: {oldHealth} -> {health}`)
end)
-- Fire for any change under a subtree (the field OR a descendant).
manager:OnChange("Player", function(_, _, metadata)
print("player changed at:", table.concat(metadata.OriginPath, "."))
end)
manager:Set("Player.Health", 80)
-- prints "health: 100 -> 80"
-- "player changed at: Player.Health"
-- Array contents have their own methods and events.
manager:OnArrayInsert("Inventory", function(index, item)
print(`picked up {item} (slot {index})`)
end)
manager:ArrayInsert("Inventory", "Potion") -- prints "picked up Potion (slot 3)"
Good Practices
- If you have very large datastructures, consider utilizing Opaque wrappers to avoid deep cloning and unnecessary change detection.
- Avoid mixed tables. This is general good practice.
- Utilize pure string keys so the linter can try to infer your types.
Types
ChangeMetadata
interface ChangeMetadata {Diff: Diff.DiffNode--
Diff node for this callback level; nil for ancestor notifications.
OriginDiff: Diff.DiffNode--
Root diff node for the assignment operation.
Snapshot: AncestorSnapshot--
Carries RootTable for ancestor value navigation.
}OriginPath is the assignment origin for both leaf and ancestor callbacks.
ConfigDefaults
interface ConfigDefaults {ListenerFireMode: ListenerFireMode?SignalFireMode: SignalFireMode?FlushMode: FlushMode?DuplicateReferenceMode: DuplicateReferenceMode?}The subset of TableManagerConfig whose defaults can be overridden at the class level via TableManager.SetDefaults. Every field is optional; only the fields you provide are changed.
DuplicateReferenceMode
type DuplicateReferenceMode = "allow" | "copy"Controls what happens when the same table is written to more than one path.
-
"allow"(default): the table becomes a SUPPORTED multi-location reference, not an error -- both paths share identity, and the write fires its own independent change events at the new path. The proxy graph still reports a single "primary" anchor (whichever path established the value's proxy first). -
"copy": opt OUT of sharing for this write by deep-cloning the value first, so the new path gets its own independent identity instead.
ListenerFireMode
type ListenerFireMode = "immediate" | "deferred" | "bindable" | "coalesced"
How listener callbacks (OnChange, OnValueChange, Observe, OnKey*,
OnArray*, For*) are scheduled when they fire.
"immediate": run now via the free-thread pool, like a signal in immediate mode."deferred": run viatask.defer, like a signal in deferred mode."bindable": mirror the engine's actualEnum.SignalBehavior, resolved once at construction time."coalesced": like"deferred", but repeated fires of the SAME listener before the deferred flush collapse into one call carrying the latest event data.
SignalFireMode
type SignalFireMode = "immediate" | "deferred" | "bindable" | "coalesced"
How the per-change Signals (ValueChanged, KeyAdded, etc.) are dispatched.
Resolved and driven by the manager's FireScheduler:
"immediate"/"deferred" map onto Signal:Fire/:FireDeferred; "bindable"
resolves once (at construction) to whichever of those the environment does
natively; "coalesced" collapses repeated fires of the SAME signal within a
frame into one carrying the latest values.
FlushMode
type FlushMode = "immediate" | "coalesced"Controls WHEN the diff-and-fire cycle runs for a direct write.
"immediate"(default): runs synchronously with the write.-
"coalesced": defers it to frame-end and merges every flush request made before then into one flush at their common ancestor path, so a frame with N writes under one subtree costs one diff/fire instead of N.
TableManagerConfig
interface TableManagerConfig {Schema: SchemaCheck?OnValidationFailed: (path: PathArray,value: any,err: string) → ()?ListenerFireMode: ListenerFireMode?--
Controls how listener callbacks (OnChange, OnValueChange, etc.) are scheduled when fired.
SignalFireMode: SignalFireMode?--
Controls how the per-change Signals (ValueChanged, KeyAdded, etc.) are scheduled when fired.
DuplicateReferenceMode: DuplicateReferenceMode?--
Defaults to "allow" (multi-location references are supported).
EnableProxies: boolean?--
Defaults to true. When false, Proxy/GetProxy are unavailable.
FrozenTablesAreOpaque: boolean?--
Defaults to false. When true, a SHALLOWLY frozen table is also treated as opaque (no clone/walk, identity-compared only), trusting the freeze as an immutability assertion the same way Opaque is opt-in trust. A DEEPLY frozen table (every table-typed descendant also frozen) is always treated as opaque, regardless of this flag — that case is provably safe and needs no opt-in.
}Implicit sharing
Linking is automatic: any table reachable (transparently) in two or more
managers' trees is observed by all of them, and a write through one propagates
to the others. Mark a region Opaque/OpaqueChildren to opt it out.
ForOptions
interface ForOptions {FireForExisting: boolean?--
Defaults to true: run the handler/transform for items already present at subscribe time.
Defer: boolean?--
Defer the initial fire (honors the registry's deferred-fire mode either way).
}Properties
TableManager.DefaultListenerFireMode
This item is read only and cannot be modified. Read OnlyStaticTableManager.TableManager.DefaultListenerFireMode: ListenerFireMode
The ListenerFireMode used when TableManagerConfig.ListenerFireMode is
omitted: it controls how listener callbacks (OnChange, OnValueChange,
Observe, OnKey*, OnArray*, For*) are scheduled when they fire.
Defaults to "bindable".
Read at construction time only -- changing it affects only TableManagers created afterwards. Read-only -- use TableManager.SetDefaults to change it.
TableManager.DefaultSignalFireMode
This item is read only and cannot be modified. Read OnlyStaticTableManager.TableManager.DefaultSignalFireMode: SignalFireMode
The SignalFireMode used when TableManagerConfig.SignalFireMode is omitted:
it controls how the public per-change Signals (ValueChanged, Changed,
ArrayInserted, etc.) are scheduled when they fire. Defaults to "bindable".
Read at construction time only -- changing it affects only TableManagers created afterwards. Read-only -- use TableManager.SetDefaults to change it.
TableManager.DefaultFlushMode
This item is read only and cannot be modified. Read OnlyStaticTableManager.TableManager.DefaultFlushMode: FlushMode
The FlushMode used when TableManagerConfig.FlushMode is omitted: it
controls whether pending changes are flushed immediately or coalesced to the
end of the frame. Defaults to "immediate".
Read at construction time only -- changing it affects only TableManagers created afterwards. Read-only -- use TableManager.SetDefaults to change it.
TableManager.DefaultDuplicateReferenceMode
This item is read only and cannot be modified. Read OnlyStaticTableManager.TableManager.DefaultDuplicateReferenceMode: DuplicateReferenceMode
The DuplicateReferenceMode used when
TableManagerConfig.DuplicateReferenceMode is omitted: it controls whether
writing the same table to more than one path shares its identity ("allow")
or stores an independent copy ("copy"). Defaults to "allow".
Read at construction time only -- changing it affects only TableManagers created afterwards. Read-only -- use TableManager.SetDefaults to change it.
Functions
SetDefaults
Static
Overrides the class-level construction defaults for the fields you provide,
mirroring the corresponding TableManagerConfig keys. Only the fields present
in defaults change; omitted fields keep their current value. Each value is
validated and an unrecognized mode string errors without changing any default.
Because the defaults are read at construction time, this affects only TableManagers created afterwards -- never existing instances.
TableManager.SetDefaults({
FlushMode = "coalesced",
SignalFireMode = "deferred",
})
new
StaticTableManager.new(initialData: table,--
The initial table data to manage. Must be a table.
) → TableManager--
The newly created TableManager instance.
Creates a new TableManager instance.
IsDestroyed
Static
Returns true once TableManager:Destroy has run on manager, false
otherwise. Pairs with TableManager:OnDestroy for teardown-time checks.
TableManager.IsDestroyed(manager) -- safe before AND after manager:Destroy()
Opaque
StaticTableManager.Opaque(value: T) → OpaqueWrapper<T>
Wraps value so that, when written, this manager treats it as opaque: it is
never cloned, frozen, or walked by the diff engine -- only identity-compared.
The wrapper is unwrapped at write time, so the stored value is the bare inner
value. Useful for large immutable blobs or foreign objects you don't want the
diff engine to traverse. See the Opaque Values guide.
GlobalOpaque
StaticTableManager.GlobalOpaque(value: T) → OpaqueWrapper<T>
Like TableManager.Opaque, but registers value as opaque in a registry
shared by every TableManager rather than just this one.
OpaqueChildren
StaticTableManager.OpaqueChildren(value: T) → OpaqueWrapper<T>Wraps a container so that its direct children are treated as opaque (each child is identity-compared, never cloned/frozen/walked), while the container itself is still diffed normally.
GlobalOpaqueChildren
StaticTableManager.GlobalOpaqueChildren(value: T) → OpaqueWrapper<T>Like TableManager.OpaqueChildren, but registers the children as opaque in a registry shared by every TableManager rather than just this one.
Get
TableManager:Get(suppressNilPartialPaths: boolean?--
Return nil instead of erroring when a segment along the path is not a table.
) → any--
The value at path.
Reads the value at path. The path may be a dot-string ("player.health")
or a path array ({ "player", "health" }); an empty path returns the root
table.
By default this errors if an intermediate segment is not a table (e.g. reading
"a.b" when a is a number). Pass suppressNilPartialPaths = true to return
nil in that case instead.
manager:Get("player.health") -- dot-string path
manager:Get({ "player", "health" }) -- array path (equivalent)
GetMatching
TableManager:GetMatching(path: Path) → {{Path: {any},Value: any,WildcardMatches: {any}?}}--
One record per matched concrete path.
Reads every value matched by a path containing wildcard ("*") segments,
returning one record per concrete match:
Path-- the fully concrete path array (every"*"replaced by the matched key).Value-- the value at that path.-
WildcardMatches-- the keys matched by each"*", left-to-right (the same convention as listenermetadata.WildcardMatches).
Each "*" matches every key present at that level; multiple wildcards compose
as the product of their branch factors. Branches where the remaining path
cannot resolve are skipped, and only existing values are reported -- zero
matches returns an empty array. Entry order follows table iteration order and
is not deterministic for dictionary keys.
A path with no wildcards returns zero-or-one records, resolved exactly like
TableManager:Get (including the error on a non-table intermediate segment;
literal segments before the first "*" error the same way).
local manager = TableManager.new({
Players = {
p123 = { Health = 100 },
p456 = { Health = 70 },
},
})
local matches = manager:GetMatching("Players.*.Health")
-- matches (order not guaranteed):
-- {
-- { Path = { "Players", "p123", "Health" }, Value = 100, WildcardMatches = { "p123" } },
-- { Path = { "Players", "p456", "Health" }, Value = 70, WildcardMatches = { "p456" } },
-- }
for _, match in matches do
print(match.WildcardMatches[1], "has", match.Value, "health")
end
-- Multiple wildcards -> one WildcardMatches entry per "*", left-to-right:
for _, match in manager:GetMatching("Players.*.Stats.*") do
local playerId, statName = match.WildcardMatches[1], match.WildcardMatches[2]
end
GetProxy
TableManager:GetProxy(suppressNilPartialPaths: boolean?--
Return nil instead of erroring when a segment along the path is not a table.
) → Proxy | any--
A proxy for the table at path, or the raw value if it is not a table.
Returns a live [Proxy] for the table at path, or the raw value when path
resolves to a non-table. Reads through the proxy see live data, and writes
through it route back into the manager exactly as TableManager:Set would.
CAUTION
Errors if this manager was created with Config.EnableProxies = false -- there
is no proxy graph to hand out. See the Proxies & Direct Table Access guide.
GetLastEmitted
TableManager:GetLastEmitted(suppressNilPartialPaths: boolean?--
Return nil instead of erroring when a segment along the path is not a table (only consulted on the live fallback).
) → any--
The value last emitted for path (or the live value when path was never observed).
Reads the value at path as listeners last saw it -- the value carried by
the most recent change event fired for path, rather than the current live
value. Under the default immediate flush mode with no pending changes this is
identical to TableManager:Get; the two diverge only when the live data has
moved ahead of what has been emitted -- inside a Batch/Suspend window, or
under FlushMode = "coalesced" / a deferred fire mode, before the pending
flush runs.
If path has never been observed (no listener, Signal, link, or OnApplied
subscriber ever covered it) there is no recorded "last emitted" value, so this
falls back to a live TableManager:Get -- the same value a first listener
would receive.
Last-emitted nil inside a pending window
The recorded baseline cannot distinguish "never observed" from "observed, and
the last value emitted was itself nil" (e.g. after an emitted deletion) -- an
absent key and a nil-valued key are indistinguishable in the baseline store.
Both fall back to the live Get. That fallback is exact except in one narrow
window: if the last emit for path was nil and the live value has since
moved ahead of it inside a pending flush (a Batch/Suspend window, or
FlushMode = "coalesced" / a deferred fire mode), this returns the pending live
value rather than the emitted nil. Once that flush drains, it converges again.
Table results are snapshots
When the value at path is a table, the returned table is the internal
baseline mirror of the last-emitted state: it follows the same stability rules
as a listener's table oldValue (a stable snapshot only for the duration of
the synchronous call -- copy it if you need to retain it). Scalar results are
always safe to keep.
local manager = TableManager.new({ Score = 0 }, { FlushMode = "coalesced" })
manager:OnValueChange("Score", function() end) -- observe so a baseline is kept
manager:Set("Score", 10)
manager:Get("Score") -- 10 (live)
manager:GetLastEmitted("Score") -- 0 (listeners have not been told yet)
manager:Flush("Score")
manager:GetLastEmitted("Score") -- 10 (now emitted)
Set
TableManager:Set(buildTablesDynamically: boolean?--
When true, missing intermediate tables along path are created rather than erroring. Not allowed with wildcard paths.
) → ()Writes value at path, firing the events for whatever changed.
manager:Set("player.health", 80) -- dot-string path
manager:Set({ "player", "health" }, 80) -- array path (equivalent)
manager:Set("player.buff", nil) -- removing a key
An empty path (Set({}, newTable) / Set("", newTable)) replaces the entire
root table: its identity is swapped to newTable, stale proxies of the old
tree are pruned, and root/child change listeners fire for the diff. The new
root must be a table (the root cannot become a scalar or nil), and the root
cannot be replaced while a batch is open.
The path may contain wildcard ("*") segments -- the same grammar the
listener methods accept. Each "*" matches every key present at that level
at call time (multiple wildcards compose as the product of their branch
factors), and the write is applied once per matched concrete path, batched
into one coherent flush when more than one path matches:
manager:Set("Players.*.Health", 100) -- every player's Health (created where missing)
manager:Set("Players.*.Stats.*", 0) -- every stat of every player
manager:Set("Players.*", nil) -- mass delete: removes every player
Wildcard branches where the remaining path cannot resolve are skipped, and
zero matches is a no-op. Writing nil only targets existing values; a
non-nil write also creates a missing literal final key on each matched
parent. buildTablesDynamically cannot be combined with a wildcard path.
Note that a table value fanned to multiple paths is subject to
[TableManagerConfig.DuplicateReferenceMode]: under "allow" (the default)
every matched path shares the SAME table identity; use "copy" for
independent clones.
Update
TableManager:Update(updater: (oldValue: any,wildcardMatches: {any}?,path: {any}?) → any--
Receives the current value (and, on wildcard paths, the matched keys and concrete path); returns the new value.
) → any--
The new value that was written (nil for wildcard paths).
Reads the value at path, passes it through updater, and writes the result
back. Equivalent to a TableManager:Get followed by TableManager:Set, so it
fires the same change events as a plain Set.
The path may contain wildcard ("*") segments: updater then runs once per
matched concrete path, the writes are batched into one coherent flush, and
the method returns nil. On a wildcard path each invocation receives, after
the current value, the keys matched by each "*" (left-to-right, one per
wildcard) and the fully concrete path -- so an updater can tell which match
it is handling:
manager:Update("Players.*.Stats.*", function(value, matches, path)
-- matches[1] = playerId, matches[2] = statName
-- path = { "Players", playerId, "Stats", statName }
return value + 1
end)
matches/path are nil for a non-wildcard Update. Only existing values
are visited -- use TableManager:Set to create keys through a wildcard. See
TableManager:GetMatching for the matching rules.
manager:Update("coins", function(current)
return current * 2
end)
Increment
TableManager:Increment(delta: any--
The amount to add (any value supporting + with the current value).
) → any--
The new value that was written (nil for wildcard paths).
Adds delta to the number at path and writes the result back. Shorthand for
an TableManager:Update that returns oldValue + delta; errors if the current
value is not addable.
The path may contain wildcard ("*") segments: delta is then added to every
matched existing value (batched into one coherent flush) and the method
returns nil. See TableManager:Update and TableManager:GetMatching.
manager:Increment("coins", 50) -- coins += 50
manager:Increment("Players.*.Health", 5) -- everyone heals 5
ArrayInsert
Inserts a value into the array at pathOrProxy. Two call shapes:
ArrayInsert(path, value)-- appendsvalueto the end.-
ArrayInsert(path, index, value)-- insertsvalueatindex, shifting the existing elements at and afterindexone slot to the right.
Fires ArrayInserted (and ancestor change events) for the affected index.
pathOrProxy may be a path or a [Proxy] of the array.
Aliased as Insert.
manager:ArrayInsert("items", "sword") -- append
manager:ArrayInsert("items", 1, "shield") -- insert at index 1
ArrayRemove
TableManager:ArrayRemove(index: number--
1-based index of the element to remove.
) → any--
The removed element.
Removes the element at index from the array at pathOrProxy, shifting the
following elements one slot to the left. Fires ArrayRemoved (and ancestor
change events) at the remove path.
Aliased as Remove.
ArrayRemoveFirstValue
TableManager:ArrayRemoveFirstValue(valueToFind: any--
The value to search for (compared by equality).
) → number?--
The index it was removed from, or nil if not found.
Finds the first element equal to valueToFind and removes it via
TableManager:ArrayRemove.
Aliased as RemoveFirstValue.
ArraySwapRemove
TableManager:ArraySwapRemove(index: number--
1-based index of the element to remove.
) → any--
The removed element, or nil if index is out of range.
Removes the element at index in O(1) by moving the last element into its
slot, instead of shifting every following element. Order is not preserved.
Because the last element backfills the hole, this emits two events rather than
one: an ArraySet at index (the backfill) followed by an ArrayRemoved at
the old last index (the shrink). When index is already the last element, only
the ArrayRemoved fires.
Aliased as SwapRemove.
ArraySwapRemoveFirstValue
TableManager:ArraySwapRemoveFirstValue(valueToFind: any--
The value to search for (compared by equality).
) → number?--
The index it was removed from, or nil if not found.
Finds the first element equal to valueToFind and removes it via
TableManager:ArraySwapRemove (O(1), order not preserved).
Aliased as SwapRemoveFirstValue.
UpdateKey
TableManager:UpdateKey(key: any,--
The child key/index under path to update.
updater: (oldValue: any) → any--
Receives the current value; returns the new value.
) → any--
The new value that was written.
Like TableManager:Update, but for the child key under path -- it updates
the value at path extended by key. Handy for a single dictionary entry or
array index without building the combined path yourself.
Aliased as ArrayUpdate.
manager:UpdateKey("inventory", "gold", function(n) return n + 10 end)
IncrementKey
TableManager:IncrementKey() → any--
The new value that was written.
Like TableManager:Increment, but for the child key under path -- it adds
delta to the value at path extended by key.
Aliased as ArrayIncrement.
Batch
TableManager:Batch(fn: () → ()) → ()Holds off firing signals for the duration of the callback, then fires needed signals at the end. Useful for batch operations where you want to suppress intermediate signals and only fire final results.
Nested calls are no-ops: the outermost Batch window covers everything.
yielding
Yielding within a batch window will leave the TableManager in a suspended state, which can cause unexpected behavior.
Suspend
TableManager:Suspend() → ()Suspends all signal and listener firing.
It is recommended to use :Batch for better ergonomics and safety,
but Suspend/Resume can be used for more manual control if needed.
Pair with Resume(). Nested calls are no-ops (the outermost window wins).
Resume
TableManager:Resume() → ()Resumes after Suspend() and flushes all pending changes.
Flush is a single array-aware pass: per dirty branch, CheckForChangesBetween
diffs the pre-batch value against the current value, collecting array-like
nodes out of the dict diff; those are then re-diffed via LCS
(ArrayDiff.emitDiff) into shift-faithful Array* events. See BatchFlush.
Flush
Diffs the persistent shadow baseline against the live value at path
(the whole tree when omitted), fires any resulting events, then
reconciles the shadow to the current live value. A no-op when nothing
observes path (no registered listener covers it, no global Signal is
connected, and no link group is active).
Outside a batch this runs immediately. Inside a Suspend/Batch window
it just marks the branch dirty for Resume's flush instead of firing, so
an explicit Flush can never break batch atomicity.
This is also the supported way to surface a mutation made by code that
bypassed Set/Proxy and wrote directly into the underlying table.
MoveTo
Move an element from one location to another within the same table. This unsets the value at the current path and sets it at the new path, firing appropriate notifications. This is specifically useful for moving tables around without breaking proxy references.
CopyTo
DeepCopy a value from one location to another within the same table. This sets the value at the new path to be the same as the value at the current path, firing appropriate notifications. This is specifically useful for copying tables around without breaking proxy references.
Swap
Swaps the values at paths a and b within this manager. The swap is applied
as a single batched pair of writes, and proxy references are preserved across
the move. If either write fails the reparenting is rolled back so neither path
is left in a partial state.
Either argument may be a path or a [Proxy]. Errors if a path is the root or if one path is an ancestor/descendant of the other (an overlapping swap is not well-defined).
manager:Swap("slots.1", "slots.2")
OnValueChange
TableManager:OnValueChange(options: ListenerOptions?) → Connection
Fires ONLY when this exact path is directly reassigned (not when a
descendant of it changes). Shorthand for OnChange with
ListenDepth = 0, ListenDepthStyle = "==".
OnChange
TableManager:OnChange(options: ListenerOptions?) → ConnectionFires when this path is directly reassigned OR any descendant of it changes.
Observe
TableManager:Observe(options: ListenerOptions?) → Connection
Immediately invokes callback with the current value at path
(oldValue and metadata both nil), then behaves like
OnValueChange for subsequent changes.
OnKeyAdd
TableManager:OnKeyAdd(options: ListenerOptions?) → Connection
Fires when a new key appears in the table at path (a key that did not exist
before is assigned a non-nil value).
manager:OnKeyAdd("inventory", function(key, newValue)
print(`gained {key} = {newValue}`)
end)
OnKeyRemove
TableManager:OnKeyRemove(options: ListenerOptions?) → Connection
Fires when a key is removed from the table at path (an existing key is set to
nil).
manager:OnKeyRemove("inventory", function(key, oldValue)
print(`lost {key} (was {oldValue})`)
end)
OnKeyChange
TableManager:OnKeyChange(options: ListenerOptions?) → Connection
Fires when an existing key in the table at path is reassigned to a different
value (not added or removed -- see TableManager:OnKeyAdd /
TableManager:OnKeyRemove for those).
OnArrayInsert
TableManager:OnArrayInsert(options: ListenerOptions?) → Connection
Fires when an element is inserted into the array at path, reporting the index
it landed at. Driven by array-aware diffing, so it reflects true insertions with
replay-faithful indices rather than a blanket "the array changed".
OnArrayRemove
TableManager:OnArrayRemove(options: ListenerOptions?) → Connection
Fires when an element is removed from the array at path, reporting the index
it was removed from and its old value.
OnArraySet
TableManager:OnArraySet(options: ListenerOptions?) → Connection
Fires when an existing array slot at path is overwritten in place (its index
kept, its value replaced), reporting the index, new value, and old value.
GetLinkedManagers
Returns the other managers currently sharing (co-observing) at least one live table identity with this one — de-duplicated, excluding self.
IsLinkedWith
Returns true if this manager currently shares a live table identity with other.
ForKeys
Reconciles the items found at path by KEY: handler runs once per key
(passed a per-item Janitor, the key, and change metadata — nil on the
initial fire), and is NOT re-run when the value at an existing key
changes. The Janitor is destroyed when that key is removed or the
connection disconnects.
ForValues
Reconciles the items found at path by VALUE (as a multiset — duplicate
values are tracked as independent items): handler runs once per value
occurrence and is NOT re-run when that value moves between keys/indices
(e.g. an ArraySwapRemove). The Janitor is destroyed when that value
occurrence is no longer present or the connection disconnects.
ForPairs
Reconciles the items found at path by KEY AND VALUE: handler re-runs
(tearing down the previous item's Janitor first) whenever EITHER the key
or the value changes — a value change at an existing key is treated as
that item leaving and a new one appearing.
SetPathIgnored
Marks path (and every descendant of it) as ignored or not. An ignored
write still happens — Get/Raw reflect it immediately — but skips all
diff/snapshot/event work, so it never fires listeners/signals and is never
cloned for change detection. See TableManagerConfig.IgnoredPaths for the
config-time equivalent (seeded once at construction).
PromiseValue
Returns a promise that resolves when the value at path satisfies predicate.
If no predicate is provided, resolves when the value at path first changes.
OnDestroy
TableManager:OnDestroy(callback: () → ()) → () → ()--
Call to unsubscribe.
Runs a callback when the TableManager is destroyed. Useful for cleaning up resources that are tied to the lifetime of the TableManager.
Destroy
TableManager:Destroy() → ()Destroy the TableManager and clean up all resources.