opencode-effect-enforcer 0.2.8 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/docs/effect-4.0.0-changelog.md +3213 -0
- package/docs/effect-4.0.0.md +110 -0
- package/guidance/effect-first-development.md +8 -6
- package/guidance/progressive-disclosure-guidance.md +15 -7
- package/package.json +6 -2
- package/patterns/avoid-any.md +2 -2
- package/patterns/avoid-direct-json.md +6 -6
- package/patterns/avoid-native-fetch.md +8 -6
- package/patterns/avoid-node-imports.md +2 -2
- package/patterns/avoid-non-null-assertion.md +2 -2
- package/patterns/avoid-object-type.md +2 -2
- package/patterns/avoid-platform-coupling.md +1 -1
- package/patterns/avoid-process-env.md +3 -4
- package/patterns/avoid-ts-ignore.md +1 -1
- package/patterns/context-tag-extends.md +11 -8
- package/patterns/effect-promise-vs-trypromise.md +6 -7
- package/patterns/prefer-arr-sort.md +1 -1
- package/patterns/prefer-effect-fn.md +21 -65
- package/patterns/prefer-schema-class.md +3 -3
- package/patterns/throw-in-effect-gen.md +1 -1
- package/patterns/use-clock-service.md +4 -0
- package/patterns/use-command-executor-service.md +2 -2
- package/patterns/use-http-client-service.md +8 -6
- package/patterns/use-random-service.md +6 -7
- package/skills/effect-ai-chat/SKILL.md +13 -7
- package/skills/effect-ai-decision-model/SKILL.md +301 -0
- package/skills/effect-ai-decision-model/openrouter.md +70 -0
- package/skills/effect-ai-language-model/SKILL.md +53 -21
- package/skills/effect-ai-prompt/SKILL.md +25 -14
- package/skills/effect-ai-provider/SKILL.md +53 -22
- package/skills/effect-ai-streaming/SKILL.md +27 -12
- package/skills/effect-ai-tool/SKILL.md +37 -28
- package/skills/effect-atom-rpc/SKILL.md +57 -36
- package/skills/effect-atom-state/SKILL.md +57 -19
- package/skills/effect-batching/SKILL.md +5 -3
- package/skills/effect-cache/SKILL.md +19 -7
- package/skills/effect-cli/SKILL.md +17 -8
- package/skills/effect-command-executor/SKILL.md +115 -64
- package/skills/effect-concurrency-testing/SKILL.md +26 -6
- package/skills/effect-config/SKILL.md +53 -2
- package/skills/effect-context-witness/SKILL.md +6 -6
- package/skills/effect-domain-modeling/SKILL.md +8 -1
- package/skills/effect-error-handling/SKILL.md +15 -2
- package/skills/effect-fiber/SKILL.md +20 -25
- package/skills/effect-filesystem/SKILL.md +69 -57
- package/skills/effect-http-api/SKILL.md +72 -22
- package/skills/effect-http-client/SKILL.md +25 -21
- package/skills/effect-http-server/SKILL.md +51 -21
- package/skills/effect-incremental-migration/SKILL.md +17 -8
- package/skills/effect-layer-design/SKILL.md +8 -0
- package/skills/effect-managed-runtime/SKILL.md +6 -0
- package/skills/effect-mcp-server/SKILL.md +64 -24
- package/skills/effect-observability/SKILL.md +61 -15
- package/skills/effect-parallelization/SKILL.md +24 -7
- package/skills/effect-path/SKILL.md +8 -2
- package/skills/effect-platform-abstraction/SKILL.md +88 -66
- package/skills/effect-platform-layers/SKILL.md +68 -67
- package/skills/effect-pubsub-event-bus/SKILL.md +56 -60
- package/skills/effect-react-composition/SKILL.md +19 -6
- package/skills/effect-rpc-api/SKILL.md +24 -24
- package/skills/effect-rpc-client/SKILL.md +33 -28
- package/skills/effect-rpc-cluster/SKILL.md +122 -78
- package/skills/effect-rpc-server/SKILL.md +56 -20
- package/skills/effect-scheduling/SKILL.md +29 -1
- package/skills/effect-schema-composition/SKILL.md +31 -13
- package/skills/effect-schema-v4/SKILL.md +94 -10
- package/skills/effect-scope/SKILL.md +13 -5
- package/skills/effect-service-implementation/SKILL.md +1 -1
- package/skills/effect-socket/SKILL.md +52 -8
- package/skills/effect-sql/SKILL.md +67 -33
- package/skills/effect-stream/SKILL.md +50 -5
- package/skills/effect-testing/SKILL.md +91 -2
- package/skills/effect-workflow/SKILL.md +76 -39
|
@@ -16,12 +16,15 @@ This skill provides patterns for testing Effect's concurrency primitives: fibers
|
|
|
16
16
|
| Simple fiber yield | `Effect.yieldNow` |
|
|
17
17
|
| Wait for subscriber ready | `Deferred.make()` + `Deferred.await` |
|
|
18
18
|
| Wait for stream element | `Latch.make()` + `Stream.tap(() => latch.open)` |
|
|
19
|
-
| Passive subscription registration |
|
|
19
|
+
| Passive subscription registration | acquire `PubSub.subscribe` before publishing, or expose a readiness signal |
|
|
20
20
|
| Time-dependent behavior | `TestClock.adjust` |
|
|
21
21
|
| Verify events published | `PubSub.subscribe` + `PubSub.takeUpTo` |
|
|
22
22
|
| Check fiber status | `fiber.pollUnsafe()` |
|
|
23
23
|
|
|
24
|
-
For `Stream.fromPubSub`
|
|
24
|
+
For `Stream.fromPubSub` registration, signal readiness after acquiring the actual
|
|
25
|
+
subscription, not merely before starting the stream. A scheduler yield is not a
|
|
26
|
+
general readiness guarantee; a sleep under `it.effect` also needs `TestClock`
|
|
27
|
+
advancement. Prefer a subscription-first test seam to arbitrary delays.
|
|
25
28
|
|
|
26
29
|
## Fiber Coordination Patterns
|
|
27
30
|
|
|
@@ -39,7 +42,7 @@ it.effect('fiber polling with yieldNow', () =>
|
|
|
39
42
|
|
|
40
43
|
const fiber = yield* latch.await.pipe(Effect.forkChild);
|
|
41
44
|
|
|
42
|
-
yield* Effect.yieldNow
|
|
45
|
+
yield* Effect.yieldNow;
|
|
43
46
|
|
|
44
47
|
expect(fiber.pollUnsafe()).toBeUndefined();
|
|
45
48
|
|
|
@@ -67,7 +70,7 @@ it.effect('latch coordination', () =>
|
|
|
67
70
|
return 'completed';
|
|
68
71
|
}).pipe(Effect.forkChild);
|
|
69
72
|
|
|
70
|
-
yield* Effect.yieldNow
|
|
73
|
+
yield* Effect.yieldNow;
|
|
71
74
|
expect(fiber.pollUnsafe()).toBeUndefined();
|
|
72
75
|
|
|
73
76
|
yield* latch.open;
|
|
@@ -436,6 +439,23 @@ it.effect('should run finalizers', () =>
|
|
|
436
439
|
|
|
437
440
|
## Interruption Testing
|
|
438
441
|
|
|
442
|
+
Test cancellation at the resource-owning boundary, using a `Deferred` to prove
|
|
443
|
+
work has started before interrupting it:
|
|
444
|
+
|
|
445
|
+
- Shared `ScopedCache` lookup: cancel one of two waiters and let the other
|
|
446
|
+
finish; then cancel the last waiter on another pending lookup and verify its
|
|
447
|
+
resource finalizer ran and a later `get` reacquires.
|
|
448
|
+
- `race` / `raceFirst` / iterable variants: assert loser cleanup is complete when
|
|
449
|
+
the race returns, including cancellation while contenders are starting.
|
|
450
|
+
- `Scope.close`: gate a finalizer, interrupt the closing fiber, release the gate,
|
|
451
|
+
and verify remaining finalizers still run.
|
|
452
|
+
- `ManagedRuntime.dispose`: verify request cleanup can use layer services before
|
|
453
|
+
those services are released.
|
|
454
|
+
- Queue batches: offer fewer than `takeN` requests, verify the taker remains
|
|
455
|
+
pending, then `end` or `fail` and assert the short final batch precedes the
|
|
456
|
+
terminal outcome. For PubSub, test the `end` value with `take`, not `takeUpTo`,
|
|
457
|
+
including a late subscriber and a full dropping buffer.
|
|
458
|
+
|
|
439
459
|
### Testing Fiber Interruption
|
|
440
460
|
|
|
441
461
|
```typescript
|
|
@@ -518,7 +538,7 @@ Effect.gen(function* () {
|
|
|
518
538
|
// GOOD - Use yieldNow for simple yielding
|
|
519
539
|
Effect.gen(function* () {
|
|
520
540
|
const fiber = yield* someEffect.pipe(Effect.forkChild);
|
|
521
|
-
yield* Effect.yieldNow
|
|
541
|
+
yield* Effect.yieldNow;
|
|
522
542
|
yield* Fiber.join(fiber);
|
|
523
543
|
});
|
|
524
544
|
```
|
|
@@ -538,7 +558,7 @@ while (fiber.pollUnsafe() === undefined) {
|
|
|
538
558
|
// GOOD - Yield between polls or use Fiber.await
|
|
539
559
|
Effect.gen(function* () {
|
|
540
560
|
while (fiber.pollUnsafe() === undefined) {
|
|
541
|
-
yield* Effect.yieldNow
|
|
561
|
+
yield* Effect.yieldNow;
|
|
542
562
|
}
|
|
543
563
|
});
|
|
544
564
|
|
|
@@ -89,9 +89,36 @@ const host = Config.String('HOST').pipe(
|
|
|
89
89
|
);
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
+
The fallback result replaces the original failure. If the fallback is absent,
|
|
93
|
+
an outer `withDefault` or `option` can recover it even when the primary input
|
|
94
|
+
was invalid. If the fallback fails, its error propagates. `Config.fail(error)`
|
|
95
|
+
has type `Config<never>` and does not widen a composed result to `unknown`.
|
|
96
|
+
|
|
97
|
+
### `Config.flatMap` — Select a Dependent Config
|
|
98
|
+
|
|
99
|
+
Use `map` for a pure value transformation, `mapEffect` for a transformation that
|
|
100
|
+
may fail with `ConfigError`, and `flatMap` when the parsed value selects another
|
|
101
|
+
`Config`. The selected config uses the same provider and nesting prefix; the
|
|
102
|
+
callback runs only after successful resolution of the first config.
|
|
103
|
+
|
|
104
|
+
<!-- typecheck -->
|
|
105
|
+
```ts
|
|
106
|
+
import { Config, ConfigProvider } from 'effect';
|
|
107
|
+
|
|
108
|
+
const host = Config.Literals(['development', 'production'], 'MODE').pipe(
|
|
109
|
+
Config.flatMap((mode) => Config.NonEmptyString(
|
|
110
|
+
mode === 'development' ? 'DEV_HOST' : 'PROD_HOST'
|
|
111
|
+
)),
|
|
112
|
+
Config.nested('APP')
|
|
113
|
+
);
|
|
114
|
+
const parsed = host.parse(ConfigProvider.fromUnknown({
|
|
115
|
+
APP: { MODE: 'production', PROD_HOST: 'api.example.com' }
|
|
116
|
+
}));
|
|
117
|
+
```
|
|
118
|
+
|
|
92
119
|
### `Config.all` — Combine Multiple Configs
|
|
93
120
|
|
|
94
|
-
Accepts a record or
|
|
121
|
+
Accepts a record, tuple, or iterable of configs:
|
|
95
122
|
|
|
96
123
|
```ts
|
|
97
124
|
// As a record
|
|
@@ -105,6 +132,28 @@ const appConfig = Config.all({
|
|
|
105
132
|
const pair = Config.all([Config.String('a'), Config.Int('b')]);
|
|
106
133
|
```
|
|
107
134
|
|
|
135
|
+
A group is absent when any child is absent and no child fails, even if other
|
|
136
|
+
children have supplied values. `Config.option(group)` then returns `None`;
|
|
137
|
+
`Config.withDefault(group, fallback)` replaces the **whole group**. Put defaults
|
|
138
|
+
on individual children to retain supplied siblings. Validation and source
|
|
139
|
+
errors still propagate. Use `Config.schema(Schema.Struct(...))` when an existing
|
|
140
|
+
but incomplete object must fail validation rather than default.
|
|
141
|
+
|
|
142
|
+
<!-- typecheck -->
|
|
143
|
+
```ts
|
|
144
|
+
import { Config, ConfigProvider, Effect } from 'effect';
|
|
145
|
+
|
|
146
|
+
const group = Config.all({
|
|
147
|
+
host: Config.String('HOST'),
|
|
148
|
+
port: Config.Int('PORT')
|
|
149
|
+
}).pipe(Config.withDefault({ host: 'localhost', port: 3000 }));
|
|
150
|
+
|
|
151
|
+
const result = Effect.runSync(group.parse(
|
|
152
|
+
ConfigProvider.fromUnknown({ HOST: 'supplied.example.com' })
|
|
153
|
+
));
|
|
154
|
+
// { host: 'localhost', port: 3000 } — the supplied host is replaced too.
|
|
155
|
+
```
|
|
156
|
+
|
|
108
157
|
### `Config.nested` — Scope Under a Prefix
|
|
109
158
|
|
|
110
159
|
Prepends a path segment to every key the inner config reads. With environment variables, nesting uses `_` as separator.
|
|
@@ -346,7 +395,9 @@ const defaults = ConfigProvider.fromUnknown({
|
|
|
346
395
|
const combined = ConfigProvider.orElse(envProvider, defaults);
|
|
347
396
|
```
|
|
348
397
|
|
|
349
|
-
At the `Config` level, `Config.orElse`
|
|
398
|
+
At the `Config` level, `Config.orElse` handles missing data and all `ConfigError`s;
|
|
399
|
+
its fallback determines whether an outer default or option can recover. Provider
|
|
400
|
+
fallback only chooses a source and does not perform that error recovery.
|
|
350
401
|
|
|
351
402
|
### `ConfigProvider.nested` — Prefix All Lookups
|
|
352
403
|
|
|
@@ -36,7 +36,7 @@ export const PaymentIntent = Schema.Struct({
|
|
|
36
36
|
Field **removed from schema**, only injected in code:
|
|
37
37
|
|
|
38
38
|
```typescript
|
|
39
|
-
import { Schema, Context, Effect
|
|
39
|
+
import { Schema, Context, Effect } from 'effect';
|
|
40
40
|
|
|
41
41
|
declare const generateId: () => string;
|
|
42
42
|
|
|
@@ -56,7 +56,7 @@ const createPaymentIntent = (amount: bigint) =>
|
|
|
56
56
|
|
|
57
57
|
// Use serial in business logic, logging, etc.
|
|
58
58
|
// but it's not part of the persisted data
|
|
59
|
-
yield*
|
|
59
|
+
yield* Effect.logInfo(`Creating payment intent ${serial}`);
|
|
60
60
|
|
|
61
61
|
return PaymentIntent.make({ id: generateId(), amount });
|
|
62
62
|
});
|
|
@@ -177,7 +177,7 @@ Good fits:
|
|
|
177
177
|
Witnesses are trivial to provide:
|
|
178
178
|
|
|
179
179
|
```typescript
|
|
180
|
-
import { Effect } from 'effect';
|
|
180
|
+
import { Context, Effect } from 'effect';
|
|
181
181
|
|
|
182
182
|
declare const myProgram: Effect.Effect<unknown, never, Serial>;
|
|
183
183
|
declare class Serial extends Context.Service<Serial, string>()('Serial') {}
|
|
@@ -188,7 +188,7 @@ const test = myProgram.pipe(Effect.provideService(Serial, 'test-serial-123'));
|
|
|
188
188
|
Capabilities need implementation:
|
|
189
189
|
|
|
190
190
|
```typescript
|
|
191
|
-
import { Effect } from 'effect';
|
|
191
|
+
import { Context, Effect } from 'effect';
|
|
192
192
|
|
|
193
193
|
declare const myProgram: Effect.Effect<unknown, never, SerialService>;
|
|
194
194
|
declare class SerialService extends Context.Service<
|
|
@@ -217,7 +217,7 @@ const test = myProgram.pipe(
|
|
|
217
217
|
- **Yes** → Keep in schema
|
|
218
218
|
|
|
219
219
|
```typescript
|
|
220
|
-
import { Schema, Context, Effect,
|
|
220
|
+
import { Schema, Context, Effect, Clock } from 'effect';
|
|
221
221
|
|
|
222
222
|
declare const LineItem: Schema.Schema<any>;
|
|
223
223
|
declare const generateId: () => string;
|
|
@@ -245,7 +245,7 @@ const createOrder = (items: Array<Schema.Schema.Type<typeof LineItem>>) =>
|
|
|
245
245
|
const requestId = yield* RequestId; // For logging
|
|
246
246
|
const timestamp = yield* Clock.currentTimeMillis; // For timestamp
|
|
247
247
|
|
|
248
|
-
yield*
|
|
248
|
+
yield* Effect.logInfo({
|
|
249
249
|
message: 'Creating order',
|
|
250
250
|
correlationId, // Used for tracing
|
|
251
251
|
requestId, // Used for logging
|
|
@@ -13,7 +13,7 @@ and `effect-typeclass-design` for reusable predicate/order APIs.
|
|
|
13
13
|
|
|
14
14
|
## Source Reference
|
|
15
15
|
|
|
16
|
-
Baseline: **Effect 4.0.0
|
|
16
|
+
Baseline: **Effect 4.0.0**. In the Effect source reference, consult
|
|
17
17
|
`packages/effect/SCHEMA.md` and `packages/effect/src/{Schema,Match,DateTime,Order}.ts`.
|
|
18
18
|
Verify the installed version and source tag before applying newer APIs.
|
|
19
19
|
|
|
@@ -38,6 +38,13 @@ when encoded values or service requirements matter; do not erase them with `any`
|
|
|
38
38
|
|
|
39
39
|
## Complete Model: Task Lifecycle
|
|
40
40
|
|
|
41
|
+
`Schema.brand` is a type-only distinction with no runtime AST metadata or extra
|
|
42
|
+
checks. Each call takes one concrete string literal (not a widened string,
|
|
43
|
+
union, or open template); compose brands with repeated calls. A brand does not
|
|
44
|
+
prove a domain invariant unless the underlying schema checks it. Representations
|
|
45
|
+
and generated schema code omit brands, so reapply them after rebuilding a model.
|
|
46
|
+
`Schema.fromBrand` preserves constructor checks and requires its sole brand key.
|
|
47
|
+
|
|
41
48
|
This module demonstrates brands, class variants, exhaustive/partial matching,
|
|
42
49
|
schema guards, equivalence, orders, dual helpers, and legal transitions.
|
|
43
50
|
|
|
@@ -89,7 +89,7 @@ Reference this for:
|
|
|
89
89
|
|
|
90
90
|
## Core Error Handling Philosophy
|
|
91
91
|
|
|
92
|
-
Effect distinguishes
|
|
92
|
+
Effect distinguishes expected errors from defects:
|
|
93
93
|
|
|
94
94
|
1. **Expected Errors (Error Channel)** - Business logic failures that should be handled
|
|
95
95
|
- Type-safe and tracked in the effect signature: `Effect<A, E, R>`
|
|
@@ -101,6 +101,12 @@ Effect distinguishes between two types of failures:
|
|
|
101
101
|
- Result from programming mistakes (null refs, unhandled cases, assertions)
|
|
102
102
|
- Usually should NOT be caught; use catchDefect only at boundaries
|
|
103
103
|
|
|
104
|
+
Interruption is cancellation, a separate `Cause` reason rather than a typed
|
|
105
|
+
business error. A cause can retain several reasons: if work fails and its
|
|
106
|
+
finalizer throws, both the original failure and the finalizer defect remain.
|
|
107
|
+
Inspect the full `Cause` at supervision boundaries rather than assuming one
|
|
108
|
+
reason or discarding cancellation during cleanup.
|
|
109
|
+
|
|
104
110
|
### Runtime Adapter Boundaries and Invariants
|
|
105
111
|
|
|
106
112
|
Do not force every impossible or adapter-internal failure into a tagged error just to satisfy a blanket rule.
|
|
@@ -1047,6 +1053,13 @@ The `instanceof` guard prevents double-wrapping when an upstream operation alrea
|
|
|
1047
1053
|
|
|
1048
1054
|
## Error Recovery Patterns
|
|
1049
1055
|
|
|
1056
|
+
For per-item typed outcomes, `Effect.partition(items, work, { concurrency })`
|
|
1057
|
+
returns `[successes, failures]`, in that order. Both arrays preserve input order.
|
|
1058
|
+
`Effect.all(..., { mode: 'result' })` instead preserves the input structure with
|
|
1059
|
+
one `Result` per slot. Their `never` error channel means typed failures are
|
|
1060
|
+
collected; defects and interruption still propagate. Use `Effect.validate` when
|
|
1061
|
+
any item failure must fail the entire batch with all typed errors.
|
|
1062
|
+
|
|
1050
1063
|
Retry timing and recurrence design belong in the dedicated effect-scheduling skill. This skill determines which failures are typed and recoverable; scheduling determines whether, when, and how often an idempotent operation is retried.
|
|
1051
1064
|
|
|
1052
1065
|
### Translate at Service Boundaries
|
|
@@ -1388,7 +1401,7 @@ Model ambiguous HTTP responses (where the body structure differs for success vs
|
|
|
1388
1401
|
|
|
1389
1402
|
```typescript
|
|
1390
1403
|
import { Effect, Schema } from 'effect';
|
|
1391
|
-
import { HttpClientResponse } from 'effect/
|
|
1404
|
+
import { HttpClientResponse } from 'effect/http';
|
|
1392
1405
|
|
|
1393
1406
|
class TokenSuccess extends Schema.Class<TokenSuccess>('TokenSuccess')({
|
|
1394
1407
|
access_token: AccessToken,
|
|
@@ -346,7 +346,7 @@ const detached = Effect.gen(function* () {
|
|
|
346
346
|
|
|
347
347
|
Whichever lifetime you pick, **a forked fiber's failure is observed by nobody unless you arrange it**: `join`/`await` the fiber, supervise it via `FiberHandle`/`FiberMap`/`FiberSet` `join`, or attach `Effect.catchCause`/`Effect.onError` plus logging inside the forked effect. v4 removed `Effect.forkWithErrorHandler`, and the runtime does not log unhandled fiber failures.
|
|
348
348
|
|
|
349
|
-
`Effect.awaitAllChildren` only waits for children forked while the wrapped effect runs — children that existed beforehand are not awaited.
|
|
349
|
+
`Effect.awaitAllChildren` only waits for children forked while the wrapped effect runs — children that existed beforehand are not awaited. The child wait respects the surrounding interruptibility. If interrupted while waiting after the wrapped effect failed, the resulting cause retains both the original failure and interruption; an enclosing uninterruptible region keeps the wait uninterruptible.
|
|
350
350
|
|
|
351
351
|
`forkIn`/`forkScoped` register an interruption finalizer on the scope and remove it when the fiber completes on its own. Forking into an already-closed scope interrupts the new fiber immediately. The same applies to `Fiber.runIn(fiber, scope)`, which only registers the finalizer — it does not wait for the fiber.
|
|
352
352
|
|
|
@@ -380,7 +380,7 @@ const program = Effect.gen(function* () {
|
|
|
380
380
|
}).pipe(Effect.scoped);
|
|
381
381
|
```
|
|
382
382
|
|
|
383
|
-
`run` options: `{ onlyIfMissing?: boolean; propagateInterruption?: boolean }`.
|
|
383
|
+
`run` options: `{ onlyIfMissing?: boolean; propagateInterruption?: boolean; startImmediately?: boolean }`. Startup is immediate by default; `startImmediately: false` defers it (see "How collection fibers relate to the caller" in section 8).
|
|
384
384
|
|
|
385
385
|
### join vs awaitEmpty
|
|
386
386
|
|
|
@@ -462,7 +462,7 @@ const program = Effect.gen(function* () {
|
|
|
462
462
|
}).pipe(Effect.scoped);
|
|
463
463
|
```
|
|
464
464
|
|
|
465
|
-
`run` options are the same as FiberHandle's: `{ onlyIfMissing?, propagateInterruption? }
|
|
465
|
+
`run` options are the same as FiberHandle's: `{ onlyIfMissing?, propagateInterruption?, startImmediately? }`. Startup defaults to immediate; `false` defers it. `set`/`setUnsafe` install existing fibers under a key with `{ onlyIfMissing?, propagateInterruption? }`.
|
|
466
466
|
|
|
467
467
|
Runtime helpers take the key as the first runner argument:
|
|
468
468
|
|
|
@@ -482,7 +482,7 @@ Closed-map behavior matches FiberHandle: `FiberMap.run` interrupts the caller; r
|
|
|
482
482
|
|
|
483
483
|
## 8. FiberSet — Grow-Only Fiber Collections
|
|
484
484
|
|
|
485
|
-
`FiberSet<A, E>` tracks an unkeyed set of fibers. No replacement semantics — every `run`/`add` grows the set; completed fibers remove themselves; scope close interrupts all. Nothing limits admission
|
|
485
|
+
`FiberSet<A, E>` tracks an unkeyed set of fibers. No replacement semantics — every `run`/`add` grows the set; completed fibers remove themselves; scope close interrupts all. Nothing limits admission. For a finite work list, prefer bounded `Effect.forEach`; for ongoing ingestion, use a bounded queue with a fixed set of workers.
|
|
486
486
|
|
|
487
487
|
```ts
|
|
488
488
|
const program = Effect.gen(function* () {
|
|
@@ -504,7 +504,7 @@ const program = Effect.gen(function* () {
|
|
|
504
504
|
}).pipe(Effect.scoped);
|
|
505
505
|
```
|
|
506
506
|
|
|
507
|
-
`run` options: `{ propagateInterruption? }` (no `onlyIfMissing` — there is no key
|
|
507
|
+
`run` options: `{ propagateInterruption?, startImmediately? }` (no `onlyIfMissing` — there is no key). Startup defaults to immediate; `false` defers it. On a closed set, `FiberSet.run` returns an already-interrupted fiber (it does **not** interrupt the caller, unlike FiberHandle/FiberMap).
|
|
508
508
|
|
|
509
509
|
Runtime helpers mirror the others in shape. `FiberSet.runtime` and `runtimePromise` forward `propagateInterruption` when registering the managed fiber:
|
|
510
510
|
|
|
@@ -519,11 +519,14 @@ const runPromise2 = yield* FiberSet.makeRuntimePromise();
|
|
|
519
519
|
|
|
520
520
|
### How collection fibers relate to the caller
|
|
521
521
|
|
|
522
|
-
Fibers forked via `FiberHandle/FiberMap/FiberSet.run`
|
|
522
|
+
Fibers forked via `FiberHandle/FiberMap/FiberSet.run` carry the caller's context but are **detached from its child lifetime** and owned by the collection. Consequences:
|
|
523
523
|
|
|
524
|
-
-
|
|
524
|
+
- `run` starts them **immediately** by default; pass `{ startImmediately: false }` to defer startup. This differs from `Effect.forkChild`, whose default is deferred.
|
|
525
525
|
- The calling fiber's completion does not interrupt them — only key replacement, `remove`/`clear`, or the collection's scope close does.
|
|
526
526
|
|
|
527
|
+
The captured `runtime()` runners instead use `Effect.runForkWith` at the callback
|
|
528
|
+
boundary. Those runners start synchronously and do not expose `startImmediately`.
|
|
529
|
+
|
|
527
530
|
---
|
|
528
531
|
|
|
529
532
|
## 9. Running Fibers at the Program Edge
|
|
@@ -647,27 +650,19 @@ const webhookProcessor = Effect.gen(function* () {
|
|
|
647
650
|
|
|
648
651
|
To fail fast when any background task crashes, run the service's main loop against `FiberSet.join(tasks)` (it fails with the first non-interruption failure).
|
|
649
652
|
|
|
650
|
-
### Bounded
|
|
653
|
+
### Bounded work lists
|
|
651
654
|
|
|
652
|
-
`FiberSet` never applies backpressure on its own.
|
|
655
|
+
`FiberSet` never applies backpressure on its own. For a work list, let
|
|
656
|
+
`Effect.forEach` own both admission and cleanup. A manual `Semaphore.take`
|
|
657
|
+
followed by `FiberSet.run` has an interruption gap, and a closed collection or a
|
|
658
|
+
fiber interrupted before startup may never install the intended release handler.
|
|
653
659
|
|
|
654
660
|
```ts
|
|
655
661
|
const boundedProcessor = Effect.gen(function* () {
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
yield* Semaphore.take(permits, 1); // waits while 16 tasks are in flight
|
|
661
|
-
yield* FiberSet.run(
|
|
662
|
-
tasks,
|
|
663
|
-
handleWebhook(event).pipe(
|
|
664
|
-
// runs on success, failure, AND interruption — permits never leak
|
|
665
|
-
Effect.ensuring(Semaphore.release(permits, 1))
|
|
666
|
-
)
|
|
667
|
-
);
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
yield* FiberSet.awaitEmpty(tasks);
|
|
662
|
+
yield* Effect.forEach(events, handleWebhook, {
|
|
663
|
+
concurrency: 16,
|
|
664
|
+
discard: true
|
|
665
|
+
});
|
|
671
666
|
}).pipe(Effect.scoped);
|
|
672
667
|
```
|
|
673
668
|
|
|
@@ -723,7 +718,7 @@ const handoff = Effect.gen(function* () {
|
|
|
723
718
|
9. **Expecting external interruption to fail `join`** — by default it doesn't. Pass `{ propagateInterruption: true }` to `run`/`set`/`add`; the collection's own internal interruptions (replacement, `clear`, scope close) never fail `join` either way.
|
|
724
719
|
10. **Expecting `onlyIfMissing: true` to error when occupied** — it succeeds, returning a shared already-interrupted fiber while keeping the existing one. Check `Exit.hasInterrupts(yield* Fiber.await(fiber))` to detect the rejected start.
|
|
725
720
|
11. **Calling `run` on a closed collection** — `FiberHandle.run`/`FiberMap.run` interrupt the *calling* fiber; `FiberSet.run` and all `runtime()` runners return a pre-interrupted fiber instead. Neither throws.
|
|
726
|
-
12. **Assuming collection fibers are children of the caller** — they
|
|
721
|
+
12. **Assuming collection fibers are children of the caller** — they carry the caller's context but survive its completion; the collection owns their lifetime. `run` starts immediately by default and honors `startImmediately: false`; callback runtime runners start synchronously.
|
|
727
722
|
13. **Relying on `Effect.runFork`/`runPromise` to keep Node alive** — a fiber suspended on `Deferred.await`/`Effect.never` won't hold the process open. Use `NodeRuntime.runMain` (built on `Runtime.makeRunMain`).
|
|
728
723
|
14. **Letting interruption leak through acquire/release** — wrap the whole sequence in `Effect.uninterruptibleMask` and `restore` only the use phase; pending interruption is delivered as soon as the region ends, so cleanup still runs exactly once.
|
|
729
724
|
15. **Leaving collection type parameters off** — `FiberHandle.make()` defaults to `<unknown, unknown>`, making `join` surface `unknown` errors. Always pass them: `FiberHandle.make<A, E>()`, `FiberMap.make<K, A, E>()`, `FiberSet.make<A, E>()`.
|
|
@@ -7,6 +7,8 @@ description: Use Effect FileSystem for platform-abstract file I/O with Node.js/B
|
|
|
7
7
|
|
|
8
8
|
Use `effect` FileSystem for platform-abstract file I/O. Stock layers are provided for Node.js and Bun; `@effect/platform-browser` does not provide a FileSystem layer, so browser code needs a custom/injected implementation.
|
|
9
9
|
|
|
10
|
+
Targets **Effect 4.0.0**. Match platform package versions to `effect`; verify APIs against the `effect@4.0.0` source tag rather than unreleased `main`.
|
|
11
|
+
|
|
10
12
|
## Basic Pattern
|
|
11
13
|
|
|
12
14
|
```typescript
|
|
@@ -195,6 +197,7 @@ const removeDir = Effect.gen(function* () {
|
|
|
195
197
|
|
|
196
198
|
### Open File Handle
|
|
197
199
|
|
|
200
|
+
<!-- typecheck -->
|
|
198
201
|
```typescript
|
|
199
202
|
import { FileSystem } from 'effect';
|
|
200
203
|
import { Effect } from 'effect';
|
|
@@ -265,36 +268,38 @@ const createDir = Effect.gen(function* () {
|
|
|
265
268
|
});
|
|
266
269
|
```
|
|
267
270
|
|
|
268
|
-
### Make Temp Directory
|
|
271
|
+
### Make Temp Directory with Explicit Ownership
|
|
269
272
|
|
|
273
|
+
<!-- typecheck -->
|
|
270
274
|
```typescript
|
|
271
|
-
import { FileSystem } from 'effect';
|
|
272
|
-
import { Effect } from 'effect';
|
|
275
|
+
import { Effect, FileSystem, Path } from 'effect';
|
|
273
276
|
|
|
274
277
|
const useTempDir = Effect.gen(function* () {
|
|
275
278
|
const fs = yield* FileSystem.FileSystem;
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
yield* fs.remove(tempPath, { recursive: true });
|
|
279
|
+
const path = yield* Path.Path;
|
|
280
|
+
return yield* Effect.acquireUseRelease(
|
|
281
|
+
fs.makeTempDirectory(),
|
|
282
|
+
(tempPath) => fs.writeFileString(path.join(tempPath, 'temp-file.txt'), 'data'),
|
|
283
|
+
(tempPath) => fs.remove(tempPath, { recursive: true }).pipe(Effect.orDie)
|
|
284
|
+
);
|
|
283
285
|
});
|
|
284
286
|
```
|
|
285
287
|
|
|
288
|
+
Release runs on success, failure, and interruption. Cleanup failure is surfaced as a defect here; prefer the built-in scoped variant for ordinary temporary work.
|
|
289
|
+
|
|
286
290
|
### Make Temp Directory (Scoped)
|
|
287
291
|
|
|
292
|
+
<!-- typecheck -->
|
|
288
293
|
```typescript
|
|
289
|
-
import { FileSystem } from 'effect';
|
|
290
|
-
import { Effect } from 'effect';
|
|
294
|
+
import { Effect, FileSystem, Path } from 'effect';
|
|
291
295
|
|
|
292
296
|
const useScopedTempDir = Effect.gen(function* () {
|
|
293
297
|
const fs = yield* FileSystem.FileSystem;
|
|
298
|
+
const path = yield* Path.Path;
|
|
294
299
|
const tempPath = yield* fs.makeTempDirectoryScoped();
|
|
295
300
|
|
|
296
301
|
// Use tempPath within scope
|
|
297
|
-
yield* fs.writeFileString(
|
|
302
|
+
yield* fs.writeFileString(path.join(tempPath, 'temp-file.txt'), 'data');
|
|
298
303
|
|
|
299
304
|
// Automatically cleaned up when scope exits
|
|
300
305
|
}).pipe(Effect.scoped);
|
|
@@ -304,6 +309,7 @@ const useScopedTempDir = Effect.gen(function* () {
|
|
|
304
309
|
|
|
305
310
|
### Stat (File Info)
|
|
306
311
|
|
|
312
|
+
<!-- typecheck -->
|
|
307
313
|
```typescript
|
|
308
314
|
import { FileSystem } from 'effect';
|
|
309
315
|
import { Effect, Console } from 'effect';
|
|
@@ -395,14 +401,15 @@ const changeOwner = Effect.gen(function* () {
|
|
|
395
401
|
|
|
396
402
|
### Update Times (utimes)
|
|
397
403
|
|
|
404
|
+
<!-- typecheck -->
|
|
398
405
|
```typescript
|
|
399
|
-
import { FileSystem } from 'effect';
|
|
400
|
-
import { Effect } from 'effect';
|
|
406
|
+
import { Clock, Effect, FileSystem } from 'effect';
|
|
401
407
|
|
|
402
408
|
const updateTimes = Effect.gen(function* () {
|
|
403
409
|
const fs = yield* FileSystem.FileSystem;
|
|
404
|
-
const
|
|
405
|
-
|
|
410
|
+
const nowMillis = yield* Clock.currentTimeMillis;
|
|
411
|
+
// Numeric utimes inputs are epoch seconds, not milliseconds.
|
|
412
|
+
yield* fs.utimes('file.txt', nowMillis / 1000, nowMillis / 1000);
|
|
406
413
|
});
|
|
407
414
|
```
|
|
408
415
|
|
|
@@ -436,6 +443,7 @@ const createSymlink = Effect.gen(function* () {
|
|
|
436
443
|
|
|
437
444
|
### Watch Files/Directories
|
|
438
445
|
|
|
446
|
+
<!-- typecheck -->
|
|
439
447
|
```typescript
|
|
440
448
|
import { FileSystem } from 'effect';
|
|
441
449
|
import { Effect, Stream, Console, pipe } from 'effect';
|
|
@@ -466,28 +474,31 @@ Directory watching is non-recursive by default. Pass `{ recursive: true }` to in
|
|
|
466
474
|
|
|
467
475
|
## Size Helpers
|
|
468
476
|
|
|
477
|
+
<!-- typecheck -->
|
|
469
478
|
```typescript
|
|
470
|
-
import { FileSystem } from 'effect';
|
|
471
|
-
import
|
|
472
|
-
|
|
473
|
-
const { Size, KiB, MiB, GiB, TiB, PiB } = FileSystem;
|
|
479
|
+
import { ByteSize, Effect, FileSystem } from 'effect';
|
|
480
|
+
import * as Schema from 'effect/Schema';
|
|
474
481
|
|
|
475
482
|
// Create size values
|
|
476
|
-
const oneKb =
|
|
477
|
-
const tenKb =
|
|
478
|
-
const oneMb =
|
|
479
|
-
const fiveGb =
|
|
480
|
-
const oneTb =
|
|
481
|
-
const onePb =
|
|
483
|
+
const oneKb = ByteSize.bytes(1024);
|
|
484
|
+
const tenKb = ByteSize.kibibytes(10);
|
|
485
|
+
const oneMb = ByteSize.mebibytes(1);
|
|
486
|
+
const fiveGb = ByteSize.gibibytes(5);
|
|
487
|
+
const oneTb = ByteSize.tebibytes(1);
|
|
488
|
+
const onePb = ByteSize.pebibytes(1);
|
|
489
|
+
|
|
490
|
+
class FileTooLarge extends Schema.TaggedError<FileTooLarge>()('FileTooLarge', {
|
|
491
|
+
path: Schema.String
|
|
492
|
+
}) {}
|
|
482
493
|
|
|
483
494
|
// Use with file operations
|
|
484
495
|
const checkFileSize = Effect.gen(function* () {
|
|
485
496
|
const fs = yield* FileSystem.FileSystem;
|
|
486
497
|
const info = yield* fs.stat('large-file.bin');
|
|
487
498
|
|
|
488
|
-
const maxSize =
|
|
499
|
+
const maxSize = ByteSize.mebibytes(100);
|
|
489
500
|
if (info.size > maxSize) {
|
|
490
|
-
yield* Effect.fail(new
|
|
501
|
+
return yield* Effect.fail(new FileTooLarge({ path: 'large-file.bin' }));
|
|
491
502
|
}
|
|
492
503
|
});
|
|
493
504
|
```
|
|
@@ -496,7 +507,7 @@ const checkFileSize = Effect.gen(function* () {
|
|
|
496
507
|
|
|
497
508
|
### SystemErrorTag Values
|
|
498
509
|
|
|
499
|
-
FileSystem operations fail with `PlatformError`
|
|
510
|
+
FileSystem operations fail with `PlatformError`. Its `reason` is `BadArgument` or `SystemError`. In **4.0.0**, `SystemError._tag` is the normalized category below; match `error.reason._tag` directly (there is no `reason.tag` field):
|
|
500
511
|
|
|
501
512
|
- `AlreadyExists` - File/directory already exists
|
|
502
513
|
- `BadResource` - Invalid file descriptor or handle
|
|
@@ -510,8 +521,11 @@ FileSystem operations fail with `PlatformError` containing a `SystemErrorTag`:
|
|
|
510
521
|
- `WouldBlock` - Operation would block
|
|
511
522
|
- `WriteZero` - Write operation wrote zero bytes
|
|
512
523
|
|
|
524
|
+
For an unknown error at a boundary, `PlatformError.isPlatformError(value)` narrows it to the wrapper before inspecting `reason`.
|
|
525
|
+
|
|
513
526
|
### Error Handling Pattern
|
|
514
527
|
|
|
528
|
+
<!-- typecheck -->
|
|
515
529
|
```typescript
|
|
516
530
|
import { FileSystem } from 'effect';
|
|
517
531
|
import { Effect, pipe } from 'effect';
|
|
@@ -525,11 +539,7 @@ const readConfigWithFallback = pipe(
|
|
|
525
539
|
if (error.reason._tag === 'NotFound') {
|
|
526
540
|
return Effect.succeed('{}');
|
|
527
541
|
}
|
|
528
|
-
|
|
529
|
-
return Effect.fail(
|
|
530
|
-
new Error('Cannot read config: permission denied')
|
|
531
|
-
);
|
|
532
|
-
}
|
|
542
|
+
// Permission and other failures remain visible to the caller.
|
|
533
543
|
return Effect.fail(error);
|
|
534
544
|
})
|
|
535
545
|
);
|
|
@@ -537,6 +547,7 @@ const readConfigWithFallback = pipe(
|
|
|
537
547
|
|
|
538
548
|
### Typed Error Recovery
|
|
539
549
|
|
|
550
|
+
<!-- typecheck -->
|
|
540
551
|
```typescript
|
|
541
552
|
import { FileSystem } from 'effect';
|
|
542
553
|
import { Effect, Schema, pipe } from 'effect';
|
|
@@ -548,50 +559,51 @@ class ConfigNotFound extends Schema.TaggedError<ConfigNotFound>()(
|
|
|
548
559
|
}
|
|
549
560
|
) {}
|
|
550
561
|
|
|
551
|
-
class
|
|
552
|
-
'
|
|
562
|
+
class ConfigReadFailed extends Schema.TaggedError<ConfigReadFailed>()(
|
|
563
|
+
'ConfigReadFailed',
|
|
553
564
|
{
|
|
554
565
|
path: Schema.String,
|
|
555
566
|
reason: Schema.String
|
|
556
567
|
}
|
|
557
568
|
) {}
|
|
558
569
|
|
|
559
|
-
const readConfig = (path: string)
|
|
560
|
-
|
|
561
|
-
const fs = yield* FileSystem.FileSystem;
|
|
570
|
+
const readConfig = Effect.fn('Config.read')(function* (path: string) {
|
|
571
|
+
const fs = yield* FileSystem.FileSystem;
|
|
562
572
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
573
|
+
const content = yield* pipe(
|
|
574
|
+
fs.readFileString(path),
|
|
575
|
+
Effect.mapError((error) =>
|
|
576
|
+
error.reason._tag === 'NotFound'
|
|
577
|
+
? new ConfigNotFound({ path })
|
|
578
|
+
: new ConfigReadFailed({ path, reason: error.message })
|
|
579
|
+
)
|
|
580
|
+
);
|
|
571
581
|
|
|
572
|
-
|
|
573
|
-
|
|
582
|
+
return content;
|
|
583
|
+
});
|
|
574
584
|
```
|
|
575
585
|
|
|
576
586
|
## Scoped Resources Pattern
|
|
577
587
|
|
|
588
|
+
<!-- typecheck -->
|
|
578
589
|
```typescript
|
|
579
|
-
import { FileSystem } from 'effect';
|
|
580
|
-
import
|
|
590
|
+
import { Effect, FileSystem, Path } from 'effect';
|
|
591
|
+
import * as Str from 'effect/String';
|
|
581
592
|
|
|
582
593
|
const processInTempDir = Effect.gen(function* () {
|
|
583
594
|
const fs = yield* FileSystem.FileSystem;
|
|
595
|
+
const path = yield* Path.Path;
|
|
584
596
|
|
|
585
597
|
// Create temp directory with automatic cleanup
|
|
586
598
|
const tempDir = yield* fs.makeTempDirectoryScoped();
|
|
587
599
|
|
|
588
600
|
// Do work in temp directory
|
|
589
|
-
const inputPath =
|
|
590
|
-
const outputPath =
|
|
601
|
+
const inputPath = path.join(tempDir, 'input.txt');
|
|
602
|
+
const outputPath = path.join(tempDir, 'output.txt');
|
|
591
603
|
|
|
592
604
|
yield* fs.writeFileString(inputPath, 'data');
|
|
593
605
|
const content = yield* fs.readFileString(inputPath);
|
|
594
|
-
yield* fs.writeFileString(outputPath,
|
|
606
|
+
yield* fs.writeFileString(outputPath, Str.toUpperCase(content));
|
|
595
607
|
|
|
596
608
|
const result = yield* fs.readFileString(outputPath);
|
|
597
609
|
|
|
@@ -638,10 +650,10 @@ Effect.runPromise(runnable);
|
|
|
638
650
|
|
|
639
651
|
- Import from `effect`
|
|
640
652
|
- Use `yield* FileSystem.FileSystem` for service injection
|
|
641
|
-
- Provide platform
|
|
653
|
+
- Provide platform layers at entry points or runtime-facing adapter `defaultLayer` exports
|
|
642
654
|
- Use scoped temp directories with `makeTempDirectoryScoped`
|
|
643
655
|
- Handle `PlatformError` with `catchTag("PlatformError", ...)`
|
|
644
|
-
- Use
|
|
656
|
+
- Use `ByteSize.bytes`, `ByteSize.kibibytes`, `ByteSize.mebibytes`, and other ByteSize constructors; the old FileSystem size helpers are removed
|
|
645
657
|
- Stream large files with `stream()` and `sink()`
|
|
646
658
|
|
|
647
659
|
## DON'T
|