You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
chore(flags): retire phoenix-optimistic-edits — inline the optimistic edit path (#3667)
The flag served on@100% in production (optimistic in-place content edits,
#1675, epic #1637, released), so the gate and its flag-off fallback were dead
weight. Rip both and keep only the served-on behavior (ADR 0136).
- `useFlag(PHOENIX_OPTIMISTIC_EDITS)` removed from the three call sites
(`PanoPostDetail` post.edit + CommentEditComposer, sozluk `DefinitionCard`);
each now passes `optimistic` unconditionally instead of spreading it away.
- `postEditOptimistic` / `bodyEditOptimistic` drop their `enabled` parameter and
the `| undefined` return — the partial is always built.
- IaC declaration deleted: the `flags/keys.ts` const + `DECLARED_FLAGS` entry,
the `flagship/resources.ts` config/factory/import/re-export, and the
`alchemy.run.ts` import + stack wiring.
- `optimistic-edits.invariant.test.ts` deleted (it pinned the retired flag's
default-off IaC record); the `optimisticEdit` unit test drops its gate cases.
- Stale prose reworded in `.patterns/fate-mutations-client.md` and
`.glossary/TERMS.md`.
Closes#3667
Copy file name to clipboardExpand all lines: .glossary/TERMS.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -123,7 +123,7 @@ The vocab-free *mechanism* lives in `packages/authz`; the kamp.us *instances + s
123
123
| LiveDO | The unified **void**-aligned live-view Durable Object — ONE class, two roles, `state.storage` KV. Fans out mutations' `live.*` events to subscribers over SSE (ADR 0037). | two classes named ConnectionDO + TopicDO (see below) |
124
124
| ConnectionDO / TopicDO | The **former, superseded** two-class live-view design. Today there is ONE class, `LiveDO`, with two **roles** (connection, topic) dispatched via `resolveRole(state.id.name)` — not two Durable Object classes (ADR 0037). | live Durable Object class names (they are roles of LiveDO, not classes) |
125
125
| void | The upstream live-streaming Durable Object reference (`VoidLiveStreamDurableObject`) that **LiveDO** mirrors; the design authority for LiveDO's `DEFAULT_LIMITS`, the queue-full→`410` behavior, the first-failed-probe reap, and the stale/replay model. | inventing LiveDO limits/reap semantics from scratch |
126
-
| optimistic slice | One per-mutation optimistic-update unit (epic #1637): the client writes the mutation's effect into fate's normalized cache the instant the user acts, then **reconciles by canonical id** against the server frame (ADR 0125) — adds are **A1** client-append + canonical-id dedup (an optimistic temp node the server row replaces, no flash/dupe), deletes are **D1** reply-aware drop (a plain edge-drop for a leaf, a conservative `[silindi]` tombstone when replies are loaded), with rollback on rejection. Each slice ships dark behind its **own** default-off flag (`phoenix-optimistic-edits`, `pano-optimistic-submit`, `pano-optimistic-comment-add`/`-delete`, `phoenix-optimistic-definition-add`/`-delete`, `pano-optimistic-post-delete`); the DOM-free decision helpers live per page (`src/pages/*Optimistic*.ts`). | one shared epic-wide flag (each slice has an independent dark-ship lifecycle); a client-invented final id (the server id is canonical — the temp node reconciles to it) |
126
+
| optimistic slice | One per-mutation optimistic-update unit (epic #1637): the client writes the mutation's effect into fate's normalized cache the instant the user acts, then **reconciles by canonical id** against the server frame (ADR 0125) — adds are **A1** client-append + canonical-id dedup (an optimistic temp node the server row replaces, no flash/dupe), deletes are **D1** reply-aware drop (a plain edge-drop for a leaf, a conservative `[silindi]` tombstone when replies are loaded), with rollback on rejection. Each slice ships dark behind its **own** default-off flag (`pano-optimistic-submit`, `pano-optimistic-comment-add`/`-delete`, `phoenix-optimistic-definition-add`/`-delete`, `pano-optimistic-post-delete`), retired once the slice releases (ADR 0136 — the in-place edit slice is retired, its optimistic path unconditional); the DOM-free decision helpers live per page (`src/pages/*Optimistic*.ts`). | one shared epic-wide flag (each slice has an independent dark-ship lifecycle); a client-invented final id (the server id is canonical — the temp node reconciles to it) |
127
127
| role (connection / topic) | LiveDO's two roles: **connection** owns one client's SSE stream + subscriptions; **topic** owns a topic's subscriber registry, publish fan-out, and reap alarm. ||
128
128
| shaper | Maps a DB row to an `Entity` field set (`shapers.ts`). ||
129
129
| source | A `Fate.source(ViewClass, {id}, handlers)` loader entry delegating to Effect services (`sources.ts`); fate never queries D1. The loader half (silent reads). | a source that throws not-found |
Copy file name to clipboardExpand all lines: .patterns/fate-mutations-client.md
+4-5Lines changed: 4 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -46,18 +46,17 @@ For inserts that create an entity, give the optimistic record a temporary id (`o
46
46
47
47
An **in-place field edit** (`post.edit`, `comment.edit`, `definition.edit`) is the simplest optimistic case: it's an entity-field write-back that already re-renders in place through its result `view`, so making it optimistic is *only* passing the edited fields (plus a fresh `updatedAt`) as the `optimistic` partial. No temp id, no membership change — the edited entity keeps its id, so the optimistic write and the server `live.update({changed:["body"|"title,body"]})` frame touch the **same fields on the same entity** and can't diverge. fate rolls the partial back and restores the prior text on a rejected edit (the boundary-class throw, [Errors](#errors)), so the existing inline error shows with no phantom-saved state.
48
48
49
-
The load-bearing pieces are hook-free and unit-testable, mirroring `voteOptimistic`: [`src/fate/optimisticEdit.ts`](../apps/web/src/fate/optimisticEdit.ts) builds the partial (`postEditOptimistic` / `bodyEditOptimistic`) and returns `undefined` when the gate is off, so the call site spreads it away under `exactOptionalPropertyTypes`:
49
+
The load-bearing pieces are hook-free and unit-testable, mirroring `voteOptimistic`: [`src/fate/optimisticEdit.ts`](../apps/web/src/fate/optimisticEdit.ts) builds the partial (`postEditOptimistic` / `bodyEditOptimistic`):
50
50
51
51
```tsx
52
-
const optimistic =bodyEditOptimistic(optimisticEdits, body); // undefined when the flag is off
53
52
awaitfate.mutations.definition.edit({
54
53
input: {id, body},
55
-
...(optimistic? {optimistic} : {}), // {body, updatedAt: <now>} when on
The fresh `updatedAt` drives the "düzenlendi" indicator (`EditedIndicator`) instantly, consistently with the reconciled frame. Shipped dark behind the `phoenix-optimistic-edits` flag (default-off, ADR 0083; #1675, epic #1637) — off, the edit passes no `optimistic` and waits for the round-trip exactly as before.
59
+
The fresh `updatedAt` drives the "düzenlendi" indicator (`EditedIndicator`) instantly, consistently with the reconciled frame. Shipped dark behind a per-slice flag (#1675, epic #1637), released, and the flag retired (ADR 0136) — the optimistic path is now unconditional.
61
60
62
61
## Connection membership — declarative, not imperative
63
62
@@ -82,7 +81,7 @@ There is no hand-written updater enumerating connection keys. For connections th
82
81
> -**add** to a nested connection: the new node is normalized into the cache, but joins the list only via a live `appendNode` or a re-read. sözlük's `definition.add` publishes no append, so its composer **reloads after a successful add**.
83
82
> -**delete** of a node in a nested connection: `delete: true` is **wrong-entity** for sözlük's `definition.delete`, a `Term`-returning mutation (it re-resolves the parent for fresh counts), so `delete:true` would `deleteRecord("Term", definitionId)`. The card calls delete **without**`delete:true`; the server publishes `deleteEdge("Definition", id)` on `Term.definitions`.
84
83
>
85
-
> Entity-field mutations (vote, edit) are unaffected: they write back through the result `view` and re-render in place. Votes are fully optimistic; the in-place edits become optimistic behind the `phoenix-optimistic-edits` flag — see [Optimistic in-place edits](#optimistic-edits).
84
+
> Entity-field mutations (vote, edit) are unaffected: they write back through the result `view` and re-render in place. Votes and the in-place edits are both fully optimistic — see [Optimistic in-place edits](#optimistic-edits).
0 commit comments