store: Where sync state lives#

A store keeps every scope, its records, and the change feed. Use SQLiteStore to keep state between runs, and MemoryStore in tests.

The storage interface every store implements.

A store only keeps data. Every sync rule lives in Replica, so each store implements the same small set of typed operations and nothing more.

class calendaring_sync.store.base.StateStore[source]#

Bases: ABC

Where the core keeps each scope, its records, and the change feed.

Every operation runs inside transaction(): one that only reads runs inside any transaction, and one that writes needs a transaction opened with write=True. One thread’s transaction runs to the end before another’s starts, so a store is safe to share between threads.

Besides the errors each method lists, every operation, transaction() and close() can raise:

No exception of the storage underneath a store reaches the caller.

The public methods check that they run inside a transaction of the right kind, then call a method of the same name with a leading underscore. A store implements those, along with _begin(), _commit() and _rollback(), and needs no checks of its own. It turns the errors of the storage it uses into store errors by overriding _store_error(). Its __init__ calls super().__init__() first.

append_entry(scope_id: str, kind: FeedEntryKind, local_key: int | None, object_id: str | None, at: datetime) → FeedEntry[source]#

Write the next change feed entry and return it.

Its number is one more than the last number written. Runs inside a write transaction.

Raises:
close() → None[source]#

Close the store. Every later transaction raises StoreError.

Closing a closed store does nothing.

Raises:
  • TransactionError – If this thread has a transaction open.

  • StoreError – If the store couldn’t release what it holds open. It counts as closed anyway.

delete_record(local_key: int) → None[source]#

Delete the record with this local key.

Runs inside a write transaction.

Raises:
delete_scope(scope_id: str) → None[source]#

Delete the scope and every record in it.

Runs inside a write transaction.

Raises:
entries(after: int, limit: int) → list[FeedEntry][source]#

Return up to limit kept entries numbered above after, in order.

Runs inside any transaction.

Raises:

TransactionRequiredError – If no transaction is open in this thread.

feed_bounds() → tuple[int, int][source]#

Return the number the feed is trimmed through, and the last number written.

Runs inside any transaction.

Raises:

TransactionRequiredError – If no transaction is open in this thread.

find_by_logical_key(scope_id: str, logical_key: str) → list[Record][source]#

Return the scope’s records with this logical key, ordered by local key.

A record matches if its synced side or its pending edit has the key. Several records can share one. Runs inside any transaction.

Raises:

TransactionRequiredError – If no transaction is open in this thread.

find_record(scope_id: str, object_id: str) → Record | None[source]#

Return the scope’s record with this object id, or None.

Runs inside any transaction.

Raises:

TransactionRequiredError – If no transaction is open in this thread.

get_record(local_key: int) → Record | None[source]#

Return the record with this local key, or None.

Runs inside any transaction.

Raises:

TransactionRequiredError – If no transaction is open in this thread.

get_scope(scope_id: str) → Scope | None[source]#

Return the scope with this id, or None.

Runs inside any transaction.

Raises:

TransactionRequiredError – If no transaction is open in this thread.

insert_record(record: Record) → int[source]#

Insert a new record and return the local key it got.

The record’s own local_key is ignored. A key is never given out twice, even after the record it went to was deleted. Runs inside a write transaction.

Raises:
  • RecordValueError – If the record holds what its state can’t. A record holds an edit exactly when it’s pending, in flight or in conflict, the server’s side of a conflict exactly when it’s in conflict, and no synced content exactly when its edit is a create. An edit holds a copy of the object unless it’s a delete, a conflict’s edit and server’s side match its kind, and only a refused create’s conflict names a holder. The record is checked first, so this is raised outside a transaction too.

  • TransactionRequiredError – If no transaction is open in this thread.

  • TransactionPermissionError – If the transaction only reads.

  • UnknownScopeError – If the record’s scope doesn’t exist.

  • DuplicateObjectError – If another record in the scope has the record’s object id.

put_scope(scope: Scope) → None[source]#

Insert the scope, or replace the one with its id.

Runs inside a write transaction.

Raises:
removed_records(through: int) → list[Record][source]#

Return the removed records no feed entry after through is about.

through is the number of a change feed entry. Each record keeps the number of the last entry about it, so these are the records removed by that entry or an earlier one, ordered by local key. Runs inside any transaction.

Raises:

TransactionRequiredError – If no transaction is open in this thread.

scope_records(scope_id: str, *, states: frozenset[RecordState] | None = None) → list[Record][source]#

Return the scope’s records, ordered by local key.

Runs inside any transaction.

Parameters:
  • scope_id – The scope to read.

  • states – Only the records in one of these states; None for every record. An empty set matches none, without asking the store, since not every database takes an empty list.

Raises:

TransactionRequiredError – If no transaction is open in this thread.

scopes() → list[Scope][source]#

Return every scope, ordered by id.

Runs inside any transaction.

Raises:

TransactionRequiredError – If no transaction is open in this thread.

transaction(*, write: bool = True) → Iterator[None][source]#

Run the operations inside the block as one transaction.

Everything the block wrote is kept if it ends normally, and nothing is kept if it raises.

Parameters:

write – False for a block that only reads. Such a block can’t write, and a store may run it without blocking other processes’ writes.

Raises:
  • TransactionError – If a transaction is already open in this thread.

  • StoreBusyError – If another process kept the store busy for too long.

  • StoreError – If the store was closed, or can’t start or keep the transaction.

trim_entries(through: int) → None[source]#

Delete every entry numbered at or below through.

feed_bounds() reports the highest through trimmed so far. Runs inside a write transaction.

Raises:
update_record(record: Record) → None[source]#

Replace the record with the same local key.

Runs inside a write transaction.

Raises:

A store that keeps everything in memory, for tests.

class calendaring_sync.store.memory.MemoryStore[source]#

Bases: StateStore

A store held in memory and lost when the process ends.

A write transaction works on a copy of the data, which replaces the data only when the transaction ends normally. Lookups scan every record, so it suits test data, not large stores. Meant for tests; use SQLiteStore to keep state.

A store kept in a SQLite database file.

class calendaring_sync.store.sqlite.SQLiteStore(path: str | PathLike[str], *, timeout: float = 5.0)[source]#

Bases: StateStore

A store in a SQLite database file, shared between threads and processes.

One connection serves every thread, one transaction at a time. Writes take SQLite’s write lock for their whole transaction, so two processes on one file never interleave. Reads don’t take that lock, but an open read still makes another process’s commit wait, so the store keeps reads short. A write waits up to timeout seconds for another process’s transaction to end.

Setting the file to SQLite’s WAL journal mode lets one process’s reads and another’s writes overlap. The mode is kept in the file, so set it once with PRAGMA journal_mode = WAL; WAL doesn’t work on a network filesystem.

Parameters:
  • path – The database file. It’s created, with the schema, if missing.

  • timeout – Seconds to wait for another process before giving up.

Raises:

Last updated on 2026-10-17.