Write a carrier's fields onto a geometry record, creating it if absent.
createGeometry returns an existing record untouched, which is what seeding
a fresh doc wants: a file listing the same geomId twice must not have the
second mention rewrite the first. An overlay is the opposite case — it exists
precisely to carry edits to records the parent already has — so routing it
through createGeometry drops every in-place geometry change silently, with
the merge still reporting success.
Carriers are PARTIAL: geometryRecordLookup in snapshot/structured-attrs.ts
omits blobHash and bbox when absent and params entirely when empty.
That is why the optional fields are guarded here — an absent field is "no
opinion", not "clear it".
params therefore MERGE rather than replace, for a reason that was measured:
merging emits one Yjs change per key at [geomId, 'params'], which
conflicts/detector.ts classifies as a geometry-param conflict, while
node.set(PARAMS, new Y.Map()) emits a single change at [geomId] that
classify returns null for — so replacing would make a concurrent param edit
stop being a detectable conflict.
Two costs, both real. A param the branch DELETED cannot be represented, the
same limit the overlay has for structured removals generally. And because
type/source are required and so always written while the optional fields
are not, a stale carrier can leave a record mixing one representation's
type with another's blobHash.
The version vector is deliberately left alone: it records who replaced this
geometry, and applying someone's overlay is not this peer replacing it.
bumpGeometryVersion is the explicit way to say that.
Write a carrier's fields onto a geometry record, creating it if absent.
createGeometryreturns an existing record untouched, which is what seeding a fresh doc wants: a file listing the samegeomIdtwice must not have the second mention rewrite the first. An overlay is the opposite case — it exists precisely to carry edits to records the parent already has — so routing it throughcreateGeometrydrops every in-place geometry change silently, with the merge still reporting success.Carriers are PARTIAL:
geometryRecordLookupinsnapshot/structured-attrs.tsomitsblobHashandbboxwhen absent andparamsentirely when empty. That is why the optional fields are guarded here — an absent field is "no opinion", not "clear it".paramstherefore MERGE rather than replace, for a reason that was measured: merging emits one Yjs change per key at[geomId, 'params'], whichconflicts/detector.tsclassifies as ageometry-paramconflict, whilenode.set(PARAMS, new Y.Map())emits a single change at[geomId]thatclassifyreturns null for — so replacing would make a concurrent param edit stop being a detectable conflict.Two costs, both real. A param the branch DELETED cannot be represented, the same limit the overlay has for structured removals generally. And because
type/sourceare required and so always written while the optional fields are not, a stale carrier can leave a record mixing one representation's type with another'sblobHash.The version vector is deliberately left alone: it records who replaced this geometry, and applying someone's overlay is not this peer replacing it.
bumpGeometryVersionis the explicit way to say that.