content: snapshot cache over op-log replay (snapshot.sx) + 20 tests (338/338)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 30s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 30s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,7 @@ if [ ! -x "$SX_SERVER" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
SUITES=(block doc render api markdown validate store crdt crdt-store sync md-import fed)
|
||||
SUITES=(block doc render api markdown validate store snapshot crdt crdt-store sync md-import fed)
|
||||
|
||||
OUT_JSON="lib/content/scoreboard.json"
|
||||
OUT_MD="lib/content/scoreboard.md"
|
||||
@@ -45,6 +45,7 @@ run_suite() {
|
||||
(load "lib/content/markdown.sx")
|
||||
(load "lib/content/validate.sx")
|
||||
(load "lib/content/store.sx")
|
||||
(load "lib/content/snapshot.sx")
|
||||
(load "lib/content/crdt.sx")
|
||||
(load "lib/content/crdt-store.sx")
|
||||
(load "lib/content/sync.sx")
|
||||
|
||||
@@ -7,13 +7,14 @@
|
||||
"markdown": {"pass": 20, "fail": 0},
|
||||
"validate": {"pass": 17, "fail": 0},
|
||||
"store": {"pass": 29, "fail": 0},
|
||||
"snapshot": {"pass": 20, "fail": 0},
|
||||
"crdt": {"pass": 34, "fail": 0},
|
||||
"crdt-store": {"pass": 14, "fail": 0},
|
||||
"sync": {"pass": 14, "fail": 0},
|
||||
"md-import": {"pass": 24, "fail": 0},
|
||||
"fed": {"pass": 20, "fail": 0}
|
||||
},
|
||||
"total_pass": 318,
|
||||
"total_pass": 338,
|
||||
"total_fail": 0,
|
||||
"total": 318
|
||||
"total": 338
|
||||
}
|
||||
|
||||
@@ -11,9 +11,10 @@ _Generated by `lib/content/conformance.sh`_
|
||||
| markdown | 20 | 0 | 20 |
|
||||
| validate | 17 | 0 | 17 |
|
||||
| store | 29 | 0 | 29 |
|
||||
| snapshot | 20 | 0 | 20 |
|
||||
| crdt | 34 | 0 | 34 |
|
||||
| crdt-store | 14 | 0 | 14 |
|
||||
| sync | 14 | 0 | 14 |
|
||||
| md-import | 24 | 0 | 24 |
|
||||
| fed | 20 | 0 | 20 |
|
||||
| **Total** | **318** | **0** | **318** |
|
||||
| **Total** | **338** | **0** | **338** |
|
||||
|
||||
90
lib/content/snapshot.sx
Normal file
90
lib/content/snapshot.sx
Normal file
@@ -0,0 +1,90 @@
|
||||
;; content-on-sx — snapshot cache over the op-log replay.
|
||||
;;
|
||||
;; Snapshots are a CACHE, never primary state: the op log stays the source of
|
||||
;; truth. A snapshot stores a materialised document at a sequence in the persist
|
||||
;; KV; cached reads start from it and replay only the tail of ops, so they return
|
||||
;; a document IDENTICAL to a full replay — just faster. Drop the snapshot and
|
||||
;; nothing is lost.
|
||||
;;
|
||||
;; Requires (loaded by harness): store.sx (+ doc.sx, persist event/log/kv/api).
|
||||
|
||||
(define content/-snap-key (fn (doc-id) (str "content-snap:" doc-id)))
|
||||
|
||||
;; take a snapshot of the current head at the current version. Returns the seq.
|
||||
(define
|
||||
content/snapshot!
|
||||
(fn
|
||||
(b doc-id)
|
||||
(let
|
||||
((seq (content/version-count b doc-id)))
|
||||
(begin (persist/kv-put b (content/-snap-key doc-id) {:doc (content/head b doc-id) :seq seq}) seq))))
|
||||
|
||||
(define
|
||||
content/-snapshot
|
||||
(fn
|
||||
(b doc-id)
|
||||
(if
|
||||
(persist/kv-has? b (content/-snap-key doc-id))
|
||||
(persist/kv-get b (content/-snap-key doc-id))
|
||||
nil)))
|
||||
|
||||
(define
|
||||
content/snapshot-seq
|
||||
(fn
|
||||
(b doc-id)
|
||||
(let
|
||||
((s (content/-snapshot b doc-id)))
|
||||
(if (= s nil) 0 (get s :seq)))))
|
||||
|
||||
(define
|
||||
content/has-snapshot?
|
||||
(fn (b doc-id) (persist/kv-has? b (content/-snap-key doc-id))))
|
||||
|
||||
(define
|
||||
content/drop-snapshot!
|
||||
(fn (b doc-id) (persist/kv-delete b (content/-snap-key doc-id))))
|
||||
|
||||
;; ── cached reads (transparent: identical result to store.sx replay) ──
|
||||
(define
|
||||
content/-tail-ops
|
||||
(fn
|
||||
(b doc-id from to)
|
||||
(map
|
||||
(fn (ev) (persist/event-data ev))
|
||||
(filter
|
||||
(fn
|
||||
(ev)
|
||||
(and
|
||||
(> (persist/event-seq ev) from)
|
||||
(<= (persist/event-seq ev) to)))
|
||||
(content/log b doc-id)))))
|
||||
|
||||
(define
|
||||
content/head-cached
|
||||
(fn
|
||||
(b doc-id)
|
||||
(let
|
||||
((snap (content/-snapshot b doc-id)))
|
||||
(if
|
||||
(= snap nil)
|
||||
(content/head b doc-id)
|
||||
(doc-apply-all
|
||||
(get snap :doc)
|
||||
(content/-tail-ops
|
||||
b
|
||||
doc-id
|
||||
(get snap :seq)
|
||||
(content/version-count b doc-id)))))))
|
||||
|
||||
(define
|
||||
content/at-cached
|
||||
(fn
|
||||
(b doc-id seq)
|
||||
(let
|
||||
((snap (content/-snapshot b doc-id)))
|
||||
(if
|
||||
(or (= snap nil) (< seq (get snap :seq)))
|
||||
(content/at b doc-id seq)
|
||||
(doc-apply-all
|
||||
(get snap :doc)
|
||||
(content/-tail-ops b doc-id (get snap :seq) seq))))))
|
||||
100
lib/content/tests/snapshot.sx
Normal file
100
lib/content/tests/snapshot.sx
Normal file
@@ -0,0 +1,100 @@
|
||||
;; Extension — snapshot cache over op-log replay. The cache is transparent:
|
||||
;; cached reads equal full replays.
|
||||
|
||||
(st-bootstrap-classes!)
|
||||
(content-bootstrap-blocks!)
|
||||
(content-bootstrap-doc!)
|
||||
|
||||
(define B (persist/open))
|
||||
(define h (mk-heading "h" 1 "T"))
|
||||
(define p (mk-text "p" "Body"))
|
||||
(define img (mk-image "img" "/c.png" "cat"))
|
||||
|
||||
(content/commit! B "post" (op-insert h nil) 1)
|
||||
(content/commit! B "post" (op-insert p "h") 2)
|
||||
(content/commit! B "post" (op-insert img "h") 3)
|
||||
(content/commit! B "post" (op-update "p" "text" "Edited") 4)
|
||||
|
||||
;; ── no snapshot yet: cached == full replay ──
|
||||
(content-test
|
||||
"no snapshot head-cached == head"
|
||||
(doc-ids (content/head-cached B "post"))
|
||||
(doc-ids (content/head B "post")))
|
||||
(content-test
|
||||
"has-snapshot? false initially"
|
||||
(content/has-snapshot? B "post")
|
||||
false)
|
||||
(content-test
|
||||
"snapshot-seq 0 initially"
|
||||
(content/snapshot-seq B "post")
|
||||
0)
|
||||
|
||||
;; ── take a snapshot at seq 4 ──
|
||||
(content-test "snapshot returns seq" (content/snapshot! B "post") 4)
|
||||
(content-test "has-snapshot? true" (content/has-snapshot? B "post") true)
|
||||
(content-test "snapshot-seq is 4" (content/snapshot-seq B "post") 4)
|
||||
|
||||
;; cached head equals full head right after snapshot
|
||||
(content-test
|
||||
"head-cached == head after snap"
|
||||
(doc-ids (content/head-cached B "post"))
|
||||
(list "h" "img" "p"))
|
||||
(content-test
|
||||
"head-cached p value"
|
||||
(str (blk-send (doc-find (content/head-cached B "post") "p") "text"))
|
||||
"Edited")
|
||||
|
||||
;; ── commit more after the snapshot; cached head replays only the tail ──
|
||||
(content/commit! B "post" (op-delete "img") 5)
|
||||
(content/commit! B "post" (op-insert (mk-text "q" "New") "p") 6)
|
||||
(content-test
|
||||
"head-cached reflects post-snapshot ops"
|
||||
(doc-ids (content/head-cached B "post"))
|
||||
(doc-ids (content/head B "post")))
|
||||
(content-test
|
||||
"head-cached order"
|
||||
(doc-ids (content/head-cached B "post"))
|
||||
(list "h" "p" "q"))
|
||||
|
||||
;; ── at-cached transparency across versions ──
|
||||
(content-test
|
||||
"at-cached seq2 (before snap) == at"
|
||||
(doc-ids (content/at-cached B "post" 2))
|
||||
(doc-ids (content/at B "post" 2)))
|
||||
(content-test
|
||||
"at-cached seq5 (after snap) == at"
|
||||
(doc-ids (content/at-cached B "post" 5))
|
||||
(doc-ids (content/at B "post" 5)))
|
||||
(content-test
|
||||
"at-cached seq6 == at"
|
||||
(doc-ids (content/at-cached B "post" 6))
|
||||
(doc-ids (content/at B "post" 6)))
|
||||
(content-test
|
||||
"at-cached seq4 == snapshot version"
|
||||
(doc-ids (content/at-cached B "post" 4))
|
||||
(list "h" "img" "p"))
|
||||
|
||||
;; ── re-snapshot moves the cache forward ──
|
||||
(content-test "re-snapshot seq" (content/snapshot! B "post") 6)
|
||||
(content-test
|
||||
"head-cached still correct after resnap"
|
||||
(doc-ids (content/head-cached B "post"))
|
||||
(list "h" "p" "q"))
|
||||
|
||||
;; ── drop snapshot falls back to full replay, same result ──
|
||||
(content/drop-snapshot! B "post")
|
||||
(content-test "snapshot dropped" (content/has-snapshot? B "post") false)
|
||||
(content-test
|
||||
"head-cached == head after drop"
|
||||
(doc-ids (content/head-cached B "post"))
|
||||
(doc-ids (content/head B "post")))
|
||||
|
||||
;; ── snapshot of empty / fresh doc ──
|
||||
(content-test
|
||||
"snapshot empty doc seq 0"
|
||||
(content/snapshot! B "empty")
|
||||
0)
|
||||
(content-test
|
||||
"head-cached empty"
|
||||
(doc-ids (content/head-cached B "empty"))
|
||||
(list))
|
||||
@@ -19,7 +19,7 @@ injected adapter, not core.
|
||||
|
||||
## Status (rolling)
|
||||
|
||||
`bash lib/content/conformance.sh` → **318/318** (Phases 1–4 COMPLETE + extensions: HTML/SX escaping, Markdown render+import, durable CRDT replication, validation)
|
||||
`bash lib/content/conformance.sh` → **338/338** (Phases 1–4 COMPLETE + extensions: HTML/SX escaping, Markdown render+import, durable CRDT replication, validation, snapshot cache)
|
||||
|
||||
## Ground rules
|
||||
|
||||
@@ -82,9 +82,18 @@ lib/content/api.sx ── (content/edit) (content/render) (content/history) ─
|
||||
- [x] durable CRDT replication (`crdt-store.sx`: ops on persist, replay + converge)
|
||||
- [x] document validation (`validate.sx`: ids, per-type fields, duplicate ids)
|
||||
- [x] Markdown import adapter (`md-import.sx`: text → blocks, round-trips export)
|
||||
- [x] snapshot cache over replay (`snapshot.sx`: cache-not-primary, transparent)
|
||||
|
||||
## Progress log
|
||||
|
||||
- 2026-06-07 — Extension: snapshot cache over op-log replay (`snapshot.sx`).
|
||||
Snapshots are a cache, never primary state — the log stays the source of truth.
|
||||
`content/snapshot!` stores a materialised head at a seq in the persist KV;
|
||||
`content/head-cached` / `content/at-cached` start from the nearest snapshot and
|
||||
replay only the tail, returning a document IDENTICAL to a full replay (tests
|
||||
assert transparency before/after snapshot, across versions, and after
|
||||
drop-snapshot fallback). `content/has-snapshot?` / `snapshot-seq` /
|
||||
`drop-snapshot!`. 20 tests; suite 338/338.
|
||||
- 2026-06-07 — Extension: Markdown import adapter (`md-import.sx`), inverse of
|
||||
asMarkdown. Line-based parser: ATX headings, fenced code (```lang), blockquotes,
|
||||
unordered/ordered lists (grouping consecutive items), thematic breaks,
|
||||
|
||||
Reference in New Issue
Block a user