Skip to content

local-first: preserve prototype-named entity IDs when rebuilding ProjectData from Yjs #548

Description

@qnbs

Summary

A valid follow-up surfaced during review convergence on PR #546 and was intentionally kept out of that PR because it belongs to the separate Local-First / Yjs reconstruction path rather than the import-to-bootstrap hydration boundary.

The current project/import contract permits arbitrary string entity IDs, including names that collide with Object.prototype such as:

__proto__
constructor
toString

PR #546 hardened cold-boot/import normalization so those IDs survive as own properties in prototype-safe EntityState records. The Local-First readback path still reconstructs character/world collections differently.

Current affected path

services/localFirst/projectDoc.ts currently rebuilds collections in readProjectDoc() using the project adapters, conceptually:

charactersAdapter.addMany(
  charactersAdapter.getInitialState(),
  readEntities<Character>(doc.getMap('characters')),
)

and equivalently for worlds.

The validated Codex review finding on PR #546 observed that with enableLocalFirstSync and an entity ID such as __proto__, RTK/Immer can attempt to treat that key through the ordinary initial {} entity map and fail with behavior equivalent to:

Object.setPrototypeOf() cannot be used on an Immer draft

The shadow-sync/readback path can therefore fail for project data that the main persistence/import contract now explicitly preserves.

Reference review: PR #546, Codex discussion #discussion_r3897865402.

Required invariant

Every entity ID accepted by the authoritative project/import persistence contract must round-trip through the Local-First shadow document without being dropped, reinterpreted as an inherited property, or causing reconstruction failure.

This includes empty/whitespace string IDs if they remain part of the established accepted contract, as well as prototype-colliding IDs.

Preferred bounded fix

Do not redesign the Local-First architecture.

At the Yjs → ProjectData reconstruction boundary, reuse the existing prototype-safe EntityState construction authority introduced by the #546 lineage (or an equivalent shared helper) rather than rebuilding through an ordinary {}-backed adapter initial state.

Preserve:

  • canonical ids ordering;
  • exact entity contents;
  • duplicate-ID rejection/fail-closed behavior where applicable;
  • ordinary UUID/string IDs;
  • existing Local-First shadow/verification semantics.

Do not reject previously accepted IDs merely to avoid the object-key collision.

Regression matrix

At minimum prove for both characters/worlds as appropriate:

  • __proto__ round-trips Redux → Y.Doc → ProjectData;
  • constructor round-trips;
  • toString round-trips;
  • ordinary IDs still round-trip;
  • entity content is preserved exactly;
  • reconstructed keys are own properties;
  • no Immer/prototype mutation exception is thrown;
  • ProjectDocBinding.verify() reports no false drift for the supported IDs;
  • subsequent shadow sync/update remains functional rather than only the first readback succeeding.

Scope

This issue is specifically the Local-First/Yjs reconstruction compatibility gap.

It is not:

The PR #546 thread correctly classified this as a valid follow-up outside that PR's authorized scope. Track and fix it here so #546 does not need to expand into Local-First B1.1 internals.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions