Context
The gitops-api console (the app this library was extracted from) just grew two things it never had:
creating a new ConfigMap and deleting a whole one. It previously only edited objects the watch
had already delivered.
The design goal was that a create, a delete, and a field edit all land in the one "review & save"
pane and commit under the one Save button with the one reason — not three separate buttons with
three separate flows.
LiveResourceStore was a clean fit for the edit half, and deliberately not for the other two — which
is correct and matches the library's read-only stance. This issue is about blessing the pattern for
whole-object create/delete so every consumer does it the same way, plus one open question. It is
explicitly not a request for the library to perform writes.
Why create/delete can't live in the store (and shouldn't)
Two properties of LiveResourceStore make this a non-fit by design, per
docs/client-state-model.md:
- Identity is
metadata.uid. A pending create has no server object and therefore no uid, so it
has no key in the store.
- The store's crown jewel is the three-way merge. A create has nothing to merge against; a delete
has no fields to reconcile. changes()/patch()/conflicts() — the whole value — simply don't apply.
And per docs/saving.md, "krm-stream is a read library; your application owns its
HTTP save endpoint." Create and delete are more of the same host-owned write surface (RBAC, attribution,
and — for save — ValidateMergePatch all stay host-side). So staging the pending intent is genuinely
the consumer's job.
What we did (reference implementation)
Small page-local state, aggregated with the store for one unified pending list:
const pendingCreates = []; // { id, name, rows: [{k, v}] } — drafts with no server object yet
const pendingDeletes = new Set(); // uids marked for removal
// The sidebar renders THREE sources as one list:
// pendingCreates → "create <name>"
// pendingDeletes → "delete <name>"
// store.changes(uid) → per-path field edits (skipped if uid is in pendingDeletes)
// One Save fans out to POST /console/{create,delete,save}; each item succeeds/fails independently.
On success we currently just drop the pending entry and let the watch echo bring the object in/out.
What we found: the optimistic primitives already exist
While wiring this we noticed the library already has everything needed to reflect the result before
the watch echoes it — we just weren't using it:
adoptSaved(object) — when the uid is unknown it calls
applyServerEvent, i.e. it inserts a brand-new object. That is the optimistic-create primitive.
removeResource(id) — the optimistic-delete primitive.
The only reason our new card flickers for a beat is a consumer-side gap: our POST /console/create
returns 204 No Content, so the client has no object to hand adoptSaved. Returning the created object
and calling adoptSaved(it) closes it — a fix we'll make on our side, not here.
So the library is not actually missing capability. What it's missing is guidance.
The ask
1. Document the create/delete pattern (the real ask). client-state-model.md and saving.md cover
edit + save but say nothing about whole-object create/delete, so the next consumer will re-derive
(or mis-derive) it. Proposed additions:
client-state-model.md: a short "Creating and deleting whole objects" note — the store keys on uid
and has no merge for these, so a consumer stages pending create/delete itself and aggregates them with
changes().
saving.md: alongside the save example, show the create/delete siblings, and point at adoptSaved /
removeResource for reflecting the response without waiting for the watch (idempotent with the echo,
per the I-IDEMPOTENT note already in the adoptSaved docstring).
Happy to open the docs PR — we have the working reference implementation.
2. Open question: is a thin helper worth it? A tiny optional PendingMutations-style aggregator
(edits + creates + deletes → one list, one "submit descriptor" set) would stop each consumer
hand-rolling the sidebar aggregation and the id-less draft handling. But it's only worth API surface if
there's more than one consumer that wants it — right now that may be just us, in which case "document
the pattern, keep it consumer-side" is the better answer. Deferring to you. If it's a yes, a couple of
conformance fixtures (create-then-watch-echo dedup by uid; delete-then-recreate-same-name → new uid)
would nail the contract the way the merge fixtures do.
Non-goal
Not asking the library to own the write. RBAC, attribution, and ValidateMergePatch stay host-side —
this is only about the client-state model and where staged create/delete belong in it.
Context
The gitops-api console (the app this library was extracted from) just grew two things it never had:
creating a new ConfigMap and deleting a whole one. It previously only edited objects the watch
had already delivered.
The design goal was that a create, a delete, and a field edit all land in the one "review & save"
pane and commit under the one Save button with the one reason — not three separate buttons with
three separate flows.
LiveResourceStorewas a clean fit for the edit half, and deliberately not for the other two — whichis correct and matches the library's read-only stance. This issue is about blessing the pattern for
whole-object create/delete so every consumer does it the same way, plus one open question. It is
explicitly not a request for the library to perform writes.
Why create/delete can't live in the store (and shouldn't)
Two properties of
LiveResourceStoremake this a non-fit by design, perdocs/client-state-model.md:metadata.uid. A pending create has no server object and therefore no uid, so ithas no key in the store.
has no fields to reconcile.
changes()/patch()/conflicts()— the whole value — simply don't apply.And per
docs/saving.md, "krm-stream is a read library; your application owns itsHTTP save endpoint." Create and delete are more of the same host-owned write surface (RBAC, attribution,
and — for save —
ValidateMergePatchall stay host-side). So staging the pending intent is genuinelythe consumer's job.
What we did (reference implementation)
Small page-local state, aggregated with the store for one unified pending list:
On success we currently just drop the pending entry and let the watch echo bring the object in/out.
What we found: the optimistic primitives already exist
While wiring this we noticed the library already has everything needed to reflect the result before
the watch echoes it — we just weren't using it:
adoptSaved(object)— when the uid is unknown it callsapplyServerEvent, i.e. it inserts a brand-new object. That is the optimistic-create primitive.removeResource(id)— the optimistic-delete primitive.The only reason our new card flickers for a beat is a consumer-side gap: our
POST /console/createreturns
204 No Content, so the client has no object to handadoptSaved. Returning the created objectand calling
adoptSaved(it)closes it — a fix we'll make on our side, not here.So the library is not actually missing capability. What it's missing is guidance.
The ask
1. Document the create/delete pattern (the real ask).
client-state-model.mdandsaving.mdcoveredit + save but say nothing about whole-object create/delete, so the next consumer will re-derive
(or mis-derive) it. Proposed additions:
client-state-model.md: a short "Creating and deleting whole objects" note — the store keys on uidand has no merge for these, so a consumer stages pending create/delete itself and aggregates them with
changes().saving.md: alongside the save example, show the create/delete siblings, and point atadoptSaved/removeResourcefor reflecting the response without waiting for the watch (idempotent with the echo,per the
I-IDEMPOTENTnote already in theadoptSaveddocstring).Happy to open the docs PR — we have the working reference implementation.
2. Open question: is a thin helper worth it? A tiny optional
PendingMutations-style aggregator(edits + creates + deletes → one list, one "submit descriptor" set) would stop each consumer
hand-rolling the sidebar aggregation and the id-less draft handling. But it's only worth API surface if
there's more than one consumer that wants it — right now that may be just us, in which case "document
the pattern, keep it consumer-side" is the better answer. Deferring to you. If it's a yes, a couple of
conformance fixtures (create-then-watch-echo dedup by uid; delete-then-recreate-same-name → new uid)
would nail the contract the way the merge fixtures do.
Non-goal
Not asking the library to own the write. RBAC, attribution, and
ValidateMergePatchstay host-side —this is only about the client-state model and where staged create/delete belong in it.