IPersistenceStore
Long-term storage for entities that opted into persistence (PersistentEntity).
Nebula · Packages/com.1by3.nebula/Runtime/Persistence/IPersistenceStore.cspublic interface IPersistenceStore : IDisposableLong-term storage for entities that opted into persistence (PersistentEntity). One store per process; the worker's NebulaPersistence writes checkpoints through it and reads containers back when it gains leases. In a mesh the orchestrator owns the store (SQLite or PostgreSQL through the standalone services' SqlPersistenceStore, next to the control plane in the same database) and workers reach it through RemotePersistenceStore; LocalPersistenceStore serves single-process runs and tests.
The contract is request/response on purpose (no subscriptions), so a relational backend fits behind it. Every callback lands on the main thread from Tick. Writes are fire-and-forget (WhenWritten is the barrier for the few callers that must wait): a save is accepted by the backend when its epoch is not older than the stored one, otherwise dropped, and the caller never waits. The store is never on the per-tick path: if it is unreachable the mesh keeps simulating and saves are retried on the next checkpoint.
Every callback runs exactly once. A store that gives up on a read answers it with the empty result and sets PersistenceAnswer.Failed for the duration of the callback; it never leaves a caller waiting.
Properties
IsConnected
bool IsConnected { get; }Connected and ready to answer loads. Saves issued before this are queued until it is true.
Backend
string Backend { get; }Short backend name for logs and the dashboard ("sqlite", "postgres", "remote", "local", "memory").
KnownCount
int KnownCount { get; }How many records the store holds as far as this process knows (-1 when unknown).
Methods
Connect
void Connect();Tick
void Tick();Pump the backend and deliver callbacks on the main thread. Call once per frame.
Save
void Save(PersistedEntityRecord record);Insert or update. The backend keeps the record only when PersistedEntityRecord.Epoch is at least the stored epoch.
Delete
void Delete(string key);Delete the record for key, if any.
WhenWritten
void WhenWritten(Action onWritten);Calls onWritten on the main thread once every save and delete issued before this call has reached the backend. It says the writes were delivered, not that each was kept (a stale epoch is still dropped): follow it with one Load when the outcome matters, instead of polling. When the store gave up on a write issued since the previous barrier, or on the barrier itself, the callback still runs, with PersistenceAnswer.Failed set.
Load
void Load(string key, Action<PersistedEntityRecord> onLoaded);The record for key (null when there is none).
LoadContainers
void LoadContainers(IReadOnlyList<string> containerIds, Action<IReadOnlyDictionary<string, IReadOnlyList<PersistedEntityRecord>>> onLoaded);The records of several containers in one read: for each id in containerIds, every record whose PersistedEntityRecord.ContainerId is that id and that is not inside a carrier. onLoaded runs once, on the main thread, with an entry for every distinct id asked for (an empty list when nothing is saved there).
This is how a worker restores the containers it gains: however many leases arrive at once, NebulaPersistence asks for them in a few bounded reads (NebulaPersistence.MaxContainersPerRestoreLoad ids each, at most NebulaPersistence.MaxRestoreLoadsInFlight at a time) instead of one read per container. Ask for one id to read one box. A store may split a longer list into several backend queries (PersistenceHost.MaxContainersPerLoad ids each) and still answers once.
LoadCarried
void LoadCarried(IReadOnlyList<string> carrierKeys, Action<IReadOnlyDictionary<string, IReadOnlyList<PersistedEntityRecord>>> onLoaded);What several carriers hold, in one read: for each key in carrierKeys, every record whose PersistedEntityRecord.CarrierKey is that key. onLoaded runs once, on the main thread, with an entry for every distinct key asked for (an empty list when nothing is saved aboard; always empty for the empty key, which is no carrier).
This is how a worker brings back what rode in the carriers it spawns: however many carriers spawn at once (a container restore that brings back a hangar full of vehicles, a re-deal), NebulaPersistence asks for them in a few bounded reads (NebulaPersistence.MaxCarriersPerRestoreLoad keys each, at most NebulaPersistence.MaxRestoreLoadsInFlight at a time) instead of one read per carrier. Ask for one key to read one carrier. A store may split a longer list into several backend queries (PersistenceHost.MaxCarriersPerLoad keys each) and still answers once.
LoadWhere
void LoadWhere(Func<PersistedEntityRecord, bool> predicate, Action<IReadOnlyList<PersistedEntityRecord>> onLoaded);Every record that matches predicate. For tools and game directors, not for the per-lease restore path. This is the offline read: the records of a scope nothing is simulating are ordinary records and can be read, and written, without activating it (docs/lifecycle-hooks.md §5).
CountRecords
void CountRecords(string scopeKey, string containerId, Action<int> onCounted);How many records the store holds for a scope, without reading them. scopeKey is matched exactly (EntityLocation.PublicScope, the empty key, is the public world) and containerId narrows the count to one container when it is non-empty; records inside a carrier are counted like any other. Cheap on purpose — a COUNT on the backend, never a list — so that "is there anything saved here?" can be asked on the path that brings a scope to life. The answer lands on the main thread like every other read. Behind NebulaLifecycle.OnScopeActivating's hasRecords.
Clear
void Clear();Delete every record (dashboard / dev reset).
RelevanceTiers
The arithmetic of RelevancePriority: pure, shared by the gateway, the worker and the tests.
LocalPersistenceStore
In-process IPersistenceStore with the exact write rules of the persistence module (epoch check, version bump), optionally backed by a file so a single-process run survives a restart.