opencode-effect-enforcer 0.2.2 → 0.2.4
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 +38 -10
- package/docs/effect-4.0.0-rc.112.md +316 -0
- package/guidance/effect-first-development.md +30 -17
- package/guidance/progressive-disclosure-guidance.md +13 -0
- package/package.json +3 -2
- package/patterns/avoid-direct-tag-checks.md +8 -2
- package/patterns/avoid-react-hooks.md +18 -37
- package/patterns/effect-run-in-body.md +1 -1
- package/patterns/require-effect-concurrency.md +11 -0
- package/patterns/use-console-service.md +6 -1
- package/skills/effect-ai-language-model/SKILL.md +10 -16
- package/skills/effect-ai-prompt/SKILL.md +36 -2
- package/skills/effect-ai-provider/SKILL.md +13 -0
- package/skills/effect-ai-streaming/SKILL.md +81 -108
- package/skills/effect-ai-tool/SKILL.md +50 -87
- package/skills/effect-atom-rpc/SKILL.md +9 -2
- package/skills/effect-atom-state/SKILL.md +5 -0
- package/skills/effect-cache/SKILL.md +32 -0
- package/skills/effect-cli/SKILL.md +22 -3
- package/skills/effect-concurrency-testing/SKILL.md +7 -9
- package/skills/effect-domain-modeling/SKILL.md +208 -1169
- package/skills/effect-domain-predicates/SKILL.md +5 -6
- package/skills/effect-error-handling/SKILL.md +5 -4
- package/skills/effect-http-api/SKILL.md +12 -1
- package/skills/effect-http-client/SKILL.md +1 -1
- package/skills/effect-http-server/SKILL.md +14 -3
- package/skills/effect-layer-design/SKILL.md +22 -56
- package/skills/effect-mcp-server/SKILL.md +1 -1
- package/skills/effect-pattern-matching/SKILL.md +44 -11
- package/skills/effect-platform-abstraction/SKILL.md +1 -1
- package/skills/effect-platform-layers/SKILL.md +1 -1
- package/skills/effect-rpc-api/SKILL.md +8 -1
- package/skills/effect-rpc-client/SKILL.md +20 -6
- package/skills/effect-rpc-cluster/SKILL.md +44 -14
- package/skills/effect-rpc-server/SKILL.md +32 -5
- package/skills/effect-scheduling/SKILL.md +1 -1
- package/skills/effect-schema-composition/SKILL.md +69 -15
- package/skills/effect-schema-v4/SKILL.md +43 -1
- package/skills/effect-scope/SKILL.md +30 -0
- package/skills/effect-service-implementation/SKILL.md +10 -4
- package/skills/effect-socket/SKILL.md +5 -5
- package/skills/effect-sql/SKILL.md +22 -0
- package/skills/effect-stream/SKILL.md +32 -1
- package/skills/effect-testing/SKILL.md +39 -31
- package/skills/effect-workflow/SKILL.md +6 -0
- package/patterns/vm-in-wrong-file.md +0 -51
- package/skills/effect-react-vm/SKILL.md +0 -675
|
@@ -22,8 +22,8 @@ Key files:
|
|
|
22
22
|
- `packages/effect/src/unstable/rpc/RpcWorker.ts` — `InitialMessage` for worker transports
|
|
23
23
|
- `packages/effect/src/unstable/rpc/RpcTest.ts` — in-process test client
|
|
24
24
|
- `packages/effect/src/unstable/rpc/RpcSchema.ts` — `ClientAbort` cause annotation, stream schema markers
|
|
25
|
-
- `packages/platform
|
|
26
|
-
- `packages/platform
|
|
25
|
+
- `packages/platform/node/test/RpcServer.test.ts` + `test/fixtures/rpc-{schemas,e2e}.ts` — the best end-to-end reference for real wiring across http/ws/tcp transports and every serialization
|
|
26
|
+
- `packages/platform/browser/test/fixtures/rpc-worker.ts` — minimal worker-side server entrypoint
|
|
27
27
|
|
|
28
28
|
## Core Model
|
|
29
29
|
|
|
@@ -349,7 +349,33 @@ Provide exactly one `RpcSerialization` layer. The load-bearing property is `incl
|
|
|
349
349
|
| `RpcSerialization.layerNdjson` | `application/ndjson` | yes | newline-delimited JSON |
|
|
350
350
|
| `RpcSerialization.layerJsonRpc({ contentType? })` | `application/json` | no | JSON-RPC 2.0 interop |
|
|
351
351
|
| `RpcSerialization.layerNdJsonRpc({ contentType? })` | `application/json-rpc` | yes | JSON-RPC 2.0, newline-framed |
|
|
352
|
-
| `RpcSerialization.layerMsgPack` | `application/msgpack` | yes |
|
|
352
|
+
| `RpcSerialization.layerMsgPack` | `application/msgpack` | yes | msgpackr `useRecords: true`; schema payloads still use JSON codecs |
|
|
353
|
+
| `RpcSerialization.layerSchemaBinary(options?)` | `application/vnd.effect.rpc+schema-binary` | yes | schema-derived binary payloads and envelopes |
|
|
354
|
+
|
|
355
|
+
### Schema-aware serialization (rc.112)
|
|
356
|
+
|
|
357
|
+
`RpcSerialization.RpcSerialization`, `RpcClient.Protocol`, and
|
|
358
|
+
`RpcServer.Protocol` now require `codecFor: RpcSerialization.CodecFor`:
|
|
359
|
+
|
|
360
|
+
```ts
|
|
361
|
+
type CodecFor = <S extends Schema.Top>(schema: S) =>
|
|
362
|
+
Schema.Codec<S['Type'], unknown, S['DecodingServices'], S['EncodingServices']>;
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Forward the selected serialization's `codecFor` when implementing a protocol.
|
|
366
|
+
This selects codecs for payloads, successes, errors, defects, and stream elements;
|
|
367
|
+
envelope framing remains the serialization's responsibility. Existing JSON,
|
|
368
|
+
NDJSON, JSON-RPC, and MsgPack wire formats keep their JSON-compatible schema
|
|
369
|
+
codecs. Workers supply `Schema.toCodecJson` themselves over structured clone and
|
|
370
|
+
still need no serialization layer. Cluster network traffic follows the protocol
|
|
371
|
+
codec; cluster persistence continues to use JSON.
|
|
372
|
+
|
|
373
|
+
`layerSchemaBinary({ maxFrameSize?, fingerprintPayloads? })` must be selected on
|
|
374
|
+
both peers. The default maximum frame size is 16 MiB. Envelopes use fingerprints
|
|
375
|
+
and a connection-local string dictionary. Payload fingerprints default to `false`
|
|
376
|
+
to allow compatible schema evolution; enable them for strict layout agreement.
|
|
377
|
+
This is an Effect RPC format, not generic MsgPack or JSON-RPC interoperability.
|
|
378
|
+
See `effect-schema-composition` for binary layout/ownership constraints.
|
|
353
379
|
|
|
354
380
|
Rules, verified against the protocol implementations and the e2e matrix:
|
|
355
381
|
|
|
@@ -608,7 +634,7 @@ it.effect('GetUser', () =>
|
|
|
608
634
|
|
|
609
635
|
### Transport integration tests
|
|
610
636
|
|
|
611
|
-
For exercising a real transport in-process, use `NodeHttpServer.layerTest` (provides both `HttpServer` and `HttpClient` on a random port) under your normal server+client layers — `packages/platform
|
|
637
|
+
For exercising a real transport in-process, use `NodeHttpServer.layerTest` (provides both `HttpServer` and `HttpClient` on a random port) under your normal server+client layers — `packages/platform/node/test/RpcServer.test.ts` is the template. Framed formats support byte streams; unframed HTTP buffers the response. Verify the selected transport/serialization pair rather than assuming every pairing streams.
|
|
612
638
|
|
|
613
639
|
### Unit-testing one handler
|
|
614
640
|
|
|
@@ -628,6 +654,7 @@ const myProtocol = RpcServer.Protocol.make((writeRequest) =>
|
|
|
628
654
|
const disconnects = yield* Queue.make<number>();
|
|
629
655
|
// wire your transport: on inbound data → writeRequest(clientId, message)
|
|
630
656
|
return {
|
|
657
|
+
codecFor: Schema.toCodecJson, // or the selected serialization.codecFor
|
|
631
658
|
disconnects,
|
|
632
659
|
send: (clientId, response, _transferables) => sendToTransport(clientId, response),
|
|
633
660
|
end: (clientId) => Effect.void,
|
|
@@ -752,7 +779,7 @@ const ServerLayer = RpcServer.layer(StoreRpcs, { concurrency: 1 }).pipe(
|
|
|
752
779
|
2. **Forgetting `protocol: 'http'` on `layerHttp`.** The default is `'websocket'` — your `POST /rpc` curl returns 404 and only a `GET` upgrade route exists. Also note `layerHttp` takes `{ group, path, ... }` as one options bag, while `layer(group, options)` takes the group positionally.
|
|
753
780
|
3. **Providing handlers/middleware but no `Protocol` or `RpcSerialization`.** `RpcServer.layer` requires all of: handler layer(s), middleware implementation layers, a `layerProtocol*`, and (for non-worker protocols) a `RpcSerialization.layer*`. Missing ones surface as unresolved layer requirements.
|
|
754
781
|
4. **`layerJson` on a raw TCP socket server.** No framing — decode breaks when messages span chunks. Sockets need `layerNdjson`, `layerNdJsonRpc()`, or `layerMsgPack`. (WebSocket is fine with `layerJson` — ws frames messages itself.)
|
|
755
|
-
5. **Streaming rpcs over `layerProtocolHttp` + `layerJson` and wondering why chunks arrive all at once.** Unframed HTTP buffers the whole response until every request in the call finishes.
|
|
782
|
+
5. **Streaming rpcs over `layerProtocolHttp` + `layerJson` and wondering why chunks arrive all at once.** Unframed HTTP buffers the whole response until every request in the call finishes. Framed HTTP streams incrementally with a bounded response queue (`streamBufferSize`, default 16). HTTP has no RPC acknowledgment protocol, but the bounded queue still backpressures producers; full-duplex transports additionally support RPC acks.
|
|
756
783
|
6. **Leaving `disableFatalDefects: false` in production.** One handler `die` then nukes every in-flight request on that connection with a connection-level defect. Set `true` to confine defects to the failing request.
|
|
757
784
|
7. **Assuming `concurrency` is per-client.** It is one semaphore per server instance shared by all clients. Use `Rpc.fork` to exempt cheap read handlers instead of raising the global limit.
|
|
758
785
|
8. **Treating `Rpc.fork`/`Rpc.uninterruptible` as rpc options.** They wrap the handler's *returned* Effect/Stream: `db.get(id).pipe(Rpc.fork)`. There is no `{ fork: true }` key on `Rpc.make`.
|
|
@@ -44,7 +44,7 @@ const retryPolicy = Schedule.exponential('100 millis').pipe(
|
|
|
44
44
|
const result = request.pipe(
|
|
45
45
|
Effect.retryOrElse(retryPolicy, (error, scheduleOutput) =>
|
|
46
46
|
Effect.logError('request retries exhausted', error).pipe(
|
|
47
|
-
Effect.
|
|
47
|
+
Effect.andThen(fallback(error, scheduleOutput))
|
|
48
48
|
)
|
|
49
49
|
)
|
|
50
50
|
);
|
|
@@ -24,19 +24,21 @@ Reference this for:
|
|
|
24
24
|
|
|
25
25
|
### The Schema Type
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Use `Schema.Schema<Type>` when only the decoded type matters, or
|
|
28
|
+
`Schema.Codec<Type, Encoded, DecodingServices, EncodingServices>` to retain the
|
|
29
|
+
full codec contract:
|
|
28
30
|
|
|
29
31
|
- **Type**: The validated, decoded output type (what you get after successful decoding)
|
|
30
32
|
- **Encoded**: The raw input type (what you provide for decoding)
|
|
31
|
-
- **
|
|
33
|
+
- **DecodingServices**: Services needed to decode (default `never`)
|
|
34
|
+
- **EncodingServices**: Services needed to encode (default `never`)
|
|
32
35
|
|
|
33
36
|
**Example:**
|
|
34
37
|
|
|
35
38
|
```typescript
|
|
36
39
|
import { Schema } from 'effect';
|
|
37
40
|
|
|
38
|
-
// Schema<number, string, never>
|
|
39
|
-
// ^Type ^Encoded ^Context
|
|
41
|
+
// Schema.Codec<number, string, never, never>
|
|
40
42
|
const NumberFromString = Schema.NumberFromString;
|
|
41
43
|
```
|
|
42
44
|
|
|
@@ -123,7 +125,7 @@ const PositiveInt = Schema.Number.check(
|
|
|
123
125
|
Schema.isGreaterThan(0)
|
|
124
126
|
);
|
|
125
127
|
|
|
126
|
-
// Type: Schema<number, number, never>
|
|
128
|
+
// Type: Schema.Codec<number, number, never, never>
|
|
127
129
|
// Both Type and Encoded are `number`
|
|
128
130
|
```
|
|
129
131
|
|
|
@@ -404,8 +406,8 @@ Schema.String.pipe(
|
|
|
404
406
|
);
|
|
405
407
|
|
|
406
408
|
// Pre-built transformation schemas
|
|
407
|
-
Schema.Trimmed; //
|
|
408
|
-
Schema.NonEmptyString; //
|
|
409
|
+
Schema.Trimmed; // checks an already-trimmed string; does not trim input
|
|
410
|
+
Schema.NonEmptyString; // checks a non-empty string
|
|
409
411
|
```
|
|
410
412
|
|
|
411
413
|
### Number Transformations
|
|
@@ -453,7 +455,7 @@ function split(separator: string) {
|
|
|
453
455
|
Schema.decodeTo(
|
|
454
456
|
Schema.Array(Schema.String),
|
|
455
457
|
SchemaTransformation.transform({
|
|
456
|
-
decode: (s) => s.split(separator)
|
|
458
|
+
decode: (s): ReadonlyArray<string> => s.split(separator),
|
|
457
459
|
encode: (as) => as.join(separator)
|
|
458
460
|
})
|
|
459
461
|
)
|
|
@@ -463,6 +465,58 @@ function split(separator: string) {
|
|
|
463
465
|
|
|
464
466
|
## Custom Transformations
|
|
465
467
|
|
|
468
|
+
### Schema-derived binary boundaries (rc.112)
|
|
469
|
+
|
|
470
|
+
Use `SchemaBinary.toCodec(schema)` from `effect/unstable/encoding` for a compact
|
|
471
|
+
`Uint8Array` representation. It derives the wire layout from the schema's
|
|
472
|
+
**encoded side**, preserving transformations, checks, and decoding/encoding
|
|
473
|
+
services. Use public Schema encode/decode adapters; `toCodecDirect` and the
|
|
474
|
+
module's internal fast-path functions are not application APIs.
|
|
475
|
+
|
|
476
|
+
<!-- typecheck -->
|
|
477
|
+
```ts
|
|
478
|
+
import { Effect } from 'effect';
|
|
479
|
+
import * as Schema from 'effect/Schema';
|
|
480
|
+
import { SchemaBinary } from 'effect/unstable/encoding';
|
|
481
|
+
|
|
482
|
+
class Reading extends Schema.Class<Reading>('Reading')({
|
|
483
|
+
id: Schema.String,
|
|
484
|
+
value: Schema.NumberFromString
|
|
485
|
+
}) {}
|
|
486
|
+
|
|
487
|
+
const ReadingBinary = SchemaBinary.toCodec(Reading);
|
|
488
|
+
const roundTrip = Effect.gen(function* () {
|
|
489
|
+
const bytes = yield* Schema.encodeEffect(ReadingBinary)(
|
|
490
|
+
new Reading({ id: 'sensor-1', value: 12 })
|
|
491
|
+
);
|
|
492
|
+
return yield* Schema.decodeUnknownEffect(ReadingBinary)(bytes);
|
|
493
|
+
});
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
One codec call handles one complete frame. For arbitrary stream chunks, use
|
|
497
|
+
`SchemaBinary.parser`, `encode` / `decode` Channels, or `duplex` (see
|
|
498
|
+
`effect-stream`). Encoded bytes are arena-backed views; copy with `bytes.slice()`
|
|
499
|
+
when independent ownership is required. Default mode supports compatible schema
|
|
500
|
+
evolution; `{ fingerprint: true }` uses positional layouts and an 8-byte layout
|
|
501
|
+
hash, requiring matching schema definitions. A fingerprint is a compatibility
|
|
502
|
+
check, not authentication or encryption.
|
|
503
|
+
|
|
504
|
+
The connection-scoped `encoder` / `parser` pair accepts `{ dictionary: true }`
|
|
505
|
+
for repeated strings. Both peers must use the same schema/options and process
|
|
506
|
+
frames in order; dictionary frames do not stand alone. This mode throws during
|
|
507
|
+
construction if the binary layer cannot fully validate that schema itself.
|
|
508
|
+
`parser.feed` lifts the synchronous parser into Effect; it does not make schema
|
|
509
|
+
transformations asynchronous or add their services. Use codec adapters or
|
|
510
|
+
`encode` / `decode` channels for async/service-dependent transformations. A parser
|
|
511
|
+
is spent after failure; call `end` at EOF to detect an incomplete trailing frame.
|
|
512
|
+
Use `maxFrameSize` to bound buffered frames. `SchemaBinary.fieldId` can assign
|
|
513
|
+
stable IDs to fields for compatible evolution; changing established IDs is a
|
|
514
|
+
wire-contract change.
|
|
515
|
+
|
|
516
|
+
Use JSON codecs for JSON contracts and binary codecs only when the transport
|
|
517
|
+
contract permits them. RPC selects a payload codec through `codecFor`; use
|
|
518
|
+
`RpcSerialization.layerSchemaBinary()` instead of manually wrapping RPC envelopes.
|
|
519
|
+
|
|
466
520
|
### SchemaTransformation.transform — Simple Transformations
|
|
467
521
|
|
|
468
522
|
Use `SchemaTransformation.transform` when the transformation always succeeds:
|
|
@@ -542,7 +596,7 @@ const OptionFromNonEmptyString = Schema.optionalKey(Schema.String).pipe(
|
|
|
542
596
|
import { Effect, Schema } from 'effect';
|
|
543
597
|
|
|
544
598
|
declare const self: Effect.Effect<unknown, unknown, unknown>;
|
|
545
|
-
declare const schema: Schema.
|
|
599
|
+
declare const schema: Schema.Codec<unknown, unknown>;
|
|
546
600
|
declare const toError: (e: unknown) => unknown;
|
|
547
601
|
|
|
548
602
|
// Streamlined
|
|
@@ -748,9 +802,9 @@ const DogWithBreed3 = Schema.Struct({
|
|
|
748
802
|
```typescript
|
|
749
803
|
import { Schema, SchemaTransformation } from 'effect';
|
|
750
804
|
|
|
751
|
-
const ReadonlySetFromArray = <A, I,
|
|
752
|
-
itemSchema: Schema.
|
|
753
|
-
): Schema.
|
|
805
|
+
const ReadonlySetFromArray = <A, I, RD, RE>(
|
|
806
|
+
itemSchema: Schema.Codec<A, I, RD, RE>
|
|
807
|
+
): Schema.Codec<ReadonlySet<A>, ReadonlyArray<I>, RD, RE> =>
|
|
754
808
|
Schema.Array(itemSchema).pipe(
|
|
755
809
|
Schema.decodeTo(
|
|
756
810
|
Schema.ReadonlySet(Schema.toType(itemSchema)),
|
|
@@ -762,7 +816,7 @@ const ReadonlySetFromArray = <A, I, R>(
|
|
|
762
816
|
);
|
|
763
817
|
|
|
764
818
|
const schema = ReadonlySetFromArray(Schema.String);
|
|
765
|
-
// Schema<ReadonlySet<string>, readonly string[], never>
|
|
819
|
+
// Schema.Codec<ReadonlySet<string>, readonly string[], never, never>
|
|
766
820
|
```
|
|
767
821
|
|
|
768
822
|
### Multi-Stage Transformations
|
|
@@ -967,9 +1021,9 @@ When creating schemas, ensure:
|
|
|
967
1021
|
- `SchemaGetter` is imported from `effect/SchemaGetter` or `{ SchemaGetter } from "effect"`
|
|
968
1022
|
- `SchemaIssue` is imported from `effect/SchemaIssue` or `{ SchemaIssue } from "effect"`
|
|
969
1023
|
- `Struct` is imported from `{ Struct } from "effect"` for `mapFields` operations
|
|
970
|
-
- Schema
|
|
1024
|
+
- Type-only schema: `Schema.Schema<Type>`; full codec: `Schema.Codec<Type, Encoded, DecodingServices, EncodingServices>`
|
|
971
1025
|
- All schemas return `readonly` types by default
|
|
972
|
-
- Use `Schema.revealCodec(schema)` to
|
|
1026
|
+
- Use `Schema.revealCodec(schema)` to expose its full `Schema.Codec` contract
|
|
973
1027
|
- Use `Schema.toType(schema)` to get the type-side schema (replaces v3 `Schema.typeSchema`)
|
|
974
1028
|
- Access struct fields with `.fields` property
|
|
975
1029
|
- Filters preserve schema type — `.check()` on a `Schema.Struct` returns a `Schema.Struct`
|
|
@@ -20,6 +20,14 @@ Reference this for:
|
|
|
20
20
|
|
|
21
21
|
## 1. Key Renames (find-and-replace safe)
|
|
22
22
|
|
|
23
|
+
### Current type model (rc.112)
|
|
24
|
+
|
|
25
|
+
`Schema.Schema<T>` describes only the decoded type. Preserve wire types and
|
|
26
|
+
services with `Schema.Codec<T, E, RD, RE>`: decoded Type, Encoded representation,
|
|
27
|
+
DecodingServices, and EncodingServices. A three-argument `Schema.Schema<A, I, R>`
|
|
28
|
+
is not a v4 type. Prefer inference or `S extends Schema.Constraint` in generic
|
|
29
|
+
schema helpers so concrete schema operations are retained.
|
|
30
|
+
|
|
23
31
|
| v3 | v4 | Notes |
|
|
24
32
|
| ----------------------------- | ----------------------------------- | ----------------------------------------- |
|
|
25
33
|
| `annotations(ann)` | `annotate(ann)` | |
|
|
@@ -374,7 +382,7 @@ const fallback = SchemaGetter.withDefault(Effect.succeed('viewer'));
|
|
|
374
382
|
|
|
375
383
|
### Later v4 Updates
|
|
376
384
|
|
|
377
|
-
- `
|
|
385
|
+
- `schema.makeEffect(input, options?)` on schemas and schema-backed classes returns an `Effect` that fails directly with `SchemaIssue.Issue`, not `Schema.SchemaError`.
|
|
378
386
|
- `Schema.resolveInto` was renamed to `Schema.resolveAnnotations`.
|
|
379
387
|
- `Schema.resolveAnnotationsKey(schema)` returns key-level annotations.
|
|
380
388
|
- `Schema.annotateEncoded({...})` annotates the encoded side of a transformed schema; use `Schema.annotate({...})` for the decoded Type side.
|
|
@@ -485,6 +493,40 @@ The input is `{ ast, occurrences, identifier }`; return a name to extract that c
|
|
|
485
493
|
|
|
486
494
|
The standalone `SchemaError` root module was removed. Parser adapters such as `Schema.decodeUnknownEffect` fail with `Schema.SchemaError`, which contains the structured `issue`; narrow unknown failures with `Schema.isSchemaError`. By contrast, schema/class `makeEffect` and constructor defaults fail directly with `SchemaIssue.Issue`.
|
|
487
495
|
|
|
496
|
+
In rc.112, `SchemaError` skips stack-frame capture for lower construction cost.
|
|
497
|
+
Use its `issue` and formatter/message for validation diagnostics; a captured
|
|
498
|
+
parser-error stack is not a diagnostic contract. Synchronous parser optimizations
|
|
499
|
+
do not change the choice between constructors, sync decoders, and Effect decoders.
|
|
500
|
+
|
|
501
|
+
### Partial tagged-union matching (rc.112)
|
|
502
|
+
|
|
503
|
+
`Schema.TaggedUnion(...)` and `Schema.Union([...]).pipe(Schema.toTaggedUnion(tag))`
|
|
504
|
+
now expose `matchOrElse(value, cases, fallback)` and its curried
|
|
505
|
+
`matchOrElse(cases, fallback)(value)` form. Use exhaustive `.match` for closed
|
|
506
|
+
business decisions. Use `.matchOrElse` when all remaining cases genuinely share
|
|
507
|
+
one behavior. The `toTaggedUnion` helper narrows the fallback to omitted variants;
|
|
508
|
+
the direct `TaggedUnion` overload types its fallback as the full union.
|
|
509
|
+
Neither matcher decodes unknown input. See `effect-pattern-matching` for a checked example.
|
|
510
|
+
|
|
511
|
+
### JSON Schema import, conversion, and Standard Schema (rc.112)
|
|
512
|
+
|
|
513
|
+
- `SchemaRepresentation.fromJsonSchemaDocument` rejects unsupported references,
|
|
514
|
+
validation keywords, object/array `const` or `enum` values, and intersections
|
|
515
|
+
it cannot represent faithfully. Do not discard the failing constraint to make
|
|
516
|
+
an import succeed.
|
|
517
|
+
- A keyword such as `minLength` does not imply `type: 'string'`. Constraints beside
|
|
518
|
+
`const`, `enum`, and `$ref` are applied. Imported `oneOf` remains `oneOf` on export,
|
|
519
|
+
and tuple imports preserve `minItems` even when `prefixItems` alone is insufficient.
|
|
520
|
+
- `JsonSchema` dialect conversion preserves custom keywords and representable
|
|
521
|
+
conditionals, contains, dependencies, identifiers, and tuples; it relocates
|
|
522
|
+
local references and throws for unsupported conversions. These synchronous
|
|
523
|
+
schema-tooling failures need an explicit `Effect.try` boundary when actionable.
|
|
524
|
+
- Import vendored V1 interoperability types from `effect/StandardSchema`, for
|
|
525
|
+
example `StandardSchemaV1` and `StandardJSONSchemaV1`. Continue to adapt Effect
|
|
526
|
+
schemas with `Schema.toStandardSchemaV1`; the new module is not a schema builder.
|
|
527
|
+
- Binary encoding is available from `effect/unstable/encoding` as `SchemaBinary`.
|
|
528
|
+
See `effect-schema-composition` for codecs and `effect-stream` for framing.
|
|
529
|
+
|
|
488
530
|
## 8. New Modules
|
|
489
531
|
|
|
490
532
|
### SchemaTransformation
|
|
@@ -305,6 +305,11 @@ Close semantics (from `internal/effect.ts` `scopeCloseFinalizers`):
|
|
|
305
305
|
- The scope transitions to `Closed` *before* finalizers run, so finalizers registered from inside finalizers execute immediately.
|
|
306
306
|
- You can inspect `scope.state._tag` (`'Empty' | 'Open' | 'Closed'`) and `scope.strategy` directly.
|
|
307
307
|
|
|
308
|
+
In rc.112, `Scope.State.Open` stores `finalizerKey` / `finalizer` inline and
|
|
309
|
+
allocates its optional `finalizers` map only for additional entries. Avoid
|
|
310
|
+
constructing or mutating that representation; use `Scope.addFinalizer*` and
|
|
311
|
+
`Scope.close`. LIFO order and failure-preserving cleanup still apply.
|
|
312
|
+
|
|
308
313
|
Manual scopes are warranted when a resource's lifetime does not align with any effect's lexical extent — for example a connection cached between requests, a resource handed off to another fiber, or interop with non-Effect lifecycle callbacks (`Scope.makeUnsafe` + `Scope.closeUnsafe` from a `dispose()` method).
|
|
309
314
|
|
|
310
315
|
---
|
|
@@ -494,6 +499,31 @@ Semantics (verified in `ScopedRef.ts` and its tests):
|
|
|
494
499
|
|
|
495
500
|
For keyed collections of scoped resources, see `LayerMap` (`ai-docs/src/01_effect/04_resources/30_layer-map.ts`); for capacity-managed pools, see `Pool` (`packages/effect/src/Pool.ts` — `Pool.make` returns a scoped pool whose `Pool.get(pool)` is itself scoped per item). `ScopedCache` is covered by the `effect-cache` skill.
|
|
496
501
|
|
|
502
|
+
### Callback-scoped pool checkout (rc.112)
|
|
503
|
+
|
|
504
|
+
Prefer `Pool.use(pool, use)` when one callback owns the entire borrow. It returns
|
|
505
|
+
the item on success, failure, or interruption without adding `Scope` to the
|
|
506
|
+
caller's requirements. Pool construction still needs an owning scope. Use
|
|
507
|
+
`Pool.get` when the caller deliberately owns a longer checkout scope.
|
|
508
|
+
|
|
509
|
+
<!-- typecheck -->
|
|
510
|
+
```ts
|
|
511
|
+
import { Effect, Pool } from 'effect';
|
|
512
|
+
|
|
513
|
+
const program = Effect.gen(function* () {
|
|
514
|
+
const pool = yield* Pool.make({
|
|
515
|
+
acquire: Effect.succeed('connection'),
|
|
516
|
+
size: 2
|
|
517
|
+
});
|
|
518
|
+
return yield* Pool.use(pool, (connection) => Effect.succeed(connection.length));
|
|
519
|
+
}).pipe(Effect.scoped);
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
`Effect.scoped(Pool.get(pool))` releases the checkout before the returned item is
|
|
523
|
+
used outside that effect. Put the use inside the scope or use `Pool.use`.
|
|
524
|
+
`Pool.State` / `Pool.PoolItem` changed in rc.112 (incremental usage and intrusive
|
|
525
|
+
FIFO tracking); use the public checkout/invalidation APIs rather than their fields.
|
|
526
|
+
|
|
497
527
|
---
|
|
498
528
|
|
|
499
529
|
## 9. Scope-Aware Utilities
|
|
@@ -344,7 +344,10 @@ This reduces cognitive load in the parent service and makes race-sensitive behav
|
|
|
344
344
|
|
|
345
345
|
## Pattern: No Requirement Leakage
|
|
346
346
|
|
|
347
|
-
|
|
347
|
+
Capture stable implementation dependencies at construction. Preserve intentional
|
|
348
|
+
call-time requirements such as `Scope`, transactions, request context, and
|
|
349
|
+
schema decoding/encoding services in method return types; capturing those in an
|
|
350
|
+
application-lifetime layer would give them the wrong lifetime or authority.
|
|
348
351
|
|
|
349
352
|
```typescript
|
|
350
353
|
// database.ts
|
|
@@ -389,7 +392,8 @@ Dependencies are handled by:
|
|
|
389
392
|
1. **`Layer.effect` closure** — services captured at construction time via `yield*`
|
|
390
393
|
2. **`Layer.provide`** — wires dependency layers into `defaultLayer`
|
|
391
394
|
|
|
392
|
-
Both
|
|
395
|
+
Both remove stable infrastructure dependencies from method signatures. `R = never`
|
|
396
|
+
is appropriate only when the operation has no intentional caller-supplied context.
|
|
393
397
|
|
|
394
398
|
## Pattern: Whole-Function Transforms
|
|
395
399
|
|
|
@@ -556,7 +560,9 @@ const TestWebhook = Layer.mock(PaymentWebhookGateway.Service)({
|
|
|
556
560
|
});
|
|
557
561
|
```
|
|
558
562
|
|
|
559
|
-
`Layer.mock(Service)({...})`
|
|
563
|
+
`Layer.mock(Service)({...})` accepts a partial implementation and supplies defecting
|
|
564
|
+
stubs for unimplemented members. Use a complete `Layer.succeed(Service,
|
|
565
|
+
Service.of({...}))` fake when every method must be implemented at compile time.
|
|
560
566
|
|
|
561
567
|
For a reusable stateful fake, expose a separate test-control service and provide the same implementation under both tags with `Layer.effectContext`. Production code sees only the production interface; tests can inspect state and trigger transitions deterministically.
|
|
562
568
|
|
|
@@ -643,7 +649,7 @@ Tag identifiers should include the domain name according to project convention:
|
|
|
643
649
|
- [ ] When using class-style `Context.Service`, the shape is its type parameter and the class body is empty
|
|
644
650
|
- [ ] Service methods use `Effect.fn("Domain.methodName")` with the projected domain prefix
|
|
645
651
|
- [ ] Service represents single capability
|
|
646
|
-
- [ ]
|
|
652
|
+
- [ ] Stable dependencies are captured; intentional caller-scoped requirements remain explicit in R
|
|
647
653
|
- [ ] Dependencies captured in `Layer.effect` closure via `yield*`; wired via `Layer.provide` on `defaultLayer`
|
|
648
654
|
- [ ] The project tag's implementation constructor is used; for `Context.Service`, use `Service.of({...})`
|
|
649
655
|
- [ ] Tagged with a descriptive, unique identifier under project convention
|
|
@@ -15,11 +15,11 @@ Key files:
|
|
|
15
15
|
|
|
16
16
|
- `packages/effect/src/unstable/socket/Socket.ts` — `Socket` interface and service tag, `make`, `CloseEvent`, the `SocketError` taxonomy, channel adapters (`toChannel`, `toChannelString`, `toChannelMap`, `makeChannel`), WebSocket constructors (`makeWebSocket`, `fromWebSocket`, `layerWebSocket`, `WebSocketConstructor`), `fromTransformStream`
|
|
17
17
|
- `packages/effect/src/unstable/socket/SocketServer.ts` — `SocketServer` service contract, `Address` (`TcpAddress` / `UnixAddress`), `SocketServerError`
|
|
18
|
-
- `packages/platform
|
|
19
|
-
- `packages/platform
|
|
20
|
-
- `packages/platform
|
|
21
|
-
- `packages/platform
|
|
22
|
-
- `packages/platform
|
|
18
|
+
- `packages/platform/node-shared/src/NodeSocket.ts` — `makeNet`, `fromDuplex`, `makeNetChannel`, `layerNet`, `NetSocket` service, `NodeWS` (`ws` re-export)
|
|
19
|
+
- `packages/platform/node-shared/src/NodeSocketServer.ts` — TCP/Unix server `make`/`layer`, WebSocket server `makeWebSocket`/`layerWebSocket`, `IncomingMessage` service
|
|
20
|
+
- `packages/platform/node/src/NodeSocket.ts` — re-exports shared module; adds `layerWebSocketConstructor`, `layerWebSocketConstructorWS`, `layerWebSocket`
|
|
21
|
+
- `packages/platform/bun/src/BunSocket.ts` — same shared re-export; Bun-global WebSocket constructor layers
|
|
22
|
+
- `packages/platform/node/test/NodeSocket.test.ts` — loopback echo server, WebSocket client semantics, transform-stream sockets
|
|
23
23
|
- `packages/effect/src/unstable/devtools/DevToolsClient.ts` — production example of NDJSON-framed request/response over a `Socket`
|
|
24
24
|
- `packages/effect/src/unstable/rpc/RpcClient.ts` (`makeProtocolSocket`) — production example of socket reconnect with retry schedules
|
|
25
25
|
|
|
@@ -606,6 +606,28 @@ const main = program.pipe(Effect.provide(DatabaseLayer));
|
|
|
606
606
|
|
|
607
607
|
### PgClient-Specific Features
|
|
608
608
|
|
|
609
|
+
### Low-level PostgreSQL codecs (rc.112)
|
|
610
|
+
|
|
611
|
+
`@effect/sql-pg` adds public `PgProtocol`, `PgTypes`, and `PgAuth` modules.
|
|
612
|
+
These are building blocks for protocol adapters; `PgClient` still uses `pg`.
|
|
613
|
+
|
|
614
|
+
- `PgProtocol`: PostgreSQL 3.0 frontend encoding and incremental backend-frame
|
|
615
|
+
parsing. Stateful parser failures are terminal and synchronous; lift a parser
|
|
616
|
+
boundary with `Effect.try` and preserve the protocol error.
|
|
617
|
+
- `PgTypes`: binary scalar and one-dimensional array OID codecs returning typed
|
|
618
|
+
`Result` failures. Internal field-reader throwing fast paths are not the public
|
|
619
|
+
application API.
|
|
620
|
+
- `PgAuth`: MD5 and SCRAM-SHA-256 authentication codecs with typed `Result`
|
|
621
|
+
failures. Treat authentication material as secrets.
|
|
622
|
+
- Encoded frames/decoded byte fields are buffer views. Copy bytes that must
|
|
623
|
+
outlive the owning message. Read `packages/sql/pg/src/{PgProtocol,PgTypes,PgAuth}.ts`
|
|
624
|
+
for exact signatures before implementing an adapter.
|
|
625
|
+
|
|
626
|
+
Driver releases also update production dependencies for D1, mysql2, and PGlite;
|
|
627
|
+
keep the Effect-family package versions aligned when upgrading those adapters.
|
|
628
|
+
|
|
629
|
+
### PgClient JSON and Notifications
|
|
630
|
+
|
|
609
631
|
```ts
|
|
610
632
|
const pg = yield* PgClient;
|
|
611
633
|
|
|
@@ -355,7 +355,7 @@ stream.pipe(
|
|
|
355
355
|
|
|
356
356
|
---
|
|
357
357
|
|
|
358
|
-
## 4. Encoding & Decoding (NDJSON / Msgpack)
|
|
358
|
+
## 4. Encoding & Decoding (NDJSON / Msgpack / SchemaBinary)
|
|
359
359
|
|
|
360
360
|
Use `Stream.pipeThroughChannel` with codec channels from `effect/unstable/encoding`.
|
|
361
361
|
|
|
@@ -363,6 +363,37 @@ Use `Stream.pipeThroughChannel` with codec channels from `effect/unstable/encodi
|
|
|
363
363
|
import { Ndjson, Msgpack } from 'effect/unstable/encoding';
|
|
364
364
|
```
|
|
365
365
|
|
|
366
|
+
### Schema-derived binary frames (rc.112)
|
|
367
|
+
|
|
368
|
+
<!-- typecheck -->
|
|
369
|
+
```ts
|
|
370
|
+
import { Stream } from 'effect';
|
|
371
|
+
import * as Schema from 'effect/Schema';
|
|
372
|
+
import { SchemaBinary } from 'effect/unstable/encoding';
|
|
373
|
+
|
|
374
|
+
class Reading extends Schema.Class<Reading>('Reading')({
|
|
375
|
+
id: Schema.String,
|
|
376
|
+
value: Schema.Number
|
|
377
|
+
}) {}
|
|
378
|
+
|
|
379
|
+
const roundTrip = Stream.make(new Reading({ id: 'sensor-1', value: 21 })).pipe(
|
|
380
|
+
Stream.pipeThroughChannel(SchemaBinary.encode(Reading)()),
|
|
381
|
+
Stream.pipeThroughChannel(SchemaBinary.decode(Reading, { maxFrameSize: 1024 })()),
|
|
382
|
+
Stream.runCollect
|
|
383
|
+
);
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
`encode(schema)()` and `decode(schema)()` are channel factories; use them with
|
|
387
|
+
`Stream.pipeThroughChannel`. Input chunks can split or concatenate frames.
|
|
388
|
+
The decoder retains completed values before a later failure and fails with
|
|
389
|
+
`Schema.SchemaError` on an incomplete final frame. Decoding/encoding services
|
|
390
|
+
from schema transformations remain in the channel requirements. `maxFrameSize`
|
|
391
|
+
limits decoding only. Keep paired schemas/options compatible, and copy any
|
|
392
|
+
arena-backed encoded bytes that must survive later writes. For synchronous
|
|
393
|
+
framing/dictionaries use `SchemaBinary.encoder` and `parser`; `duplex` adapts a
|
|
394
|
+
bidirectional channel. See `effect-schema-composition` for codec layout,
|
|
395
|
+
fingerprints, and ownership. Use `RpcSerialization.layerSchemaBinary` for RPC.
|
|
396
|
+
|
|
366
397
|
### Text decoding (split multi-byte characters)
|
|
367
398
|
|
|
368
399
|
To turn a byte stream into text, use `Stream.decodeText` (or `Channel.decodeText`) rather than hand-rolling `new TextDecoder().decode(chunk)` per chunk. These helpers decode with streaming enabled, so multi-byte UTF-8 characters split across `Uint8Array` chunk boundaries are reassembled correctly; per-chunk `TextDecoder` calls would corrupt characters that straddle a boundary.
|
|
@@ -14,13 +14,18 @@ Browse and read files there directly to look up APIs, types, and implementations
|
|
|
14
14
|
|
|
15
15
|
Reference this for:
|
|
16
16
|
|
|
17
|
-
- Testing utilities: `packages/effect/src/
|
|
17
|
+
- Testing utilities: `packages/effect/src/testing/`
|
|
18
18
|
- @effect/vitest source: `packages/vitest/`
|
|
19
19
|
- Migration guide: `MIGRATION.md`
|
|
20
20
|
- Effect source: `packages/effect/src/`
|
|
21
21
|
|
|
22
22
|
## Framework Selection
|
|
23
23
|
|
|
24
|
+
Keep `@effect/vitest` aligned with the Effect release. The rc.112 adapter's
|
|
25
|
+
Vitest peer range is `>=4.1.0 <5.0.0`; do not install it into a Vitest 3 project
|
|
26
|
+
without migrating that project's test framework. Plain compiler/inventory tests
|
|
27
|
+
can keep using their existing Vitest runner without the adapter.
|
|
28
|
+
|
|
24
29
|
**CRITICAL**: Choose the correct testing framework based on the code being tested.
|
|
25
30
|
|
|
26
31
|
### Use @effect/vitest for Effect Code
|
|
@@ -168,13 +173,13 @@ import {
|
|
|
168
173
|
assertFalse,
|
|
169
174
|
assertSome, // For Option.Some
|
|
170
175
|
assertNone, // For Option.None
|
|
171
|
-
assertSuccess, // For
|
|
172
|
-
assertFailure // For
|
|
176
|
+
assertSuccess, // For Result.Success / Exit.Success
|
|
177
|
+
assertFailure // For Result.Failure / Exit.Failure
|
|
173
178
|
} from '@effect/vitest/utils';
|
|
174
|
-
import { Effect, Option,
|
|
179
|
+
import { Effect, Option, Result } from 'effect';
|
|
175
180
|
|
|
176
181
|
declare const someOptionalEffect: Effect.Effect<Option.Option<number>>;
|
|
177
|
-
declare const
|
|
182
|
+
declare const someResultEffect: Effect.Effect<Result.Result<number, Error>>;
|
|
178
183
|
declare const expectedValue: number;
|
|
179
184
|
|
|
180
185
|
it.effect('with effect assertions', () =>
|
|
@@ -182,8 +187,8 @@ it.effect('with effect assertions', () =>
|
|
|
182
187
|
const option = yield* someOptionalEffect;
|
|
183
188
|
assertSome(option, expectedValue);
|
|
184
189
|
|
|
185
|
-
const
|
|
186
|
-
assertSuccess(
|
|
190
|
+
const result = yield* someResultEffect;
|
|
191
|
+
assertSuccess(result, expectedValue);
|
|
187
192
|
})
|
|
188
193
|
);
|
|
189
194
|
```
|
|
@@ -223,7 +228,10 @@ const TestUserService = Layer.mock(UserService)({
|
|
|
223
228
|
});
|
|
224
229
|
```
|
|
225
230
|
|
|
226
|
-
`Layer.mock
|
|
231
|
+
`Layer.mock` accepts a partial implementation and supplies defecting stubs for
|
|
232
|
+
omitted methods. It is not equivalent to a complete, compiler-checked
|
|
233
|
+
`Layer.succeed(Service, Service.of({...}))`. Prefer complete fakes for reusable
|
|
234
|
+
test services; use partial mocks only when omitted operations should defect.
|
|
227
235
|
|
|
228
236
|
### First-Class Controllable Test Services
|
|
229
237
|
|
|
@@ -809,7 +817,7 @@ import { Effect, Logger } from 'effect';
|
|
|
809
817
|
it.effect('logs visible', () =>
|
|
810
818
|
Effect.gen(function* () {
|
|
811
819
|
yield* Effect.log('This will appear');
|
|
812
|
-
}).pipe(Effect.provide(Logger.
|
|
820
|
+
}).pipe(Effect.provide(Logger.layer([Logger.consolePretty()])))
|
|
813
821
|
);
|
|
814
822
|
|
|
815
823
|
// Use it.live only when the live console itself is under test.
|
|
@@ -857,23 +865,23 @@ describe('UserService', () => {
|
|
|
857
865
|
});
|
|
858
866
|
```
|
|
859
867
|
|
|
860
|
-
### Testing
|
|
868
|
+
### Testing Transactions
|
|
869
|
+
|
|
870
|
+
In v4, transactional collections use `TxRef` and `Effect.tx`; there is no
|
|
871
|
+
separate `STM` effect type or `STM.commit`. Test rollback as well as success:
|
|
861
872
|
|
|
862
873
|
```typescript
|
|
863
874
|
import { it, expect } from '@effect/vitest';
|
|
864
|
-
import { Effect,
|
|
875
|
+
import { Effect, TxRef } from 'effect';
|
|
865
876
|
|
|
866
|
-
it.effect('
|
|
877
|
+
it.effect('rolls back a failed transaction', () =>
|
|
867
878
|
Effect.gen(function* () {
|
|
868
|
-
const counter = yield*
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
yield*
|
|
874
|
-
|
|
875
|
-
const final = yield* STM.commit(TRef.get(counter));
|
|
876
|
-
expect(final).toBe(2);
|
|
879
|
+
const counter = yield* TxRef.make(0);
|
|
880
|
+
yield* Effect.gen(function* () {
|
|
881
|
+
yield* TxRef.set(counter, 1);
|
|
882
|
+
return yield* Effect.fail('cancel');
|
|
883
|
+
}).pipe(Effect.tx, Effect.flip);
|
|
884
|
+
expect(yield* TxRef.get(counter)).toBe(0);
|
|
877
885
|
})
|
|
878
886
|
);
|
|
879
887
|
```
|
|
@@ -882,14 +890,14 @@ it.effect('should handle concurrent updates', () =>
|
|
|
882
890
|
|
|
883
891
|
```typescript
|
|
884
892
|
import { it, expect } from '@effect/vitest';
|
|
885
|
-
import { Effect
|
|
893
|
+
import { Effect } from 'effect';
|
|
886
894
|
|
|
887
895
|
declare const GCounter: {
|
|
888
896
|
make: (id: string) => Effect.Effect<unknown>;
|
|
889
|
-
increment: (counter: unknown, value: number) =>
|
|
890
|
-
query: (counter: unknown) =>
|
|
891
|
-
merge: (counter: unknown, state: unknown) =>
|
|
892
|
-
value: (counter: unknown) =>
|
|
897
|
+
increment: (counter: unknown, value: number) => Effect.Effect<void>;
|
|
898
|
+
query: (counter: unknown) => Effect.Effect<unknown>;
|
|
899
|
+
merge: (counter: unknown, state: unknown) => Effect.Effect<void>;
|
|
900
|
+
value: (counter: unknown) => Effect.Effect<number>;
|
|
893
901
|
};
|
|
894
902
|
|
|
895
903
|
declare const ReplicaId: (id: string) => string;
|
|
@@ -899,13 +907,13 @@ it.effect('should merge states correctly', () =>
|
|
|
899
907
|
const counter1 = yield* GCounter.make(ReplicaId('replica-1'));
|
|
900
908
|
const counter2 = yield* GCounter.make(ReplicaId('replica-2'));
|
|
901
909
|
|
|
902
|
-
yield*
|
|
903
|
-
yield*
|
|
910
|
+
yield* Effect.tx(GCounter.increment(counter1, 10));
|
|
911
|
+
yield* Effect.tx(GCounter.increment(counter2, 20));
|
|
904
912
|
|
|
905
|
-
const state2 = yield*
|
|
906
|
-
yield*
|
|
913
|
+
const state2 = yield* Effect.tx(GCounter.query(counter2));
|
|
914
|
+
yield* Effect.tx(GCounter.merge(counter1, state2));
|
|
907
915
|
|
|
908
|
-
const result = yield*
|
|
916
|
+
const result = yield* Effect.tx(GCounter.value(counter1));
|
|
909
917
|
expect(result).toBe(30);
|
|
910
918
|
})
|
|
911
919
|
);
|
|
@@ -125,6 +125,12 @@ yield* SendEmail.resume(executionId);
|
|
|
125
125
|
|
|
126
126
|
### Deterministic Execution ID
|
|
127
127
|
|
|
128
|
+
As of rc.112, generated `WorkflowProxy.toRpcGroup` `<Name>Discard` RPCs and
|
|
129
|
+
`WorkflowProxy.toHttpApiGroup` discard HTTP endpoints also return the execution
|
|
130
|
+
ID (`Schema.String`). Consumers can persist that ID to poll/resume the workflow.
|
|
131
|
+
Update generated client response types and tests that expected `void`; ordinary
|
|
132
|
+
RPC `discard: true` call options still discard the response and cannot return it.
|
|
133
|
+
|
|
128
134
|
The execution ID is computed as a hash of `"${name}-${idempotencyKey(payload)}"`. This means executing the same workflow with the same payload is idempotent — it returns the existing execution rather than starting a new one.
|
|
129
135
|
|
|
130
136
|
```ts
|