Files
DawnsorrowandCursor 0d6b0b2f80 Initial SyncGames tree: agent, Android, deploy, docs.
Session-gated MinIO save sync with AppImage GUI, CLI edit/session flow, and Gitea release helper.

Co-authored-by: Cursor <[email protected]>
2026-07-14 22:06:36 -05:00

80 lines
2.4 KiB
Python

from __future__ import annotations
import uuid
from datetime import timedelta
from typing import Any
from syncgames.errors import LeaseHeldError
from syncgames.store import Meta, ObjectStore, parse_expires, utc_now
def lease_active(lease: dict[str, Any] | None) -> bool:
if not lease:
return False
exp = parse_expires(lease.get("expires_at"))
if exp is None:
return False
return exp > utc_now()
def acquire_lease(
store: ObjectStore,
game_id: str,
device_id: str,
*,
ttl_hours: int,
versions_to_keep: int,
break_other: bool = False,
) -> Meta:
meta = store.get_meta(game_id)
if meta is None:
from syncgames.store import empty_meta
meta = empty_meta(game_id, versions_to_keep)
lease = meta.lease
if lease_active(lease) and lease and lease.get("holder") != device_id:
if not break_other:
raise LeaseHeldError(
f"Lease held by {lease.get('holder')} until {lease.get('expires_at')}"
)
session_id = str(uuid.uuid4())
expires = utc_now() + timedelta(hours=ttl_hours)
meta.raw["lease"] = {
"holder": device_id,
"expires_at": expires.strftime("%Y-%m-%dT%H:%M:%SZ"),
"session_id": session_id,
}
meta.raw["versions_to_keep"] = versions_to_keep
store.put_meta(game_id, meta)
# Re-read to reduce lost-update window (optimistic)
again = store.get_meta(game_id)
if again is None or not again.lease or again.lease.get("holder") != device_id:
raise LeaseHeldError("Lost lease race — another device acquired the lease")
return again
def release_lease(store: ObjectStore, game_id: str, device_id: str, meta: Meta | None = None) -> Meta:
meta = meta or store.get_meta(game_id)
if meta is None:
from syncgames.store import empty_meta
meta = empty_meta(game_id)
lease = meta.lease
if lease and lease.get("holder") not in (None, device_id):
if lease_active(lease):
raise LeaseHeldError(f"Cannot release lease owned by {lease.get('holder')}")
meta.raw["lease"] = None
store.put_meta(game_id, meta)
return meta
def break_lease(store: ObjectStore, game_id: str) -> Meta:
from syncgames.store import empty_meta
meta = store.get_meta(game_id) or empty_meta(game_id)
meta.raw["lease"] = None
store.put_meta(game_id, meta)
return meta