Skip to content

Commit 44e31b5

Browse files
docs(effect): record wave 3.5 stage 3 outcomes, hub decision, helped/hurt addendum (#162)
1 parent f596377 commit 44e31b5

1 file changed

Lines changed: 58 additions & 2 deletions

File tree

‎docs/effect-conventions.md‎

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,61 @@ public edge. Pattern files:
9999
- [agent-patterns/effect-concurrency.md](../agent-patterns/effect-concurrency.md)
100100
- [agent-patterns/effect-errors.md](../agent-patterns/effect-errors.md)
101101

102+
## Stage 3 (dev seam) outcomes
103+
104+
One subsystem per PR, all behind unchanged Promise APIs and wire contracts
105+
(#158 boundary, #159 MCP session lifecycles, #160 rebuild scheduler,
106+
#161 EpochStore). Leaf filesystem/SDK helpers stay imperative and are
107+
identity-lifted through `src/effect/lift.ts`; the orchestration —
108+
lifecycles, mutual exclusion, coalescing, compensation, failure
109+
aggregation — is Effect.
110+
111+
**`ProjectEventHub` (the SSE hub) stays imperative.** Its public contract is
112+
*synchronous* re-entrant fan-out: `publish` delivers to listeners in the same
113+
turn (foreground shutdown depends on tombstone frames reaching socket buffers
114+
before one `setImmediate`, `foreground-server.ts` documents the invariant),
115+
`subscribe` replays retained history plus replay-gap frames synchronously,
116+
and listener failures remove the subscription mid-dispatch. Effect `PubSub`
117+
is an asynchronous fan-out structure; fiber-delivered events would change
118+
that observable contract, and driving a `PubSub` through unsafe synchronous
119+
drains would re-implement today's dispatch loop with none of Effect's
120+
guarantees. Revisit only if the hub's consumers ever move onto fibers
121+
end-to-end.
122+
123+
### Stage 3 helped / hurt addendum
124+
125+
Helped:
126+
127+
- `Semaphore.make(1)` + `withPermit` is a drop-in for the hand-rolled serial
128+
queues (FIFO waiters), including the process-wide per-project lease mutex
129+
map in the epoch store.
130+
- `Deferred` gives coalesced rebuild waiters one shared completion;
131+
`Effect.onExit` sits exactly where a `.finally` drain hook sat.
132+
- `Effect.forEach(..., { concurrency: 'unbounded' })` with per-element
133+
`Effect.exit` is the exact analogue of `Promise.allSettled` for
134+
settle-then-aggregate teardown contracts (`McpSessionServiceCloseError`,
135+
`EpochCleanupError`, publication rollback `AggregateError`).
136+
- `Effect.gen({ self: this }, function* (this: X) { ... })` keeps `#private`
137+
member access inside class-internal Effect programs.
138+
- `Effect.acquireRelease` + a transferred-ownership flag models "release on
139+
failure only until the constructed resource takes ownership" (MCP session
140+
open chain).
141+
142+
Hurt / gotchas:
143+
144+
- Effect finalizers are infallible by type. Teardown contracts that
145+
*propagate* cleanup failures (the dev seam's last-failure-wins `finally`
146+
chains, the staging-root removal that replaces the publish outcome) must be
147+
explicit effect sequences — capture the attempt's `Exit`, run the cleanup,
148+
then unwrap — never scope finalizers.
149+
- Bare `Effect.tryPromise(fn)` wraps rejections in `Cause.UnknownError`;
150+
always route through `src/effect/lift.ts` so typed dev errors and raw
151+
rejection values (`AbortSignal.reason`) cross the boundary untouched.
152+
- Synchronous admission windows are load-bearing: same-turn rebuild
153+
coalescing and session-close invalidation must not move inside a fiber.
154+
Construct `Semaphore`/`Deferred` with the boundary's `runSync` and keep the
155+
admission bookkeeping synchronous.
156+
102157
## Banned modules and APIs
103158

104159
- `Effect.runPromise` / `runSync` / `runFork` / `runCallback` (and `*With` /
@@ -108,13 +163,14 @@ public edge. Pattern files:
108163
- `@effect/vitest` — this repo uses rstest.
109164
- `NodeRuntime.runMain` / `BunRuntime` as a substitute for the boundary.
110165
- Ad-hoc `ManagedRuntime` outside a boundary module.
111-
- `effect/unstable/*` until listed below (Stage 2 listed none).
166+
- `effect/unstable/*` until listed below (Stages 2 and 3 listed none).
112167

113168
## Unstable-module adoptions
114169

115170
Re-pin chores re-verify every row. Stage 2 adopts none: Flight is a React
116171
binary stream, not Ndjson/SchemaBinary, and no other `effect/unstable/*`
117-
module fits the dispatcher rewrite.
172+
module fits the dispatcher rewrite. Stage 3 also adopts none: the dev seam
173+
needed only stable `Semaphore`, `Deferred`, `Scope`, and `Exit`.
118174

119175
| Module | Adopted in | Re-verify |
120176
| --- | --- | --- |

0 commit comments

Comments
 (0)