host: relations-as-posts slice 2.5 — picker title reads are O(page), not O(pool)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 45s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 45s
relate-candidates computes the available candidate SLUGS (slug-sorted, no per-candidate read), then reads titles only for the page it returns. On the unfiltered path (q="" — the initial picker load AND every editor server-fill, the common case) that's ~limit durable reads instead of one-per-post, cutting the http-listen suspend/resume churn. A filter (q≠"") still resolves titles across the pool since it matches on the title. (A boot slug→title cache would make the filter O(1)-perform too, but it's blocked: no bulk KV read, and a per-post host/blog-get loop at boot hits the JIT 'durable read in a boot loop drops all-but-first' bug — see plans/relations-as-posts.md.) conformance 291/291, run-picker-check 3/3 (incl. the title filter + paging). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -418,24 +418,34 @@
|
||||
(fn (kind other)
|
||||
(contains? (host/blog--candidate-pool kind) other)))
|
||||
|
||||
(define host/blog--title (fn (s) (get (host/blog-get s) :title))) ;; one durable read
|
||||
|
||||
;; One PAGE of candidates (records {:slug :title}) for relating `slug` under `kind`.
|
||||
;; Slice 2.5 — title reads are O(page), not O(pool): the available candidate SLUGS are
|
||||
;; computed + slug-sorted with NO per-candidate read; then titles are fetched only for
|
||||
;; the rows actually returned. On the unfiltered path (q="" — the initial picker load
|
||||
;; AND every editor server-fill) that's ~`limit` reads instead of one-per-post, which
|
||||
;; was the durable-read churn under http-listen. A filter (q≠"") still resolves titles
|
||||
;; across the pool, since it matches on the title — but that's the interactive path.
|
||||
(define host/blog--relate-candidates
|
||||
(fn (slug q kind)
|
||||
(let ((spec (host/blog--kind-spec kind)))
|
||||
(let ((pool (host/blog--candidate-pool kind))
|
||||
(already (host/blog-out slug kind))
|
||||
(ql (lower (or q ""))))
|
||||
;; pool is slugs; resolve titles, drop self + already-linked, filter by q
|
||||
(let ((cands
|
||||
(filter
|
||||
(fn (p)
|
||||
(or (= ql "")
|
||||
(contains? (lower (get p :title)) ql)
|
||||
(contains? (get p :slug) ql)))
|
||||
(map (fn (s) {:slug s :title (get (host/blog-get s) :title)})
|
||||
(filter (fn (s) (and (not (= s slug)) (not (contains? already s)))) pool)))))
|
||||
;; title-sort via [title slug] pairs (sort compares the title first)
|
||||
(map (fn (pair) {:slug (nth pair 1) :title (nth pair 0)})
|
||||
(sort (map (fn (p) (list (get p :title) (get p :slug))) cands))))))))
|
||||
(fn (slug q kind offset limit)
|
||||
(let ((pool (host/blog--candidate-pool kind))
|
||||
(already (host/blog-out slug kind))
|
||||
(ql (lower (or q ""))))
|
||||
(let ((avail (sort (filter (fn (s) (and (not (= s slug)) (not (contains? already s)))) pool))))
|
||||
(if (= ql "")
|
||||
;; no filter: page by slug, then read titles for just the page
|
||||
(map (fn (s) {:slug s :title (host/blog--title s)})
|
||||
(take (drop avail offset) limit))
|
||||
;; filter: resolve titles, match on title|slug, then page
|
||||
(let ((recs (map (fn (s) {:slug s :title (host/blog--title s)}) avail)))
|
||||
(take
|
||||
(drop
|
||||
(filter (fn (r) (or (contains? (lower (get r :title)) ql)
|
||||
(contains? (get r :slug) ql)))
|
||||
recs)
|
||||
offset)
|
||||
limit)))))))
|
||||
|
||||
;; One candidate row: a tiny form whose button adds the relation under `kind`.
|
||||
(define host/blog--picker-item
|
||||
@@ -492,8 +502,7 @@
|
||||
;; so a filter like "Item 13" arrives as "Item%2013" — decode it.
|
||||
(q (dr/url-decode (or (dream-query-param req "q") "")))
|
||||
(offset (host/query-int req "offset" 0)))
|
||||
(let ((page (take (drop (host/blog--relate-candidates slug q kind) offset)
|
||||
host/blog--picker-limit)))
|
||||
(let ((page (host/blog--relate-candidates slug q kind offset host/blog--picker-limit)))
|
||||
(let ((rows (join "" (map (fn (p) (render-page (host/blog--picker-item slug p kind))) page)))
|
||||
(more (if (= (len page) host/blog--picker-limit)
|
||||
(render-page (host/blog--picker-more slug kind q (+ offset host/blog--picker-limit)))
|
||||
@@ -686,8 +695,7 @@
|
||||
;; li-trees splice in as children (component args would evaluate them).
|
||||
(results-ul
|
||||
(let ((rows (if with-cands
|
||||
(let ((cands (take (host/blog--relate-candidates slug "" kind)
|
||||
host/blog--picker-limit)))
|
||||
(let ((cands (host/blog--relate-candidates slug "" kind 0 host/blog--picker-limit)))
|
||||
(append
|
||||
(map (fn (p) (host/blog--picker-item slug p kind)) cands)
|
||||
(if (= (len cands) host/blog--picker-limit)
|
||||
|
||||
@@ -58,10 +58,20 @@ the relation's object-end declaration from the anchor**, which includes the root
|
||||
`host/blog-rel-kinds` is a VALUE the boot populates and the cache loads are UNROLLED.
|
||||
**Conformance green ≠ correct live — verify the rendered edit page.** (Re-fold the
|
||||
enumeration once plans/jit-bytecode-correctness.md lands.)
|
||||
- **Follow-up (Slice 2.5):** `relate-candidates` does a `host/blog-get` per pool member
|
||||
(O(posts) for `related`). A boot-time **title cache** (updated on put!/delete!) would make
|
||||
the picker O(1)-perform and cut the suspend/resume churn. Subject-end declarations + a
|
||||
proper relation-subtype closure (when relations get subtyped) also belong here.
|
||||
### Slice 2.5 — picker title reads are O(page), not O(pool) — DONE
|
||||
- `relate-candidates` computes the available candidate SLUGS (slug-sorted, no per-candidate
|
||||
read), then reads titles ONLY for the page it returns. On the unfiltered path (q="" — the
|
||||
initial picker load AND every editor server-fill, the common case) that's ~`limit` reads
|
||||
instead of one-per-post — killing the durable-read churn under http-listen. A filter
|
||||
(q≠"") still resolves titles across the pool (it matches on the title), but that's the
|
||||
interactive path.
|
||||
- A boot-time slug→title **cache** would make even the filter O(1)-perform, BUT it's blocked
|
||||
for now: there's no bulk KV read, and a per-post `host/blog-get` loop **at boot** hits the
|
||||
JIT bug (a durable read inside a boot loop drops all-but-first — `load-edges!` only works
|
||||
because its loop body is perform-free). Revisit with a bulk read or once the JIT lands.
|
||||
|
||||
**Remaining follow-ups:** subject-end declarations (who may be the *source*); a proper
|
||||
relation-subtype closure when relations get subtyped; the boot title cache above.
|
||||
|
||||
### Slice 3 — typed relations (target-type constraints) — DONE
|
||||
- The declaration's `declares`-anchor IS the target-type constraint: `is-a`/`subtype-of`
|
||||
|
||||
Reference in New Issue
Block a user