Skip to content

fix(publish): acquire DO stubs in sync let to avoid RpcPromise clone#12521

Open
deephbz wants to merge 1 commit intologseq:masterfrom
deephbz:fix/publish-rpc-promise-clone
Open

fix(publish): acquire DO stubs in sync let to avoid RpcPromise clone#12521
deephbz wants to merge 1 commit intologseq:masterfrom
deephbz:fix/publish-rpc-promise-clone

Conversation

@deephbz
Copy link
Copy Markdown
Contributor

@deephbz deephbz commented Apr 19, 2026

Summary

Every publish / unpublish / page-view handler in the Cloudflare publish worker fails with a DataCloneError because promesa's p/let awaits Durable Object stubs that are thenable under the new cloudflare:workers RPC runtime. Moving stub acquisition into a surrounding sync let fixes the whole worker.

How I hit this

I'm running a self-hosted copy of this publish worker (own CF account, own R2 bucket, own PublishMetaDO) deployed fresh from the current master build. Every request against the worker failed:

  • POST /pages returned 500 with DataCloneError: Could not serialize object of type \"RpcPromise\".
  • GET /page/:graph/:page returned 500 with the same error.
  • DELETE /pages/:graph/:page same.

The desktop client showed the matching error toast after clicking Publish page. Worker tail logs had the RpcPromise clone error on every handler that touches PUBLISH_META_DO.

Why the official worker isn't affected (hypothesis)

The production logseq-publish worker at logseq.io was deployed from a pre-cljs JS bundle (the generation before deps/publish/ was ported to ClojureScript). That older code acquired the DO stub synchronously:

const doStub = env.PUBLISH_META_DO.get(doId);
await doStub.fetch(...);

When the codebase was rewritten in cljs (deps/publish/src/logseq/publish/routes.cljs), the stub acquisition was pulled into p/let bindings alongside the fetch calls. Tests presumably pass because test DOs aren't thenable in the same way. But the official worker apparently hasn't been redeployed from the cljs bundle yet, so production keeps running the old JS and never trips the new bug.

Once someone fresh-deploys the current cljs source (which is what I did), every handler explodes.

Root cause

In deps/db-sync's RPC-enabled Durable Object (extends DurableObject from cloudflare:workers), the stub returned by DurableObjectNamespace.get(id) is a thenable (it exposes a .then method so callers can await an RPC return value). When this stub is bound inside a (p/let ...):

(p/let [do-ns (aget env \"PUBLISH_META_DO\")
        do-id (.idFromName do-ns \"index\")
        do-stub (.get do-ns do-id)   ;; <-- thenable
        resp (.fetch do-stub req)]
  ...)

promesa sees do-stub is thenable and awaits it. Awaiting a DO stub is not a documented operation; the runtime tries to structure-clone it and fails with DataCloneError: Could not serialize object of type \"RpcPromise\".

Fix

Acquire the stub in a surrounding sync let so promesa never awaits it, then use it normally inside p/let:

(let [^js do-ns (aget env \"PUBLISH_META_DO\")
      do-id (.idFromName do-ns \"index\")
      do-stub (.get do-ns do-id)]
  (p/let [resp (.fetch do-stub req)]
    ...))

Applied to every handler in routes.cljs that touches PUBLISH_META_DO: page GET/POST/DELETE, tag page handlers, password hash fetch, index rebuild, etc. The fetch-page-password-hash helper carries a short comment explaining the pattern so future contributors don't re-inline the stub into p/let.

Local repro (before)

  1. cd deps/publish && yarn install && yarn release
  2. wrangler deploy to any fresh CF account with a PublishMetaDO DO binding.
  3. From desktop, click Make public then Publish page on any DB-graph page.
  4. Observe: toast shows 500, worker tail logs DataCloneError: Could not serialize object of type \"RpcPromise\", no R2 object written, no DO row inserted.

Also reproducible via plain HTTP:

curl -i -X POST https://<worker-host>/pages -H \"Authorization: Bearer <jwt>\" -d '{...}'
# HTTP/1.1 500 Internal Server Error

After the fix

Same worker, redeployed with this patch:

  • POST /pages returns 200 with the public URL.
  • GET /page/:graph/:page returns SSR HTML.
  • DELETE /pages/:graph/:page returns 200 and purges both the R2 transit blob and the DO row.
  • Desktop Publish page shows the success notification and sets logseq.property.publish/published-url on the page.
  • just personal-publish-tail is silent, no more RpcPromise errors.

Test plan

  • Redeployed self-hosted lseek-publish-createdat20260418 worker from this branch; publish / view / unpublish all succeed end-to-end.
  • Verified the DataCloneError is gone from worker tail logs across all three request types.
  • Publish-side unit tests (deps/publish has no handler-level tests today; behaviour verified live against a real CF worker + R2 + DO).

🤖 Generated with Claude Code

Under the new cloudflare:workers DurableObject RPC runtime, stubs
returned from (.get do-ns do-id) are thenable. Binding them in a
(p/let) causes promesa to await the stub, triggering a structured
clone that fails with DataCloneError on RpcPromise. Pulling stub
acquisition into a surrounding sync (let) avoids the await entirely.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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