Skip to content

Docs/pattern: staging whole-object create + delete (edit/create/delete in one review + save) #15

Description

@sunib

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions