Spaces:
Running on Zero
Running on Zero
File size: 5,383 Bytes
31c93b1 481b78e 31c93b1 a190f73 f08047d 31c93b1 4aaae80 c91b229 4aaae80 31c93b1 4aaae80 31c93b1 4aaae80 31c93b1 4aaae80 31c93b1 4aaae80 31c93b1 4aaae80 31c93b1 4aaae80 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | from __future__ import annotations
import uuid
from datetime import datetime, timedelta, timezone as _tz
from hearthnet.bus.capability import CapabilityDescriptor, RouteRequest
UTC = _tz.utc
from hearthnet.constants import MARKET_DEFAULT_TTL_SECONDS
from hearthnet.services.marketplace.views import MarketplaceView
class MarketplaceService:
name = "marketplace"
version = "1.0"
def __init__(self, event_log=None, node_id: str = "") -> None:
self._event_log = event_log # optional X02 EventLog
self._node_id = node_id
self._view = MarketplaceView()
self._sweep_task = None
self._posts_demo: list[dict] = []
@property
def posts(self) -> list[dict]:
"""Backward-compatible access to demo-mode post list."""
return self._posts_demo
def capabilities(self) -> list[tuple]:
return [
(
CapabilityDescriptor(name="market.post", max_concurrent=4, idempotent=True),
self.handle_post,
None,
),
(
CapabilityDescriptor(name="market.list", max_concurrent=8, idempotent=True),
self.handle_list,
None,
),
(
CapabilityDescriptor(name="market.expire", max_concurrent=4, idempotent=True),
self.handle_expire,
None,
),
(
CapabilityDescriptor(name="market.search", max_concurrent=4, idempotent=True),
self.handle_search,
None,
),
(
CapabilityDescriptor(name="market.delete", max_concurrent=4),
self.handle_expire, # delete = immediate expire
None,
),
]
async def handle_post(self, req: RouteRequest) -> dict:
payload = dict(req.body.get("input", {}))
event_id = payload.get("event_id") or f"evt:{uuid.uuid4().hex}"
payload.setdefault("client_id", event_id)
payload.setdefault("author", req.caller)
payload.setdefault("created_at", _iso_now())
payload.setdefault("expires_at", _iso_after(MARKET_DEFAULT_TTL_SECONDS))
payload.setdefault("category", "info")
if self._event_log is not None:
try:
event = self._event_log.append_local(
event_type="market.post.created",
author=req.caller,
payload=payload,
)
self._view.apply(event)
return {
"output": {"event_id": event.event_id, "lamport": event.lamport},
"meta": {},
}
except Exception:
pass # fall through to demo mode
# Demo mode (no event log)
payload["event_id"] = event_id
payload["lamport"] = len(self._posts_demo) + 1
self._posts_demo.append(payload)
return {"output": {"event_id": event_id, "lamport": len(self._posts_demo)}, "meta": {}}
async def handle_list(self, req: RouteRequest) -> dict:
category = req.body.get("input", {}).get("category")
if self._event_log is not None:
posts = self._view.all_active()
result = [p.as_dict() for p in posts if not category or p.category == category]
else:
result = [p for p in self._posts_demo if not category or p.get("category") == category]
return {"output": {"posts": result, "max_lamport": len(result)}, "meta": {}}
async def handle_expire(self, req: RouteRequest) -> dict:
inp = req.body.get("input", {})
target_event_id = inp.get("event_id", "")
if self._event_log is not None:
try:
event = self._event_log.append_local(
event_type="market.post.expired",
author=req.caller,
payload={
"target_event_id": target_event_id,
"reason": inp.get("reason", "manual"),
},
)
self._view.apply(event)
return {"output": {"expired": True, "event_id": target_event_id}, "meta": {}}
except Exception:
pass
# Demo mode
self._posts_demo = [p for p in self._posts_demo if p.get("event_id") != target_event_id]
return {"output": {"expired": True, "event_id": target_event_id}, "meta": {}}
async def handle_search(self, req: RouteRequest) -> dict:
query = req.body.get("input", {}).get("query", "").lower()
if self._event_log is not None:
posts = self._view.all_active()
result = [
p.as_dict() for p in posts if query in p.title.lower() or query in p.body.lower()
]
return {"output": {"posts": result}, "meta": {}}
# Demo mode
result = [
p
for p in self._posts_demo
if query in p.get("title", "").lower() or query in p.get("body", "").lower()
]
return {"output": {"posts": result}, "meta": {}}
def _iso_now() -> str:
return datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
def _iso_after(seconds: int) -> str:
return (datetime.now(UTC) + timedelta(seconds=seconds)).strftime("%Y-%m-%dT%H:%M:%SZ")
|