Tolerate unknown IChange $type via PeekThenConcreteChangeConverter#80
Tolerate unknown IChange $type via PeekThenConcreteChangeConverter#80hahn-kev-bot wants to merge 5 commits into
Conversation
Co-authored-by: Cursor <cursoragent@cursor.com>
Unknown $type values will be held as OpaqueChange once the converter owns discrimination. Co-authored-by: Cursor <cursoragent@cursor.com>
CrdtConfig owns IChange discrimination with synthetic $type on write; SnapshotWorker skips opaque creates. Co-authored-by: Cursor <cursoragent@cursor.com>
📝 WalkthroughWalkthroughThe PR replaces interface-level polymorphic JSON handling with a custom converter, adds ChangesChange serialization and replay
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant CommitJson
participant CrdtConfig
participant PeekThenConcreteChangeConverter
participant SnapshotWorker
CommitJson->>CrdtConfig: Create serializer options and discriminator maps
CommitJson->>PeekThenConcreteChangeConverter: Deserialize change JSON
PeekThenConcreteChangeConverter->>PeekThenConcreteChangeConverter: Match first $type discriminator
PeekThenConcreteChangeConverter->>CommitJson: Return known concrete change
PeekThenConcreteChangeConverter->>CommitJson: Return OpaqueChange with raw JSON
SnapshotWorker->>OpaqueChange: Check SupportsNewEntity()
SnapshotWorker->>SnapshotWorker: Skip unsupported opaque creation
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/SIL.Harmony/CrdtConfig.cs`:
- Around line 49-65: Update BuildChangeDiscriminatorMaps to construct the
reverse discriminator map in a local dictionary alongside knownChanges, populate
both fully within the loop, then assign the completed map to
_changeTypeDiscriminators immediately before returning. Do not publish the field
until all entries have been validated and added.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 00791495-bffd-4c2a-baae-784dde3c8328
📒 Files selected for processing (8)
.gitignoreAGENTS.mdsrc/SIL.Harmony.Tests/ChangeConverterTests.cssrc/SIL.Harmony/Changes/Change.cssrc/SIL.Harmony/Changes/OpaqueChange.cssrc/SIL.Harmony/Changes/PeekThenConcreteChangeConverter.cssrc/SIL.Harmony/CrdtConfig.cssrc/SIL.Harmony/SnapshotWorker.cs
_changeTypeDiscriminators was assigned before its loop finished populating it, so a concurrent JsonTypeModifier callback (invoked lazily by STJ on arbitrary threads) could observe a partially-built dictionary. Build into a local and assign the field once, so readers only ever see null or a complete map. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This reverts commit 8277851.
myieye
left a comment
There was a problem hiding this comment.
Just some improvement ideas/suggestions, but the feature looks solid
| if (prevSnapshot is null) | ||
| { | ||
| // create brand new entity - this will (and should) throw if the change doesn't support NewEntity | ||
| if (!commitChange.Change.SupportsNewEntity()) |
There was a problem hiding this comment.
I'd opt to keep throwing if it's not an opaque change.
It's an unexpected scenario that has exposed my user errors before.
| return new OpaqueChange | ||
| { | ||
| TypeName = typeName, | ||
| EntityId = element.TryGetProperty("EntityId", out var id) && id.ValueKind == JsonValueKind.String |
There was a problem hiding this comment.
| EntityId = element.TryGetProperty("EntityId", out var id) && id.ValueKind == JsonValueKind.String | |
| EntityId = element.TryGetProperty(nameof(IChange.EntityId), out var id) && id.ValueKind == JsonValueKind.String |
| }; | ||
| } | ||
|
|
||
| private bool TryFindKnown(ref Utf8JsonReader reader, out int index, out string? unknownTypeName) |
There was a problem hiding this comment.
I'm guessing there's no way to somehow return/out a ref to the known-type instead of passing out an index and doing the lookup a second time?
| _changeTypeDiscriminators = discriminators; | ||
| return knownChanges; |
There was a problem hiding this comment.
It's no very pretty that that're one side-effect and one return value.
You can leave it if you want or you could make more Lazy<> fields for one or both dictionaries.
|
|
||
| // IChange polymorphism is owned by PeekThenConcreteChangeConverter — do not set PolymorphismOptions. | ||
| if (_changeTypeDiscriminators is not null | ||
| && typeInfo.Kind == JsonTypeInfoKind.Object |
There was a problem hiding this comment.
Isn't the original condition still ok and more meaningful?
| && typeInfo.Kind == JsonTypeInfoKind.Object | |
| && typeInfo.Type == typeof(IChange) |
Summary
JsonPolymorphiconIChangewithPeekThenConcreteChangeConverter: known$typevalues deserialize via cached concreteJsonTypeInfo; unknown values becomeOpaqueChange(raw JSON preserved for round-trip).$typeon registered concrete change types fromCrdtConfig, and skip snapshot apply for changes that do not supportNewEntity(opaque/unknown creates).$type-first requirements.Test plan
dotnet test src/SIL.Harmony.Tests --filter ChangeConverterTestsdotnet test src/SIL.Harmony.Tests(full suite)$typekeeps the unknown payload and does not throwSummary by CodeRabbit