Skip to content

Add synced app-defined metadata for remote resources#75

Merged
hahn-kev merged 11 commits into
mainfrom
cursor/remote-resource-metadata
Jul 1, 2026
Merged

Add synced app-defined metadata for remote resources#75
hahn-kev merged 11 commits into
mainfrom
cursor/remote-resource-metadata

Conversation

@hahn-kev

@hahn-kev hahn-kev commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Resolves #76 (though that is not the primary goal of this PR)

Summary

  • Add generic RemoteResource<TMetadata> with synced Metadata so clients can list media without downloading blobs
  • Introduce ResourceService<TMetadata>, HarmonyResource<TMetadata>, and AddCrdtRemoteResources<TMetadata>() DI registration
  • Add CRDT changes: SetRemoteResourceMetadataChange<TMetadata>, DeleteRemoteResourceChange<TMetadata> (stable delete:RemoteResource type name)
  • Make IRemoteResourceService<TMetadata> generic; UploadResult<TMetadata> can return server-authored metadata that overrides client-passed metadata on upload
  • Add NoMetadata marker type for apps that do not need synced metadata fields
  • Sample: MediaMetadata record in SIL.Harmony.Sample

Breaking changes

  • RemoteResource, ResourceService, HarmonyResource, and IRemoteResourceService are now generic-only
  • CrdtConfig.AddRemoteResourceEntity() (non-generic) removed — use AddCrdtRemoteResources<TMetadata>()
  • DeleteChange<RemoteResource> replaced by DeleteRemoteResourceChange<TMetadata>
  • SyncWithResourceUpload now requires <TMetadata> type argument

Summary by CodeRabbit

  • New Features

    • Remote resources now support optional metadata, so uploads/downloads and resource listings can include details like file name, MIME type, and size.
    • Resource handling and sync flows now use typed metadata end-to-end (typed resource/service APIs for better compile-time safety).
  • Bug Fixes

    • Improved metadata behavior across upload scenarios: metadata can be preserved when uploads return none, overridden when uploads provide it, and retained through pending uploads.
    • Remote/local deletion and pending-queue behavior is now consistent, with strengthened expectations around queued downloads/uploads.

hahn-kev and others added 3 commits June 17, 2026 10:02
…teResource<TMetadata>.

Apps opt in with ResourceService<TMetadata> and AddCrdtRemoteResources<TMetadata> so metadata participates in CRDT sync.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Remote resource APIs, CRDT change types, service wiring, sync helpers, and tests are updated to use generic TMetadata types. Remote resources now carry typed metadata, and sample/test code is adjusted to register, persist, upload, and verify MediaMetadata.

Changes

Generic metadata remote resource pipeline

Layer / File(s) Summary
Core generic contracts and models
src/SIL.Harmony.Core/IRemoteResourceService.cs, src/SIL.Harmony/Resource/RemoteResource.cs, src/SIL.Harmony/Resource/HarmonyResource.cs, src/SIL.Harmony.Sample/MediaMetadata.cs
IRemoteResourceService<TMetadata> and UploadResult<TMetadata> are generic; RemoteResource<TMetadata> and HarmonyResource<TMetadata> carry optional metadata, and MediaMetadata is added as a sample metadata record.
Generic remote-resource change types
src/SIL.Harmony/Resource/CreateRemoteResourceChange.cs, src/SIL.Harmony/Resource/CreateRemoteResourcePendingUpload.cs, src/SIL.Harmony/Resource/RemoteResourceUploadedChange.cs, src/SIL.Harmony/Resource/SetRemoteResourceMetadataChange.cs, src/SIL.Harmony/Resource/DeleteRemoteResourceChange.cs
Create, pending-upload, uploaded, metadata-update, and delete change types are updated or added in generic TMetadata form.
CRDT configuration and DI wiring
src/SIL.Harmony/CrdtConfig.cs, src/SIL.Harmony/CrdtKernel.cs, src/SIL.Harmony/Resource/RemoteResourceNotEnabledException.cs, src/SIL.Harmony.Sample/CrdtSampleKernel.cs
CRDT registration now binds RemoteResource<TMetadata> and typed change types, DI adds AddCrdtRemoteResources<TMetadata>, and the sample kernel switches to MediaMetadata.
Typed resource service behavior
src/SIL.Harmony/ResourceService.cs
ResourceService<TMetadata> updates add/upload/pending/download/list/get/delete flows to use typed resource models and carry metadata through uploads and change creation.
Sync helper updates
src/SIL.Harmony/SyncHelper.cs
SyncWithResourceUpload becomes generic, and the JSON-clone path for missing sync items is removed from SyncWith and SyncMany.
Tests, mocks, and model verification
src/SIL.Harmony.Tests/DbContextTests.VerifyModel.verified.txt, src/SIL.Harmony.Tests/ResourceTests/RemoteServiceMock.cs, src/SIL.Harmony.Tests/ResourceTests/RemoteResourcesMetadataTests.cs, src/SIL.Harmony.Tests/ResourceTests/RemoteResourcesTests.cs, src/SIL.Harmony.Tests/ResourceTests/WordResourceTests.cs
The mock service, new metadata-focused tests, existing resource tests, word-resource test, and verified model are updated for typed metadata behavior and typed CRDT mappings.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • sillsdev/harmony#43: This PR continues the DI/repository wiring changes around CrdtKernel and ResourceService registration.
  • sillsdev/harmony#44: This PR extends the remote-resource upload API refactor by making IRemoteResourceService and UploadResult generic over metadata.

Suggested reviewers

  • rmunn
  • myieye

Poem

🐇 I hop through typed fields, so tidy and neat,
Metadata jingles with every upload beat.
The sync path grew lean as the clone step took flight,
And resource tracks shimmer in generic light.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.68% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding synced app-defined metadata support for remote resources.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/remote-resource-metadata

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/SIL.Harmony/ResourceService.cs (1)

126-146: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Server-returned metadata not persisted during pending upload completion.

When UploadPendingResources processes pending uploads, the uploadResult may contain server-authored Metadata (similar to AddLocalResource at line 80). However, only RemoteResourceUploadedChange is created, which sets the RemoteId but doesn't persist any metadata returned by the server.

If the server can return authoritative metadata during upload (e.g., computed hashes, timestamps), this would be lost for resources that were initially created as pending uploads.

Consider adding a SetRemoteResourceMetadataChange when uploadResult.Metadata is not null:

Proposed fix
 foreach (var localResource in pendingUploads)
 {
     var uploadResult =
         await remoteResourceService.UploadResource(localResource.Id, localResource.LocalPath);
     changes.Add(new RemoteResourceUploadedChange<TMetadata>(localResource.Id, uploadResult.RemoteId));
+    if (uploadResult.Metadata is not null)
+    {
+        changes.Add(new SetRemoteResourceMetadataChange<TMetadata>(localResource.Id, uploadResult.Metadata));
+    }
 }
🤖 Prompt for 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.

In `@src/SIL.Harmony/ResourceService.cs` around lines 126 - 146, In the
UploadPendingResources method, after creating the RemoteResourceUploadedChange
for each uploaded resource, check if uploadResult.Metadata is not null. When
metadata exists, also create a SetRemoteResourceMetadataChange with the
server-returned metadata and add it to the changes list alongside the
RemoteResourceUploadedChange. This ensures that any authoritative metadata
returned by the server during upload is persisted in the same way as handled in
the AddLocalResource method.
🧹 Nitpick comments (1)
src/SIL.Harmony/CrdtConfig.cs (1)

89-96: 💤 Low value

Consider using consistent JSON serialization options.

The Metadata property conversion uses (JsonSerializerOptions?)null (default options), while the rest of the CRDT system uses configured JsonSerializerOptions from CrdtConfig.JsonSerializerOptions. If custom type resolvers or converters are needed for metadata types, this could cause serialization mismatches.

If this is intentional to keep metadata serialization simple and independent, consider adding a brief comment explaining the design choice.

🤖 Prompt for 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.

In `@src/SIL.Harmony/CrdtConfig.cs` around lines 89 - 96, The Metadata property
conversion is using default JSON serialization options (null) for both
JsonSerializer.Serialize and JsonSerializer.Deserialize calls, while the rest of
the CRDT system uses configured options from CrdtConfig.JsonSerializerOptions.
Either update both the Serialize and Deserialize conversions to use
CrdtConfig.JsonSerializerOptions instead of null to maintain consistency, or if
intentional, add a brief comment in the HasConversion block explaining why
Metadata serialization is kept simple and independent from the rest of the CRDT
configuration.
🤖 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.Tests/ResourceTests/RemoteServiceMock.cs`:
- Around line 47-52: The _metadata dictionary is being written with normalized
keys using Path.GetFullPath(localPath) in the SetUploadMetadata method on line
52, but read with raw localPath in the TryGetValue call on line 47. This causes
relative and absolute path variants to have mismatched keys and fail lookups.
Fix this by normalizing the key in the TryGetValue call on line 47 to use
Path.GetFullPath(localPath) instead of raw localPath, ensuring consistency
between how metadata is stored in SetUploadMetadata and retrieved in the upload
result method.

In `@src/SIL.Harmony/Resource/RemoteResource.cs`:
- Around line 47-50: The CloneMetadata method lacks validation to ensure
TMetadata can be safely serialized and deserialized to JSON, which can cause
runtime crashes during copy operations when non-serializable types are used. Add
fail-fast validation in the CloneMetadata method to check whether the metadata
instance can be successfully serialized before attempting the JSON round-trip
serialization, and throw a clear, descriptive exception if serialization fails.
This ensures deterministic error handling and prevents silent failures during
sync flows.

In `@src/SIL.Harmony/ResourceService.cs`:
- Around line 157-164: The UploadPendingResource method does not persist server
metadata from the uploadResult when available. When creating the
RemoteResourceUploadedChange<TMetadata> object, you need to extract and include
the metadata from uploadResult (in addition to the localResource.Id and
uploadResult.RemoteId that are already being passed). Check the
RemoteResourceUploadedChange<TMetadata> constructor to understand what metadata
parameter it expects, then update the method to pass the appropriate metadata
from uploadResult to ensure server-provided metadata is properly persisted in
the change log.

---

Outside diff comments:
In `@src/SIL.Harmony/ResourceService.cs`:
- Around line 126-146: In the UploadPendingResources method, after creating the
RemoteResourceUploadedChange for each uploaded resource, check if
uploadResult.Metadata is not null. When metadata exists, also create a
SetRemoteResourceMetadataChange with the server-returned metadata and add it to
the changes list alongside the RemoteResourceUploadedChange. This ensures that
any authoritative metadata returned by the server during upload is persisted in
the same way as handled in the AddLocalResource method.

---

Nitpick comments:
In `@src/SIL.Harmony/CrdtConfig.cs`:
- Around line 89-96: The Metadata property conversion is using default JSON
serialization options (null) for both JsonSerializer.Serialize and
JsonSerializer.Deserialize calls, while the rest of the CRDT system uses
configured options from CrdtConfig.JsonSerializerOptions. Either update both the
Serialize and Deserialize conversions to use CrdtConfig.JsonSerializerOptions
instead of null to maintain consistency, or if intentional, add a brief comment
in the HasConversion block explaining why Metadata serialization is kept simple
and independent from the rest of the CRDT configuration.
🪄 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: 40f5d8c9-4321-4140-a9d9-35674e8f033c

📥 Commits

Reviewing files that changed from the base of the PR and between 94ffcfa and b364dc8.

📒 Files selected for processing (20)
  • src/SIL.Harmony.Core/IRemoteResourceService.cs
  • src/SIL.Harmony.Sample/CrdtSampleKernel.cs
  • src/SIL.Harmony.Sample/MediaMetadata.cs
  • src/SIL.Harmony.Tests/DbContextTests.VerifyModel.verified.txt
  • src/SIL.Harmony.Tests/ResourceTests/RemoteResourcesMetadataTests.cs
  • src/SIL.Harmony.Tests/ResourceTests/RemoteResourcesTests.cs
  • src/SIL.Harmony.Tests/ResourceTests/RemoteServiceMock.cs
  • src/SIL.Harmony.Tests/ResourceTests/WordResourceTests.cs
  • src/SIL.Harmony/CrdtConfig.cs
  • src/SIL.Harmony/CrdtKernel.cs
  • src/SIL.Harmony/Resource/CreateRemoteResourceChange.cs
  • src/SIL.Harmony/Resource/CreateRemoteResourcePendingUpload.cs
  • src/SIL.Harmony/Resource/DeleteRemoteResourceChange.cs
  • src/SIL.Harmony/Resource/HarmonyResource.cs
  • src/SIL.Harmony/Resource/RemoteResource.cs
  • src/SIL.Harmony/Resource/RemoteResourceNotEnabledException.cs
  • src/SIL.Harmony/Resource/RemoteResourceUploadedChange.cs
  • src/SIL.Harmony/Resource/SetRemoteResourceMetadataChange.cs
  • src/SIL.Harmony/ResourceService.cs
  • src/SIL.Harmony/SyncHelper.cs

Comment thread src/SIL.Harmony.Tests/ResourceTests/RemoteServiceMock.cs Outdated
Comment thread src/SIL.Harmony/Resource/RemoteResource.cs
Comment thread src/SIL.Harmony/ResourceService.cs Outdated
@hahn-kev hahn-kev marked this pull request as ready for review June 24, 2026 04:54

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/SIL.Harmony.Tests/ResourceTests/RemoteServiceMock.cs (1)

47-52: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Normalize metadata keys consistently to avoid lookup misses.

SetUploadMetadata (Line 52) now stores with the raw localPath passed by tests, while UploadResource is invoked by ResourceService.AddLocalResource/UploadPendingResources with Path.GetFullPath(...) (see ResourceService.cs). If a test seeds metadata via a relative path, the Line 47 lookup will miss and the upload-override behavior will silently be skipped. This works today only because tests happen to pass absolute paths. Normalizing both the write and read with Path.GetFullPath removes the dependence on caller path form.

This is the same root cause as the previous review note, just in the reversed direction.

Proposed fix
-        _metadata.TryGetValue(localPath, out var mockMetadata);
+        _metadata.TryGetValue(Path.GetFullPath(localPath), out var mockMetadata);
         return new UploadResult<MediaMetadata>(remoteId, mockMetadata ?? metadata);
     }

     public void SetUploadMetadata(string localPath, MediaMetadata metadata) =>
-        _metadata[localPath] = metadata;
+        _metadata[Path.GetFullPath(localPath)] = metadata;
🤖 Prompt for 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.

In `@src/SIL.Harmony.Tests/ResourceTests/RemoteServiceMock.cs` around lines 47 -
52, Normalize metadata keys consistently in RemoteServiceMock so seeded upload
metadata is found regardless of whether tests pass relative or absolute paths.
Update both UploadResource and SetUploadMetadata to use the same normalized key,
ideally by calling Path.GetFullPath on the localPath before storing and looking
up in the _metadata dictionary. Keep the behavior aligned with
ResourceService.AddLocalResource/UploadPendingResources so the mock matches the
real caller path form.
🤖 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/Resource/HarmonyResource.cs`:
- Around line 14-20: The HarmonyResource constructor currently merges
localResource and remoteResource without verifying they belong to the same
entity, so add a validation step before assigning Id, RemoteId, LocalPath, and
Metadata. In HarmonyResource(LocalResource? localResource,
RemoteResource<TMetadata>? remoteResource), when both inputs are present,
compare their IDs and throw if they do not match; otherwise continue
constructing the composite resource. Keep the fix localized to the
HarmonyResource constructor so mismatched local/remote pairs are rejected early.

In `@src/SIL.Harmony/Resource/RemoteResourceUploadedChange.cs`:
- Around line 17-20: The upload change is overwriting existing metadata with
null when the remote service returns no override. Update
RemoteResourceUploadedChange.ApplyChange so it always applies RemoteId, but only
assigns entity.Metadata when the change actually contains server-authored
metadata; otherwise leave the current metadata untouched. Use the existing
RemoteResourceUploadedChange and ApplyChange flow to preserve prior values
coming from UploadPendingResources.

---

Duplicate comments:
In `@src/SIL.Harmony.Tests/ResourceTests/RemoteServiceMock.cs`:
- Around line 47-52: Normalize metadata keys consistently in RemoteServiceMock
so seeded upload metadata is found regardless of whether tests pass relative or
absolute paths. Update both UploadResource and SetUploadMetadata to use the same
normalized key, ideally by calling Path.GetFullPath on the localPath before
storing and looking up in the _metadata dictionary. Keep the behavior aligned
with ResourceService.AddLocalResource/UploadPendingResources so the mock matches
the real caller path form.
🪄 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: 2cfe7711-8ca2-473b-81ee-f1a51cbebf9b

📥 Commits

Reviewing files that changed from the base of the PR and between b364dc8 and 60d42a5.

📒 Files selected for processing (7)
  • src/SIL.Harmony.Core/IRemoteResourceService.cs
  • src/SIL.Harmony.Tests/ResourceTests/RemoteResourcesMetadataTests.cs
  • src/SIL.Harmony.Tests/ResourceTests/RemoteResourcesTests.cs
  • src/SIL.Harmony.Tests/ResourceTests/RemoteServiceMock.cs
  • src/SIL.Harmony/Resource/HarmonyResource.cs
  • src/SIL.Harmony/Resource/RemoteResourceUploadedChange.cs
  • src/SIL.Harmony/ResourceService.cs
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/SIL.Harmony.Core/IRemoteResourceService.cs
  • src/SIL.Harmony.Tests/ResourceTests/RemoteResourcesMetadataTests.cs
  • src/SIL.Harmony.Tests/ResourceTests/RemoteResourcesTests.cs
  • src/SIL.Harmony/ResourceService.cs

Comment thread src/SIL.Harmony/Resource/HarmonyResource.cs
Comment thread src/SIL.Harmony/Resource/RemoteResourceUploadedChange.cs Outdated

@myieye myieye left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm having trouble figuring out if Devin AI's feedback is helpful.
So, I'm gonna be lazy and just point you at it 🙈
https://app.devin.ai/review/sillsdev/harmony/pull/75

Comment thread src/SIL.Harmony.Tests/ResourceTests/RemoteResourcesMetadataTests.cs
claude and others added 5 commits June 29, 2026 15:31
Add an optional CommitMetadata? parameter to every commit-creating
ResourceService method (including SetResourceMetadata) and forward it to
DataModel.AddChange/AddChanges, so resource/media commits can carry author
info like every other change. Backward-compatible: omitting it behaves as
before.

Stacked on #75 (cursor/remote-resource-metadata), which reworks
ResourceService into the generic ResourceService<TMetadata>. The resource
TMetadata (app-defined, synced) and the commit CommitMetadata (author) are
orthogonal; #75 still authored resource commits without CommitMetadata.

Closes #76

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JH8DKhRUSnxnGsLRaavy6D
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@hahn-kev

Copy link
Copy Markdown
Collaborator Author

@myieye that devin review had some helpful stuff. Also I didn't realize it at first but the commit made by claude above isn't actually in this PR, it just referenced it, so I merged it in.

@myieye

myieye commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

@hahn-kev Thanks for adding the Claude commit. I started that commit before this PR existed and just rebased it yesterday. I was planning it for a follow up PR, but folding it in is fine with me. I've noted that this PR will resolve the relevant issue.

@hahn-kev hahn-kev enabled auto-merge (squash) July 1, 2026 02:08
@hahn-kev hahn-kev merged commit 01efe25 into main Jul 1, 2026
6 checks passed
@hahn-kev hahn-kev deleted the cursor/remote-resource-metadata branch July 1, 2026 08:39
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.

ResourceService doesn't let callers attach CommitMetadata to resource commits

3 participants