Skip to content

fix: store subscription propagation and temp ID rekey#19

Closed
vparys wants to merge 8 commits intomainfrom
fix/store-subscription-propagation
Closed

fix: store subscription propagation and temp ID rekey#19
vparys wants to merge 8 commits intomainfrom
fix/store-subscription-propagation

Conversation

@vparys
Copy link
Copy Markdown
Member

@vparys vparys commented Apr 13, 2026

Summary

Fixes a set of related bugs where React UI would not update after persisting inline-created nested entities. The root cause was that mapTempIdToPersistedId didn't migrate subscriptions, parent-child chains, or relation state when moving entity data from temp keys to persisted keys.

This PR builds on top of fix/nested-hasmany-create-mutation (commits c6dfd50..d1a6127 which fix nested hasMany mutation generation and embedded entity materialization).

New commits in this PR:

1. fix: migrate subscriptions and parent-child chain on temp→persisted ID rekey

  • SnapshotStore: adds rekeyedEntities map so getEntityKey/getRelationKey transparently resolve temp IDs for all store operations. mapTempIdToPersistedId now atomically rekeys snapshots, metadata, relations, errors, touched state, and subscriptions.
  • SubscriptionManager: adds rekeyedKeys map so unsubscribe closures (which capture the original key at subscribe time) can find migrated callbacks after rekey. rekey() migrates entity subscribers, relation subscribers (by prefix), and childToParents entries (both as child and parent references).
  • Sub-store rekey() methods added to EntityMetaStore, EntitySnapshotStore, ErrorStore, RelationStore, TouchedStore.
  • 15 tests covering: 3-level deep parent-child propagation after rekey, useSyncExternalStore unsub/resub pattern, relation subscriber migration, re-registration via temp IDs, reverse-order rekey.

2. feat: add $create() method to HasOne handle

  • handle.$create({ status: 'pending' }) creates a new entity and connects it to the hasOne relation in one call, replacing the low-level store.createEntity() + $connect() pattern.
  • Added to HasOneRefInterface (available on both Ref and Accessor types).
  • 6 tests covering: entity creation + connection, data accessibility, replace behavior, parent-child propagation, new entity state.

3. fix: register parent-child in ActionDispatcher for relation actions

  • registerParentChild was previously only called from handle methods. Dispatching actions directly (ADD_TO_LIST, CONNECT_TO_LIST, CONNECT_RELATION) would not set up the subscription propagation chain.
  • ActionDispatcher.execute now calls registerParentChild for all three relation action types.
  • Added targetType field to ConnectRelationAction and ConnectToListAction interfaces.
  • Removed redundant registerParentChild calls from HasManyListHandle.add(), HasManyListHandle.connect(), HasOneHandle.create().
  • 4 tests verifying propagation through dispatcher and confirming low-level store.addToHasMany intentionally does not register parent-child.

Test plan

  • bun test — 1412 pass, 9 pre-existing failures (8 browser tests needing running server, 1 unrelated formRelations test)
  • bun run typecheck — clean (only pre-existing errors in nestedHasManyCreate.test.ts)
  • 25 new tests across 3 test files
  • Manual verification: create nested entities via UI → persist → modify → persist again → UI should update without page refresh

vparys added 8 commits April 13, 2026 11:58
Reviews added via store.addToHasMany to a Round nested inside
an Approval create are not included in the mutation because
collectHasManyOperations uses processNestedData instead of
collectCreateData for nested entities.
collectHasManyOperations was using processNestedData for created
entities, which only processed raw snapshot data and ignored
store-tracked hasMany relations. This caused deeply nested
hasMany items (e.g. Round → Reviews) to be missing from mutations
when the parent was itself nested in another create.
…ter persist

- MutationCollector now materializes embedded hasMany/hasOne data into
  proper store entities during collectCreateData, enabling tracking and
  post-persist ID mapping
- Added connectExistingToHasMany to store for connecting existing entities
  without marking them as created
- Extended TransactionMutationResult with nestedResults for adapters to
  return server-assigned IDs for inline-created entities
- BatchPersister now commits nested entities after successful persist and
  processes nestedResults for ID mapping
Build recursive GraphQL node selection from mutation data so Contember
returns server-assigned IDs for all inline-created entities. The
BatchPersister extracts these IDs from the node response and maps
temp IDs to persisted IDs via nestedResults.

- Add buildNodeSelectionFromMutationData() that traverses mutation
  data structure to build node { id, relation { id, ... } } selection
- ContemberAdapter.persist() now includes node selection and returns
  node data with nested entity IDs
- ContemberAdapter.create() enhanced with recursive node selection
- BatchPersister.extractNestedResultsFromNode() walks mutation data
  and node response together to match temp IDs to server IDs
- Extend PersistResult with data field for node response
- Add test for sequential fallback path with node data extraction
…D rekey

After persistAll() maps temp IDs to server-assigned UUIDs via
mapTempIdToPersistedId, React components subscribed to the old temp key
never received notifications — the entity data moved to the new key but
subscribers stayed on the old one.

SnapshotStore changes:
- Add rekeyedEntities map so getEntityKey/getRelationKey transparently
  resolve temp IDs to persisted keys for all store operations
- mapTempIdToPersistedId now rekeys entity snapshots, metadata, relations,
  errors, touched state, and subscriptions in one atomic operation

SubscriptionManager changes:
- Add rekeyedKeys map so unsubscribe closures (which capture the original
  key) can find migrated callbacks after rekey
- rekey() now migrates relation subscribers by prefix in addition to
  entity subscribers
- rekey() updates childToParents both as child entries and parent
  references, preserving the full propagation chain

Sub-store rekey methods added to EntityMetaStore, EntitySnapshotStore,
ErrorStore, RelationStore, and TouchedStore.

Tests cover: 3-level deep parent-child propagation after rekey,
useSyncExternalStore unsub/resub pattern, relation subscriber migration,
re-registration via temp IDs, and reverse-order rekey.
Allows creating a new entity and connecting it to a hasOne relation in
one call instead of using the low-level store.createEntity + $connect
pattern.

  // before
  const id = store.createEntity('Author', { name: 'Jane' })
  article.author.$connect(id)

  // after
  article.author.$create({ name: 'Jane' })

The method creates the entity, dispatches CONNECT_RELATION, registers
parent-child for change propagation, and returns the temp ID.

Added to HasOneRefInterface so it's available on both Ref and Accessor
types. JSX collector proxy updated with a noop stub.
registerParentChild (which sets up the subscription propagation chain)
was previously only called from high-level handle methods. Anyone using
the dispatcher directly or the low-level store API would not get change
propagation from child to parent entities.

Now ActionDispatcher.execute registers parent-child for:
- ADD_TO_LIST — new entity added to hasMany
- CONNECT_TO_LIST — existing entity connected to hasMany
- CONNECT_RELATION — entity connected to hasOne

Added targetType to ConnectRelationAction and ConnectToListAction
interfaces so the dispatcher knows the child entity type.

Removed redundant registerParentChild calls from HasManyListHandle.add(),
HasManyListHandle.connect(), and HasOneHandle.create() since the
dispatcher now handles it.
@vparys vparys closed this Apr 13, 2026
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