Import already had this: `comparable()` in import.rs, added last week, strips
identity fields off a model so a re-import can tell a change from a conflict.
Request versioning arrived with its own copy of the same idea.
Now both call `yaak_models::content`, which owns the mechanism and — more
usefully — the reasoning. Two things had no single home before: why identity
fields can never count as content, and why hashing has to sort object keys
itself rather than trusting `serde_json::Map` to be a `BTreeMap`
(`preserve_order` is on in some builds of this workspace and off in others).
The key lists stay separate on purpose, as IDENTITY_KEYS and PLACEMENT_KEYS.
Import counts a move as a change, because equality there means "same content in
the same place"; versioning must not, or dragging a request around the sidebar
would mint versions nobody asked for. A test on each side pins that difference,
since the obvious next refactor is to collapse the two lists into one and
neither behaviour would fail loudly if you did.
Directory sync deliberately keeps its own Sha1 and is not folded in. It
checksums the bytes of a file to notice someone edited it on disk, so it has to
reflect formatting and key order — the exact things this module discards.
Two sends of the same request can both miss the content-hash lookup and race to
insert. The unique index settles it; the loser wants exactly what the winner
wrote, not an error that costs its response a version link.
Drives the real HTTP pipeline against a stub executor and a real database, so
the assertion is the one the feature actually rests on: a stored send leaves a
response naming the request behind it, two sends of an unchanged request name
the same version, and editing afterwards is something the response pane can
detect.
Three commands, all host-independent so the browser build answers them from
the same model layer as the desktop:
- models_snapshot_request, for the edit-session boundaries only the frontend
can see. It takes a reason and nothing else — the caller does not decide
whether anything changed, because content addressing already has.
- models_request_version, which returns a version and the live request's
content together so they are guaranteed comparable, plus the same
content-hash verdict the backend uses rather than a second opinion formed in
TypeScript.
- models_restore_request_version.
The browser host also snapshots before its own send, since its send pipeline
is in TypeScript rather than in the shared crate.
Bindings regenerated with CI's `cargo test --all --features
yaak-app-client/wry`, which also drops a stale ImportSourceResource from the
rpc-schema copy and adds a missing ImportSource to the plugins copy.
HTTP snapshots in resolve_send_inputs, the last point in the pipeline that
still holds the stored request — below it the request has been resolved against
its folder and workspace and then rendered, and neither is what a restore
should put back. Every host reaches sending through that function, so the
desktop, the CLI and plugin-triggered sends all get versions without each
knowing about them. gRPC and WebSocket connect do the same at their connection
upserts.
snapshot_request_for_send swallows its own errors: a send is not worth failing
over history that couldn't be written, and an ephemeral request has no id to
version. Either way the response just has no version to offer.
One table versions HTTP, gRPC and WebSocket requests alike. A version stores
the model's editable content — the serialized model minus model/id/timestamps/
workspace/folder/sortPriority — and is addressed by the sha256 of that
document, with (model_id, content_hash) unique.
Content addressing is what makes the trigger side simple: snapshot_request can
be called by a send, a window blur and an idle timer on the same unedited
request and leave one row behind, so no caller has to reason about whether
anything changed. Dropping bookkeeping keys is what makes it stable — moving a
request between folders or re-sorting it rewrites those fields and nothing
else, and neither should mint a version or show up in a diff. The same rule
covers all three request types because they differ only in content fields.
Responses and gRPC/WebSocket connections gain a nullable version_id.
Versions are local history, so ModelVersion is deliberately absent from
AnyModel: no model-change rows, no frontend store bucket, no sync, no export.
Retention keeps anything a response points at, plus the newest 50 per request
within 30 days; request and workspace deletes cascade.