Skip to content

fix(api): create_page must store real map properties, not a cljs-bean view - #42

Open
Lcstyle wants to merge 1 commit into
logseq:version/filefrom
Lcstyle:fix/plugin-create-page-bean-properties
Open

fix(api): create_page must store real map properties, not a cljs-bean view#42
Lcstyle wants to merge 1 commit into
logseq:version/filefrom
Lcstyle:fix/plugin-create-page-bean-properties

Conversation

@Lcstyle

@Lcstyle Lcstyle commented Aug 5, 2026

Copy link
Copy Markdown

Fixes the root cause of logseq/logseq#8536, open
since Feb 2023 with no known reproduction until now.

The bug

logseq.api/create_page converts the caller's properties object with bean/->clj
(src/main/logseq/api.cljs:559). That returns a lazy cljs-bean Bean view, not a persistent
map, and frontend.handler.page/create! stores it verbatim as the page entity's
:block/properties (src/main/frontend/handler/page.cljs:107).

datascript-transit 0.3.0 registers write handlers only for DB, Datom and BTSet, so a Bean
is unwritable. From the moment any plugin creates a page with a non-empty properties map, every
frontend.db/persist! throws:

Error: Cannot write $cljs_bean$core$Bean$$
    at com.cognitect.transit.impl.writer/marshal
    ... frontend.db.utils/db->string (db/utils.cljs:12) ... frontend.db/persist! (db.cljs:94)

The graph cache is then never written again for the rest of the session, and because the failure
reply has no handler in the main process the window can never be closed either (that half is
addressed separately, see the companion PR).

This is why the issue looked random for three years: it isn't sleep, hibernation, git auto-commit,
graph size or sync. It is any plugin that creates a page with properties. Several people in that
thread listed plugins that do exactly that.

Reproduction

Any graph, developer console:

await logseq.api.force_save_graph();                    // ok
await logseq.api.create_page('zz-repro', { a: 'b' });   // the trigger
await logseq.api.force_save_graph();                    // throws "Cannot write ...Bean"
await logseq.api.delete_page('zz-repro');
await logseq.api.force_save_graph();                    // ok again

Measured on 0.10.15 against a 5228-page graph, using the .transit mtime as ground truth for
"did the save actually happen":

Step ~/.logseq/graphs/<graph>.transit Error
baseline persist 08:27:46 → 08:32:34 none
create_page(name, {foo:'bar'})
persist 08:32:34 (frozen) Cannot write $cljs_bean$core$Bean$$
delete that one page, persist 08:33:32 none

Single variable changed, GREEN → RED → GREEN.

Only a non-empty properties map triggers it, which matches create!'s (when (seq properties) ...)
guard:

Call Persist
create_page(name) ok
create_page(name, {}) ok
create_page(name, {a:'b'}) broken

The fix

js->clj with :keywordize-keys converts deeply and eagerly, so nested objects and arrays become
real maps and vectors instead of nested Bean / ArrayVector views. The depth matters: a shallow
conversion would still leave an ArrayVector for something like tags: [].

Only the properties argument is changed. opts is destructured immediately and never stored, so it
can stay as it is.

Worth noting the current master (2.x) uses exactly this idiom in the same function
(js->clj opts :keywordize-keys true), so this aligns the file version with where upstream already went.

Verification

Built this branch (clojure -M:cljs release app electron --debug, 0 warnings) and ran it against a
real 5228-page graph:

transit Bean error
baseline persist 103437381 → 103437416 none
create_page(name, {foo:'bar', baz:'qux'})
persist with properties present 103437416 → 103438657 none
after deleting the page → 103437416 none

Properties read back correctly as {"foo":"bar","baz":"qux"}. The same sequence on the unpatched
build freezes the transit file and throws.

Scope

I audited the other API entry points that call bean/->clj on caller data. The block-writing ones
(insert_block, insert_batch_block, append_block_in_page) are not affected: they render
properties into the block's markdown as a:: b and re-parse them, which produces a genuine map, so
the Bean is transient. create_page is the only path that stores the converted value directly.

… view

logseq.api/create_page converted the caller-supplied properties object with
bean/->clj. That returns a lazy cljs-bean Bean view rather than a persistent
map, and frontend.handler.page/create! stores it verbatim as the page entity's
:block/properties.

datascript-transit registers write handlers only for DB, Datom and BTSet, so a
Bean is unwritable. From the moment a plugin creates a page with a non-empty
properties map, every frontend.db/persist! throws

    Error: Cannot write $cljs_bean$core$Bean$$
        at com.cognitect.transit.impl.writer/marshal
        ... frontend.db.utils/db->string ... frontend.db/persist!

and the graph cache is never written again for the rest of the session.

js->clj with :keywordize-keys converts deeply and eagerly, so nested objects
and arrays become maps and vectors rather than nested Bean/ArrayVector views.
Only the properties argument is changed; opts is destructured immediately and
never stored, so it can stay as it is.

Empty property maps were unaffected because create! guards with (seq properties),
which is why this only reproduced for plugins that pass actual properties.

Refs logseq/logseq#8536
@Lcstyle

Lcstyle commented Aug 5, 2026

Copy link
Copy Markdown
Author

Context on the red check, so you don't have to dig into it:

The only workflow that ran is PR Labeler, and it fails with
HttpError: No commit found for the ref fix/plugin-create-page-bean-properties.
That's pull_request_target resolving the head ref against the base repo, so it can't see a branch
that lives in a fork — a known limitation of TimonVS/pr-labeler-action. It's not specific to this
PR: every PR Labeler run in this repo has failed the same way, including chore/readme-og-explanation
and og-readme on 2026-07-17. Nothing in the diff causes it and there's nothing I can do from a fork.

Also worth flagging: no build or test workflow ran here at all. build.yml, db.yml, e2e.yml,
graph-parser.yml and publishing.yml are all gated on branches: [master] for both push and
pull_request, so PRs targeting version/file don't trigger any of them. pr-labeler.yml is the
only workflow without a branch filter, which is why it's the only one that ran.

Since CI won't cover this, here's what I did verify locally — a release app electron --debug build
of this branch (0 warnings), run against a real 5228-page file graph:

.transit Bean error
baseline persist 103437381 → 103437416 none
create_page(name, {foo:'bar', baz:'qux'})
persist with properties present 103437416 → 103438657 none
after deleting the page → 103437416 none

Properties read back as {"foo":"bar","baz":"qux"}. The same sequence on an unpatched build of the
same tag freezes the transit file and throws Cannot write $cljs_bean$core$Bean$$.

Happy to rebase or adjust if you'd rather this target something else.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant