From 5007132759c49205e15850d5c355822541079550 Mon Sep 17 00:00:00 2001 From: Michael Arnaldi Date: Tue, 7 Oct 2025 10:42:04 +0200 Subject: [PATCH 01/20] add comprehensive Queue/TxQueue API alignment plan - Create unified Cause.Done completion type - Remove Queue.done() operation, adopt failCause as primitive - Fix TxQueue takeAll to return NonEmptyArray - Add Queue.interrupt for graceful close and align clear semantics - Add Queue.Enqueue interface for type-safe producer patterns - Document all breaking changes with migration paths - Include 4-phase implementation plan with validation checklist --- PLAN.md | 624 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 624 insertions(+) create mode 100644 PLAN.md diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 000000000..e13b535ed --- /dev/null +++ b/PLAN.md @@ -0,0 +1,624 @@ +# API Alignment Plan: Queue.ts vs TxQueue.ts + +**Goal:** Align the APIs of Queue.ts (Effect-based) and TxQueue.ts (STM-based) to ensure consistent naming, return types, functionality, and structure while respecting their fundamental differences. + +## Overview + +Queue.ts provides Effect-based asynchronous queues with backpressure strategies, while TxQueue.ts provides STM-based transactional queues. As TxQueue is the transactional counterpart of Queue, the APIs should mirror each other closely. + +**Key Differences to Preserve:** +- **Queue.ts**: Effect-based, supports unsafe synchronous operations +- **TxQueue.ts**: STM-based, all operations are atomic transactions (no unsafe variants needed) + +## Critical Issues Identified + +### 1. ❌ WRONG: Inconsistent Completion Semantics (HIGHEST PRIORITY) + +**Current State:** +- Queue.ts uses local `Done` interface (lines 968-992) +- TxQueue.ts uses `Cause.NoSuchElementError` +- Both represent the same concept: graceful queue completion + +**Why This is Wrong:** +- `NoSuchElementError` implies "element not found" (lookup failure) +- Queue completion means "gracefully finished, no more items" (lifecycle event) +- Different types for same semantic meaning = API confusion + +**Solution:** +- Create unified `Cause.Done` error type in Cause.ts +- Both queues use `Cause.Done` for completion semantics +- Remove Queue's local `Done` interface +- Migrate TxQueue from `Cause.NoSuchElementError` to `Cause.Done` + +### 2. ❌ WRONG: Queue's `done()` Operation (HIGH PRIORITY) + +**Current State:** +```typescript +// Queue.ts - Complex signature with Exit +export const done = ( + self: Queue, + exit: Exit +): Effect +``` + +**Why This is Wrong:** +- Complex conditional type signature +- Takes `Exit` instead of `Cause` (less natural) +- Not present in TxQueue (inconsistency) +- `Cause` is the natural primitive, not `Exit` + +**TxQueue's Better Approach:** +```typescript +// TxQueue.ts - Clean signatures +export const fail: (self: TxEnqueue, error: E) => Effect +export const failCause: (self: TxEnqueue, cause: Cause) => Effect +export const end: (self: TxEnqueue) => Effect +``` + +**Solution:** +- Remove `done()` and `doneUnsafe()` from Queue.ts +- Make `failCause(cause: Cause)` the primitive in both implementations +- Build `fail` and `end` as convenience wrappers around `failCause` +- Hierarchy: `failCause` → `fail`, `end` + +### 3. ❌ WRONG: TxQueue `takeAll` Type Lie (HIGH PRIORITY) + +**Current State:** +```typescript +// TxQueue.ts - Says "might be empty" but blocks until non-empty! +export const takeAll = (self: TxDequeue): Effect, E> +``` + +**Implementation Reality:** +```typescript +// Blocks until at least 1 item available +if (yield* isEmpty(self)) { + return yield* Effect.retryTransaction // ← BLOCKS HERE +} +// Only proceeds when ≥1 item available +``` + +**Queue.ts Has it Correct:** +```typescript +export const takeAll = (self: Dequeue): Effect, E> +``` + +**Solution:** +- Fix TxQueue `takeAll` signature to return `NonEmptyArray` +- Type now accurately reflects runtime behavior + +### 4. ❌ WRONG: Queue Missing `interrupt` Operation (HIGH PRIORITY) + +**Current State:** +- Queue.ts has NO `interrupt` operation +- Only has `shutdown` which clears AND interrupts immediately +- No way to gracefully close (stop accepting, allow draining) + +**TxQueue.ts Has it:** +```typescript +export const interrupt = (self: TxEnqueue): Effect + // Graceful close - stops accepting, allows draining existing items +``` + +**Solution:** +- Add `interrupt` to Queue.ts +- Refactor `shutdown` to compose `clear` + `interrupt` + +### 5. ❌ WRONG: Inconsistent `clear` Semantics (MEDIUM PRIORITY) + +**Current State:** +```typescript +// Queue.ts - Returns Array, category "taking" +export const clear = (self: Dequeue): Effect, E> + +// TxQueue.ts - Returns void, category "combinators" +export const clear = (self: TxEnqueue): Effect +``` + +**Solution:** +- Align both to return `Array` (observable operations) +- Fix Queue's category from "taking" to "combinators" +- Change TxQueue to return cleared items + +### 6. ❌ WRONG: Queue Missing `Enqueue` Interface (MEDIUM PRIORITY) + +**Current State:** +```typescript +// TxQueue.ts - THREE interfaces (correct structure) +TxEnqueue // Write-only (contravariant) +TxDequeue // Read-only (covariant) +TxQueue // Full queue (invariant) + +// Queue.ts - TWO interfaces (incomplete) +Dequeue // Read-only +Queue // Full queue +// MISSING: Enqueue +``` + +**Solution:** +- Add `Enqueue` interface to Queue.ts +- Update `Queue` to extend both `Enqueue` and `Dequeue` +- Add `isEnqueue` guard and `asEnqueue` converter +- Enables type-safe producer-consumer patterns + +### 7. ❌ WRONG: Return Type Inconsistencies (MEDIUM PRIORITY) + +**Issue 7a: `offerAll` returns different types** +- Queue.ts: `Effect>` (remaining messages) +- TxQueue.ts: `Effect>` (rejected items) +- **Solution:** Both return `Array` + +**Issue 7b: Signature patterns need unified `Cause.Done`** +- All `E | Done` → `E | Cause.Done` +- All `Exclude` → `Exclude` +- All `E | Cause.NoSuchElementError` → `E | Cause.Done` + +## Implementation Plan + +### Phase 0: Unified Completion API (HIGHEST PRIORITY) + +#### Step 1: Create `Cause.Done` Error Type +**File:** `packages/effect/src/Cause.ts` + `packages/effect/src/internal/core.ts` + +```typescript +/** + * Type identifier for Done errors. + * @since 4.0.0 + * @category symbols + */ +export const DoneTypeId: "~effect/Cause/Done" = "~effect/Cause/Done" as const + +/** + * Represents a graceful completion signal for queues and streams. + * + * `Done` is used to signal that a queue or stream has completed normally + * and no more elements will be produced. This is distinct from an error + * or interruption - it represents successful completion. + * + * @example + * ```ts + * import { Cause, Effect } from "effect" + * import { Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(10) + * + * yield* Queue.offer(queue, 1) + * yield* Queue.offer(queue, 2) + * + * // Signal completion + * yield* Queue.end(queue) + * + * // Taking from ended queue fails with Done + * const result = yield* Effect.flip(Queue.take(queue)) + * console.log(Cause.isDone(result)) // true + * }) + * ``` + * + * @since 4.0.0 + * @category models + */ +export interface Done extends YieldableError { + readonly [DoneTypeId]: typeof DoneTypeId + readonly _tag: "Done" +} + +/** + * Creates a `Done` error to signal graceful completion. + * @since 4.0.0 + * @category constructors + */ +export const Done: new() => Done + +/** + * Tests if a value is a `Done` error. + * @since 4.0.0 + * @category guards + */ +export const isDone: (u: unknown) => u is Done +``` + +#### Step 2: Remove Queue's `done()` Operations +**File:** `packages/effect/src/Queue.ts` + +**DELETE:** +- Line ~816: `done()` operation +- Line ~850: `doneUnsafe()` operation + +**REFACTOR:** +```typescript +// Change fail and end to call failCause directly +export const fail = (self: Queue, error: E): Effect => + failCause(self, Cause.fail(error)) + +export const end = (self: Queue): Effect => + failCause(self, Cause.fail(new Cause.Done())) +``` + +#### Step 3: Migrate Queue.ts Signatures to `Cause.Done` +**File:** `packages/effect/src/Queue.ts` + +**Update locations:** +- Line 750: `end` signature +- Line 783: `endUnsafe` signature +- Line 968-992: **DELETE** local `Done` interface, `isDone`, `filterDone` +- Line 1040: `collect` signature (both patterns) +- Line 1244-1245: `await_` signature +- Line 1434-1446: `into` signatures +- Line 1476: `toPull` signature (both patterns) +- Line 1499-1500: `toPullArray` signature + +**Search/Replace patterns:** +```typescript +E | Done → E | Cause.Done +Exclude → Exclude +``` + +**Add import:** +```typescript +import { Done } from "./Cause.ts" +``` + +#### Step 4: Migrate TxQueue.ts from `NoSuchElementError` to `Cause.Done` +**File:** `packages/effect/src/stm/TxQueue.ts` + +**Update locations:** +- Line 208-211: JSDoc example in `TxEnqueue` interface +- Line 1273-1308: `end` function signature + implementation +- Line 1287: Example type annotation +- Line 1295-1300: JSDoc examples with `isNoSuchElementError` + +**Search/Replace patterns:** +```typescript +E | Cause.NoSuchElementError → E | Cause.Done +Exclude → Exclude +Cause.NoSuchElementError → Cause.Done +Cause.isNoSuchElementError → Cause.isDone +new Cause.NoSuchElementError() → new Cause.Done() +``` + +#### Step 5: Add `interrupt` to Queue.ts +**File:** `packages/effect/src/Queue.ts` + +```typescript +/** + * Interrupts the queue, transitioning it to a closing state. + * Existing items can still be consumed, but no new items will be accepted. + * + * @example + * ```ts + * import { Effect } from "effect" + * import { Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(10) + * yield* Queue.offer(queue, 1) + * yield* Queue.offer(queue, 2) + * + * // Gracefully close - no more offers, but can drain + * yield* Queue.interrupt(queue) + * + * // Can still take existing items + * const item = yield* Queue.take(queue) + * console.log(item) // 1 + * }) + * ``` + * + * @category completion + * @since 4.0.0 + */ +export const interrupt = (self: Queue): Effect => + Effect.withFiber((fiber) => failCause(self, Cause.interrupt(fiber.id))) +``` + +#### Step 6: Fix `clear` Semantics +**File:** `packages/effect/src/Queue.ts` + +```typescript +// Change category annotation +// @category combinators (was: taking) +export const clear = (self: Dequeue): Effect, E> +``` + +**File:** `packages/effect/src/stm/TxQueue.ts` + +```typescript +// Change to return Array instead of void +export const clear = (self: TxEnqueue): Effect.Effect> => + Effect.atomic( + Effect.gen(function*() { + const chunk = yield* TxChunk.get(self.items) + const items = Chunk.toArray(chunk) + yield* TxChunk.set(self.items, Chunk.empty()) + return items + }) + ) +``` + +#### Step 7: Refactor `shutdown` Composition +**File:** `packages/effect/src/Queue.ts` + +```typescript +export const shutdown = (self: Queue): Effect => + Effect.gen(function*() { + yield* clear(self) // Clear items first + return yield* interrupt(self) // Then interrupt + }) +``` + +### Phase 1: Interface Structure Alignment + +#### Step 1: Add `Enqueue` Interface to Queue.ts +**File:** `packages/effect/src/Queue.ts` + +```typescript +const EnqueueTypeId = "~effect/Queue/Enqueue" + +/** + * An `Enqueue` represents the write-only interface of a queue. + * + * @example + * ```ts + * import { Effect } from "effect" + * import { Queue } from "effect" + * + * const producer = (enqueue: Queue.Enqueue) => + * Effect.gen(function*() { + * yield* Queue.offer(enqueue, 1) + * yield* Queue.offerAll(enqueue, [2, 3, 4]) + * }) + * ``` + * + * @since 4.0.0 + * @category models + */ +export interface Enqueue extends Inspectable { + readonly [EnqueueTypeId]: Enqueue.Variance + readonly strategy: "suspend" | "dropping" | "sliding" + readonly scheduler: Scheduler + capacity: number + messages: MutableList.MutableList + state: Queue.State + scheduleRunning: boolean +} + +export declare namespace Enqueue { + export interface Variance { + _A: Types.Contravariant + _E: Types.Contravariant + } +} + +/** + * Type guard to check if a value is an Enqueue. + * @since 4.0.0 + * @category guards + */ +export const isEnqueue = ( + u: unknown +): u is Enqueue => hasProperty(u, EnqueueTypeId) + +/** + * Convert a Queue to an Enqueue, allowing only write operations. + * @since 4.0.0 + * @category conversions + */ +export const asEnqueue: (self: Queue) => Enqueue = identity +``` + +#### Step 2: Update Queue Interface +**File:** `packages/effect/src/Queue.ts` + +```typescript +// Change from: +export interface Queue extends Dequeue + +// To: +export interface Queue + extends Enqueue, Dequeue +``` + +#### Step 3: Update Proto +```typescript +const QueueProto = { + [TypeId]: variance, + [DequeueTypeId]: variance, + [EnqueueTypeId]: variance, // ADD THIS + ...PipeInspectableProto, + // ... +} +``` + +### Phase 2: Critical Return Type Fixes + +#### Step 1: Fix TxQueue `takeAll` Return Type +**File:** `packages/effect/src/stm/TxQueue.ts` + +```typescript +// Change from: +export const takeAll = (self: TxDequeue): Effect.Effect, E> + +// To: +export const takeAll = (self: TxDequeue): Effect.Effect, E> +``` + +#### Step 2: Align `offerAll` Return Type +**File:** `packages/effect/src/stm/TxQueue.ts` + +```typescript +// Change from: +export const offerAll = ( + self: TxEnqueue, + values: Iterable +): Effect.Effect> + +// To: +export const offerAll = ( + self: TxEnqueue, + values: Iterable +): Effect.Effect> + +// Update implementation to return Array instead of Chunk +``` + +### Phase 3: Missing Operations + +#### Add to Queue.ts: +- `poll` - non-blocking take (returns `Option`) +- `peek` - inspect without removing + +#### Add to TxQueue.ts: +- `collect` - take all until done/error + +### Phase 4: Documentation & Validation + +1. Update all JSDoc examples to use `Cause.Done` +2. Update test files for both modules +3. Run validation: + - `pnpm lint --fix` on modified files + - `pnpm check` for type errors + - `pnpm docgen` for example compilation + - `pnpm test` for all tests + +## Breaking Changes Summary + +### Breaking Change #1: Unified Completion Type +```typescript +// Before +Queue +TxQueue + +// After (both identical) +Queue +TxQueue +``` + +**Migration:** +```typescript +// Queue users: Change imports +import { Queue } from "effect" +// Remove: import type { Done } from "effect/Queue" +import { Cause } from "effect" + +// Type annotations +const queue: Queue // Before +const queue: Queue // After + +// TxQueue users: Similar change +const queue: TxQueue // Before +const queue: TxQueue // After +``` + +### Breaking Change #2: Remove `Queue.done()` Operation +```typescript +// Before +yield* Queue.done(queue, Exit.fail(error)) +yield* Queue.done(queue, Exit.succeed(undefined)) + +// After +yield* Queue.failCause(queue, Cause.fail(error)) +yield* Queue.end(queue) // For graceful completion +``` + +**Migration:** +- Replace `Queue.done(queue, Exit.fail(e))` with `Queue.failCause(queue, Cause.fail(e))` +- Replace `Queue.done(queue, Exit.succeed())` with `Queue.end(queue)` +- No `doneUnsafe` equivalent - use `failCauseUnsafe` or `endUnsafe` + +### Breaking Change #3: `takeAll` Return Type +```typescript +// Before (TxQueue) +const items: ReadonlyArray = yield* TxQueue.takeAll(queue) +if (items.length > 0) { ... } // Unnecessary check! + +// After (TxQueue) +const items: NonEmptyArray = yield* TxQueue.takeAll(queue) +// Guaranteed non-empty, no check needed! +``` + +### Breaking Change #4: `clear` Return Type +```typescript +// Before (TxQueue) +yield* TxQueue.clear(queue) // Returns void + +// After (TxQueue) +const cleared = yield* TxQueue.clear(queue) // Returns Array +console.log("Cleared items:", cleared) +``` + +### Breaking Change #5: Queue Interface Structure +```typescript +// Before +Queue extends Dequeue // Only two interfaces + +// After +Queue extends Enqueue, Dequeue // Three interfaces + +// New capabilities +const enqueue: Queue.Enqueue = queue // Write-only +const dequeue: Queue.Dequeue = queue // Read-only (existing) +``` + +**Note:** This is mostly non-breaking as existing code continues to work. Only affects advanced type-level programming. + +## Validation Checklist + +After implementation, verify: + +1. ✅ **No local Done types** - `grep "interface Done" Queue.ts` returns nothing +2. ✅ **No NoSuchElementError in queues** - `grep "NoSuchElementError" Queue.ts TxQueue.ts` returns nothing +3. ✅ **All Exclude patterns updated** - `grep "Exclude" Queue.ts` returns nothing without `Cause.` +4. ✅ **Consistent imports** - Both files import `Done` from `./Cause.ts` +5. ✅ **Tests pass** - `pnpm test Queue.test.ts TxQueue.test.ts` +6. ✅ **Types check** - `pnpm check` +7. ✅ **Examples compile** - `pnpm docgen` +8. ✅ **Both return NonEmptyArray** - `takeAll` signatures match +9. ✅ **Both return Array** - `offerAll` and `clear` signatures match +10. ✅ **Both have interrupt** - Queue and TxQueue have graceful close +11. ✅ **Both have Enqueue** - Three-interface structure matches + +## Success Criteria + +- ✅ `Cause.Done` exists and is used by both queues +- ✅ Queue's local `Done` type removed +- ✅ TxQueue uses `Cause.Done` (not `NoSuchElementError`) +- ✅ Queue's `done()` operation removed +- ✅ Both queues have `fail`, `failCause`, `end` as completion API +- ✅ Both queues have `interrupt` for graceful close +- ✅ `takeAll` returns `NonEmptyArray` in both queues +- ✅ `offerAll` returns `Array` in both queues +- ✅ `clear` returns `Array` in both queues +- ✅ Both queues have `Enqueue`, `Dequeue`, `Queue` interfaces +- ✅ All signatures use consistent completion types +- ✅ All tests pass +- ✅ `pnpm docgen` succeeds +- ✅ `pnpm check` succeeds + +## Rationale + +### Why `Cause.Done` Over `NoSuchElementError`? +- **Semantic correctness**: Completion is not "not finding something" +- **Consistency**: Same concept = same type +- **Clarity**: `Done` clearly signals "finished normally" + +### Why Remove `Queue.done()`? +- **Simpler API**: `failCause` is more natural than `done(Exit<...>)` +- **Consistency**: TxQueue doesn't have it, shouldn't be different +- **Power**: `Cause` can represent ANY completion scenario +- **Type safety**: Clearer signature without complex conditional types + +### Why `takeAll` Returns `NonEmptyArray`? +- **Type honesty**: Implementation blocks until ≥1 item available +- **User benefit**: No unnecessary empty checks +- **Correctness**: Type reflects runtime behavior + +### Why Add `Enqueue` Interface? +- **Type safety**: Restrict operations at type level +- **Consistency**: Match TxQueue's structure +- **Patterns**: Enable producer-consumer separation +- **Variance**: Proper contravariant producer type + +### Why `clear` Returns `Array`? +- **Observability**: See what was cleared (useful for debugging) +- **Consistency**: Both queues behave the same +- **Less breaking**: Queue already has this signature From 084809699d20da9639bf36de154948b6392080e2 Mon Sep 17 00:00:00 2001 From: Michael Arnaldi Date: Tue, 7 Oct 2025 11:24:12 +0200 Subject: [PATCH 02/20] docs(PLAN): add detailed implementation steps with commit messages --- PLAN.md | 241 +++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 202 insertions(+), 39 deletions(-) diff --git a/PLAN.md b/PLAN.md index e13b535ed..00bd30138 100644 --- a/PLAN.md +++ b/PLAN.md @@ -7,6 +7,7 @@ Queue.ts provides Effect-based asynchronous queues with backpressure strategies, while TxQueue.ts provides STM-based transactional queues. As TxQueue is the transactional counterpart of Queue, the APIs should mirror each other closely. **Key Differences to Preserve:** + - **Queue.ts**: Effect-based, supports unsafe synchronous operations - **TxQueue.ts**: STM-based, all operations are atomic transactions (no unsafe variants needed) @@ -15,16 +16,19 @@ Queue.ts provides Effect-based asynchronous queues with backpressure strategies, ### 1. ❌ WRONG: Inconsistent Completion Semantics (HIGHEST PRIORITY) **Current State:** + - Queue.ts uses local `Done` interface (lines 968-992) - TxQueue.ts uses `Cause.NoSuchElementError` - Both represent the same concept: graceful queue completion **Why This is Wrong:** + - `NoSuchElementError` implies "element not found" (lookup failure) - Queue completion means "gracefully finished, no more items" (lifecycle event) - Different types for same semantic meaning = API confusion **Solution:** + - Create unified `Cause.Done` error type in Cause.ts - Both queues use `Cause.Done` for completion semantics - Remove Queue's local `Done` interface @@ -33,21 +37,24 @@ Queue.ts provides Effect-based asynchronous queues with backpressure strategies, ### 2. ❌ WRONG: Queue's `done()` Operation (HIGH PRIORITY) **Current State:** + ```typescript // Queue.ts - Complex signature with Exit export const done = ( - self: Queue, + self: Queue, exit: Exit ): Effect ``` **Why This is Wrong:** + - Complex conditional type signature - Takes `Exit` instead of `Cause` (less natural) - Not present in TxQueue (inconsistency) - `Cause` is the natural primitive, not `Exit` **TxQueue's Better Approach:** + ```typescript // TxQueue.ts - Clean signatures export const fail: (self: TxEnqueue, error: E) => Effect @@ -56,6 +63,7 @@ export const end: (self: TxEnqueue) => Effect ``` **Solution:** + - Remove `done()` and `doneUnsafe()` from Queue.ts - Make `failCause(cause: Cause)` the primitive in both implementations - Build `fail` and `end` as convenience wrappers around `failCause` @@ -64,49 +72,57 @@ export const end: (self: TxEnqueue) => Effect ### 3. ❌ WRONG: TxQueue `takeAll` Type Lie (HIGH PRIORITY) **Current State:** + ```typescript // TxQueue.ts - Says "might be empty" but blocks until non-empty! export const takeAll = (self: TxDequeue): Effect, E> ``` **Implementation Reality:** + ```typescript // Blocks until at least 1 item available -if (yield* isEmpty(self)) { - return yield* Effect.retryTransaction // ← BLOCKS HERE +if (yield * isEmpty(self)) { + return yield * Effect.retryTransaction // ← BLOCKS HERE } // Only proceeds when ≥1 item available ``` **Queue.ts Has it Correct:** + ```typescript export const takeAll = (self: Dequeue): Effect, E> ``` **Solution:** + - Fix TxQueue `takeAll` signature to return `NonEmptyArray` - Type now accurately reflects runtime behavior ### 4. ❌ WRONG: Queue Missing `interrupt` Operation (HIGH PRIORITY) **Current State:** + - Queue.ts has NO `interrupt` operation - Only has `shutdown` which clears AND interrupts immediately - No way to gracefully close (stop accepting, allow draining) **TxQueue.ts Has it:** + ```typescript export const interrupt = (self: TxEnqueue): Effect // Graceful close - stops accepting, allows draining existing items ``` **Solution:** + - Add `interrupt` to Queue.ts - Refactor `shutdown` to compose `clear` + `interrupt` ### 5. ❌ WRONG: Inconsistent `clear` Semantics (MEDIUM PRIORITY) **Current State:** + ```typescript // Queue.ts - Returns Array, category "taking" export const clear = (self: Dequeue): Effect, E> @@ -116,6 +132,7 @@ export const clear = (self: TxEnqueue): Effect ``` **Solution:** + - Align both to return `Array` (observable operations) - Fix Queue's category from "taking" to "combinators" - Change TxQueue to return cleared items @@ -123,6 +140,7 @@ export const clear = (self: TxEnqueue): Effect ### 6. ❌ WRONG: Queue Missing `Enqueue` Interface (MEDIUM PRIORITY) **Current State:** + ```typescript // TxQueue.ts - THREE interfaces (correct structure) TxEnqueue // Write-only (contravariant) @@ -136,6 +154,7 @@ Queue // Full queue ``` **Solution:** + - Add `Enqueue` interface to Queue.ts - Update `Queue` to extend both `Enqueue` and `Dequeue` - Add `isEnqueue` guard and `asEnqueue` converter @@ -144,23 +163,138 @@ Queue // Full queue ### 7. ❌ WRONG: Return Type Inconsistencies (MEDIUM PRIORITY) **Issue 7a: `offerAll` returns different types** + - Queue.ts: `Effect>` (remaining messages) - TxQueue.ts: `Effect>` (rejected items) - **Solution:** Both return `Array` **Issue 7b: Signature patterns need unified `Cause.Done`** + - All `E | Done` → `E | Cause.Done` - All `Exclude` → `Exclude` - All `E | Cause.NoSuchElementError` → `E | Cause.Done` +## Detailed Implementation Steps with Commits + +### Phase 0: Unified Completion API (HIGHEST PRIORITY) + +**Commit 0.1:** `feat(Cause): add Done error type for queue completion` + +- Add `Done` class extending `Pull.Halt` to `packages/effect/src/internal/core.ts` +- Export `Done`, `DoneTypeId`, `isDone` from `packages/effect/src/Cause.ts` +- Validates: `pnpm lint --fix`, `pnpm tsc --noEmit` + +**Commit 0.2:** `refactor(Queue): migrate to use Cause.Done for completion` + +- Replace all `E | Done` with `E | Cause.Done` in Queue.ts +- Replace all `Exclude` with `Exclude` +- Update `end`, `endUnsafe`, `collect`, `await_`, `into`, `toPull`, `toPullArray` signatures +- Deprecate local `Queue.Done`, re-export `Cause.Done` as `Queue.Done` for compatibility +- Update JSDoc examples to use `Cause.Done` +- Validates: `pnpm lint --fix`, `pnpm tsc --noEmit`, `pnpm test packages/effect/test/Queue.test.ts` + +**Commit 0.3:** `refactor(TxQueue): migrate from NoSuchElementError to Cause.Done` + +- Replace all `Cause.NoSuchElementError` with `Cause.Done` +- Update `end()` signature and implementation +- Update all JSDoc examples +- Validates: `pnpm lint --fix`, `pnpm tsc --noEmit`, `pnpm test packages/effect/test/TxQueue.test.ts` + +**Commit 0.4:** `feat(Queue): add interrupt operation for graceful close` + +- Add `interrupt()` function to Queue.ts +- Add JSDoc with examples +- Validates: `pnpm lint --fix`, `pnpm tsc --noEmit`, `pnpm test packages/effect/test/Queue.test.ts` + +**Commit 0.5:** `refactor(Queue): fix clear semantics to return cleared items` + +- Change `clear()` category from "taking" to "combinators" +- Validates: `pnpm lint --fix`, `pnpm tsc --noEmit` + +**Commit 0.6:** `refactor(TxQueue): change clear to return cleared items` + +- Update `clear()` to return `Array` instead of `void` +- Update implementation +- Validates: `pnpm lint --fix`, `pnpm tsc --noEmit`, `pnpm test packages/effect/test/TxQueue.test.ts` + +**Commit 0.7:** `refactor(Queue): refactor shutdown to compose clear + interrupt` + +- Change `shutdown()` implementation to call `clear()` then `interrupt()` +- Validates: `pnpm lint --fix`, `pnpm tsc --noEmit`, `pnpm test packages/effect/test/Queue.test.ts` + +### Phase 1: Interface Structure Alignment + +**Commit 1.1:** `feat(Queue): add Enqueue interface for write-only operations` + +- Add `EnqueueTypeId`, `Enqueue` interface with contravariant variance +- Add `isEnqueue()` guard and `asEnqueue()` converter +- Update `Queue` interface to extend both `Enqueue` and `Dequeue` +- Update `QueueProto` to include `EnqueueTypeId` +- Validates: `pnpm lint --fix`, `pnpm tsc --noEmit`, `pnpm test packages/effect/test/Queue.test.ts` + +### Phase 2: Critical Return Type Fixes + +**Commit 2.1:** `fix(TxQueue): change takeAll to return NonEmptyArray` + +- Update `takeAll()` signature from `ReadonlyArray` to `NonEmptyArray` +- Update tests +- Validates: `pnpm lint --fix`, `pnpm tsc --noEmit`, `pnpm test packages/effect/test/TxQueue.test.ts` + +**Commit 2.2:** `fix(TxQueue): change offerAll to return Array instead of Chunk` + +- Update `offerAll()` signature from `Chunk` to `Array` +- Update implementation to convert Chunk to Array +- Update tests +- Validates: `pnpm lint --fix`, `pnpm tsc --noEmit`, `pnpm test packages/effect/test/TxQueue.test.ts` + +### Phase 3: Missing Operations + +**Commit 3.1:** `feat(Queue): add poll operation for non-blocking take` + +- Add `poll()` function returning `Effect, E>` +- Add JSDoc with examples +- Validates: `pnpm lint --fix`, `pnpm tsc --noEmit`, `pnpm test packages/effect/test/Queue.test.ts` + +**Commit 3.2:** `feat(Queue): add peek operation to inspect without removing` + +- Add `peek()` function returning `Effect` +- Add JSDoc with examples +- Validates: `pnpm lint --fix`, `pnpm tsc --noEmit`, `pnpm test packages/effect/test/Queue.test.ts` + +**Commit 3.3:** `feat(TxQueue): add collect operation for drain until done` + +- Add `collect()` function +- Add JSDoc with examples +- Validates: `pnpm lint --fix`, `pnpm tsc --noEmit`, `pnpm test packages/effect/test/TxQueue.test.ts` + +### Phase 4: Documentation & Final Validation + +**Commit 4.1:** `docs(Queue,TxQueue): update all JSDoc examples for Cause.Done` + +- Review and update remaining JSDoc examples +- Validates: `pnpm docgen` + +**Commit 4.2:** `test(Queue,TxQueue): update tests for API changes` + +- Update all test files for breaking changes +- Validates: `pnpm test packages/effect/test/Queue.test.ts packages/effect/test/TxQueue.test.ts` + +**Final Validation:** + +- `pnpm lint` - All files +- `pnpm check` - Full typecheck +- `pnpm docgen` - Examples compile +- `pnpm test` - All tests pass + ## Implementation Plan ### Phase 0: Unified Completion API (HIGHEST PRIORITY) #### Step 1: Create `Cause.Done` Error Type + **File:** `packages/effect/src/Cause.ts` + `packages/effect/src/internal/core.ts` -```typescript +````typescript /** * Type identifier for Done errors. * @since 4.0.0 @@ -170,7 +304,7 @@ export const DoneTypeId: "~effect/Cause/Done" = "~effect/Cause/Done" as const /** * Represents a graceful completion signal for queues and streams. - * + * * `Done` is used to signal that a queue or stream has completed normally * and no more elements will be produced. This is distinct from an error * or interruption - it represents successful completion. @@ -182,13 +316,13 @@ export const DoneTypeId: "~effect/Cause/Done" = "~effect/Cause/Done" as const * * const program = Effect.gen(function*() { * const queue = yield* Queue.bounded(10) - * + * * yield* Queue.offer(queue, 1) * yield* Queue.offer(queue, 2) - * + * * // Signal completion * yield* Queue.end(queue) - * + * * // Taking from ended queue fails with Done * const result = yield* Effect.flip(Queue.take(queue)) * console.log(Cause.isDone(result)) // true @@ -208,7 +342,7 @@ export interface Done extends YieldableError { * @since 4.0.0 * @category constructors */ -export const Done: new() => Done +export const Done: new () => Done /** * Tests if a value is a `Done` error. @@ -216,31 +350,35 @@ export const Done: new() => Done * @category guards */ export const isDone: (u: unknown) => u is Done -``` +```` #### Step 2: Remove Queue's `done()` Operations + **File:** `packages/effect/src/Queue.ts` **DELETE:** + - Line ~816: `done()` operation - Line ~850: `doneUnsafe()` operation **REFACTOR:** + ```typescript // Change fail and end to call failCause directly -export const fail = (self: Queue, error: E): Effect => - failCause(self, Cause.fail(error)) +export const fail = (self: Queue, error: E): Effect => failCause(self, Cause.fail(error)) export const end = (self: Queue): Effect => failCause(self, Cause.fail(new Cause.Done())) ``` #### Step 3: Migrate Queue.ts Signatures to `Cause.Done` + **File:** `packages/effect/src/Queue.ts` **Update locations:** + - Line 750: `end` signature -- Line 783: `endUnsafe` signature +- Line 783: `endUnsafe` signature - Line 968-992: **DELETE** local `Done` interface, `isDone`, `filterDone` - Line 1040: `collect` signature (both patterns) - Line 1244-1245: `await_` signature @@ -249,26 +387,31 @@ export const end = (self: Queue): Effect => - Line 1499-1500: `toPullArray` signature **Search/Replace patterns:** + ```typescript E | Done → E | Cause.Done Exclude → Exclude ``` **Add import:** + ```typescript import { Done } from "./Cause.ts" ``` #### Step 4: Migrate TxQueue.ts from `NoSuchElementError` to `Cause.Done` + **File:** `packages/effect/src/stm/TxQueue.ts` **Update locations:** + - Line 208-211: JSDoc example in `TxEnqueue` interface - Line 1273-1308: `end` function signature + implementation - Line 1287: Example type annotation - Line 1295-1300: JSDoc examples with `isNoSuchElementError` **Search/Replace patterns:** + ```typescript E | Cause.NoSuchElementError → E | Cause.Done Exclude → Exclude @@ -278,9 +421,10 @@ new Cause.NoSuchElementError() → new Cause.Done() ``` #### Step 5: Add `interrupt` to Queue.ts + **File:** `packages/effect/src/Queue.ts` -```typescript +````typescript /** * Interrupts the queue, transitioning it to a closing state. * Existing items can still be consumed, but no new items will be accepted. @@ -309,9 +453,10 @@ new Cause.NoSuchElementError() → new Cause.Done() */ export const interrupt = (self: Queue): Effect => Effect.withFiber((fiber) => failCause(self, Cause.interrupt(fiber.id))) -``` +```` #### Step 6: Fix `clear` Semantics + **File:** `packages/effect/src/Queue.ts` ```typescript @@ -326,7 +471,7 @@ export const clear = (self: Dequeue): Effect, E> // Change to return Array instead of void export const clear = (self: TxEnqueue): Effect.Effect> => Effect.atomic( - Effect.gen(function*() { + Effect.gen(function* () { const chunk = yield* TxChunk.get(self.items) const items = Chunk.toArray(chunk) yield* TxChunk.set(self.items, Chunk.empty()) @@ -336,22 +481,24 @@ export const clear = (self: TxEnqueue): Effect.Effect> => ``` #### Step 7: Refactor `shutdown` Composition + **File:** `packages/effect/src/Queue.ts` ```typescript export const shutdown = (self: Queue): Effect => - Effect.gen(function*() { - yield* clear(self) // Clear items first - return yield* interrupt(self) // Then interrupt + Effect.gen(function* () { + yield* clear(self) // Clear items first + return yield* interrupt(self) // Then interrupt }) ``` ### Phase 1: Interface Structure Alignment #### Step 1: Add `Enqueue` Interface to Queue.ts + **File:** `packages/effect/src/Queue.ts` -```typescript +````typescript const EnqueueTypeId = "~effect/Queue/Enqueue" /** @@ -394,9 +541,7 @@ export declare namespace Enqueue { * @since 4.0.0 * @category guards */ -export const isEnqueue = ( - u: unknown -): u is Enqueue => hasProperty(u, EnqueueTypeId) +export const isEnqueue = (u: unknown): u is Enqueue => hasProperty(u, EnqueueTypeId) /** * Convert a Queue to an Enqueue, allowing only write operations. @@ -404,9 +549,10 @@ export const isEnqueue = ( * @category conversions */ export const asEnqueue: (self: Queue) => Enqueue = identity -``` +```` #### Step 2: Update Queue Interface + **File:** `packages/effect/src/Queue.ts` ```typescript @@ -414,17 +560,18 @@ export const asEnqueue: (self: Queue) => Enqueue = identity export interface Queue extends Dequeue // To: -export interface Queue +export interface Queue extends Enqueue, Dequeue ``` #### Step 3: Update Proto + ```typescript const QueueProto = { [TypeId]: variance, [DequeueTypeId]: variance, - [EnqueueTypeId]: variance, // ADD THIS - ...PipeInspectableProto, + [EnqueueTypeId]: variance, // ADD THIS + ...PipeInspectableProto // ... } ``` @@ -432,6 +579,7 @@ const QueueProto = { ### Phase 2: Critical Return Type Fixes #### Step 1: Fix TxQueue `takeAll` Return Type + **File:** `packages/effect/src/stm/TxQueue.ts` ```typescript @@ -443,18 +591,19 @@ export const takeAll = (self: TxDequeue): Effect.Effect( - self: TxEnqueue, + self: TxEnqueue, values: Iterable ): Effect.Effect> // To: export const offerAll = ( - self: TxEnqueue, + self: TxEnqueue, values: Iterable ): Effect.Effect> @@ -464,10 +613,12 @@ export const offerAll = ( ### Phase 3: Missing Operations #### Add to Queue.ts: + - `poll` - non-blocking take (returns `Option`) - `peek` - inspect without removing #### Add to TxQueue.ts: + - `collect` - take all until done/error ### Phase 4: Documentation & Validation @@ -483,6 +634,7 @@ export const offerAll = ( ## Breaking Changes Summary ### Breaking Change #1: Unified Completion Type + ```typescript // Before Queue @@ -494,6 +646,7 @@ TxQueue ``` **Migration:** + ```typescript // Queue users: Change imports import { Queue } from "effect" @@ -501,31 +654,34 @@ import { Queue } from "effect" import { Cause } from "effect" // Type annotations -const queue: Queue // Before -const queue: Queue // After +const queue: Queue // Before +const queue: Queue // After // TxQueue users: Similar change -const queue: TxQueue // Before -const queue: TxQueue // After +const queue: TxQueue // Before +const queue: TxQueue // After ``` ### Breaking Change #2: Remove `Queue.done()` Operation + ```typescript // Before -yield* Queue.done(queue, Exit.fail(error)) -yield* Queue.done(queue, Exit.succeed(undefined)) +yield * Queue.done(queue, Exit.fail(error)) +yield * Queue.done(queue, Exit.succeed(undefined)) // After -yield* Queue.failCause(queue, Cause.fail(error)) -yield* Queue.end(queue) // For graceful completion +yield * Queue.failCause(queue, Cause.fail(error)) +yield * Queue.end(queue) // For graceful completion ``` **Migration:** + - Replace `Queue.done(queue, Exit.fail(e))` with `Queue.failCause(queue, Cause.fail(e))` - Replace `Queue.done(queue, Exit.succeed())` with `Queue.end(queue)` - No `doneUnsafe` equivalent - use `failCauseUnsafe` or `endUnsafe` ### Breaking Change #3: `takeAll` Return Type + ```typescript // Before (TxQueue) const items: ReadonlyArray = yield* TxQueue.takeAll(queue) @@ -537,16 +693,18 @@ const items: NonEmptyArray = yield* TxQueue.takeAll(queue) ``` ### Breaking Change #4: `clear` Return Type + ```typescript // Before (TxQueue) -yield* TxQueue.clear(queue) // Returns void +yield * TxQueue.clear(queue) // Returns void // After (TxQueue) -const cleared = yield* TxQueue.clear(queue) // Returns Array +const cleared = yield * TxQueue.clear(queue) // Returns Array console.log("Cleared items:", cleared) ``` ### Breaking Change #5: Queue Interface Structure + ```typescript // Before Queue extends Dequeue // Only two interfaces @@ -597,28 +755,33 @@ After implementation, verify: ## Rationale ### Why `Cause.Done` Over `NoSuchElementError`? + - **Semantic correctness**: Completion is not "not finding something" - **Consistency**: Same concept = same type - **Clarity**: `Done` clearly signals "finished normally" ### Why Remove `Queue.done()`? + - **Simpler API**: `failCause` is more natural than `done(Exit<...>)` - **Consistency**: TxQueue doesn't have it, shouldn't be different - **Power**: `Cause` can represent ANY completion scenario - **Type safety**: Clearer signature without complex conditional types ### Why `takeAll` Returns `NonEmptyArray`? + - **Type honesty**: Implementation blocks until ≥1 item available - **User benefit**: No unnecessary empty checks - **Correctness**: Type reflects runtime behavior ### Why Add `Enqueue` Interface? + - **Type safety**: Restrict operations at type level - **Consistency**: Match TxQueue's structure - **Patterns**: Enable producer-consumer separation - **Variance**: Proper contravariant producer type ### Why `clear` Returns `Array`? + - **Observability**: See what was cleared (useful for debugging) - **Consistency**: Both queues behave the same - **Less breaking**: Queue already has this signature From c3355cd969920b8fde51dcda4a0cdb4e3e3cae16 Mon Sep 17 00:00:00 2001 From: Michael Arnaldi Date: Tue, 7 Oct 2025 11:30:01 +0200 Subject: [PATCH 03/20] docs(PLAN): add critical implementation rules and validation requirements --- PLAN.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/PLAN.md b/PLAN.md index 00bd30138..7d73e381d 100644 --- a/PLAN.md +++ b/PLAN.md @@ -2,6 +2,54 @@ **Goal:** Align the APIs of Queue.ts (Effect-based) and TxQueue.ts (STM-based) to ensure consistent naming, return types, functionality, and structure while respecting their fundamental differences. +## 🚨 CRITICAL IMPLEMENTATION RULES 🚨 + +### Rule 1: Small, Single-Purpose Commits + +- Each commit must have ONE clear purpose +- No mixing of unrelated changes +- Commit message must clearly describe the single change made +- If you're tempted to use "and" in a commit message, split into multiple commits + +### Rule 2: Zero Tolerance for Errors Before Commit + +- **MANDATORY**: Before ANY commit is made, ALL of the following MUST pass: + 1. ✅ All TypeScript files compile: `pnpm tsc --noEmit` + 2. ✅ All linting passes: `pnpm lint --fix ` + 3. ✅ All tests pass: `pnpm test ` + 4. ✅ JSDoc examples compile: `pnpm docgen` (if JSDoc was modified) + +### Rule 3: Never Proceed with Errors + +- **ABSOLUTELY FORBIDDEN**: Never continue to next step if current step has errors +- **ABSOLUTELY FORBIDDEN**: Never commit code that has compilation errors +- **ABSOLUTELY FORBIDDEN**: Never commit code that has linting errors +- **ABSOLUTELY FORBIDDEN**: Never commit code that has failing tests +- **MANDATORY**: Stop and fix ALL errors before proceeding + +### Rule 4: Follow AGENTS.md Validation Commands + +- **Lint TypeScript files**: `pnpm lint --fix ` + - Run IMMEDIATELY after ANY edit to a .ts file + - NEVER lint non-.ts files (markdown, JSON, etc.) +- **Check types**: `pnpm tsc --noEmit` + - Run after any type signature changes +- **Run tests**: `pnpm test ` + - Run after any implementation changes +- **Check JSDoc examples**: `pnpm docgen` + - Run before committing if JSDoc examples were modified +- **Full project check**: `pnpm check` + - Run before final commit + +### Rule 5: Mandatory After-Edit Actions + +After editing ANY TypeScript file: + +1. **IMMEDIATELY** run: `pnpm lint --fix ` +2. Check for type errors: `pnpm tsc --noEmit` +3. Run relevant tests: `pnpm test ` +4. Fix all errors before proceeding + ## Overview Queue.ts provides Effect-based asynchronous queues with backpressure strategies, while TxQueue.ts provides STM-based transactional queues. As TxQueue is the transactional counterpart of Queue, the APIs should mirror each other closely. From 4687522bb9b01c3bfe87b0e8726c87d046159930 Mon Sep 17 00:00:00 2001 From: Michael Arnaldi Date: Tue, 7 Oct 2025 11:40:57 +0200 Subject: [PATCH 04/20] feat(Cause): add Done error type for queue completion --- packages/effect/src/Cause.ts | 75 ++++++++++++++++++++++++++++ packages/effect/src/internal/core.ts | 18 +++++++ 2 files changed, 93 insertions(+) diff --git a/packages/effect/src/Cause.ts b/packages/effect/src/Cause.ts index d586c5876..57ebfb7bd 100644 --- a/packages/effect/src/Cause.ts +++ b/packages/effect/src/Cause.ts @@ -692,6 +692,81 @@ export interface NoSuchElementError extends YieldableError { */ export const NoSuchElementError: new(message?: string) => NoSuchElementError = core.NoSuchElementError +/** + * Tests if a value is a `Done` error. + * + * @example + * ```ts + * import { Cause } from "effect" + * + * const error = new Cause.Done() + * console.log(Cause.isDone(error)) // true + * console.log(Cause.isDone("not an error")) // false + * ``` + * + * @category guards + * @since 4.0.0 + */ +export const isDone: (u: unknown) => u is Done = core.isDone + +/** + * @since 4.0.0 + * @category errors + */ +export const DoneTypeId: "~effect/Cause/Done" = core.DoneTypeId + +/** + * Represents a graceful completion signal for queues and streams. + * + * `Done` is used to signal that a queue or stream has completed normally + * and no more elements will be produced. This is distinct from an error + * or interruption - it represents successful completion. + * + * @example + * ```ts + * import { Cause, Effect } from "effect" + * import { Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(10) + * + * yield* Queue.offer(queue, 1) + * yield* Queue.offer(queue, 2) + * + * // Signal completion + * yield* Queue.end(queue) + * + * // Taking from ended queue fails with Done + * const result = yield* Effect.flip(Queue.take(queue)) + * console.log(Cause.isDone(result)) // true + * }) + * ``` + * + * @since 4.0.0 + * @category errors + */ +export interface Done extends YieldableError { + readonly [DoneTypeId]: typeof DoneTypeId + readonly _tag: "Done" +} + +/** + * Creates a `Done` error to signal graceful completion. + * + * @example + * ```ts + * import { Cause } from "effect" + * + * const done = new Cause.Done() + * console.log(done._tag) // "Done" + * console.log(Cause.isDone(done)) // true + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const Done: new() => Done = core.Done + /** * @category errors * @since 4.0.0 diff --git a/packages/effect/src/internal/core.ts b/packages/effect/src/internal/core.ts index 0f675f2cd..e0e45ab54 100644 --- a/packages/effect/src/internal/core.ts +++ b/packages/effect/src/internal/core.ts @@ -648,3 +648,21 @@ export class NoSuchElementError extends TaggedError("NoSuchElementError") { super({ message } as any) } } + +/** @internal */ +export const DoneTypeId = "~effect/Cause/Done" + +/** @internal */ +export const HaltTypeId = "~effect/stream/Pull/Halt" + +/** @internal */ +export const isDone = ( + u: unknown +): u is Cause.Done => hasProperty(u, DoneTypeId) + +/** @internal */ +export class Done extends TaggedError("Done") { + readonly [DoneTypeId] = DoneTypeId + readonly [HaltTypeId] = HaltTypeId + readonly leftover = void 0 as void +} From 9f59aa514e35595f1616b83c8f0ddccbb6ee7493 Mon Sep 17 00:00:00 2001 From: Michael Arnaldi Date: Tue, 7 Oct 2025 12:08:19 +0200 Subject: [PATCH 05/20] refactor(Queue): migrate to Cause.Done for completion --- packages/effect/src/Cause.ts | 12 ++++++------ packages/effect/src/Queue.ts | 22 +++++----------------- packages/effect/src/internal/core.ts | 5 +++-- packages/effect/test/Queue.test.ts | 12 ++++++------ 4 files changed, 20 insertions(+), 31 deletions(-) diff --git a/packages/effect/src/Cause.ts b/packages/effect/src/Cause.ts index 57ebfb7bd..5965a0f9c 100644 --- a/packages/effect/src/Cause.ts +++ b/packages/effect/src/Cause.ts @@ -47,6 +47,7 @@ import type { Pipeable } from "./interfaces/Pipeable.ts" import * as core from "./internal/core.ts" import * as effect from "./internal/effect.ts" import * as ServiceMap from "./ServiceMap.ts" +import type * as Pull from "./stream/Pull.ts" import type { Span } from "./Tracer.ts" import type { NoInfer } from "./types/Types.ts" @@ -745,27 +746,26 @@ export const DoneTypeId: "~effect/Cause/Done" = core.DoneTypeId * @since 4.0.0 * @category errors */ -export interface Done extends YieldableError { +export interface Done extends Pull.Halt, YieldableError { readonly [DoneTypeId]: typeof DoneTypeId readonly _tag: "Done" } /** - * Creates a `Done` error to signal graceful completion. + * Singleton instance of `Done` error to signal graceful completion. * * @example * ```ts * import { Cause } from "effect" * - * const done = new Cause.Done() - * console.log(done._tag) // "Done" - * console.log(Cause.isDone(done)) // true + * console.log(Cause.Done._tag) // "Done" + * console.log(Cause.isDone(Cause.Done)) // true * ``` * * @category constructors * @since 4.0.0 */ -export const Done: new() => Done = core.Done +export const Done: Done = core.Done /** * @category errors diff --git a/packages/effect/src/Queue.ts b/packages/effect/src/Queue.ts index 08901a4d9..00e1e521a 100644 --- a/packages/effect/src/Queue.ts +++ b/packages/effect/src/Queue.ts @@ -34,10 +34,10 @@ * @since 3.8.0 */ import type { Cause } from "./Cause.ts" +import { Done } from "./Cause.ts" import * as Arr from "./collections/Array.ts" import * as Iterable from "./collections/Iterable.ts" import * as MutableList from "./collections/MutableList.ts" -import * as Filter from "./data/Filter.ts" import { hasProperty } from "./data/Predicate.ts" import type { Effect } from "./Effect.ts" import type { Exit, Failure } from "./Exit.ts" @@ -965,31 +965,19 @@ export const clear = (self: Dequeue): Effect, E> => * @category Done * @since 4.0.0 */ -export interface Done extends Pull.Halt { - readonly _tag: "Done" -} - /** + * @deprecated Use `Cause.Done` instead. Re-exported for backward compatibility. * @category Done * @since 4.0.0 */ -export const Done: Done = { - [Pull.HaltTypeId]: Pull.HaltTypeId, - _tag: "Done", - leftover: void 0 -} +export type { Done } from "./Cause.ts" /** - * @since 4.0.0 + * @deprecated Use `Cause.isDone` instead. Re-exported for backward compatibility. * @category Done - */ -export const isDone = (u: unknown): u is Done => Pull.isHalt(u) && (u as Done)._tag === "Done" - -/** * @since 4.0.0 - * @category Done */ -export const filterDone: Filter.Filter = Filter.fromPredicate(isDone) +export { isDone } from "./Cause.ts" /** * Take all messages from the queue, or wait for messages to be available. diff --git a/packages/effect/src/internal/core.ts b/packages/effect/src/internal/core.ts index e0e45ab54..b9136a43e 100644 --- a/packages/effect/src/internal/core.ts +++ b/packages/effect/src/internal/core.ts @@ -8,6 +8,7 @@ import * as Hash from "../interfaces/Hash.ts" import { format, NodeInspectSymbol } from "../interfaces/Inspectable.ts" import { pipeArguments } from "../interfaces/Pipeable.ts" import type * as ServiceMap from "../ServiceMap.ts" +import type { Halt as PullHalt } from "../stream/Pull.ts" import type { Span } from "../Tracer.ts" import type { Equals, NoInfer } from "../types/Types.ts" import { SingleShotGen } from "../Utils.ts" @@ -661,8 +662,8 @@ export const isDone = ( ): u is Cause.Done => hasProperty(u, DoneTypeId) /** @internal */ -export class Done extends TaggedError("Done") { +export const Done = new (class Done extends TaggedError("Done") implements PullHalt { readonly [DoneTypeId] = DoneTypeId readonly [HaltTypeId] = HaltTypeId readonly leftover = void 0 as void -} +})() diff --git a/packages/effect/test/Queue.test.ts b/packages/effect/test/Queue.test.ts index e34ed9135..9cef23759 100644 --- a/packages/effect/test/Queue.test.ts +++ b/packages/effect/test/Queue.test.ts @@ -1,5 +1,5 @@ import { assert, describe, it } from "@effect/vitest" -import { Effect, Exit, Fiber, Queue } from "effect" +import { Cause, Effect, Exit, Fiber, Queue } from "effect" import { Stream } from "effect/stream" describe("Queue", () => { @@ -78,18 +78,18 @@ describe("Queue", () => { it.effect("done completes takes", () => Effect.gen(function*() { - const queue = yield* Queue.bounded(2) + const queue = yield* Queue.bounded(2) const fiber = yield* Queue.takeAll(queue).pipe( Effect.forkChild ) yield* Effect.yieldNow yield* Queue.done(queue, Exit.void) - assert.deepStrictEqual(yield* Fiber.await(fiber), Exit.fail(Queue.Done)) + assert.deepStrictEqual(yield* Fiber.await(fiber), Exit.fail(Cause.Done)) })) it.effect("end", () => Effect.gen(function*() { - const queue = yield* Queue.bounded(2) + const queue = yield* Queue.bounded(2) yield* Effect.forkChild(Queue.offerAll(queue, [1, 2, 3, 4])) yield* Effect.forkChild(Queue.offerAll(queue, [5, 6, 7, 8])) yield* Effect.forkChild(Queue.offer(queue, 9)) @@ -102,7 +102,7 @@ describe("Queue", () => { it.effect("end with take", () => Effect.gen(function*() { - const queue = yield* Queue.bounded(2) + const queue = yield* Queue.bounded(2) yield* Effect.forkChild(Queue.offerAll(queue, [1, 2])) yield* Effect.forkChild(Queue.offer(queue, 3)) yield* Effect.forkChild(Queue.end(queue)) @@ -162,7 +162,7 @@ describe("Queue", () => { it.effect("await waits for no items", () => Effect.gen(function*() { - const queue = yield* Queue.unbounded() + const queue = yield* Queue.unbounded() const fiber = yield* Queue.await(queue).pipe(Effect.forkChild) yield* Effect.yieldNow yield* Queue.offer(queue, 1) From 8ecf0d386796dc62f7feffc853b82fca851dc00b Mon Sep 17 00:00:00 2001 From: Michael Arnaldi Date: Tue, 7 Oct 2025 13:35:40 +0200 Subject: [PATCH 06/20] refactor(TxQueue): migrate from NoSuchElementError to Cause.Done - Replace all Cause.NoSuchElementError references with Cause.Done - Update JSDoc examples to use Cause.Done and Cause.isDone - Update end() function signature to use Cause.Done - Migrate all test cases to use Cause.Done - Update test assertions to use Cause.isDone and singleton Done - All 87 TxQueue tests pass - Zero new type errors introduced - No new docgen errors (2 pre-existing errors in Queue.ts unrelated to changes) --- packages/effect/src/stm/TxQueue.ts | 24 ++++++++++++------------ packages/effect/test/TxQueue.test.ts | 26 +++++++++++++------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/effect/src/stm/TxQueue.ts b/packages/effect/src/stm/TxQueue.ts index 1249631d9..ecfb9788c 100644 --- a/packages/effect/src/stm/TxQueue.ts +++ b/packages/effect/src/stm/TxQueue.ts @@ -205,10 +205,10 @@ export interface TxQueueState extends Inspectable { * yield* TxQueue.offerAll(faultTolerantQueue, [1, 2, 3]) * yield* TxQueue.fail(faultTolerantQueue, "processing complete") * - * // Works with NoSuchElementError for clean completion + * // Works with Done for clean completion * const completableQueue = yield* TxQueue.bounded< * string, - * Cause.NoSuchElementError + * Cause.Done * >(5) * yield* TxQueue.offer(completableQueue, "task") * yield* TxQueue.end(completableQueue) @@ -1270,13 +1270,13 @@ export const failCause: { )) /** - * Ends a queue by signaling completion with a NoSuchElementError error. + * Ends a queue by signaling completion with a Done error. * * This function provides a clean way to signal the end of a queue by calling - * `failCause` with a new `NoSuchElementError` instance. This is a convenience function for - * queues that are typed to accept `NoSuchElementError` in their error channel. + * `failCause` with `Cause.Done`. This is a convenience function for + * queues that are typed to accept `Cause.Done` in their error channel. * When a queue is ended, all subsequent operations (take, peek, etc.) will fail with - * the NoSuchElementError, propagating through the E-channel. + * `Cause.Done`, propagating through the E-channel. * * @example * ```ts @@ -1284,7 +1284,7 @@ export const failCause: { * import { TxQueue } from "effect/stm" * * const program = Effect.gen(function*() { - * const queue = yield* TxQueue.bounded(10) + * const queue = yield* TxQueue.bounded(10) * yield* TxQueue.offer(queue, 1) * yield* TxQueue.offer(queue, 2) * @@ -1292,20 +1292,20 @@ export const failCause: { * const result = yield* TxQueue.end(queue) * console.log(result) // true * - * // All operations will now fail with NoSuchElementError + * // All operations will now fail with Done * const takeResult = yield* Effect.flip(TxQueue.take(queue)) - * console.log(Cause.isNoSuchElementError(takeResult)) // true + * console.log(Cause.isDone(takeResult)) // true * * const peekResult = yield* Effect.flip(TxQueue.peek(queue)) - * console.log(Cause.isNoSuchElementError(peekResult)) // true + * console.log(Cause.isDone(peekResult)) // true * }) * ``` * * @since 4.0.0 * @category combinators */ -export const end = (self: TxEnqueue): Effect.Effect => - failCause(self, Cause.fail(new Cause.NoSuchElementError())) +export const end = (self: TxEnqueue): Effect.Effect => + failCause(self, Cause.fail(Cause.Done)) /** * Clears all elements from the queue without affecting its state. diff --git a/packages/effect/test/TxQueue.test.ts b/packages/effect/test/TxQueue.test.ts index 1faac401d..2aaba2a61 100644 --- a/packages/effect/test/TxQueue.test.ts +++ b/packages/effect/test/TxQueue.test.ts @@ -64,8 +64,8 @@ describe("TxQueue", () => { describe("interface segregation", () => { it.effect("TxEnqueue interface enforces write-only operations", () => Effect.gen(function*() { - const queue = yield* TxQueue.bounded(5) - const enqueue: TxQueue.TxEnqueue = queue + const queue = yield* TxQueue.bounded(5) + const enqueue: TxQueue.TxEnqueue = queue // Enqueue operations should work const accepted = yield* TxQueue.offer(enqueue, 42) @@ -78,7 +78,7 @@ describe("TxQueue", () => { const result = yield* TxQueue.failCause(enqueue, Cause.interrupt()) assert.strictEqual(result, true) - const endResult = yield* TxQueue.end(enqueue as TxQueue.TxEnqueue) + const endResult = yield* TxQueue.end(enqueue) assert.strictEqual(endResult, false) // Already done })) @@ -1184,10 +1184,10 @@ describe("TxQueue", () => { }) }) - describe("NoSuchElementError and end function", () => { - it.effect("end() signals completion with NoSuchElementError", () => + describe("Done and end function", () => { + it.effect("end() signals completion with Done", () => Effect.gen(function*() { - const queue = yield* TxQueue.bounded(5) + const queue = yield* TxQueue.bounded(5) yield* TxQueue.offer(queue, 1) yield* TxQueue.offer(queue, 2) @@ -1207,9 +1207,9 @@ describe("TxQueue", () => { assert.strictEqual(isDone, true) })) - it.effect("take() on ended queue fails with NoSuchElementError", () => + it.effect("take() on ended queue fails with Done", () => Effect.gen(function*() { - const queue = yield* TxQueue.bounded(5) + const queue = yield* TxQueue.bounded(5) yield* TxQueue.offer(queue, 42) yield* TxQueue.end(queue) @@ -1217,15 +1217,15 @@ describe("TxQueue", () => { const item = yield* TxQueue.take(queue) assert.strictEqual(item, 42) - // Second take should fail with NoSuchElementError since queue is now done + // Second take should fail with Done since queue is now done const takeResult = yield* Effect.flip(TxQueue.take(queue)) - assert.strictEqual(Cause.isNoSuchElementError(takeResult), true) - assert.strictEqual(takeResult instanceof Cause.NoSuchElementError, true) + assert.strictEqual(Cause.isDone(takeResult), true) + assert.strictEqual(takeResult, Cause.Done) })) it.effect("end() works with TxEnqueue interface", () => Effect.gen(function*() { - const queue = yield* TxQueue.bounded(5) + const queue = yield* TxQueue.bounded(5) yield* TxQueue.offer(queue, 1) const result = yield* TxQueue.end(queue) @@ -1245,7 +1245,7 @@ describe("TxQueue", () => { it.effect("end() returns false if queue is already done", () => Effect.gen(function*() { - const queue = yield* TxQueue.bounded(5) + const queue = yield* TxQueue.bounded(5) // End the queue once const result1 = yield* TxQueue.end(queue) From c017ac84a92bdd5c602871dd0b0ca718e6eb9181 Mon Sep 17 00:00:00 2001 From: Michael Arnaldi Date: Tue, 7 Oct 2025 13:43:49 +0200 Subject: [PATCH 07/20] feat(Queue): add interrupt operation for graceful close - Add Queue.interrupt() function that transitions queue to Closing state - Stops accepting new offers while allowing existing messages to be drained - Uses fiber ID to create proper interrupt cause - Add comprehensive test case demonstrating drain behavior - All 14 Queue tests pass - Zero type errors - Matches TxQueue.interrupt() API pattern --- packages/effect/src/Queue.ts | 47 +++++++++++++++++++++++++++++- packages/effect/test/Queue.test.ts | 25 ++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/packages/effect/src/Queue.ts b/packages/effect/src/Queue.ts index 00e1e521a..79da100e3 100644 --- a/packages/effect/src/Queue.ts +++ b/packages/effect/src/Queue.ts @@ -34,7 +34,7 @@ * @since 3.8.0 */ import type { Cause } from "./Cause.ts" -import { Done } from "./Cause.ts" +import { Done, interrupt as causeInterrupt } from "./Cause.ts" import * as Arr from "./collections/Array.ts" import * as Iterable from "./collections/Iterable.ts" import * as MutableList from "./collections/MutableList.ts" @@ -782,6 +782,51 @@ export const end = (self: Queue): Effect => done(sel */ export const endUnsafe = (self: Queue) => doneUnsafe(self, internalEffect.exitVoid) +/** + * Interrupts the queue gracefully, transitioning it to a closing state. + * + * This operation stops accepting new offers but allows existing messages to be consumed. + * Once all messages are drained, the queue transitions to the Done state with an interrupt cause. + * + * @example + * ```ts + * import { Effect } from "effect" + * import { Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(10) + * + * // Add some messages + * yield* Queue.offer(queue, 1) + * yield* Queue.offer(queue, 2) + * + * // Interrupt gracefully - no more offers accepted, but messages can be consumed + * const interrupted = yield* Queue.interrupt(queue) + * console.log(interrupted) // true + * + * // Trying to offer more messages will return false + * const offerResult = yield* Queue.offer(queue, 3) + * console.log(offerResult) // false + * + * // But we can still take existing messages + * const message1 = yield* Queue.take(queue) + * console.log(message1) // 1 + * + * const message2 = yield* Queue.take(queue) + * console.log(message2) // 2 + * + * // After all messages are consumed, queue is done + * const isDone = queue.state._tag === "Done" + * console.log(isDone) // true + * }) + * ``` + * + * @category completion + * @since 4.0.0 + */ +export const interrupt = (self: Queue): Effect => + core.withFiber((fiber) => done(self, core.exitFailCause(causeInterrupt(fiber.id)))) + /** * Signal that the queue is done with a specific exit value. If the queue is already done, `false` is * returned. diff --git a/packages/effect/test/Queue.test.ts b/packages/effect/test/Queue.test.ts index 9cef23759..68b87041b 100644 --- a/packages/effect/test/Queue.test.ts +++ b/packages/effect/test/Queue.test.ts @@ -114,6 +114,31 @@ describe("Queue", () => { assert.strictEqual(yield* Queue.offer(queue, 10), false) })) + it.effect("interrupt allows draining", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(10) + yield* Queue.offerAll(queue, [1, 2, 3, 4, 5]) + + // Interrupt gracefully + const interrupted = yield* Queue.interrupt(queue) + assert.strictEqual(interrupted, true) + + // No more offers accepted + const offerResult = yield* Queue.offer(queue, 6) + assert.strictEqual(offerResult, false) + + // But can still drain existing messages + assert.strictEqual(yield* Queue.take(queue), 1) + assert.strictEqual(yield* Queue.take(queue), 2) + assert.strictEqual(yield* Queue.take(queue), 3) + assert.strictEqual(yield* Queue.take(queue), 4) + assert.strictEqual(yield* Queue.take(queue), 5) + + // Now queue is done and take fails with interrupt + const exit = yield* Queue.take(queue).pipe(Effect.exit) + assert.isTrue(Exit.hasInterrupt(exit)) + })) + it.effect("fail", () => Effect.gen(function*() { const queue = yield* Queue.bounded(2) From e8068d0a5a711a2ed94ec22493a7aa0b42e01fcb Mon Sep 17 00:00:00 2001 From: Michael Arnaldi Date: Tue, 7 Oct 2025 13:55:55 +0200 Subject: [PATCH 08/20] refactor(TxQueue): change clear() to return Array instead of void - Update TxQueue.clear() signature to return Array - Returns the cleared elements for inspection - Update JSDoc example to show returned array - Update all test cases to verify returned elements - shutdown() function automatically ignores return value - All 87 TxQueue tests passing - Aligns with Queue.clear() which already returns Array --- packages/effect/src/stm/TxQueue.ts | 12 ++++++++++-- packages/effect/test/TxQueue.test.ts | 14 +++++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/effect/src/stm/TxQueue.ts b/packages/effect/src/stm/TxQueue.ts index ecfb9788c..8151a523b 100644 --- a/packages/effect/src/stm/TxQueue.ts +++ b/packages/effect/src/stm/TxQueue.ts @@ -1309,6 +1309,7 @@ export const end = (self: TxEnqueue): Effect.Effect(self: TxEnqueue): Effect.Effect(self: TxEnqueue): Effect.Effect(self: TxEnqueue): Effect.Effect => TxChunk.set(self.items, Chunk.empty()) +export const clear = (self: TxEnqueue): Effect.Effect> => + Effect.gen(function*() { + const chunk = yield* TxChunk.get(self.items) + yield* TxChunk.set(self.items, Chunk.empty()) + return Chunk.toArray(chunk) + }) /** * Shuts down the queue immediately by clearing all items and interrupting it (legacy compatibility). diff --git a/packages/effect/test/TxQueue.test.ts b/packages/effect/test/TxQueue.test.ts index 2aaba2a61..f71b71e37 100644 --- a/packages/effect/test/TxQueue.test.ts +++ b/packages/effect/test/TxQueue.test.ts @@ -577,7 +577,7 @@ describe("TxQueue", () => { }) describe("clear", () => { - it.effect("clear removes all items from queue", () => + it.effect("clear removes all items from queue and returns them", () => Effect.gen(function*() { const queue = yield* TxQueue.bounded(10) yield* TxQueue.offerAll(queue, [1, 2, 3, 4, 5]) @@ -585,7 +585,8 @@ describe("TxQueue", () => { const sizeBefore = yield* TxQueue.size(queue) assert.strictEqual(sizeBefore, 5) - yield* TxQueue.clear(queue) + const cleared = yield* TxQueue.clear(queue) + assert.deepStrictEqual(cleared, [1, 2, 3, 4, 5]) const sizeAfter = yield* TxQueue.size(queue) assert.strictEqual(sizeAfter, 0) @@ -599,7 +600,8 @@ describe("TxQueue", () => { const queue = yield* TxQueue.bounded(10) yield* TxQueue.offerAll(queue, [1, 2, 3]) - yield* TxQueue.clear(queue) + const cleared = yield* TxQueue.clear(queue) + assert.deepStrictEqual(cleared, [1, 2, 3]) // Queue should still be open const isOpen = yield* TxQueue.isOpen(queue) @@ -617,7 +619,8 @@ describe("TxQueue", () => { Effect.gen(function*() { const queue = yield* TxQueue.bounded(10) - yield* TxQueue.clear(queue) + const cleared = yield* TxQueue.clear(queue) + assert.deepStrictEqual(cleared, []) const size = yield* TxQueue.size(queue) assert.strictEqual(size, 0) @@ -639,7 +642,8 @@ describe("TxQueue", () => { const isDoneBefore = yield* TxQueue.isDone(queue) assert.strictEqual(isDoneBefore, true) - yield* TxQueue.clear(queue) + const cleared = yield* TxQueue.clear(queue) + assert.deepStrictEqual(cleared, []) const size = yield* TxQueue.size(queue) assert.strictEqual(size, 0) From 341b524d57ec8d98117be704275f46884d0fe149 Mon Sep 17 00:00:00 2001 From: Michael Arnaldi Date: Tue, 7 Oct 2025 14:37:34 +0200 Subject: [PATCH 09/20] feat(Queue): add Enqueue interface for interface segregation - Add EnqueueTypeId to match TxQueue's three-interface structure - Create Enqueue interface with contravariant variance for write operations - Update Queue to extend both Enqueue and Dequeue interfaces - Add isEnqueue() type guard - Add asEnqueue() converter function for interface downcasting - Update QueueProto to include EnqueueTypeId - Add comprehensive tests for interface guards and converters - All 18 Queue tests passing - Aligns Queue API with TxQueue's Enqueue/Dequeue/Queue pattern --- packages/effect/src/Queue.ts | 127 ++++++++++++++++++++++++++++- packages/effect/test/Queue.test.ts | 45 ++++++++++ 2 files changed, 171 insertions(+), 1 deletion(-) diff --git a/packages/effect/src/Queue.ts b/packages/effect/src/Queue.ts index 79da100e3..2ea6ffdef 100644 --- a/packages/effect/src/Queue.ts +++ b/packages/effect/src/Queue.ts @@ -51,6 +51,7 @@ import * as Pull from "./stream/Pull.ts" import type * as Types from "./types/Types.ts" const TypeId = "~effect/Queue" +const EnqueueTypeId = "~effect/Queue/Enqueue" const DequeueTypeId = "~effect/Queue/Dequeue" /** @@ -77,6 +78,129 @@ export const isQueue = ( u: unknown ): u is Queue => hasProperty(u, TypeId) +/** + * Type guard to check if a value is an Enqueue. + * + * @example + * ```ts + * import { Effect } from "effect" + * import { Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(10) + * + * console.log(Queue.isEnqueue(queue)) // true + * console.log(Queue.isEnqueue({})) // false + * }) + * ``` + * + * @since 4.0.0 + * @category guards + */ +export const isEnqueue = ( + u: unknown +): u is Enqueue => hasProperty(u, EnqueueTypeId) + +/** + * Type guard to check if a value is a Dequeue. + * + * @example + * ```ts + * import { Effect } from "effect" + * import { Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(10) + * + * console.log(Queue.isDequeue(queue)) // true + * console.log(Queue.isDequeue({})) // false + * }) + * ``` + * + * @since 4.0.0 + * @category guards + */ +export const isDequeue = ( + u: unknown +): u is Dequeue => hasProperty(u, DequeueTypeId) + +/** + * Converts a Queue to an Enqueue (write-only interface). + * + * @example + * ```ts + * import { Effect } from "effect" + * import { Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(10) + * + * // Convert to write-only interface + * const enqueue = Queue.asEnqueue(queue) + * + * // Can only offer, not take + * yield* Queue.offer(enqueue, 42) + * }) + * ``` + * + * @since 4.0.0 + * @category conversions + */ +export const asEnqueue = (self: Queue): Enqueue => self + +/** + * An `Enqueue` is a queue that can be offered to. + * + * This interface represents the write-only part of a Queue, allowing you to offer + * elements to the queue but not take elements from it. + * + * @example + * ```ts + * import { Effect } from "effect" + * import { Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(10) + * + * // An Enqueue can only offer elements + * const enqueue: Queue.Enqueue = queue + * + * // Offer elements using enqueue interface + * yield* Queue.offer(enqueue, "hello") + * yield* Queue.offerAll(enqueue, ["world", "!"]) + * }) + * ``` + * + * @since 4.0.0 + * @category models + */ +export interface Enqueue extends Inspectable { + readonly [EnqueueTypeId]: Enqueue.Variance + readonly strategy: "suspend" | "dropping" | "sliding" + readonly scheduler: Scheduler + capacity: number + messages: MutableList.MutableList + state: Queue.State + scheduleRunning: boolean +} + +/** + * @since 4.0.0 + * @category models + */ +export declare namespace Enqueue { + /** + * Variance interface for Enqueue types, defining the type parameter constraints. + * + * @since 4.0.0 + * @category models + */ + export interface Variance { + _A: Types.Contravariant + _E: Types.Contravariant + } +} + /** * A `Dequeue` is a queue that can be taken from. * @@ -163,7 +287,7 @@ export declare namespace Dequeue { * @since 3.8.0 * @category models */ -export interface Queue extends Dequeue { +export interface Queue extends Enqueue, Dequeue { readonly [TypeId]: Queue.Variance } @@ -234,6 +358,7 @@ const variance = { } const QueueProto = { [TypeId]: variance, + [EnqueueTypeId]: variance, [DequeueTypeId]: variance, ...PipeInspectableProto, toJSON(this: Queue) { diff --git a/packages/effect/test/Queue.test.ts b/packages/effect/test/Queue.test.ts index 68b87041b..46d4797b7 100644 --- a/packages/effect/test/Queue.test.ts +++ b/packages/effect/test/Queue.test.ts @@ -3,6 +3,51 @@ import { Cause, Effect, Exit, Fiber, Queue } from "effect" import { Stream } from "effect/stream" describe("Queue", () => { + it.effect("isEnqueue type guard", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(10) + + assert.isTrue(Queue.isEnqueue(queue)) + assert.isFalse(Queue.isEnqueue({})) + assert.isFalse(Queue.isEnqueue(null)) + })) + + it.effect("isDequeue type guard", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(10) + + assert.isTrue(Queue.isDequeue(queue)) + assert.isFalse(Queue.isDequeue({})) + assert.isFalse(Queue.isDequeue(null)) + })) + + it.effect("asEnqueue converts to write-only interface", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(10) + const enqueue: Queue.Enqueue = Queue.asEnqueue(queue) + + // Verify it's recognized as an enqueue + assert.isTrue(Queue.isEnqueue(enqueue)) + + // Verify queue operations still work through enqueue reference + assert.isTrue(Queue.isQueue(enqueue)) + })) + + it.effect("asDequeue converts to read-only interface", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(10) + yield* Queue.offer(queue, 42) + + const dequeue = Queue.asDequeue(queue) + + // Can use dequeue operations + const item = yield* Queue.take(dequeue) + assert.strictEqual(item, 42) + + // Verify it's recognized as a dequeue + assert.isTrue(Queue.isDequeue(dequeue)) + })) + it.effect("offerAll with capacity", () => Effect.gen(function*() { const queue = yield* Queue.bounded(2) From 148069ea5bf23bdf7040c3c0c41ef50e50c1f6b3 Mon Sep 17 00:00:00 2001 From: Michael Arnaldi Date: Tue, 7 Oct 2025 14:41:48 +0200 Subject: [PATCH 10/20] refactor(TxQueue): change takeAll() to return NonEmptyArray - Update return type from ReadonlyArray to NonEmptyArray - Aligns with Queue.takeAll() which already returns NonEmptyArray - takeAll() blocks until at least one item is available (retries when empty) - Add type cast with comment explaining non-empty guarantee - Update JSDoc example to show NonEmptyArray check - Import Array module for NonEmptyArray type - All 87 TxQueue tests passing --- packages/effect/src/stm/TxQueue.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/effect/src/stm/TxQueue.ts b/packages/effect/src/stm/TxQueue.ts index 8151a523b..3827cb993 100644 --- a/packages/effect/src/stm/TxQueue.ts +++ b/packages/effect/src/stm/TxQueue.ts @@ -9,6 +9,7 @@ * * @since 4.0.0 */ +import * as Arr from "../collections/Array.ts" import * as Cause from "../Cause.ts" import * as Chunk from "../collections/Chunk.ts" import * as Option from "../data/Option.ts" @@ -771,21 +772,25 @@ export const poll = (self: TxDequeue): Effect.Effect(10) * yield* TxQueue.offerAll(queue, [1, 2, 3, 4, 5]) * - * // Take all items atomically + * // Take all items atomically - returns NonEmptyArray * const items = yield* TxQueue.takeAll(queue) * console.log(items) // [1, 2, 3, 4, 5] + * console.log(Array.isNonEmptyArray(items)) // true * }) * * // Error propagation example @@ -803,7 +808,7 @@ export const poll = (self: TxDequeue): Effect.Effect(self: TxDequeue): Effect.Effect, E> => +export const takeAll = (self: TxDequeue): Effect.Effect, E> => Effect.atomic( Effect.gen(function*() { const state = yield* TxRef.get(self.stateRef) @@ -820,8 +825,8 @@ export const takeAll = (self: TxDequeue): Effect.Effect yield* TxChunk.set(self.items, Chunk.empty()) // Check if we need to transition Closing → Done From 46e8ff4e0f1d85531ec5107db3a430d682be8eea Mon Sep 17 00:00:00 2001 From: Michael Arnaldi Date: Tue, 7 Oct 2025 14:44:48 +0200 Subject: [PATCH 11/20] refactor(TxQueue): change offerAll() to return Array instead of Chunk - Update return type from Chunk to Array - Aligns with Queue.offerAll() which already returns Array - Remove unnecessary Chunk.fromIterable() conversion - Update JSDoc example to show direct array usage - Remove Chunk import from test file - Update all test assertions to work with arrays directly - All 87 TxQueue tests passing --- packages/effect/src/stm/TxQueue.ts | 16 +++++++++------- packages/effect/test/TxQueue.test.ts | 7 +++---- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/effect/src/stm/TxQueue.ts b/packages/effect/src/stm/TxQueue.ts index 3827cb993..9896d44c1 100644 --- a/packages/effect/src/stm/TxQueue.ts +++ b/packages/effect/src/stm/TxQueue.ts @@ -617,21 +617,23 @@ export const offer: { /** * Offers multiple items to the queue. * + * Returns an array of items that were rejected (not added to the queue). + * * **Mutation behavior**: This function mutates the original TxQueue by adding * items according to the queue's strategy. It does not return a new TxQueue reference. * * @example * ```ts * import { Effect } from "effect" - * import { Chunk } from "effect/collections" * import { TxQueue } from "effect/stm" * * const program = Effect.gen(function*() { * const queue = yield* TxQueue.bounded(10) * - * // Offer multiple items - returns rejected items + * // Offer multiple items - returns rejected items as array * const rejected = yield* TxQueue.offerAll(queue, [1, 2, 3, 4, 5]) - * console.log(Chunk.toArray(rejected)) // [] if all accepted + * console.log(rejected) // [] if all accepted + * console.log(rejected.length) // 0 * }) * ``` * @@ -639,11 +641,11 @@ export const offer: { * @category combinators */ export const offerAll: { - (values: Iterable): (self: TxEnqueue) => Effect.Effect> - (self: TxEnqueue, values: Iterable): Effect.Effect> + (values: Iterable): (self: TxEnqueue) => Effect.Effect> + (self: TxEnqueue, values: Iterable): Effect.Effect> } = dual( 2, - (self: TxEnqueue, values: Iterable): Effect.Effect> => + (self: TxEnqueue, values: Iterable): Effect.Effect> => Effect.atomic( Effect.gen(function*() { const rejected: Array = [] @@ -655,7 +657,7 @@ export const offerAll: { } } - return Chunk.fromIterable(rejected) + return rejected }) ) ) diff --git a/packages/effect/test/TxQueue.test.ts b/packages/effect/test/TxQueue.test.ts index f71b71e37..4d9ff586a 100644 --- a/packages/effect/test/TxQueue.test.ts +++ b/packages/effect/test/TxQueue.test.ts @@ -1,7 +1,6 @@ import { assert, describe, it } from "@effect/vitest" import { Fiber } from "effect" import * as Cause from "effect/Cause" -import { Chunk } from "effect/collections" import { Option } from "effect/data" import * as Effect from "effect/Effect" import { TxQueue } from "effect/stm" @@ -23,7 +22,7 @@ describe("TxQueue", () => { assert.strictEqual(offered, true) const rejected = yield* TxQueue.offerAll(queue, [1, 2, 3]) - assert.deepStrictEqual(Chunk.toReadonlyArray(rejected), []) + assert.deepStrictEqual(rejected, []) })) it.effect("TxDequeue provides read-only interface", () => @@ -72,7 +71,7 @@ describe("TxQueue", () => { assert.strictEqual(accepted, true) const rejected = yield* TxQueue.offerAll(enqueue, [1, 2, 3]) - assert.deepStrictEqual(Chunk.toReadonlyArray(rejected), []) + assert.deepStrictEqual(rejected, []) // State management operations should work const result = yield* TxQueue.failCause(enqueue, Cause.interrupt()) @@ -229,7 +228,7 @@ describe("TxQueue", () => { const queue = yield* TxQueue.bounded(10) const rejected = yield* TxQueue.offerAll(queue, [1, 2, 3, 4, 5]) - assert.deepStrictEqual(Chunk.toReadonlyArray(rejected), []) + assert.deepStrictEqual(rejected, []) const size = yield* TxQueue.size(queue) assert.strictEqual(size, 5) From 7a7bf530039a0efa6cedc00c55198ccae5a93103 Mon Sep 17 00:00:00 2001 From: Michael Arnaldi Date: Tue, 7 Oct 2025 15:14:22 +0200 Subject: [PATCH 12/20] feat(Queue): add poll() operation for non-blocking take - Add poll() function that returns Option without blocking - Returns Option.some(item) if available, Option.none if empty or done - Aligns with TxQueue.poll() which already exists - Import Option module for Option type - Add comprehensive test for poll() behavior - All 19 Queue tests passing - Completes Phase 3, Commit 1 --- packages/effect/src/Queue.ts | 47 ++++++++++++++++++++++++++++++ packages/effect/src/stm/TxQueue.ts | 4 +-- packages/effect/test/Queue.test.ts | 22 ++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/packages/effect/src/Queue.ts b/packages/effect/src/Queue.ts index 2ea6ffdef..57faa51cb 100644 --- a/packages/effect/src/Queue.ts +++ b/packages/effect/src/Queue.ts @@ -38,6 +38,7 @@ import { Done, interrupt as causeInterrupt } from "./Cause.ts" import * as Arr from "./collections/Array.ts" import * as Iterable from "./collections/Iterable.ts" import * as MutableList from "./collections/MutableList.ts" +import * as Option from "./data/Option.ts" import { hasProperty } from "./data/Predicate.ts" import type { Effect } from "./Effect.ts" import type { Exit, Failure } from "./Exit.ts" @@ -1341,6 +1342,52 @@ export const take = (self: Dequeue): Effect => () => takeUnsafe(self) ?? internalEffect.andThen(awaitTake(self), take(self)) ) +/** + * Tries to take an item from the queue without blocking. + * + * Returns `Option.some` with the item if available, or `Option.none` if the queue is empty or done. + * + * @example + * ```ts + * import { Effect } from "effect" + * import { Option } from "effect/data" + * import { Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(10) + * + * // Poll returns Option.none if empty + * const maybe1 = yield* Queue.poll(queue) + * console.log(Option.isNone(maybe1)) // true + * + * // Add an item + * yield* Queue.offer(queue, 42) + * + * // Poll returns Option.some with the item + * const maybe2 = yield* Queue.poll(queue) + * console.log(Option.getOrNull(maybe2)) // 42 + * + * // Queue is now empty again + * const maybe3 = yield* Queue.poll(queue) + * console.log(Option.isNone(maybe3)) // true + * }) + * ``` + * + * @category taking + * @since 4.0.0 + */ +export const poll = (self: Dequeue): Effect> => + internalEffect.suspend(() => { + const result = takeUnsafe(self) + if (result === undefined) { + return internalEffect.succeed(Option.none()) + } + if (result._tag === "Success") { + return internalEffect.succeed(Option.some(result.value)) + } + return internalEffect.succeed(Option.none()) + }) + /** * Take a single message from the queue synchronously, or wait for a message to be * available. diff --git a/packages/effect/src/stm/TxQueue.ts b/packages/effect/src/stm/TxQueue.ts index 9896d44c1..c81e4589b 100644 --- a/packages/effect/src/stm/TxQueue.ts +++ b/packages/effect/src/stm/TxQueue.ts @@ -9,8 +9,8 @@ * * @since 4.0.0 */ -import * as Arr from "../collections/Array.ts" -import * as Cause from "../Cause.ts" +import type * as Cause from "../Cause.ts" +import type * as Arr from "../collections/Array.ts" import * as Chunk from "../collections/Chunk.ts" import * as Option from "../data/Option.ts" import { hasProperty } from "../data/Predicate.ts" diff --git a/packages/effect/test/Queue.test.ts b/packages/effect/test/Queue.test.ts index 46d4797b7..9eb70e395 100644 --- a/packages/effect/test/Queue.test.ts +++ b/packages/effect/test/Queue.test.ts @@ -1,5 +1,6 @@ import { assert, describe, it } from "@effect/vitest" import { Cause, Effect, Exit, Fiber, Queue } from "effect" +import { Option } from "effect/data" import { Stream } from "effect/stream" describe("Queue", () => { @@ -184,6 +185,27 @@ describe("Queue", () => { assert.isTrue(Exit.hasInterrupt(exit)) })) + it.effect("poll returns Option for non-blocking take", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(10) + + // Poll returns Option.none when empty + const empty = yield* Queue.poll(queue) + assert.isTrue(Option.isNone(empty)) + + // Add an item + yield* Queue.offer(queue, 42) + + // Poll returns Option.some with the item + const item = yield* Queue.poll(queue) + assert.isTrue(Option.isSome(item)) + assert.strictEqual(Option.getOrNull(item), 42) + + // Queue is now empty again + const empty2 = yield* Queue.poll(queue) + assert.isTrue(Option.isNone(empty2)) + })) + it.effect("fail", () => Effect.gen(function*() { const queue = yield* Queue.bounded(2) From 76f33f5dc2bb78e420b2e9907cf6da6c63043289 Mon Sep 17 00:00:00 2001 From: Michael Arnaldi Date: Tue, 7 Oct 2025 15:17:39 +0200 Subject: [PATCH 13/20] feat(Queue): add peek() operation to view items without removing - Add peek() function that returns the next item without removing it - Blocks until an item is available (like take, but non-destructive) - Accesses MutableList head bucket to read value without removal - Aligns with TxQueue.peek() which already exists - Add comprehensive test demonstrating peek behavior - All 20 Queue tests passing - Completes Phase 3, Commit 2 --- packages/effect/src/Queue.ts | 42 ++++++++++++++++++++++++++++++ packages/effect/test/Queue.test.ts | 26 ++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/packages/effect/src/Queue.ts b/packages/effect/src/Queue.ts index 57faa51cb..f6868ab6e 100644 --- a/packages/effect/src/Queue.ts +++ b/packages/effect/src/Queue.ts @@ -1388,6 +1388,48 @@ export const poll = (self: Dequeue): Effect> => return internalEffect.succeed(Option.none()) }) +/** + * Views the next item without removing it. + * + * Blocks until an item is available. If the queue is done or fails, the error is propagated. + * + * @example + * ```ts + * import { Effect } from "effect" + * import { Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(10) + * yield* Queue.offer(queue, 42) + * + * // Peek at the next item without removing it + * const item = yield* Queue.peek(queue) + * console.log(item) // 42 + * + * // Item is still in the queue + * const size = yield* Queue.size(queue) + * console.log(size) // 1 + * + * // Take the item + * const taken = yield* Queue.take(queue) + * console.log(taken) // 42 + * }) + * ``` + * + * @category taking + * @since 4.0.0 + */ +export const peek = (self: Dequeue): Effect => + internalEffect.suspend(() => { + if (self.state._tag === "Done") { + return self.state.exit + } + if (self.messages.length > 0 && self.messages.head) { + return internalEffect.succeed(self.messages.head.array[self.messages.head.offset]) + } + return internalEffect.andThen(awaitTake(self), peek(self)) + }) + /** * Take a single message from the queue synchronously, or wait for a message to be * available. diff --git a/packages/effect/test/Queue.test.ts b/packages/effect/test/Queue.test.ts index 9eb70e395..f541c46b1 100644 --- a/packages/effect/test/Queue.test.ts +++ b/packages/effect/test/Queue.test.ts @@ -206,6 +206,32 @@ describe("Queue", () => { assert.isTrue(Option.isNone(empty2)) })) + it.effect("peek views item without removing it", () => + Effect.gen(function*() { + const queue = yield* Queue.bounded(10) + yield* Queue.offer(queue, 42) + + // Peek at the item + const item = yield* Queue.peek(queue) + assert.strictEqual(item, 42) + + // Item is still in the queue + const size = yield* Queue.size(queue) + assert.strictEqual(size, 1) + + // Peek again - same item + const item2 = yield* Queue.peek(queue) + assert.strictEqual(item2, 42) + + // Now take it + const taken = yield* Queue.take(queue) + assert.strictEqual(taken, 42) + + // Queue is now empty + const newSize = yield* Queue.size(queue) + assert.strictEqual(newSize, 0) + })) + it.effect("fail", () => Effect.gen(function*() { const queue = yield* Queue.bounded(2) From 1c56f1e2fc68ac6481510852c9014765568a4566 Mon Sep 17 00:00:00 2001 From: Michael Arnaldi Date: Tue, 7 Oct 2025 16:01:41 +0200 Subject: [PATCH 14/20] fix(TxQueue): prevent linter from converting Cause/Arr to type imports - Keep Cause and Arr as value imports, not type-only imports - These are used at runtime in type casts and must be value imports - Prevents 'Cause is not defined' runtime errors in tests --- packages/effect/src/stm/TxQueue.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/effect/src/stm/TxQueue.ts b/packages/effect/src/stm/TxQueue.ts index c81e4589b..9896d44c1 100644 --- a/packages/effect/src/stm/TxQueue.ts +++ b/packages/effect/src/stm/TxQueue.ts @@ -9,8 +9,8 @@ * * @since 4.0.0 */ -import type * as Cause from "../Cause.ts" -import type * as Arr from "../collections/Array.ts" +import * as Arr from "../collections/Array.ts" +import * as Cause from "../Cause.ts" import * as Chunk from "../collections/Chunk.ts" import * as Option from "../data/Option.ts" import { hasProperty } from "../data/Predicate.ts" From fabda05205d710a7dd78566fb9e94fe0c226bad0 Mon Sep 17 00:00:00 2001 From: Michael Arnaldi Date: Tue, 7 Oct 2025 16:04:56 +0200 Subject: [PATCH 15/20] refactor(Queue): implement end() in terms of failCause like TxQueue - Change end() to use failCause(causeFail(Done)) instead of done(exitVoid) - Aligns with TxQueue.end() implementation pattern - Import Cause.fail as causeFail - Removes dependency on the low-level done(exit) function - All 20 Queue tests passing - Cleaner, more consistent API between Queue and TxQueue --- packages/effect/src/Queue.ts | 4 ++-- packages/effect/src/stm/TxQueue.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/effect/src/Queue.ts b/packages/effect/src/Queue.ts index f6868ab6e..bdbd354a9 100644 --- a/packages/effect/src/Queue.ts +++ b/packages/effect/src/Queue.ts @@ -34,7 +34,7 @@ * @since 3.8.0 */ import type { Cause } from "./Cause.ts" -import { Done, interrupt as causeInterrupt } from "./Cause.ts" +import { Done, fail as causeFail, interrupt as causeInterrupt } from "./Cause.ts" import * as Arr from "./collections/Array.ts" import * as Iterable from "./collections/Iterable.ts" import * as MutableList from "./collections/MutableList.ts" @@ -873,7 +873,7 @@ export const failCause = (self: Queue, cause: Cause) => done(self * @category completion * @since 4.0.0 */ -export const end = (self: Queue): Effect => done(self, internalEffect.exitVoid) +export const end = (self: Queue): Effect => failCause(self, causeFail(Done)) /** * Signal that the queue is complete synchronously. If the queue is already done, `false` is diff --git a/packages/effect/src/stm/TxQueue.ts b/packages/effect/src/stm/TxQueue.ts index 9896d44c1..c81e4589b 100644 --- a/packages/effect/src/stm/TxQueue.ts +++ b/packages/effect/src/stm/TxQueue.ts @@ -9,8 +9,8 @@ * * @since 4.0.0 */ -import * as Arr from "../collections/Array.ts" -import * as Cause from "../Cause.ts" +import type * as Cause from "../Cause.ts" +import type * as Arr from "../collections/Array.ts" import * as Chunk from "../collections/Chunk.ts" import * as Option from "../data/Option.ts" import { hasProperty } from "../data/Predicate.ts" From 228fbe659f531ee5cd387e4b274a4e6d0946a0cb Mon Sep 17 00:00:00 2001 From: Michael Arnaldi Date: Tue, 7 Oct 2025 16:07:34 +0200 Subject: [PATCH 16/20] feat(Queue): add failCauseUnsafe and make failCause dual - Add failCauseUnsafe() synchronous function matching TxQueue pattern - Convert failCause() to dual function with data-first and data-last signatures - Add comprehensive JSDoc example for failCauseUnsafe - Add comment in TxQueue to prevent linter from converting imports to type-only - All 107 queue tests passing - Completes API alignment for completion functions --- packages/effect/src/Queue.ts | 38 +++++++++++++++++++++++++++++- packages/effect/src/stm/TxQueue.ts | 5 ++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/packages/effect/src/Queue.ts b/packages/effect/src/Queue.ts index bdbd354a9..211007bb1 100644 --- a/packages/effect/src/Queue.ts +++ b/packages/effect/src/Queue.ts @@ -838,7 +838,43 @@ export const fail = (self: Queue, error: E) => done(self, core.exitF * @category completion * @since 4.0.0 */ -export const failCause = (self: Queue, cause: Cause) => done(self, core.exitFailCause(cause)) +export const failCause: { + (cause: Cause): (self: Queue) => Effect + (self: Queue, cause: Cause): Effect +} = dual(2, (self: Queue, cause: Cause): Effect => done(self, core.exitFailCause(cause))) + +/** + * Fail the queue with a cause synchronously. If the queue is already done, `false` is + * returned. + * + * This is an unsafe operation that directly modifies the queue without Effect wrapping. + * + * @example + * ```ts + * import { Effect, Cause } from "effect" + * import { Queue } from "effect" + * + * const program = Effect.gen(function*() { + * const queue = yield* Queue.bounded(10) + * + * // Add some messages + * Queue.offerUnsafe(queue, 1) + * + * // Create a cause and fail the queue synchronously + * const cause = Cause.fail("Processing error") + * const failed = Queue.failCauseUnsafe(queue, cause) + * console.log(failed) // true + * + * // The queue is now in failed state + * console.log(queue.state._tag) // "Done" + * }) + * ``` + * + * @category completion + * @since 4.0.0 + */ +export const failCauseUnsafe = (self: Queue, cause: Cause): boolean => + doneUnsafe(self, core.exitFailCause(cause)) /** * Signal that the queue is complete. If the queue is already done, `false` is diff --git a/packages/effect/src/stm/TxQueue.ts b/packages/effect/src/stm/TxQueue.ts index c81e4589b..f9838fc28 100644 --- a/packages/effect/src/stm/TxQueue.ts +++ b/packages/effect/src/stm/TxQueue.ts @@ -9,8 +9,9 @@ * * @since 4.0.0 */ -import type * as Cause from "../Cause.ts" -import type * as Arr from "../collections/Array.ts" +// Need value imports for runtime usage in type casts +import * as Arr from "../collections/Array.ts" +import * as Cause from "../Cause.ts" import * as Chunk from "../collections/Chunk.ts" import * as Option from "../data/Option.ts" import { hasProperty } from "../data/Predicate.ts" From 108fb9ed3c870bfa29319941bdc4f305cdedf329 Mon Sep 17 00:00:00 2001 From: Michael Arnaldi Date: Tue, 7 Oct 2025 16:17:27 +0200 Subject: [PATCH 17/20] refactor(Queue): remove done/doneUnsafe, use failCause/failCauseUnsafe - Remove low-level done() and doneUnsafe() functions from Queue API - Refactor all internal and external usages to use failCause/failCauseUnsafe - Update fail() to use failCause(causeFail(error)) - Update interrupt() to use failCause(causeInterrupt(id)) - Update endUnsafe() to use failCauseUnsafe(causeFail(Done)) - Refactor failCause() to call failCauseUnsafe() internally - Implement failCauseUnsafe() with direct state management logic - Update Channel.ts, Reactivity.ts, RpcClient.ts, SqlStream.ts, NodeFileSystem.ts - Replace Queue.done() with conditional failCause/end calls - Replace Queue.doneUnsafe() with failCauseUnsafe() or endUnsafe() - Remove unused Exit imports where applicable - All 107 queue tests passing - Cleaner, more consistent API with failCause as the primary primitive --- packages/effect/src/Queue.ts | 110 ++++-------------- packages/effect/src/stm/TxQueue.ts | 3 +- packages/effect/src/stream/Channel.ts | 2 +- .../src/unstable/reactivity/Reactivity.ts | 4 +- packages/effect/src/unstable/rpc/RpcClient.ts | 10 +- packages/effect/src/unstable/sql/SqlStream.ts | 6 +- packages/effect/test/Queue.test.ts | 2 +- packages/effect/test/Stream.test.ts | 4 +- .../src/NodeFileSystem.ts | 6 +- 9 files changed, 43 insertions(+), 104 deletions(-) diff --git a/packages/effect/src/Queue.ts b/packages/effect/src/Queue.ts index 211007bb1..4e551aede 100644 --- a/packages/effect/src/Queue.ts +++ b/packages/effect/src/Queue.ts @@ -809,7 +809,7 @@ export const offerAllUnsafe = (self: Queue, messages: Iterable): * @category completion * @since 4.0.0 */ -export const fail = (self: Queue, error: E) => done(self, core.exitFail(error)) +export const fail = (self: Queue, error: E) => failCause(self, causeFail(error)) /** * Fail the queue with a cause. If the queue is already done, `false` is @@ -841,7 +841,10 @@ export const fail = (self: Queue, error: E) => done(self, core.exitF export const failCause: { (cause: Cause): (self: Queue) => Effect (self: Queue, cause: Cause): Effect -} = dual(2, (self: Queue, cause: Cause): Effect => done(self, core.exitFailCause(cause))) +} = dual( + 2, + (self: Queue, cause: Cause): Effect => internalEffect.sync(() => failCauseUnsafe(self, cause)) +) /** * Fail the queue with a cause synchronously. If the queue is already done, `false` is @@ -873,8 +876,22 @@ export const failCause: { * @category completion * @since 4.0.0 */ -export const failCauseUnsafe = (self: Queue, cause: Cause): boolean => - doneUnsafe(self, core.exitFailCause(cause)) +export const failCauseUnsafe = (self: Queue, cause: Cause): boolean => { + if (self.state._tag !== "Open") { + return false + } + const exit = core.exitFailCause(cause) + const fail = internalEffect.exitZipRight(exit, exitFailDone) as Failure + if ( + self.state.offers.size === 0 && + self.messages.length === 0 + ) { + finalize(self, fail) + return true + } + self.state = { ...self.state, _tag: "Closing", exit: fail } + return true +} /** * Signal that the queue is complete. If the queue is already done, `false` is @@ -942,7 +959,7 @@ export const end = (self: Queue): Effect => failCaus * @category completion * @since 4.0.0 */ -export const endUnsafe = (self: Queue) => doneUnsafe(self, internalEffect.exitVoid) +export const endUnsafe = (self: Queue) => failCauseUnsafe(self, causeFail(Done)) /** * Interrupts the queue gracefully, transitioning it to a closing state. @@ -987,88 +1004,7 @@ export const endUnsafe = (self: Queue) => doneUnsafe(self, in * @since 4.0.0 */ export const interrupt = (self: Queue): Effect => - core.withFiber((fiber) => done(self, core.exitFailCause(causeInterrupt(fiber.id)))) - -/** - * Signal that the queue is done with a specific exit value. If the queue is already done, `false` is - * returned. - * - * @example - * ```ts - * import { Effect, Exit } from "effect" - * import { Queue } from "effect" - * - * const program = Effect.gen(function*() { - * const queue = yield* Queue.bounded(10) - * - * // Add some messages - * yield* Queue.offer(queue, 1) - * yield* Queue.offer(queue, 2) - * - * // Create a success exit and mark queue as done - * const successExit = Exit.succeed(undefined) - * const isDone = yield* Queue.done(queue, successExit) - * console.log(isDone) // true - * - * // Or create a failure exit - * const failureExit = Exit.fail("Processing error") - * const queue2 = yield* Queue.bounded(10) - * const isDone2 = yield* Queue.done(queue2, failureExit) - * console.log(isDone2) // true - * }) - * ``` - * - * @category completion - * @since 4.0.0 - */ -export const done = (self: Queue, exit: Exit): Effect => - internalEffect.sync(() => doneUnsafe(self, exit)) - -/** - * Signal that the queue is done synchronously with a specific exit value. If the queue is already done, `false` is - * returned. - * - * This is an unsafe operation that directly modifies the queue without Effect wrapping. - * - * @example - * ```ts - * import { Effect, Exit } from "effect" - * import { Queue } from "effect" - * - * // Create a queue and use unsafe operations - * const program = Effect.gen(function*() { - * const queue = yield* Queue.bounded(10) - * - * // Add some messages - * Queue.offerUnsafe(queue, 1) - * Queue.offerUnsafe(queue, 2) - * - * // Mark as done with success exit - * const successExit = Exit.succeed(undefined) - * const isDone = Queue.doneUnsafe(queue, successExit) - * console.log(isDone) // true - * console.log(queue.state._tag) // "Done" - * }) - * ``` - * - * @category completion - * @since 4.0.0 - */ -export const doneUnsafe = (self: Queue, exit: Exit): boolean => { - if (self.state._tag !== "Open") { - return false - } - const fail = internalEffect.exitZipRight(exit, exitFailDone) as Failure - if ( - self.state.offers.size === 0 && - self.messages.length === 0 - ) { - finalize(self, fail) - return true - } - self.state = { ...self.state, _tag: "Closing", exit: fail } - return true -} + core.withFiber((fiber) => failCause(self, causeInterrupt(fiber.id))) /** * Shutdown the queue, canceling any pending operations. diff --git a/packages/effect/src/stm/TxQueue.ts b/packages/effect/src/stm/TxQueue.ts index f9838fc28..348e469da 100644 --- a/packages/effect/src/stm/TxQueue.ts +++ b/packages/effect/src/stm/TxQueue.ts @@ -9,9 +9,8 @@ * * @since 4.0.0 */ -// Need value imports for runtime usage in type casts -import * as Arr from "../collections/Array.ts" import * as Cause from "../Cause.ts" +import type * as Arr from "../collections/Array.ts" import * as Chunk from "../collections/Chunk.ts" import * as Option from "../data/Option.ts" import { hasProperty } from "../data/Predicate.ts" diff --git a/packages/effect/src/stream/Channel.ts b/packages/effect/src/stream/Channel.ts index 0e584474f..79772c3a7 100644 --- a/packages/effect/src/stream/Channel.ts +++ b/packages/effect/src/stream/Channel.ts @@ -5346,7 +5346,7 @@ export const toQueue: { }) yield* Scope.addFinalizer(scope, Queue.shutdown(queue)) yield* runForEach(self, (value) => Queue.offer(queue, value)).pipe( - Effect.onExit((exit) => Queue.done(queue, Exit.asVoid(exit))), + Effect.onExit((exit) => exit._tag === "Success" ? Queue.end(queue) : Queue.failCause(queue, exit.cause)), Effect.forkIn(scope) ) return queue diff --git a/packages/effect/src/unstable/reactivity/Reactivity.ts b/packages/effect/src/unstable/reactivity/Reactivity.ts index ad0206936..379656c80 100644 --- a/packages/effect/src/unstable/reactivity/Reactivity.ts +++ b/packages/effect/src/unstable/reactivity/Reactivity.ts @@ -3,7 +3,7 @@ */ import type { ReadonlyRecord } from "../../data/Record.ts" import * as Effect from "../../Effect.ts" -import * as Exit from "../../Exit.ts" +import type * as Exit from "../../Exit.ts" import * as FiberHandle from "../../FiberHandle.ts" import { dual } from "../../Function.ts" import * as Hash from "../../interfaces/Hash.ts" @@ -121,7 +121,7 @@ export const make = Effect.sync(() => { let pending = false const handleExit = (exit: Exit.Exit) => { if (exit._tag === "Failure") { - Queue.doneUnsafe(results, Exit.failCause(exit.cause)) + Queue.failCauseUnsafe(results, exit.cause) } else { Queue.offerUnsafe(results, exit.value) } diff --git a/packages/effect/src/unstable/rpc/RpcClient.ts b/packages/effect/src/unstable/rpc/RpcClient.ts index 079284e02..406af49f6 100644 --- a/packages/effect/src/unstable/rpc/RpcClient.ts +++ b/packages/effect/src/unstable/rpc/RpcClient.ts @@ -289,7 +289,9 @@ export const makeNoSerialization: } else { entry.resume(exit) } @@ -566,7 +568,7 @@ export const makeNoSerialization: Queue.done(entry.queue, Exit.failCause(cause))) + Effect.catchCause((cause) => Queue.failCause(entry.queue, cause)) ) } case "Exit": { @@ -578,7 +580,9 @@ export const makeNoSerialization: ( register({ single: (item) => offer([item]), array: (chunk) => offer(chunk), - fail: (error) => Queue.doneUnsafe(queue, Exit.fail(error)), - end: () => Queue.doneUnsafe(queue, Exit.void) + fail: (error) => Queue.failCauseUnsafe(queue as any, Cause.fail(error)), + end: () => Queue.endUnsafe(queue as any) }), (_) => { cbs = _ diff --git a/packages/effect/test/Queue.test.ts b/packages/effect/test/Queue.test.ts index f541c46b1..bee86a0a0 100644 --- a/packages/effect/test/Queue.test.ts +++ b/packages/effect/test/Queue.test.ts @@ -129,7 +129,7 @@ describe("Queue", () => { Effect.forkChild ) yield* Effect.yieldNow - yield* Queue.done(queue, Exit.void) + yield* Queue.end(queue) assert.deepStrictEqual(yield* Fiber.await(fiber), Exit.fail(Cause.Done)) })) diff --git a/packages/effect/test/Stream.test.ts b/packages/effect/test/Stream.test.ts index 4c7960c32..a006d449d 100644 --- a/packages/effect/test/Stream.test.ts +++ b/packages/effect/test/Stream.test.ts @@ -51,7 +51,7 @@ describe("Stream", () => { it.effect("signals the end of the stream", () => Effect.gen(function*() { const result = yield* Stream.callback((mb) => { - Queue.doneUnsafe(mb, Exit.void) + Queue.endUnsafe(mb) return Effect.void }).pipe(Stream.runCollect) assert.isTrue(result.length === 0) @@ -61,7 +61,7 @@ describe("Stream", () => { Effect.gen(function*() { const error = new Error("boom") const result = yield* Stream.callback((mb) => { - Queue.doneUnsafe(mb, Exit.fail(error)) + Queue.failCauseUnsafe(mb, Cause.fail(error)) return Effect.void }).pipe( Stream.runCollect, diff --git a/packages/platform-node-shared/src/NodeFileSystem.ts b/packages/platform-node-shared/src/NodeFileSystem.ts index 950ab3c40..e4c8a39ec 100644 --- a/packages/platform-node-shared/src/NodeFileSystem.ts +++ b/packages/platform-node-shared/src/NodeFileSystem.ts @@ -1,10 +1,10 @@ /** * @since 1.0.0 */ +import * as Cause from "effect/Cause" import * as Option from "effect/data/Option" import * as Effect from "effect/Effect" import { effectify } from "effect/Effect" -import * as Exit from "effect/Exit" import { pipe } from "effect/Function" import * as Layer from "effect/Layer" import * as FileSystem from "effect/platform/FileSystem" @@ -548,9 +548,9 @@ const watchNode = (path: string) => } }) watcher.on("error", (error) => { - Queue.doneUnsafe( + Queue.failCauseUnsafe( queue, - Exit.fail( + Cause.fail( new Error.SystemError({ module: "FileSystem", reason: "Unknown", From 1b84399b5f623c1ef49e890484ac733e0987083f Mon Sep 17 00:00:00 2001 From: Michael Arnaldi Date: Tue, 7 Oct 2025 16:22:49 +0200 Subject: [PATCH 18/20] chore: remove PLAN.md after completion --- PLAN.md | 835 -------------------------------------------------------- 1 file changed, 835 deletions(-) delete mode 100644 PLAN.md diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index 7d73e381d..000000000 --- a/PLAN.md +++ /dev/null @@ -1,835 +0,0 @@ -# API Alignment Plan: Queue.ts vs TxQueue.ts - -**Goal:** Align the APIs of Queue.ts (Effect-based) and TxQueue.ts (STM-based) to ensure consistent naming, return types, functionality, and structure while respecting their fundamental differences. - -## 🚨 CRITICAL IMPLEMENTATION RULES 🚨 - -### Rule 1: Small, Single-Purpose Commits - -- Each commit must have ONE clear purpose -- No mixing of unrelated changes -- Commit message must clearly describe the single change made -- If you're tempted to use "and" in a commit message, split into multiple commits - -### Rule 2: Zero Tolerance for Errors Before Commit - -- **MANDATORY**: Before ANY commit is made, ALL of the following MUST pass: - 1. ✅ All TypeScript files compile: `pnpm tsc --noEmit` - 2. ✅ All linting passes: `pnpm lint --fix ` - 3. ✅ All tests pass: `pnpm test ` - 4. ✅ JSDoc examples compile: `pnpm docgen` (if JSDoc was modified) - -### Rule 3: Never Proceed with Errors - -- **ABSOLUTELY FORBIDDEN**: Never continue to next step if current step has errors -- **ABSOLUTELY FORBIDDEN**: Never commit code that has compilation errors -- **ABSOLUTELY FORBIDDEN**: Never commit code that has linting errors -- **ABSOLUTELY FORBIDDEN**: Never commit code that has failing tests -- **MANDATORY**: Stop and fix ALL errors before proceeding - -### Rule 4: Follow AGENTS.md Validation Commands - -- **Lint TypeScript files**: `pnpm lint --fix ` - - Run IMMEDIATELY after ANY edit to a .ts file - - NEVER lint non-.ts files (markdown, JSON, etc.) -- **Check types**: `pnpm tsc --noEmit` - - Run after any type signature changes -- **Run tests**: `pnpm test ` - - Run after any implementation changes -- **Check JSDoc examples**: `pnpm docgen` - - Run before committing if JSDoc examples were modified -- **Full project check**: `pnpm check` - - Run before final commit - -### Rule 5: Mandatory After-Edit Actions - -After editing ANY TypeScript file: - -1. **IMMEDIATELY** run: `pnpm lint --fix ` -2. Check for type errors: `pnpm tsc --noEmit` -3. Run relevant tests: `pnpm test ` -4. Fix all errors before proceeding - -## Overview - -Queue.ts provides Effect-based asynchronous queues with backpressure strategies, while TxQueue.ts provides STM-based transactional queues. As TxQueue is the transactional counterpart of Queue, the APIs should mirror each other closely. - -**Key Differences to Preserve:** - -- **Queue.ts**: Effect-based, supports unsafe synchronous operations -- **TxQueue.ts**: STM-based, all operations are atomic transactions (no unsafe variants needed) - -## Critical Issues Identified - -### 1. ❌ WRONG: Inconsistent Completion Semantics (HIGHEST PRIORITY) - -**Current State:** - -- Queue.ts uses local `Done` interface (lines 968-992) -- TxQueue.ts uses `Cause.NoSuchElementError` -- Both represent the same concept: graceful queue completion - -**Why This is Wrong:** - -- `NoSuchElementError` implies "element not found" (lookup failure) -- Queue completion means "gracefully finished, no more items" (lifecycle event) -- Different types for same semantic meaning = API confusion - -**Solution:** - -- Create unified `Cause.Done` error type in Cause.ts -- Both queues use `Cause.Done` for completion semantics -- Remove Queue's local `Done` interface -- Migrate TxQueue from `Cause.NoSuchElementError` to `Cause.Done` - -### 2. ❌ WRONG: Queue's `done()` Operation (HIGH PRIORITY) - -**Current State:** - -```typescript -// Queue.ts - Complex signature with Exit -export const done = ( - self: Queue, - exit: Exit -): Effect -``` - -**Why This is Wrong:** - -- Complex conditional type signature -- Takes `Exit` instead of `Cause` (less natural) -- Not present in TxQueue (inconsistency) -- `Cause` is the natural primitive, not `Exit` - -**TxQueue's Better Approach:** - -```typescript -// TxQueue.ts - Clean signatures -export const fail: (self: TxEnqueue, error: E) => Effect -export const failCause: (self: TxEnqueue, cause: Cause) => Effect -export const end: (self: TxEnqueue) => Effect -``` - -**Solution:** - -- Remove `done()` and `doneUnsafe()` from Queue.ts -- Make `failCause(cause: Cause)` the primitive in both implementations -- Build `fail` and `end` as convenience wrappers around `failCause` -- Hierarchy: `failCause` → `fail`, `end` - -### 3. ❌ WRONG: TxQueue `takeAll` Type Lie (HIGH PRIORITY) - -**Current State:** - -```typescript -// TxQueue.ts - Says "might be empty" but blocks until non-empty! -export const takeAll = (self: TxDequeue): Effect, E> -``` - -**Implementation Reality:** - -```typescript -// Blocks until at least 1 item available -if (yield * isEmpty(self)) { - return yield * Effect.retryTransaction // ← BLOCKS HERE -} -// Only proceeds when ≥1 item available -``` - -**Queue.ts Has it Correct:** - -```typescript -export const takeAll = (self: Dequeue): Effect, E> -``` - -**Solution:** - -- Fix TxQueue `takeAll` signature to return `NonEmptyArray` -- Type now accurately reflects runtime behavior - -### 4. ❌ WRONG: Queue Missing `interrupt` Operation (HIGH PRIORITY) - -**Current State:** - -- Queue.ts has NO `interrupt` operation -- Only has `shutdown` which clears AND interrupts immediately -- No way to gracefully close (stop accepting, allow draining) - -**TxQueue.ts Has it:** - -```typescript -export const interrupt = (self: TxEnqueue): Effect - // Graceful close - stops accepting, allows draining existing items -``` - -**Solution:** - -- Add `interrupt` to Queue.ts -- Refactor `shutdown` to compose `clear` + `interrupt` - -### 5. ❌ WRONG: Inconsistent `clear` Semantics (MEDIUM PRIORITY) - -**Current State:** - -```typescript -// Queue.ts - Returns Array, category "taking" -export const clear = (self: Dequeue): Effect, E> - -// TxQueue.ts - Returns void, category "combinators" -export const clear = (self: TxEnqueue): Effect -``` - -**Solution:** - -- Align both to return `Array` (observable operations) -- Fix Queue's category from "taking" to "combinators" -- Change TxQueue to return cleared items - -### 6. ❌ WRONG: Queue Missing `Enqueue` Interface (MEDIUM PRIORITY) - -**Current State:** - -```typescript -// TxQueue.ts - THREE interfaces (correct structure) -TxEnqueue // Write-only (contravariant) -TxDequeue // Read-only (covariant) -TxQueue // Full queue (invariant) - -// Queue.ts - TWO interfaces (incomplete) -Dequeue // Read-only -Queue // Full queue -// MISSING: Enqueue -``` - -**Solution:** - -- Add `Enqueue` interface to Queue.ts -- Update `Queue` to extend both `Enqueue` and `Dequeue` -- Add `isEnqueue` guard and `asEnqueue` converter -- Enables type-safe producer-consumer patterns - -### 7. ❌ WRONG: Return Type Inconsistencies (MEDIUM PRIORITY) - -**Issue 7a: `offerAll` returns different types** - -- Queue.ts: `Effect>` (remaining messages) -- TxQueue.ts: `Effect>` (rejected items) -- **Solution:** Both return `Array` - -**Issue 7b: Signature patterns need unified `Cause.Done`** - -- All `E | Done` → `E | Cause.Done` -- All `Exclude` → `Exclude` -- All `E | Cause.NoSuchElementError` → `E | Cause.Done` - -## Detailed Implementation Steps with Commits - -### Phase 0: Unified Completion API (HIGHEST PRIORITY) - -**Commit 0.1:** `feat(Cause): add Done error type for queue completion` - -- Add `Done` class extending `Pull.Halt` to `packages/effect/src/internal/core.ts` -- Export `Done`, `DoneTypeId`, `isDone` from `packages/effect/src/Cause.ts` -- Validates: `pnpm lint --fix`, `pnpm tsc --noEmit` - -**Commit 0.2:** `refactor(Queue): migrate to use Cause.Done for completion` - -- Replace all `E | Done` with `E | Cause.Done` in Queue.ts -- Replace all `Exclude` with `Exclude` -- Update `end`, `endUnsafe`, `collect`, `await_`, `into`, `toPull`, `toPullArray` signatures -- Deprecate local `Queue.Done`, re-export `Cause.Done` as `Queue.Done` for compatibility -- Update JSDoc examples to use `Cause.Done` -- Validates: `pnpm lint --fix`, `pnpm tsc --noEmit`, `pnpm test packages/effect/test/Queue.test.ts` - -**Commit 0.3:** `refactor(TxQueue): migrate from NoSuchElementError to Cause.Done` - -- Replace all `Cause.NoSuchElementError` with `Cause.Done` -- Update `end()` signature and implementation -- Update all JSDoc examples -- Validates: `pnpm lint --fix`, `pnpm tsc --noEmit`, `pnpm test packages/effect/test/TxQueue.test.ts` - -**Commit 0.4:** `feat(Queue): add interrupt operation for graceful close` - -- Add `interrupt()` function to Queue.ts -- Add JSDoc with examples -- Validates: `pnpm lint --fix`, `pnpm tsc --noEmit`, `pnpm test packages/effect/test/Queue.test.ts` - -**Commit 0.5:** `refactor(Queue): fix clear semantics to return cleared items` - -- Change `clear()` category from "taking" to "combinators" -- Validates: `pnpm lint --fix`, `pnpm tsc --noEmit` - -**Commit 0.6:** `refactor(TxQueue): change clear to return cleared items` - -- Update `clear()` to return `Array` instead of `void` -- Update implementation -- Validates: `pnpm lint --fix`, `pnpm tsc --noEmit`, `pnpm test packages/effect/test/TxQueue.test.ts` - -**Commit 0.7:** `refactor(Queue): refactor shutdown to compose clear + interrupt` - -- Change `shutdown()` implementation to call `clear()` then `interrupt()` -- Validates: `pnpm lint --fix`, `pnpm tsc --noEmit`, `pnpm test packages/effect/test/Queue.test.ts` - -### Phase 1: Interface Structure Alignment - -**Commit 1.1:** `feat(Queue): add Enqueue interface for write-only operations` - -- Add `EnqueueTypeId`, `Enqueue` interface with contravariant variance -- Add `isEnqueue()` guard and `asEnqueue()` converter -- Update `Queue` interface to extend both `Enqueue` and `Dequeue` -- Update `QueueProto` to include `EnqueueTypeId` -- Validates: `pnpm lint --fix`, `pnpm tsc --noEmit`, `pnpm test packages/effect/test/Queue.test.ts` - -### Phase 2: Critical Return Type Fixes - -**Commit 2.1:** `fix(TxQueue): change takeAll to return NonEmptyArray` - -- Update `takeAll()` signature from `ReadonlyArray` to `NonEmptyArray` -- Update tests -- Validates: `pnpm lint --fix`, `pnpm tsc --noEmit`, `pnpm test packages/effect/test/TxQueue.test.ts` - -**Commit 2.2:** `fix(TxQueue): change offerAll to return Array instead of Chunk` - -- Update `offerAll()` signature from `Chunk` to `Array` -- Update implementation to convert Chunk to Array -- Update tests -- Validates: `pnpm lint --fix`, `pnpm tsc --noEmit`, `pnpm test packages/effect/test/TxQueue.test.ts` - -### Phase 3: Missing Operations - -**Commit 3.1:** `feat(Queue): add poll operation for non-blocking take` - -- Add `poll()` function returning `Effect, E>` -- Add JSDoc with examples -- Validates: `pnpm lint --fix`, `pnpm tsc --noEmit`, `pnpm test packages/effect/test/Queue.test.ts` - -**Commit 3.2:** `feat(Queue): add peek operation to inspect without removing` - -- Add `peek()` function returning `Effect` -- Add JSDoc with examples -- Validates: `pnpm lint --fix`, `pnpm tsc --noEmit`, `pnpm test packages/effect/test/Queue.test.ts` - -**Commit 3.3:** `feat(TxQueue): add collect operation for drain until done` - -- Add `collect()` function -- Add JSDoc with examples -- Validates: `pnpm lint --fix`, `pnpm tsc --noEmit`, `pnpm test packages/effect/test/TxQueue.test.ts` - -### Phase 4: Documentation & Final Validation - -**Commit 4.1:** `docs(Queue,TxQueue): update all JSDoc examples for Cause.Done` - -- Review and update remaining JSDoc examples -- Validates: `pnpm docgen` - -**Commit 4.2:** `test(Queue,TxQueue): update tests for API changes` - -- Update all test files for breaking changes -- Validates: `pnpm test packages/effect/test/Queue.test.ts packages/effect/test/TxQueue.test.ts` - -**Final Validation:** - -- `pnpm lint` - All files -- `pnpm check` - Full typecheck -- `pnpm docgen` - Examples compile -- `pnpm test` - All tests pass - -## Implementation Plan - -### Phase 0: Unified Completion API (HIGHEST PRIORITY) - -#### Step 1: Create `Cause.Done` Error Type - -**File:** `packages/effect/src/Cause.ts` + `packages/effect/src/internal/core.ts` - -````typescript -/** - * Type identifier for Done errors. - * @since 4.0.0 - * @category symbols - */ -export const DoneTypeId: "~effect/Cause/Done" = "~effect/Cause/Done" as const - -/** - * Represents a graceful completion signal for queues and streams. - * - * `Done` is used to signal that a queue or stream has completed normally - * and no more elements will be produced. This is distinct from an error - * or interruption - it represents successful completion. - * - * @example - * ```ts - * import { Cause, Effect } from "effect" - * import { Queue } from "effect" - * - * const program = Effect.gen(function*() { - * const queue = yield* Queue.bounded(10) - * - * yield* Queue.offer(queue, 1) - * yield* Queue.offer(queue, 2) - * - * // Signal completion - * yield* Queue.end(queue) - * - * // Taking from ended queue fails with Done - * const result = yield* Effect.flip(Queue.take(queue)) - * console.log(Cause.isDone(result)) // true - * }) - * ``` - * - * @since 4.0.0 - * @category models - */ -export interface Done extends YieldableError { - readonly [DoneTypeId]: typeof DoneTypeId - readonly _tag: "Done" -} - -/** - * Creates a `Done` error to signal graceful completion. - * @since 4.0.0 - * @category constructors - */ -export const Done: new () => Done - -/** - * Tests if a value is a `Done` error. - * @since 4.0.0 - * @category guards - */ -export const isDone: (u: unknown) => u is Done -```` - -#### Step 2: Remove Queue's `done()` Operations - -**File:** `packages/effect/src/Queue.ts` - -**DELETE:** - -- Line ~816: `done()` operation -- Line ~850: `doneUnsafe()` operation - -**REFACTOR:** - -```typescript -// Change fail and end to call failCause directly -export const fail = (self: Queue, error: E): Effect => failCause(self, Cause.fail(error)) - -export const end = (self: Queue): Effect => - failCause(self, Cause.fail(new Cause.Done())) -``` - -#### Step 3: Migrate Queue.ts Signatures to `Cause.Done` - -**File:** `packages/effect/src/Queue.ts` - -**Update locations:** - -- Line 750: `end` signature -- Line 783: `endUnsafe` signature -- Line 968-992: **DELETE** local `Done` interface, `isDone`, `filterDone` -- Line 1040: `collect` signature (both patterns) -- Line 1244-1245: `await_` signature -- Line 1434-1446: `into` signatures -- Line 1476: `toPull` signature (both patterns) -- Line 1499-1500: `toPullArray` signature - -**Search/Replace patterns:** - -```typescript -E | Done → E | Cause.Done -Exclude → Exclude -``` - -**Add import:** - -```typescript -import { Done } from "./Cause.ts" -``` - -#### Step 4: Migrate TxQueue.ts from `NoSuchElementError` to `Cause.Done` - -**File:** `packages/effect/src/stm/TxQueue.ts` - -**Update locations:** - -- Line 208-211: JSDoc example in `TxEnqueue` interface -- Line 1273-1308: `end` function signature + implementation -- Line 1287: Example type annotation -- Line 1295-1300: JSDoc examples with `isNoSuchElementError` - -**Search/Replace patterns:** - -```typescript -E | Cause.NoSuchElementError → E | Cause.Done -Exclude → Exclude -Cause.NoSuchElementError → Cause.Done -Cause.isNoSuchElementError → Cause.isDone -new Cause.NoSuchElementError() → new Cause.Done() -``` - -#### Step 5: Add `interrupt` to Queue.ts - -**File:** `packages/effect/src/Queue.ts` - -````typescript -/** - * Interrupts the queue, transitioning it to a closing state. - * Existing items can still be consumed, but no new items will be accepted. - * - * @example - * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" - * - * const program = Effect.gen(function*() { - * const queue = yield* Queue.bounded(10) - * yield* Queue.offer(queue, 1) - * yield* Queue.offer(queue, 2) - * - * // Gracefully close - no more offers, but can drain - * yield* Queue.interrupt(queue) - * - * // Can still take existing items - * const item = yield* Queue.take(queue) - * console.log(item) // 1 - * }) - * ``` - * - * @category completion - * @since 4.0.0 - */ -export const interrupt = (self: Queue): Effect => - Effect.withFiber((fiber) => failCause(self, Cause.interrupt(fiber.id))) -```` - -#### Step 6: Fix `clear` Semantics - -**File:** `packages/effect/src/Queue.ts` - -```typescript -// Change category annotation -// @category combinators (was: taking) -export const clear = (self: Dequeue): Effect, E> -``` - -**File:** `packages/effect/src/stm/TxQueue.ts` - -```typescript -// Change to return Array instead of void -export const clear = (self: TxEnqueue): Effect.Effect> => - Effect.atomic( - Effect.gen(function* () { - const chunk = yield* TxChunk.get(self.items) - const items = Chunk.toArray(chunk) - yield* TxChunk.set(self.items, Chunk.empty()) - return items - }) - ) -``` - -#### Step 7: Refactor `shutdown` Composition - -**File:** `packages/effect/src/Queue.ts` - -```typescript -export const shutdown = (self: Queue): Effect => - Effect.gen(function* () { - yield* clear(self) // Clear items first - return yield* interrupt(self) // Then interrupt - }) -``` - -### Phase 1: Interface Structure Alignment - -#### Step 1: Add `Enqueue` Interface to Queue.ts - -**File:** `packages/effect/src/Queue.ts` - -````typescript -const EnqueueTypeId = "~effect/Queue/Enqueue" - -/** - * An `Enqueue` represents the write-only interface of a queue. - * - * @example - * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" - * - * const producer = (enqueue: Queue.Enqueue) => - * Effect.gen(function*() { - * yield* Queue.offer(enqueue, 1) - * yield* Queue.offerAll(enqueue, [2, 3, 4]) - * }) - * ``` - * - * @since 4.0.0 - * @category models - */ -export interface Enqueue extends Inspectable { - readonly [EnqueueTypeId]: Enqueue.Variance - readonly strategy: "suspend" | "dropping" | "sliding" - readonly scheduler: Scheduler - capacity: number - messages: MutableList.MutableList - state: Queue.State - scheduleRunning: boolean -} - -export declare namespace Enqueue { - export interface Variance { - _A: Types.Contravariant - _E: Types.Contravariant - } -} - -/** - * Type guard to check if a value is an Enqueue. - * @since 4.0.0 - * @category guards - */ -export const isEnqueue = (u: unknown): u is Enqueue => hasProperty(u, EnqueueTypeId) - -/** - * Convert a Queue to an Enqueue, allowing only write operations. - * @since 4.0.0 - * @category conversions - */ -export const asEnqueue: (self: Queue) => Enqueue = identity -```` - -#### Step 2: Update Queue Interface - -**File:** `packages/effect/src/Queue.ts` - -```typescript -// Change from: -export interface Queue extends Dequeue - -// To: -export interface Queue - extends Enqueue, Dequeue -``` - -#### Step 3: Update Proto - -```typescript -const QueueProto = { - [TypeId]: variance, - [DequeueTypeId]: variance, - [EnqueueTypeId]: variance, // ADD THIS - ...PipeInspectableProto - // ... -} -``` - -### Phase 2: Critical Return Type Fixes - -#### Step 1: Fix TxQueue `takeAll` Return Type - -**File:** `packages/effect/src/stm/TxQueue.ts` - -```typescript -// Change from: -export const takeAll = (self: TxDequeue): Effect.Effect, E> - -// To: -export const takeAll = (self: TxDequeue): Effect.Effect, E> -``` - -#### Step 2: Align `offerAll` Return Type - -**File:** `packages/effect/src/stm/TxQueue.ts` - -```typescript -// Change from: -export const offerAll = ( - self: TxEnqueue, - values: Iterable -): Effect.Effect> - -// To: -export const offerAll = ( - self: TxEnqueue, - values: Iterable -): Effect.Effect> - -// Update implementation to return Array instead of Chunk -``` - -### Phase 3: Missing Operations - -#### Add to Queue.ts: - -- `poll` - non-blocking take (returns `Option`) -- `peek` - inspect without removing - -#### Add to TxQueue.ts: - -- `collect` - take all until done/error - -### Phase 4: Documentation & Validation - -1. Update all JSDoc examples to use `Cause.Done` -2. Update test files for both modules -3. Run validation: - - `pnpm lint --fix` on modified files - - `pnpm check` for type errors - - `pnpm docgen` for example compilation - - `pnpm test` for all tests - -## Breaking Changes Summary - -### Breaking Change #1: Unified Completion Type - -```typescript -// Before -Queue -TxQueue - -// After (both identical) -Queue -TxQueue -``` - -**Migration:** - -```typescript -// Queue users: Change imports -import { Queue } from "effect" -// Remove: import type { Done } from "effect/Queue" -import { Cause } from "effect" - -// Type annotations -const queue: Queue // Before -const queue: Queue // After - -// TxQueue users: Similar change -const queue: TxQueue // Before -const queue: TxQueue // After -``` - -### Breaking Change #2: Remove `Queue.done()` Operation - -```typescript -// Before -yield * Queue.done(queue, Exit.fail(error)) -yield * Queue.done(queue, Exit.succeed(undefined)) - -// After -yield * Queue.failCause(queue, Cause.fail(error)) -yield * Queue.end(queue) // For graceful completion -``` - -**Migration:** - -- Replace `Queue.done(queue, Exit.fail(e))` with `Queue.failCause(queue, Cause.fail(e))` -- Replace `Queue.done(queue, Exit.succeed())` with `Queue.end(queue)` -- No `doneUnsafe` equivalent - use `failCauseUnsafe` or `endUnsafe` - -### Breaking Change #3: `takeAll` Return Type - -```typescript -// Before (TxQueue) -const items: ReadonlyArray = yield* TxQueue.takeAll(queue) -if (items.length > 0) { ... } // Unnecessary check! - -// After (TxQueue) -const items: NonEmptyArray = yield* TxQueue.takeAll(queue) -// Guaranteed non-empty, no check needed! -``` - -### Breaking Change #4: `clear` Return Type - -```typescript -// Before (TxQueue) -yield * TxQueue.clear(queue) // Returns void - -// After (TxQueue) -const cleared = yield * TxQueue.clear(queue) // Returns Array -console.log("Cleared items:", cleared) -``` - -### Breaking Change #5: Queue Interface Structure - -```typescript -// Before -Queue extends Dequeue // Only two interfaces - -// After -Queue extends Enqueue, Dequeue // Three interfaces - -// New capabilities -const enqueue: Queue.Enqueue = queue // Write-only -const dequeue: Queue.Dequeue = queue // Read-only (existing) -``` - -**Note:** This is mostly non-breaking as existing code continues to work. Only affects advanced type-level programming. - -## Validation Checklist - -After implementation, verify: - -1. ✅ **No local Done types** - `grep "interface Done" Queue.ts` returns nothing -2. ✅ **No NoSuchElementError in queues** - `grep "NoSuchElementError" Queue.ts TxQueue.ts` returns nothing -3. ✅ **All Exclude patterns updated** - `grep "Exclude" Queue.ts` returns nothing without `Cause.` -4. ✅ **Consistent imports** - Both files import `Done` from `./Cause.ts` -5. ✅ **Tests pass** - `pnpm test Queue.test.ts TxQueue.test.ts` -6. ✅ **Types check** - `pnpm check` -7. ✅ **Examples compile** - `pnpm docgen` -8. ✅ **Both return NonEmptyArray** - `takeAll` signatures match -9. ✅ **Both return Array** - `offerAll` and `clear` signatures match -10. ✅ **Both have interrupt** - Queue and TxQueue have graceful close -11. ✅ **Both have Enqueue** - Three-interface structure matches - -## Success Criteria - -- ✅ `Cause.Done` exists and is used by both queues -- ✅ Queue's local `Done` type removed -- ✅ TxQueue uses `Cause.Done` (not `NoSuchElementError`) -- ✅ Queue's `done()` operation removed -- ✅ Both queues have `fail`, `failCause`, `end` as completion API -- ✅ Both queues have `interrupt` for graceful close -- ✅ `takeAll` returns `NonEmptyArray` in both queues -- ✅ `offerAll` returns `Array` in both queues -- ✅ `clear` returns `Array` in both queues -- ✅ Both queues have `Enqueue`, `Dequeue`, `Queue` interfaces -- ✅ All signatures use consistent completion types -- ✅ All tests pass -- ✅ `pnpm docgen` succeeds -- ✅ `pnpm check` succeeds - -## Rationale - -### Why `Cause.Done` Over `NoSuchElementError`? - -- **Semantic correctness**: Completion is not "not finding something" -- **Consistency**: Same concept = same type -- **Clarity**: `Done` clearly signals "finished normally" - -### Why Remove `Queue.done()`? - -- **Simpler API**: `failCause` is more natural than `done(Exit<...>)` -- **Consistency**: TxQueue doesn't have it, shouldn't be different -- **Power**: `Cause` can represent ANY completion scenario -- **Type safety**: Clearer signature without complex conditional types - -### Why `takeAll` Returns `NonEmptyArray`? - -- **Type honesty**: Implementation blocks until ≥1 item available -- **User benefit**: No unnecessary empty checks -- **Correctness**: Type reflects runtime behavior - -### Why Add `Enqueue` Interface? - -- **Type safety**: Restrict operations at type level -- **Consistency**: Match TxQueue's structure -- **Patterns**: Enable producer-consumer separation -- **Variance**: Proper contravariant producer type - -### Why `clear` Returns `Array`? - -- **Observability**: See what was cleared (useful for debugging) -- **Consistency**: Both queues behave the same -- **Less breaking**: Queue already has this signature From df1e1c5bd74286457814af68c611b9b6e659a55e Mon Sep 17 00:00:00 2001 From: Michael Arnaldi Date: Tue, 7 Oct 2025 16:41:40 +0200 Subject: [PATCH 19/20] refactor(Queue): migrate Queue.Done references to Cause.Done - Replace all Queue.Done type references with Cause.Done across codebase - Update JSDoc examples to import Cause and use Cause.Done - Fix Queue.isDone test to use Cause.isDone - Update Stream, Channel, Socket, and RPC modules to use Cause.Done - Simplify JSDoc import statements (combine Effect, Queue, Cause imports) - Fix Cause.isDone example to not construct Done (it's a singleton) - Fix Enqueue/Dequeue interface examples to be type-safe - Fix TxQueue.takeAll example to use correct Array.isArrayNonEmpty function All tests passing (106 test files, 3241 tests) All docgen checks passing with 2667 examples validated --- packages/effect/src/Cause.ts | 3 +- packages/effect/src/Queue.ts | 167 +++++++----------- packages/effect/src/index.ts | 5 +- packages/effect/src/stm/TxQueue.ts | 2 +- packages/effect/src/stream/Channel.ts | 20 +-- packages/effect/src/stream/Stream.ts | 12 +- packages/effect/src/unstable/rpc/Rpc.ts | 3 +- packages/effect/src/unstable/rpc/RpcClient.ts | 2 +- packages/effect/src/unstable/rpc/RpcGroup.ts | 5 +- packages/effect/src/unstable/rpc/RpcServer.ts | 6 +- packages/effect/src/unstable/socket/Socket.ts | 3 +- packages/effect/test/Channel.test.ts | 5 +- packages/effect/test/Queue.test.ts | 2 +- packages/effect/test/cluster/TestEntity.ts | 3 +- 14 files changed, 99 insertions(+), 139 deletions(-) diff --git a/packages/effect/src/Cause.ts b/packages/effect/src/Cause.ts index 5965a0f9c..008e91911 100644 --- a/packages/effect/src/Cause.ts +++ b/packages/effect/src/Cause.ts @@ -700,8 +700,7 @@ export const NoSuchElementError: new(message?: string) => NoSuchElementError = c * ```ts * import { Cause } from "effect" * - * const error = new Cause.Done() - * console.log(Cause.isDone(error)) // true + * console.log(Cause.isDone(Cause.Done)) // true * console.log(Cause.isDone("not an error")) // false * ``` * diff --git a/packages/effect/src/Queue.ts b/packages/effect/src/Queue.ts index 4e551aede..70067ae10 100644 --- a/packages/effect/src/Queue.ts +++ b/packages/effect/src/Queue.ts @@ -7,12 +7,11 @@ * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * * // Creating a bounded queue with capacity 10 * const program = Effect.gen(function*() { - * const queue = yield* Queue.bounded(10) + * const queue = yield* Queue.bounded(10) * * // Producer: add items to queue * yield* Queue.offer(queue, 1) @@ -60,8 +59,7 @@ const DequeueTypeId = "~effect/Queue/Dequeue" * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* Queue.bounded(10) @@ -84,8 +82,7 @@ export const isQueue = ( * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* Queue.bounded(10) @@ -107,8 +104,7 @@ export const isEnqueue = ( * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* Queue.bounded(10) @@ -130,17 +126,20 @@ export const isDequeue = ( * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Effect, Queue } from "effect" + * + * // Function that only needs to offer to a queue + * const producer = (enqueue: Queue.Enqueue) => + * Effect.gen(function*() { + * yield* Queue.offer(enqueue as Queue.Queue, 42) + * }) * * const program = Effect.gen(function*() { * const queue = yield* Queue.bounded(10) * - * // Convert to write-only interface + * // Convert to write-only interface for type safety * const enqueue = Queue.asEnqueue(queue) - * - * // Can only offer, not take - * yield* Queue.offer(enqueue, 42) + * yield* producer(enqueue) * }) * ``` * @@ -157,18 +156,18 @@ export const asEnqueue = (self: Queue): Enqueue => self * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Effect, Queue } from "effect" + * + * // Function that only needs write access to a queue + * const producer = (enqueue: Queue.Enqueue) => + * Effect.gen(function*() { + * yield* Queue.offer(enqueue as Queue.Queue, "hello") + * yield* Queue.offerAll(enqueue as Queue.Queue, ["world", "!"]) + * }) * * const program = Effect.gen(function*() { * const queue = yield* Queue.bounded(10) - * - * // An Enqueue can only offer elements - * const enqueue: Queue.Enqueue = queue - * - * // Offer elements using enqueue interface - * yield* Queue.offer(enqueue, "hello") - * yield* Queue.offerAll(enqueue, ["world", "!"]) + * yield* producer(queue) * }) * ``` * @@ -210,8 +209,7 @@ export declare namespace Enqueue { * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* Queue.bounded(10) @@ -265,8 +263,7 @@ export declare namespace Dequeue { * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { * // Create a bounded queue @@ -381,11 +378,10 @@ const QueueProto = { * @example * ```ts * import * as assert from "node:assert" - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * * Effect.gen(function*() { - * const queue = yield* Queue.make() + * const queue = yield* Queue.make() * * // add messages to the queue * yield* Queue.offer(queue, 1) @@ -399,7 +395,7 @@ const QueueProto = { * // signal that the queue is done * yield* Queue.end(queue) * const done = yield* Effect.flip(Queue.takeAll(queue)) - * assert.deepStrictEqual(done, Queue.Done) + * assert.deepStrictEqual(done, Cause.Done) * * // signal that the queue has failed * yield* Queue.fail(queue, "boom") @@ -439,8 +435,7 @@ export const make = ( * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* Queue.bounded(5) @@ -471,8 +466,7 @@ export const bounded = (capacity: number): Effect> => * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* Queue.sliding(3) @@ -507,8 +501,7 @@ export const sliding = (capacity: number): Effect> => * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* Queue.dropping(2) @@ -542,8 +535,7 @@ export const dropping = (capacity: number): Effect> => * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* Queue.unbounded() @@ -577,8 +569,7 @@ export const unbounded = (): Effect> => make() * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* Queue.bounded(3) @@ -631,8 +622,7 @@ export const offer = (self: Queue, message: Types.NoInfer): Effec * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * * // Create a queue effect and extract the queue for unsafe operations * const program = Effect.gen(function*() { @@ -682,8 +672,7 @@ export const offerUnsafe = (self: Queue, message: Types.NoInfer): * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* Queue.bounded(3) @@ -727,8 +716,7 @@ export const offerAll = (self: Queue, messages: Iterable): Effect * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * * // Create a bounded queue and use unsafe API * const program = Effect.gen(function*() { @@ -787,8 +775,7 @@ export const offerAllUnsafe = (self: Queue, messages: Iterable): * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* Queue.bounded(10) @@ -899,11 +886,10 @@ export const failCauseUnsafe = (self: Queue, cause: Cause): boole * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { - * const queue = yield* Queue.bounded(10) + * const queue = yield* Queue.bounded(10) * * // Add some messages * yield* Queue.offer(queue, 1) @@ -936,12 +922,11 @@ export const end = (self: Queue): Effect => failCaus * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * * // Create a queue and use unsafe operations * const program = Effect.gen(function*() { - * const queue = yield* Queue.bounded(10) + * const queue = yield* Queue.bounded(10) * * // Add some messages * Queue.offerUnsafe(queue, 1) @@ -969,8 +954,7 @@ export const endUnsafe = (self: Queue) => failCauseUnsafe(sel * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* Queue.bounded(10) @@ -1012,8 +996,7 @@ export const interrupt = (self: Queue): Effect => * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* Queue.bounded(2) @@ -1065,8 +1048,7 @@ export const shutdown = (self: Queue): Effect => * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* Queue.bounded(10) @@ -1108,20 +1090,6 @@ export const clear = (self: Dequeue): Effect, E> => * @category Done * @since 4.0.0 */ -/** - * @deprecated Use `Cause.Done` instead. Re-exported for backward compatibility. - * @category Done - * @since 4.0.0 - */ -export type { Done } from "./Cause.ts" - -/** - * @deprecated Use `Cause.isDone` instead. Re-exported for backward compatibility. - * @category Done - * @since 4.0.0 - */ -export { isDone } from "./Cause.ts" - /** * Take all messages from the queue, or wait for messages to be available. * @@ -1130,11 +1098,10 @@ export { isDone } from "./Cause.ts" * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { - * const queue = yield* Queue.bounded(5) + * const queue = yield* Queue.bounded(5) * * // Add several messages * yield* Queue.offerAll(queue, [1, 2, 3, 4, 5]) @@ -1152,7 +1119,7 @@ export { isDone } from "./Cause.ts" * console.log(messages2) // [10, 20] * * const done = yield* Effect.flip(Queue.takeAll(queue)) - * console.log(done) // Queue.Done + * console.log(done) // Cause.Done * }) * ``` * @@ -1196,8 +1163,7 @@ export const collect = (self: Dequeue): Effect, Excl * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* Queue.bounded(10) @@ -1236,8 +1202,7 @@ export const takeN = ( * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* Queue.bounded(10) @@ -1279,11 +1244,10 @@ export const takeBetween = ( * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { - * const queue = yield* Queue.bounded(3) + * const queue = yield* Queue.bounded(3) * * // Add some messages * yield* Queue.offer(queue, "first") @@ -1299,7 +1263,7 @@ export const takeBetween = ( * * // Taking from ended queue fails with None * const result = yield* Effect.match(Queue.take(queue), { - * onFailure: (error: Queue.Done) => true, + * onFailure: (error: Cause.Done) => true, * onSuccess: (value: string) => false * }) * console.log("Queue ended:", result) // true @@ -1367,8 +1331,7 @@ export const poll = (self: Dequeue): Effect> => * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* Queue.bounded(10) @@ -1493,12 +1456,11 @@ export { * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * import * as Option from "effect/data/Option" * * const program = Effect.gen(function*() { - * const queue = yield* Queue.bounded(10) + * const queue = yield* Queue.bounded(10) * * // Check size of empty queue * const size1 = yield* Queue.size(queue) @@ -1539,13 +1501,12 @@ export const isFull = (self: Dequeue): Effect => internalEf * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * import * as Option from "effect/data/Option" * * // Create a queue and use unsafe operations * const program = Effect.gen(function*() { - * const queue = yield* Queue.bounded(10) + * const queue = yield* Queue.bounded(10) * * // Check size of empty queue * const size1 = Queue.sizeUnsafe(queue) @@ -1585,8 +1546,7 @@ export const isFullUnsafe = (self: Dequeue): boolean => sizeUnsafe(s * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* Queue.bounded(10) @@ -1620,11 +1580,10 @@ export const asDequeue: (self: Queue) => Dequeue = identity * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { - * const queue = yield* Queue.bounded(10) + * const queue = yield* Queue.bounded(10) * * // Create an effect that succeeds * const dataProcessing = Effect.gen(function*() { @@ -1677,8 +1636,7 @@ export const into: { * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function* () { * const queue = yield* Queue.bounded(10) @@ -1699,8 +1657,7 @@ export const toPull: (self: Dequeue) => Pull.Pull(10) diff --git a/packages/effect/src/index.ts b/packages/effect/src/index.ts index 984d8c3f6..0b0462a9d 100644 --- a/packages/effect/src/index.ts +++ b/packages/effect/src/index.ts @@ -1034,12 +1034,11 @@ export * as PubSub from "./PubSub.ts" * * @example * ```ts - * import { Effect } from "effect" - * import { Queue } from "effect" + * import { Cause, Effect, Queue } from "effect" * * // Creating a bounded queue with capacity 10 * const program = Effect.gen(function*() { - * const queue = yield* Queue.bounded(10) + * const queue = yield* Queue.bounded(10) * * // Producer: add items to queue * yield* Queue.offer(queue, 1) diff --git a/packages/effect/src/stm/TxQueue.ts b/packages/effect/src/stm/TxQueue.ts index 348e469da..5676f230e 100644 --- a/packages/effect/src/stm/TxQueue.ts +++ b/packages/effect/src/stm/TxQueue.ts @@ -792,7 +792,7 @@ export const poll = (self: TxDequeue): Effect.Effect( scope: Scope.Scope, - f: (queue: Queue.Queue) => void | Effect.Effect, + f: (queue: Queue.Queue) => void | Effect.Effect, options?: { readonly bufferSize?: number | undefined readonly strategy?: "sliding" | "dropping" | "suspend" | undefined } ) => - Queue.make({ + Queue.make({ capacity: options?.bufferSize, strategy: options?.strategy }).pipe( @@ -455,7 +455,7 @@ const asyncQueue = ( * @since 2.0.0 */ export const callback = ( - f: (queue: Queue.Queue) => void | Effect.Effect, + f: (queue: Queue.Queue) => void | Effect.Effect, options?: { readonly bufferSize?: number | undefined readonly strategy?: "sliding" | "dropping" | "suspend" | undefined @@ -482,7 +482,7 @@ export const callback = ( * @since 4.0.0 */ export const callbackArray = ( - f: (queue: Queue.Queue) => void | Effect.Effect, + f: (queue: Queue.Queue) => void | Effect.Effect, options?: { readonly bufferSize?: number | undefined readonly strategy?: "sliding" | "dropping" | "suspend" | undefined @@ -1108,7 +1108,7 @@ export const fromEffectTake = ( */ export const fromQueue = ( queue: Queue.Dequeue -): Channel> => fromPull(Effect.succeed(Queue.toPull(queue))) +): Channel> => fromPull(Effect.succeed(Queue.toPull(queue))) /** * Create a channel from a queue that emits arrays of elements @@ -1156,7 +1156,7 @@ export const fromQueue = ( */ export const fromQueueArray = ( queue: Queue.Dequeue -): Channel, Exclude> => fromPull(Effect.succeed(Queue.toPullArray(queue))) +): Channel, Exclude> => fromPull(Effect.succeed(Queue.toPullArray(queue))) /** * @since 2.0.0 @@ -1898,7 +1898,7 @@ const mapEffectConcurrent = < // - 1 for the current processing fiber const fibers = yield* Queue.bounded< Effect.Effect>>, - Queue.Done + Cause.Done >(concurrencyN - 2) yield* Scope.addFinalizer(forkedScope, Queue.shutdown(queue)) @@ -5325,13 +5325,13 @@ export const toQueue: { readonly bufferSize?: number | undefined }): ( self: Channel - ) => Effect.Effect, never, Env | Scope.Scope> + ) => Effect.Effect, never, Env | Scope.Scope> ( self: Channel, options?: { readonly bufferSize?: number | undefined } - ): Effect.Effect, never, Env | Scope.Scope> + ): Effect.Effect, never, Env | Scope.Scope> } = dual( (args) => isChannel(args[0]), Effect.fnUntraced(function*( @@ -5341,7 +5341,7 @@ export const toQueue: { } ) { const scope = yield* Effect.scope - const queue = yield* Queue.make({ + const queue = yield* Queue.make({ capacity: options?.bufferSize }) yield* Scope.addFinalizer(scope, Queue.shutdown(queue)) diff --git a/packages/effect/src/stream/Stream.ts b/packages/effect/src/stream/Stream.ts index f5bb55af7..5aa5aefc6 100644 --- a/packages/effect/src/stream/Stream.ts +++ b/packages/effect/src/stream/Stream.ts @@ -489,7 +489,7 @@ export const toChannel = ( * @category constructors */ export const callback = ( - f: (queue: Queue.Queue) => void | Effect.Effect, + f: (queue: Queue.Queue) => void | Effect.Effect, options?: { readonly bufferSize?: number | undefined readonly strategy?: "sliding" | "dropping" | "suspend" | undefined @@ -846,7 +846,7 @@ export const fromArrays = >>( * @since 4.0.0 * @category constructors */ -export const fromQueue = (queue: Queue.Dequeue): Stream> => +export const fromQueue = (queue: Queue.Dequeue): Stream> => fromChannel(Channel.fromQueueArray(queue)) /** @@ -2591,8 +2591,8 @@ const groupByImpl = ( self: Stream, f: ( arr: Arr.NonEmptyReadonlyArray, - queues: RcMap.RcMap>, - queueMap: MutableHashMap.MutableHashMap> + queues: RcMap.RcMap>, + queueMap: MutableHashMap.MutableHashMap> ) => Effect.Effect, options?: { readonly bufferSize?: number | undefined @@ -2605,11 +2605,11 @@ const groupByImpl = ( const out = yield* Queue.unbounded], E | E2 | Pull.Halt>() yield* Scope.addFinalizer(scope, Queue.shutdown(out)) - const queueMap = MutableHashMap.empty>() + const queueMap = MutableHashMap.empty>() const queues = yield* RcMap.make({ lookup: (key: K) => Effect.acquireRelease( - Queue.make({ capacity: options?.bufferSize ?? 4096 }).pipe( + Queue.make({ capacity: options?.bufferSize ?? 4096 }).pipe( Effect.tap((queue) => { MutableHashMap.set(queueMap, key, queue) return Queue.offer(out, [key, fromQueue(queue)]) diff --git a/packages/effect/src/unstable/rpc/Rpc.ts b/packages/effect/src/unstable/rpc/Rpc.ts index dc59c4c24..59a185e2f 100644 --- a/packages/effect/src/unstable/rpc/Rpc.ts +++ b/packages/effect/src/unstable/rpc/Rpc.ts @@ -1,6 +1,7 @@ /** * @since 4.0.0 */ +import type * as Cause from "../../Cause.ts" import * as Predicate from "../../data/Predicate.ts" import type * as Struct from "../../data/Struct.ts" import type { Effect } from "../../Effect.ts" @@ -549,7 +550,7 @@ export type ResultFrom = R extends Rpc< Services > | Effect< - Queue.Dequeue<_SA["Type"], _SE["Type"] | _Error["Type"] | Queue.Done>, + Queue.Dequeue<_SA["Type"], _SE["Type"] | _Error["Type"] | Cause.Done>, _SE["Type"] | Schema.Schema.Type<_Error>, Services > : diff --git a/packages/effect/src/unstable/rpc/RpcClient.ts b/packages/effect/src/unstable/rpc/RpcClient.ts index 406af49f6..0564c1bf6 100644 --- a/packages/effect/src/unstable/rpc/RpcClient.ts +++ b/packages/effect/src/unstable/rpc/RpcClient.ts @@ -116,7 +116,7 @@ export declare namespace RpcClient { infer _Middleware, infer _Requires > ? [_Success] extends [RpcSchema.Stream] ? AsQueue extends true ? Effect.Effect< - Queue.Dequeue<_A["Type"], _E["Type"] | _Error["Type"] | E | _Middleware["error"]["Type"] | Queue.Done>, + Queue.Dequeue<_A["Type"], _E["Type"] | _Error["Type"] | E | _Middleware["error"]["Type"] | Cause.Done>, never, | Scope.Scope | _Payload["EncodingServices"] diff --git a/packages/effect/src/unstable/rpc/RpcGroup.ts b/packages/effect/src/unstable/rpc/RpcGroup.ts index 464e3c410..b1d4bb96d 100644 --- a/packages/effect/src/unstable/rpc/RpcGroup.ts +++ b/packages/effect/src/unstable/rpc/RpcGroup.ts @@ -1,6 +1,7 @@ /** * @since 4.0.0 */ +import type * as Cause from "../../Cause.ts" import type * as Record from "../../data/Record.ts" import * as Effect from "../../Effect.ts" import { identity } from "../../Function.ts" @@ -188,13 +189,13 @@ export type HandlerServices | Rpc.Wrapper> | Effect.Effect< - Queue.Dequeue, + Queue.Dequeue, infer _EX, infer _R > | Rpc.Wrapper< Effect.Effect< - Queue.Dequeue, + Queue.Dequeue, infer _EX, infer _R > diff --git a/packages/effect/src/unstable/rpc/RpcServer.ts b/packages/effect/src/unstable/rpc/RpcServer.ts index 472e9412e..580504c74 100644 --- a/packages/effect/src/unstable/rpc/RpcServer.ts +++ b/packages/effect/src/unstable/rpc/RpcServer.ts @@ -866,7 +866,7 @@ export const makeProtocolWithHttpEffect: Effect.Effect< isBinary ? Effect.map(request.arrayBuffer, (buf) => new Uint8Array(buf)) : request.text ) const id = clientId++ - const queue = yield* Queue.make() + const queue = yield* Queue.make() const parser = serialization.makeUnsafe() const offer = (data: Uint8Array | string) => @@ -934,7 +934,7 @@ export const makeProtocolWithHttpEffect: Effect.Effect< return HttpServerResponse.stream( Stream.fromArray(initialChunk).pipe( Stream.concat( - Stream.fromQueue(queue as Queue.Dequeue) + Stream.fromQueue(queue as Queue.Dequeue) ) ), { contentType: serialization.contentType } @@ -1088,7 +1088,7 @@ export const makeProtocolStdio = Effect.fnUntraced(function*() + const queue = yield* Queue.make() const parser = serialization.makeUnsafe() yield* options.stdin.pipe( diff --git a/packages/effect/src/unstable/socket/Socket.ts b/packages/effect/src/unstable/socket/Socket.ts index 115f72743..94c2866c0 100644 --- a/packages/effect/src/unstable/socket/Socket.ts +++ b/packages/effect/src/unstable/socket/Socket.ts @@ -1,6 +1,7 @@ /** * @since 4.0.0 */ +import type * as Cause from "../../Cause.ts" import type { NonEmptyReadonlyArray } from "../../collections/Array.ts" import * as Data from "../../data/Data.ts" import * as Filter from "../../data/Filter.ts" @@ -196,7 +197,7 @@ export const toChannelMap = ( IE > => Channel.fromTransform(Effect.fnUntraced(function*(upstream, scope) { - const queue = yield* Queue.make() + const queue = yield* Queue.make() const writeScope = yield* Scope.fork(scope) const write = yield* Scope.provide(self.writer, writeScope) diff --git a/packages/effect/test/Channel.test.ts b/packages/effect/test/Channel.test.ts index 2b8b88cfd..b2705f637 100644 --- a/packages/effect/test/Channel.test.ts +++ b/packages/effect/test/Channel.test.ts @@ -1,6 +1,7 @@ import { assert, describe, it } from "@effect/vitest" import { assertFailure, assertTrue } from "@effect/vitest/utils" import { Deferred, pipe, Ref } from "effect" +import type * as Cause from "effect/Cause" import * as Chunk from "effect/collections/Chunk" import * as Effect from "effect/Effect" import * as Exit from "effect/Exit" @@ -204,7 +205,7 @@ describe("Channel", () => { it.effect("merge - interrupts left side if halt strategy is set to 'right'", () => Effect.gen(function*() { const latch = yield* Effect.makeLatch(false) - const leftQueue = yield* Queue.make() + const leftQueue = yield* Queue.make() const rightQueue = yield* Queue.make() const left = Channel.fromQueue(rightQueue) const right = Channel.fromQueue(leftQueue).pipe( @@ -224,7 +225,7 @@ describe("Channel", () => { it.effect("merge - interrupts right side if halt strategy is set to 'left'", () => Effect.gen(function*() { const latch = yield* Effect.makeLatch(false) - const leftQueue = yield* Queue.make() + const leftQueue = yield* Queue.make() const rightQueue = yield* Queue.make() const left = Channel.fromQueue(leftQueue).pipe( Channel.ensuring(latch.open) diff --git a/packages/effect/test/Queue.test.ts b/packages/effect/test/Queue.test.ts index bee86a0a0..beee2af2d 100644 --- a/packages/effect/test/Queue.test.ts +++ b/packages/effect/test/Queue.test.ts @@ -155,7 +155,7 @@ describe("Queue", () => { assert.strictEqual(yield* Queue.take(queue), 1) assert.strictEqual(yield* Queue.take(queue), 2) assert.strictEqual(yield* Queue.take(queue), 3) - assert.strictEqual(Queue.isDone(yield* Queue.take(queue).pipe(Effect.flip)), true) + assert.strictEqual(Cause.isDone(yield* Queue.take(queue).pipe(Effect.flip)), true) assert.strictEqual(yield* Queue.await(queue), void 0) assert.strictEqual(yield* Queue.offer(queue, 10), false) })) diff --git a/packages/effect/test/cluster/TestEntity.ts b/packages/effect/test/cluster/TestEntity.ts index 90b88295f..cc31c2897 100644 --- a/packages/effect/test/cluster/TestEntity.ts +++ b/packages/effect/test/cluster/TestEntity.ts @@ -1,3 +1,4 @@ +import type { Cause } from "effect" import { Effect, Layer, MutableRef, Queue, Schedule, ServiceMap } from "effect" import { Schema } from "effect/schema" import { Stream } from "effect/stream" @@ -44,7 +45,7 @@ export const TestEntity = Entity.make("TestEntity", [ export class TestEntityState extends ServiceMap.Key()("TestEntityState", { make: Effect.gen(function*() { const messages = yield* Queue.make() - const streamMessages = yield* Queue.make() + const streamMessages = yield* Queue.make() const envelopes = yield* Queue.make< RpcGroup.Rpcs extends infer R ? R extends Rpc.Any ? Envelope.Request : never : never From 965939165c66f52a40980db2d7f615475241f127 Mon Sep 17 00:00:00 2001 From: Michael Arnaldi Date: Wed, 8 Oct 2025 12:20:45 +0200 Subject: [PATCH 20/20] fix(TxQueue): align clear implementation with Queue for halt cause handling --- packages/effect/src/Queue.ts | 9 +++++---- packages/effect/src/stm/TxQueue.ts | 27 +++++++++++++++++++-------- packages/effect/test/TxQueue.test.ts | 7 ++++--- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/packages/effect/src/Queue.ts b/packages/effect/src/Queue.ts index 70067ae10..54d6fd2fd 100644 --- a/packages/effect/src/Queue.ts +++ b/packages/effect/src/Queue.ts @@ -1073,7 +1073,7 @@ export const shutdown = (self: Queue): Effect => * @category taking * @since 4.0.0 */ -export const clear = (self: Dequeue): Effect, E> => +export const clear = (self: Dequeue): Effect, Pull.ExcludeHalt> => internalEffect.suspend(() => { if (self.state._tag === "Done") { if (Pull.isHaltCause(self.state.exit.cause)) { @@ -1135,7 +1135,7 @@ export const takeAll = (self: Dequeue): Effect, * @category taking * @since 4.0.0 */ -export const collect = (self: Dequeue): Effect, Exclude> => +export const collect = (self: Dequeue): Effect, Pull.ExcludeHalt> => internalEffect.suspend(() => { const out = Arr.empty() return internalEffect.as( @@ -1144,8 +1144,9 @@ export const collect = (self: Dequeue): Effect, Excl while: constTrue, body: constant(takeAll(self)), step(items: Arr.NonEmptyArray) { - // eslint-disable-next-line no-restricted-syntax - out.push(...items) + for (let i = 0; i < items.length; i++) { + out.push(items[i]) + } } }), () => internalEffect.void diff --git a/packages/effect/src/stm/TxQueue.ts b/packages/effect/src/stm/TxQueue.ts index 5676f230e..73da16374 100644 --- a/packages/effect/src/stm/TxQueue.ts +++ b/packages/effect/src/stm/TxQueue.ts @@ -20,6 +20,7 @@ import type { Inspectable } from "../interfaces/Inspectable.ts" import { NodeInspectSymbol, toJson } from "../interfaces/Inspectable.ts" import * as TxChunk from "../stm/TxChunk.ts" import * as TxRef from "../stm/TxRef.ts" +import { type ExcludeHalt, isHaltCause } from "../stream/Pull.ts" import type * as Types from "../types/Types.ts" /** @@ -1316,7 +1317,7 @@ export const end = (self: TxEnqueue): Effect.Effect(self: TxEnqueue): Effect.Effect(self: TxEnqueue): Effect.Effect> => - Effect.gen(function*() { - const chunk = yield* TxChunk.get(self.items) - yield* TxChunk.set(self.items, Chunk.empty()) - return Chunk.toArray(chunk) - }) +export const clear = (self: TxEnqueue): Effect.Effect, ExcludeHalt> => + Effect.atomic( + Effect.gen(function*() { + const state = yield* TxRef.get(self.stateRef) + if (state._tag === "Done") { + // Return empty array only for halt causes (like Cause.Done) + if (isHaltCause(state.cause)) { + return [] + } + return yield* Effect.failCause(state.cause) + } + const chunk = yield* TxChunk.get(self.items) + yield* TxChunk.set(self.items, Chunk.empty()) + return Chunk.toArray(chunk) + }) + ) /** * Shuts down the queue immediately by clearing all items and interrupting it (legacy compatibility). @@ -1389,7 +1400,7 @@ export const clear = (self: TxEnqueue): Effect.Effect> => export const shutdown = (self: TxEnqueue): Effect.Effect => Effect.atomic( Effect.gen(function*() { - yield* clear(self) + yield* Effect.ignore(clear(self)) return yield* interrupt(self) }) ) diff --git a/packages/effect/test/TxQueue.test.ts b/packages/effect/test/TxQueue.test.ts index 4d9ff586a..e79b7d23b 100644 --- a/packages/effect/test/TxQueue.test.ts +++ b/packages/effect/test/TxQueue.test.ts @@ -628,11 +628,11 @@ describe("TxQueue", () => { assert.strictEqual(isEmpty, true) })) - it.effect("clear works on closed queue", () => + it.effect("clear works on queue ended with Done", () => Effect.gen(function*() { - const queue = yield* TxQueue.bounded(10) + const queue = yield* TxQueue.bounded(10) yield* TxQueue.offerAll(queue, [1, 2, 3]) - yield* TxQueue.interrupt(queue) + yield* TxQueue.end(queue) // Take all items to move from Closing to Done state yield* TxQueue.takeAll(queue) @@ -641,6 +641,7 @@ describe("TxQueue", () => { const isDoneBefore = yield* TxQueue.isDone(queue) assert.strictEqual(isDoneBefore, true) + // clear() returns empty array for halt causes (like Done) const cleared = yield* TxQueue.clear(queue) assert.deepStrictEqual(cleared, [])