diff --git a/lib/host/blog.sx b/lib/host/blog.sx index 136da17b..947f38d7 100644 --- a/lib/host/blog.sx +++ b/lib/host/blog.sx @@ -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) diff --git a/plans/relations-as-posts.md b/plans/relations-as-posts.md index 83b4343f..4529253a 100644 --- a/plans/relations-as-posts.md +++ b/plans/relations-as-posts.md @@ -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`