Skip to content

Cross-device publish state: deterministic rkeys + PDS reconcile#36

Merged
jpdevries merged 1 commit into
mainfrom
feature/publish-state-reconcile
Jul 12, 2026
Merged

Cross-device publish state: deterministic rkeys + PDS reconcile#36
jpdevries merged 1 commit into
mainfrom
feature/publish-state-reconcile

Conversation

@jpdevries

Copy link
Copy Markdown
Owner

Follow-up to #33. Publish state was a per-browser localStorage receipt, which meant:

  • a second device showed "Share to Bluesky" for an already-published recipe — and re-publishing there minted a duplicate record;
  • an external delete (from another AT client) left a dead "View on Bluesky" pointing at a URI that no longer resolves.

Rather than persist state in the app DB (which only helps the shared-DB self-hosted tier, requires a schema migration touching Rust + Node + web, and still drifts from PDS reality), this reads the actual source of truth — the user's PDS.

Changes (packages/shared)

Deterministic rkeys (rkeyForLocal, atproto-publish.ts) — publishes now always putRecord at an rkey derived from the local recipe/menu id. First publish and every re-publish target one record, so a second device (or a cleared cache) can't create a duplicate. Records published before this change keep their original rkey via the localStorage receipt (opts.rkey still wins).

PDS reconcile (PublishToBlueskyButton.tsx) — on sign-in, once per item, non-dry-run:

  • a stored receipt is verified against the PDS (recordExists) and cleared if the record was deleted elsewhere;
  • with no receipt, a record found at the deterministic rkey (findPublishedRecord) is adopted, so a fresh device shows "View on Bluesky".
  • localStorage stays as a fast optimistic cache; the PDS decides. Both helpers are public getRecord reads (no auth).

Latent bug fixedpublishCollection passed the menu's rkey down to inline child recipe publishes, so a menu re-publish would overwrite the menu's key with each child. Children now get their own deterministic rkey.

Verified

Stub-agent + real-PDS runtime test:

  • first publish → putRecord at rkey = id, createRecord never called
  • re-publish honors an explicit legacy (TID) rkey → old records aren't orphaned
  • collection: children publish to child-A/child-B, collection to the menu key (bug fix confirmed)
  • recordExists true on the live Wine Night collection, false on a bogus rkey; findPublishedRecord null when absent
  • both tiers typecheck at baseline (web 50 / app 119, unchanged); vite build passes

Scope notes

  • Tier 1 (web) cross-device is inherently partial: OPFS recipe ids differ per device, so two independently-created copies of "the same" recipe can't be unified — but each device is now idempotent and reflects real PDS state.
  • Records published before this PR (e.g. the existing Wine Night set, at TID rkeys) still dedup on re-publish from the same browser via their receipt; cross-device they'd match only after being re-published under a deterministic rkey. Finite, known, not worth a migration.

Separately filed: the self-hosted OAuth gap (real publishing only works via 127.0.0.1 loopback today) — see the linked issue.

🤖 Generated with Claude Code

…e publish state

Publish state was a per-browser localStorage receipt: a second
device showed "Share to Bluesky" for an already-published recipe
and would mint a DUPLICATE record, and an external delete left a
dead "View on Bluesky". Rather than copy state into a DB (helps
only the shared-DB self-hosted tier, and drifts from PDS truth),
read the source of truth — the user's PDS.

- Deterministic rkey (rkeyForLocal, from the local id): publishes
  now always putRecord at that key, so first publish and every
  re-publish target ONE record. No duplicate on a second device or
  cleared cache. Records published before this change keep their
  original rkey via the localStorage receipt (opts.rkey wins).
- Reconcile effect: on sign-in (non-dry, once per item) verify a
  stored receipt still resolves (clear if deleted elsewhere), or
  with no receipt adopt a record found at the deterministic rkey —
  so a fresh device shows "View on Bluesky". localStorage stays as
  an optimistic cache; the PDS decides. findPublishedRecord /
  recordExists are public getRecord reads (no auth).
- Fixes a latent bug: publishCollection passed the menu's rkey to
  inline child publishes, so on menu re-publish every child would
  overwrite the menu's key. Children now get their own deterministic
  rkey.

Verified with a stub agent + real PDS reads: first publish → putRecord
at rkey=id (no createRecord); re-publish honors an explicit legacy
rkey; collection children publish to their own keys while the
collection uses the menu key; recordExists true on the live Wine
Night record, false on a bogus rkey; findPublishedRecord null when
absent. Both tiers typecheck at baseline; web build passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
pantry-host-marketing 7003f66 Jul 12 2026, 08:30 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 12, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
pantry-host-web 7003f66 Commit Preview URL

Branch Preview URL
Jul 12 2026, 08:32 PM

@jpdevries jpdevries merged commit cf80b10 into main Jul 12, 2026
2 checks passed
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