fix(api): create_page must store real map properties, not a cljs-bean view - #42
fix(api): create_page must store real map properties, not a cljs-bean view#42Lcstyle wants to merge 1 commit into
Conversation
… 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
|
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 Also worth flagging: no build or test workflow ran here at all. Since CI won't cover this, here's what I did verify locally — a
Properties read back as Happy to rebase or adjust if you'd rather this target something else. |
Fixes the root cause of logseq/logseq#8536, open
since Feb 2023 with no known reproduction until now.
The bug
logseq.api/create_pageconverts the caller'spropertiesobject withbean/->clj(
src/main/logseq/api.cljs:559). That returns a lazycljs-beanBeanview, not a persistentmap, and
frontend.handler.page/create!stores it verbatim as the page entity's:block/properties(src/main/frontend/handler/page.cljs:107).datascript-transit0.3.0 registers write handlers only forDB,DatomandBTSet, so aBeanis unwritable. From the moment any plugin creates a page with a non-empty properties map, every
frontend.db/persist!throws: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:
Measured on 0.10.15 against a 5228-page graph, using the
.transitmtime as ground truth for"did the save actually happen":
~/.logseq/graphs/<graph>.transitcreate_page(name, {foo:'bar'})Cannot write $cljs_bean$core$Bean$$Single variable changed, GREEN → RED → GREEN.
Only a non-empty properties map triggers it, which matches
create!'s(when (seq properties) ...)guard:
create_page(name)create_page(name, {})create_page(name, {a:'b'})The fix
js->cljwith:keywordize-keysconverts deeply and eagerly, so nested objects and arrays becomereal maps and vectors instead of nested
Bean/ArrayVectorviews. The depth matters: a shallowconversion would still leave an
ArrayVectorfor something liketags: [].Only the
propertiesargument is changed.optsis destructured immediately and never stored, so itcan 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 areal 5228-page graph:
create_page(name, {foo:'bar', baz:'qux'})Properties read back correctly as
{"foo":"bar","baz":"qux"}. The same sequence on the unpatchedbuild freezes the transit file and throws.
Scope
I audited the other API entry points that call
bean/->cljon caller data. The block-writing ones(
insert_block,insert_batch_block,append_block_in_page) are not affected: they renderproperties into the block's markdown as
a:: band re-parse them, which produces a genuine map, sothe Bean is transient.
create_pageis the only path that stores the converted value directly.