diff --git a/src/energiamesh/core.py b/src/energiamesh/core.py index fe22c14..b001051 100644 --- a/src/energiamesh/core.py +++ b/src/energiamesh/core.py @@ -93,6 +93,8 @@ class PlanDelta: nonce: str | None = None # Optional source attribution for auditing and routing source: str | None = None + # Optional replay trace for offline/delta-sync recovery (lightweight) + replay_log: list[str] = field(default_factory=list) @dataclass diff --git a/src/energiamesh/sync.py b/src/energiamesh/sync.py index c06aef1..6881145 100644 --- a/src/energiamesh/sync.py +++ b/src/energiamesh/sync.py @@ -30,3 +30,27 @@ def compute_delta(lp: LocalProblem, sv: SharedVariables) -> PlanDelta: delta = PlanDelta(delta=payload, delta_id=f"d-{int(time.time())}") return delta + + +class DeltaStore: + """Simple, in-process store for PlanDelta objects per site. + + This enables offline-first delta synchronization by persisting deltas + locally and replaying them in order when connectivity is restored. + """ + + def __init__(self) -> None: + # Mapping: site_id -> list of PlanDelta (in order of arrival) + self._store: Dict[str, list[PlanDelta]] = {} + + def add_delta(self, site_id: str, delta: PlanDelta) -> None: + if site_id not in self._store: + self._store[site_id] = [] + self._store[site_id].append(delta) + + def get_deltas(self, site_id: str) -> list[PlanDelta]: + return list(self._store.get(site_id, [])) + + def replay(self, site_id: str) -> list[PlanDelta]: + """Return a copy of the deltas for replay in the original order.""" + return self.get_deltas(site_id) diff --git a/tests/test_delta_store.py b/tests/test_delta_store.py new file mode 100644 index 0000000..c9c4622 --- /dev/null +++ b/tests/test_delta_store.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +import time +from energiamesh.core import LocalProblem, SharedVariables, PlanDelta +from energiamesh.sync import compute_delta, DeltaStore + + +def test_delta_store_basic_store_and_replay(): + # Prepare sample problem/state + lp = LocalProblem(site_id="site-1", problem_id="p1", description="test") + sv = SharedVariables(signals={"load_forecast": 123.0}) + + # Create two deltas to simulate incremental updates + d1 = compute_delta(lp, sv) + time.sleep(0.001) + sv.update("load_forecast", 120.0) + d2 = compute_delta(lp, sv) + + store = DeltaStore() + store.add_delta(lp.site_id, d1) + store.add_delta(lp.site_id, d2) + + # Replay should yield deltas in order + deltas = store.replay(lp.site_id) + assert len(deltas) == 2 + assert isinstance(deltas[0], PlanDelta) + assert deltas[0].delta == d1.delta + assert deltas[1].delta == d2.delta