State and Transactions Specification¶
Status: implemented (Task 5). Mirrors in TypeScript
(packages/driver/src/state/*.ts) and Python (python/reactivegraph/reactivegraph/state.py).
1. Canonical paths¶
A path is an ordered list of segments: string segments address object keys,
numeric segments address array indexes. Canonical string encoding a.b[0].c:
- string segments joined by ., with . escaped as \. and [ as \[;
- numeric segments written as [n] with no leading dot.
Round-trip is exact in both languages; pathToString/parsePath in TS and
path_to_string/parse_path in Python are the byte-identical implementations.
2. Patches¶
A patch carries:
interface Patch {
path: Path
operation: "set" | "delete" | "append" | "insert" | "remove"
| "map_set" | "map_delete" | "set_add" | "set_delete"
beforeHash: string | null // SHA-256 of canonical msgpack pre-image
value?: unknown
taskId: string
transactionId: string
}
beforeHashis the SHA-256 hex digest of the canonical msgpack encoding (RGP/1 codec) of the value atpathbefore the operation.- Missing paths hash the fixed marker
"rgp:missing"in both languages, so a pre-image for a never-set path is stable and equal everywhere. applyPatchrejects a mismatched pre-image withPatchErrorunless a reducer owns the path (Task 10).
3. Store and versioning¶
ReactiveStore wraps @vue/reactivity (TypeScript):
- raw holds the plain state object; state is the deep reactive proxy.
- In-transaction mutations go to raw directly (no auto-trigger).
- commitVersion(writes) applies one synchronous burst of public trigger
calls (with ITERATE_KEY complements for add/delete) so all computed values
observe a single atomic version bump. Version increments once per commit.
- Dependency capture uses public onTrack/onTrigger (dev) and manual
track/trigger (prod) — never Vue private internals (Architecture Invariant 6;
see docs/research/vue-reactivity-api.md).
4. Transactions¶
Transaction semantics:
open → committed | rolled_back, with an optionalexpectedVersion.read(path)records read paths (canonical strings) for dependency/conflict sets.set(path, value)mutates raw, records a write with pre-image; rejects readonly paths (ReadonlyPathError) and stale versions (StaleVersionError).commit()returns the patch list and bumps the store version once.rollback()restores all prior values without triggering.- After commit/rollback any further write raises.
Conflict policy (Appendix C.5): reads record transaction version + path hashes; disjoint writes may commit concurrently; same-path writes require a reducer, explicit priority, or serialized execution; a stale pure task may rerun with a new snapshot; a stale effect task may not rerun until its receipt is inspected.
5. Python TrackedStateProxy¶
TrackedStateProxy is the Python binding mirror:
- recursive dict/list wrappers with base_path chains rooted at a shared raw object;
- reads record canonical path strings (recorded on every ancestor, deduplicated);
- __setitem__/__delitem__/append record mutation patches with pre-image hash;
- snapshot() returns a plain deep copy for opaque libraries;
- canonical_hash produces the byte-identical digest to the TS side.
6. Shared patch corpus¶
Both test suites hard-code the identical SHA-256 digests for the same values
(packages/driver/test/state/transaction.test.ts and
python/reactivegraph/tests/test_tracked_state.py):
| Value | SHA-256 (hex, canonical msgpack) |
|---|---|
1 |
4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a |
"hello" |
2b57c5b79a3aee10237006d2fc64b7ecd13b761867f5992f43eda5777a0726d9 |
[1,2,3] |
efd2ce5d1b243784f054828796128a9e3f85044cbfc21f7144a7a448ea3361e6 |
{a:1,b:[true,null]} |
781ba872c03932379df02707033108b09a372c8e65767985971101cde7d79c0a |
"rgp:missing" |
e6849aec7e0b007792f96dd2ed18e7388696b0d6eca1d01f2a4b1f28d6889351 |
A regression in either language's canonical encoding is caught by the corpus.