Merge worktree-zero-tooling-essay into macros
Resolves conflicts by keeping both: - HEAD: cache invalidation, service worker, sw-post-message - Worktree: optimistic mutations, offline queue, action endpoint Plans.sx unified with combined 7c/7d documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -887,6 +887,9 @@ def auto_mount_pages(app: Any, service_name: str) -> None:
|
||||
# Mount service worker at root scope for offline data layer
|
||||
mount_service_worker(app)
|
||||
|
||||
# Mount action endpoint for Phase 7c: optimistic data mutations
|
||||
mount_action_endpoint(app, service_name)
|
||||
|
||||
|
||||
def mount_pages(bp: Any, service_name: str,
|
||||
names: set[str] | list[str] | None = None) -> None:
|
||||
@@ -1225,3 +1228,56 @@ def mount_service_worker(app: Any) -> None:
|
||||
|
||||
app.add_url_rule("/sx-sw.js", endpoint="sx_service_worker", view_func=serve_sw)
|
||||
logger.info("Mounted service worker at /sx-sw.js")
|
||||
|
||||
|
||||
def mount_action_endpoint(app: Any, service_name: str) -> None:
|
||||
"""Mount /sx/action/<name> endpoint for client-side data mutations.
|
||||
|
||||
The client can POST to trigger a named action (registered via
|
||||
register_page_helpers with an 'action:' prefix). The action receives
|
||||
the JSON payload, performs the mutation, and returns the new page data
|
||||
as SX wire format.
|
||||
|
||||
This is the server counterpart to submit-mutation in orchestration.sx.
|
||||
"""
|
||||
from quart import make_response, request, abort as quart_abort
|
||||
from .parser import serialize
|
||||
from shared.browser.app.csrf import csrf_exempt
|
||||
|
||||
@csrf_exempt
|
||||
async def action_handler(name: str) -> Any:
|
||||
# Look up action helper
|
||||
helpers = get_page_helpers(service_name)
|
||||
action_fn = helpers.get(f"action:{name}")
|
||||
if action_fn is None:
|
||||
quart_abort(404)
|
||||
|
||||
# Parse JSON payload
|
||||
import asyncio as _asyncio
|
||||
data = await request.get_json(silent=True) or {}
|
||||
|
||||
try:
|
||||
result = action_fn(**data)
|
||||
if _asyncio.iscoroutine(result):
|
||||
result = await result
|
||||
except Exception as e:
|
||||
logger.warning("Action %s failed: %s", name, e)
|
||||
resp = await make_response(f'(dict "error" "{e}")', 500)
|
||||
resp.content_type = "text/sx; charset=utf-8"
|
||||
return resp
|
||||
|
||||
result_sx = serialize(result) if result is not None else "nil"
|
||||
resp = await make_response(result_sx, 200)
|
||||
resp.content_type = "text/sx; charset=utf-8"
|
||||
return resp
|
||||
|
||||
action_handler.__name__ = "sx_action"
|
||||
action_handler.__qualname__ = "sx_action"
|
||||
|
||||
app.add_url_rule(
|
||||
"/sx/action/<name>",
|
||||
endpoint="sx_action",
|
||||
view_func=action_handler,
|
||||
methods=["POST"],
|
||||
)
|
||||
logger.info("Mounted action endpoint for %s at /sx/action/<name>", service_name)
|
||||
|
||||
@@ -663,6 +663,139 @@
|
||||
(update-page-cache hdr-update data)))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Optimistic data updates (Phase 7c)
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Client-side predicted mutations with rollback.
|
||||
;; submit-mutation applies a predicted update immediately, sends the mutation
|
||||
;; to the server, and either confirms or reverts based on the response.
|
||||
|
||||
(define _optimistic-snapshots (dict))
|
||||
|
||||
(define optimistic-cache-update
|
||||
(fn (cache-key mutator)
|
||||
;; Apply predicted mutation to cached data. Saves snapshot for rollback.
|
||||
;; Returns predicted data or nil if no cached data exists.
|
||||
(let ((cached (page-data-cache-get cache-key)))
|
||||
(when cached
|
||||
(let ((predicted (mutator cached)))
|
||||
;; Save original for revert
|
||||
(dict-set! _optimistic-snapshots cache-key cached)
|
||||
;; Update cache with prediction
|
||||
(page-data-cache-set cache-key predicted)
|
||||
predicted)))))
|
||||
|
||||
(define optimistic-cache-revert
|
||||
(fn (cache-key)
|
||||
;; Revert to pre-mutation snapshot. Returns restored data or nil.
|
||||
(let ((snapshot (get _optimistic-snapshots cache-key)))
|
||||
(when snapshot
|
||||
(page-data-cache-set cache-key snapshot)
|
||||
(dict-delete! _optimistic-snapshots cache-key)
|
||||
snapshot))))
|
||||
|
||||
(define optimistic-cache-confirm
|
||||
(fn (cache-key)
|
||||
;; Server accepted — discard the rollback snapshot.
|
||||
(dict-delete! _optimistic-snapshots cache-key)))
|
||||
|
||||
(define submit-mutation
|
||||
(fn (page-name params action-name payload mutator-fn on-complete)
|
||||
;; Optimistic mutation: predict locally, send to server, confirm or revert.
|
||||
;; on-complete is called with "confirmed" or "reverted" status.
|
||||
(let ((cache-key (page-data-cache-key page-name params))
|
||||
(predicted (optimistic-cache-update cache-key mutator-fn)))
|
||||
;; Re-render with predicted data immediately
|
||||
(when predicted
|
||||
(try-rerender-page page-name params predicted))
|
||||
;; Send to server
|
||||
(execute-action action-name payload
|
||||
(fn (result)
|
||||
;; Success: update cache with server truth, confirm
|
||||
(when result
|
||||
(page-data-cache-set cache-key result))
|
||||
(optimistic-cache-confirm cache-key)
|
||||
(when result
|
||||
(try-rerender-page page-name params result))
|
||||
(log-info (str "sx:optimistic confirmed " page-name))
|
||||
(when on-complete (on-complete "confirmed")))
|
||||
(fn (error)
|
||||
;; Failure: revert to snapshot
|
||||
(let ((reverted (optimistic-cache-revert cache-key)))
|
||||
(when reverted
|
||||
(try-rerender-page page-name params reverted))
|
||||
(log-warn (str "sx:optimistic reverted " page-name ": " error))
|
||||
(when on-complete (on-complete "reverted"))))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Offline data layer (Phase 7d)
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Connectivity tracking + offline mutation queue.
|
||||
;; When offline, mutations are queued locally. On reconnect, queued mutations
|
||||
;; are replayed in order via submit-mutation.
|
||||
|
||||
(define _is-online true)
|
||||
(define _offline-queue (list))
|
||||
|
||||
(define offline-is-online?
|
||||
(fn () _is-online))
|
||||
|
||||
(define offline-set-online!
|
||||
(fn (val)
|
||||
(set! _is-online val)))
|
||||
|
||||
(define offline-queue-mutation
|
||||
(fn (action-name payload page-name params mutator-fn)
|
||||
;; Queue a mutation for later sync. Apply optimistic update locally.
|
||||
(let ((cache-key (page-data-cache-key page-name params))
|
||||
(entry (dict
|
||||
"action" action-name
|
||||
"payload" payload
|
||||
"page" page-name
|
||||
"params" params
|
||||
"timestamp" (now-ms)
|
||||
"status" "pending")))
|
||||
(append! _offline-queue entry)
|
||||
;; Apply optimistic locally (reuses Phase 7c)
|
||||
(let ((predicted (optimistic-cache-update cache-key mutator-fn)))
|
||||
(when predicted
|
||||
(try-rerender-page page-name params predicted)))
|
||||
(log-info (str "sx:offline queued " action-name " (" (len _offline-queue) " pending)"))
|
||||
entry)))
|
||||
|
||||
(define offline-sync
|
||||
(fn ()
|
||||
;; Replay all pending mutations. Called on reconnect.
|
||||
(let ((pending (filter (fn (e) (= (get e "status") "pending")) _offline-queue)))
|
||||
(when (not (empty? pending))
|
||||
(log-info (str "sx:offline syncing " (len pending) " mutations"))
|
||||
(for-each
|
||||
(fn (entry)
|
||||
(execute-action (get entry "action") (get entry "payload")
|
||||
(fn (result)
|
||||
(dict-set! entry "status" "synced")
|
||||
(log-info (str "sx:offline synced " (get entry "action"))))
|
||||
(fn (error)
|
||||
(dict-set! entry "status" "failed")
|
||||
(log-warn (str "sx:offline sync failed " (get entry "action") ": " error)))))
|
||||
pending)))))
|
||||
|
||||
(define offline-pending-count
|
||||
(fn ()
|
||||
(len (filter (fn (e) (= (get e "status") "pending")) _offline-queue))))
|
||||
|
||||
(define offline-aware-mutation
|
||||
(fn (page-name params action-name payload mutator-fn on-complete)
|
||||
;; Top-level mutation function. Routes to submit-mutation when online,
|
||||
;; offline-queue-mutation when offline.
|
||||
(if _is-online
|
||||
(submit-mutation page-name params action-name payload mutator-fn on-complete)
|
||||
(do
|
||||
(offline-queue-mutation action-name payload page-name params mutator-fn)
|
||||
(when on-complete (on-complete "queued"))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Client-side routing
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -1137,6 +1270,10 @@
|
||||
;; when data is available. params is a dict of URL/route parameters.
|
||||
;; (parse-sx-data text) → parsed SX data value, or nil on error.
|
||||
;; Used by cache update to parse server-provided data in SX format.
|
||||
;; (execute-action name payload on-success on-error) → void; POST to server,
|
||||
;; calls (on-success data-dict) or (on-error message).
|
||||
;; (try-rerender-page page-name params data) → void; re-evaluate and swap
|
||||
;; the current page content with updated data bindings.
|
||||
;;
|
||||
;; From boot.sx:
|
||||
;; _page-routes → list of route entries
|
||||
@@ -1160,4 +1297,8 @@
|
||||
;; === Cache management ===
|
||||
;; (parse-sx-data text) → parsed SX data value, or nil on error
|
||||
;; (sw-post-message msg) → void; post message to active service worker
|
||||
;;
|
||||
;; === Offline persistence ===
|
||||
;; (persist-offline-data key data) → void; write to IndexedDB
|
||||
;; (retrieve-offline-data key cb) → void; read from IndexedDB, calls (cb data)
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user