Skip to content
Homeostate

Core

Persistence API

createPersistence, all PersistenceConfig options, loading and cleanup methods, and the document and storage adapter contracts.

@homeostate/core@0.3.0npmSource

Import these APIs and types from @homeostate/core. The persistence guide shows how to combine them with a sync engine and choose browser storage.

createPersistence

declare function createPersistence(
  doc: PersistableDoc,
  adapter: PersistenceAdapter,
  config: PersistenceConfig,
): Persistence;

Subscribes to document updates immediately and queues the initial load. Await whenLoaded before connecting a sync engine that could seed initial state.

ParameterRequiredDescription
docYesBinary encoding, merging and subscriptions for a CRDT document. Use the create*Persistable factory from your backend package.
adapterYesStorage implementing the append-only update log contract below.
configYesThe storage key and optional compaction/error settings.

The initial load merges stored updates into the document and compacts them into one snapshot, which also stores whatever the document already held. Later updates, local and from peers, are appended one at a time, including those made during the load.

Example

This browser example stores a Yjs document using the IndexedDB adapter:

persistence-example.ts
import * as Y from "yjs";
import { createPersistence } from "@homeostate/core";
import { createYjsPersistable } from "@homeostate/crdt-yjs";
import { createIndexedDbAdapter } from "@homeostate/persist-indexeddb";

const doc = new Y.Doc();
const storage = createIndexedDbAdapter();
const persistence = createPersistence(createYjsPersistable(doc), storage, {
  key: "counter-room",
  compactAfter: 50,
  onError: (error) => console.error("Could not persist the counter", error),
});

await persistence.whenLoaded;
// Connect your sync engine here, after the saved state has been applied.
doc.getMap("shared").set("count", 1);
await persistence.flush();

// On teardown: finish queued work and release resources; stored data stays.
await persistence.destroy();
storage.close();
doc.destroy();

PersistenceConfig

interface PersistenceConfig {
  key: string;
  compactAfter?: number;
  onError?: (error: unknown) => void;
}
OptionDefaultBehavior
keyRequiredIdentifies the document in the storage adapter. Use different keys for independent documents.
compactAfter100Number of successful appends between compactions. Use a positive integer, or Infinity to compact only during the initial load.
onErrorLogs through console.errorReceives storage failures and errors applying stored updates. Supply a handler that reports the error without throwing.

Compaction loads the current log, merges it into the document, and replaces the loaded portion with one snapshot; updates another writer appended after that load remain.

Persistence

interface Persistence {
  whenLoaded: Promise<void>;
  flush: () => Promise<void>;
  compact: () => Promise<void>;
  stats: () => Promise<PersistenceStats>;
  destroy: () => Promise<void>;
  clear: () => Promise<void>;
}

interface PersistenceStats {
  updates: number;
  bytes: number;
}

whenLoaded

Resolves once the stored updates have been applied. If loading fails, it resolves after onError, so the app starts without its saved state; it never rejects. It may resolve before the initial snapshot is written, and it does not wait for network peers. Await flush() to wait for storage work too.

flush

Waits for storage work queued at the time of the call, including any compaction it triggers. Updates arriving later need another call.

compact

Queues a compaction now, as every compactAfter appends do, and resolves once it is written. Failures go to onError. After destroy() or clear() it does nothing.

stats

Waits for storage work queued at the time of the call, then loads the stored log and resolves with a PersistenceStats: updates counts the stored entries, compacted snapshots included, and bytes adds up their sizes as handed to the adapter, which may encode them larger. Unlike the other methods, it rejects when storage cannot be read, rather than calling onError. The devtools Storage tab shows it.

destroy

Stops storing updates immediately and waits for queued work; calling it again is safe. The stored data stays for a later createPersistence with the same adapter and key. It does not destroy the document, disconnect its provider or sync engine, or close the storage adapter.

clear

Stops storing updates and removes the stored document after pending work, also after destroy(). The live document and other peers keep their state, and other persistence instances using the same key can write it again. To resume storing after clear() or destroy(), create a new persistence.

Error handling

Storage errors and stored updates that cannot be applied go to onError, and the queue moves on to the next operation. So the lifecycle promises resolving does not mean every write succeeded: track failures in onError if the app needs to show whether persistence works. Keep onError from throwing, or queued work rejects.

PersistableDoc

interface PersistableDoc {
  encode: () => Uint8Array;
  apply: (update: Uint8Array) => void;
  subscribe: (onUpdate: (update: Uint8Array) => void) => Unsubscribe;
}
MemberContract
encode()Encodes the entire document as an update accepted by apply.
apply(update)Merges binary updates or snapshots. Repeated and out-of-order updates must be safe.
subscribe(callback)Reports local changes and changes received from peers as binary updates. Changes made through this interface's own apply must not be reported again. Returns an unsubscribe function.

Not reporting apply's own changes keeps restored updates from being stored again. Use createYjsPersistable, createLoroPersistable or createAutomergePersistable from the backend package; they store the whole document, including data outside the synced subtree.

PersistenceAdapter and StoredUpdates

interface StoredUpdates {
  updates: Uint8Array[];
  version: number;
}

interface PersistenceAdapter {
  load: (key: string) => Promise<StoredUpdates>;
  append: (key: string, update: Uint8Array) => Promise<void>;
  compact: (
    key: string,
    snapshot: Uint8Array,
    version: number,
  ) => Promise<void>;
  clear: (key: string) => Promise<void>;
}
MemberContract
load(key)Returns updates oldest first and the position of the newest returned update. For a missing key, return { updates: [], version: 0 }.
append(key, update)Adds one binary update to that key's log.
compact(key, snapshot, version)Atomically removes updates through the supplied version and stores the snapshot, retaining later appends and snapshots from concurrent compactions.
clear(key)Removes all stored data for that key. Other keys are unaffected.

version is a storage-log position, not an application schema version. Store the bytes in the supplied Uint8Array view, which may cover only part of its backing buffer. Reject failed storage operations so createPersistence can report them through onError.

Use IndexedDB, localStorage, or the in-memory testing adapter. Unsubscribe is defined in the sync engine reference.

Source: persistence.ts and types.ts.