diff --git a/.changeset/add-flag-hidden.md b/.changeset/add-flag-hidden.md new file mode 100644 index 0000000000..1c31e6cd8b --- /dev/null +++ b/.changeset/add-flag-hidden.md @@ -0,0 +1,15 @@ +--- +"effect": patch +--- + +Add `Flag.withHidden` (and `Param.withHidden`) to hide flags from `--help` output and shell completions while keeping them fully parseable on the command line. + +Useful for experimental, internal, or deprecated flags that should be accepted but not advertised, e.g. `--experimental-foo`, debug toggles, or escape hatches that are not yet committed to the public CLI surface. + +```ts +import { Flag } from "effect/unstable/cli" + +const experimental = Flag.boolean("experimental-foo").pipe( + Flag.withHidden +) +``` diff --git a/.changeset/add-stream-broadcastn.md b/.changeset/add-stream-broadcastn.md index d8b41a34cf..ca1fa612f2 100644 --- a/.changeset/add-stream-broadcastn.md +++ b/.changeset/add-stream-broadcastn.md @@ -1,5 +1,5 @@ --- -"effect": minor +"effect": patch --- Add Stream.broadcastN for fixed-size stream broadcasts. diff --git a/.changeset/green-rings-prove.md b/.changeset/green-rings-prove.md new file mode 100644 index 0000000000..c7a2ea6063 --- /dev/null +++ b/.changeset/green-rings-prove.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +update Model uuid helpers diff --git a/.changeset/platform-crypto-service.md b/.changeset/platform-crypto-service.md new file mode 100644 index 0000000000..afbf216e6c --- /dev/null +++ b/.changeset/platform-crypto-service.md @@ -0,0 +1,9 @@ +--- +"effect": patch +"@effect/platform-node": patch +"@effect/platform-node-shared": patch +"@effect/platform-bun": patch +"@effect/platform-browser": patch +--- + +Add a platform-agnostic `Crypto` service for cryptographic random bytes, secure random generators, UUIDv4 / UUIDv7 generation, and digest operations. UUID generation should now use the `Crypto` service's `randomUUIDv4` or `randomUUIDv7`, which format bytes from the platform `Crypto` service; UUIDv7 also uses the `Clock` service timestamp. `Random.nextUUIDv4` has been removed because the base `Random` service is not cryptographically secure. diff --git a/.changeset/pre.json b/.changeset/pre.json index ad382c8918..d59f3aa6ac 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -59,6 +59,7 @@ "add-schema-string-encoding", "add-sql-pglite", "add-standard-jsdoc-rule", + "add-stream-broadcastn", "add-unstable-encoding-export", "afraid-cobras-like", "ai-openai-config-field-leak", @@ -292,6 +293,7 @@ "green-chips-wash", "green-moons-smile", "green-pugs-play", + "green-rings-prove", "happy-mirrors-dream", "heavy-loops-cut", "heavy-trams-fix", @@ -354,6 +356,7 @@ "olive-poems-visit", "open-hotels-remain", "petite-months-allow", + "platform-crypto-service", "plenty-moons-pull", "polite-brooms-tickle", "polite-pigs-speak", @@ -392,6 +395,7 @@ "ripe-lies-battle", "rpc-middleware-provides-fix", "schema-as-class", + "schema-asserts-signature", "schema-clean-up-additionalProperties", "schema-datetime-utc-from-string", "schema-decoding-defaults-services", @@ -415,6 +419,7 @@ "shiny-trains-hug", "short-cows-relate", "short-foxes-admire", + "shy-cycles-flow", "shy-geckos-sniff", "silent-geckos-matter", "silent-needles-design", @@ -445,6 +450,7 @@ "soft-seals-allow", "solid-doors-ring", "solid-items-tease", + "solid-towns-smoke", "sour-canyons-rescue", "sparkly-bears-act", "sparkly-coins-sit", diff --git a/.changeset/schema-asserts-signature.md b/.changeset/schema-asserts-signature.md new file mode 100644 index 0000000000..173f65875f --- /dev/null +++ b/.changeset/schema-asserts-signature.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Change `Schema.asserts` and `SchemaParser.asserts` to assert a value directly with `asserts(schema, input)` and remove `Schema.Codec.ToAsserts`. diff --git a/.github/workflows/bundle-comment.yml b/.github/workflows/bundle-comment.yml index 15a8973ad0..3ce7040e53 100644 --- a/.github/workflows/bundle-comment.yml +++ b/.github/workflows/bundle-comment.yml @@ -24,6 +24,8 @@ jobs: - name: Download Artifact uses: actions/download-artifact@v8 with: + name: bundle-stats + path: bundle-stats run-id: ${{ github.event.workflow_run.id }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: Get stats @@ -31,7 +33,7 @@ jobs: run: | { echo 'stats<> $GITHUB_OUTPUT # https://github.com/orgs/community/discussions/25220#discussioncomment-11300118 diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 3c778fcb66..a16b645ad5 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -104,7 +104,9 @@ jobs: if: github.event_name == 'pull_request' uses: actions/upload-artifact@v4 with: + name: bundle-stats path: stats.txt + if-no-files-found: error test: name: Test @@ -146,33 +148,6 @@ jobs: if: matrix.runtime == 'Deno' run: deno task test --shard ${{ matrix.shard }} - jsdoc-analysis: - name: JSDoc Analysis - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - permissions: - contents: read - timeout-minutes: 5 - steps: - - uses: actions/checkout@v6 - - name: Install node - uses: actions/setup-node@v6 - - name: Generate JSDoc Analysis - shell: bash - run: | - # Run JSDoc analysis and format output for GitHub - node scripts/analyze-jsdoc.mjs > jsdoc-analysis.txt 2>&1 || true - echo "" > jsdoc-stats.md - echo "" >> jsdoc-stats.md - echo "\`\`\`" >> jsdoc-stats.md - cat jsdoc-analysis.txt >> jsdoc-stats.md - echo "\`\`\`" >> jsdoc-stats.md - - name: Upload JSDoc stats artifact - uses: actions/upload-artifact@v4 - with: - name: jsdoc-stats - path: jsdoc-stats.md - docgen: name: Documentation Generation runs-on: ubuntu-latest diff --git a/.github/workflows/jsdoc-analysis-comment.yml b/.github/workflows/jsdoc-analysis-comment.yml deleted file mode 100644 index 12fc3a9115..0000000000 --- a/.github/workflows/jsdoc-analysis-comment.yml +++ /dev/null @@ -1,79 +0,0 @@ -name: JSDoc Analysis Comment -on: - workflow_run: - workflows: ["Check"] - types: - - completed - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: {} - -jobs: - comment: - name: JSDoc Analysis - if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' - runs-on: ubuntu-latest - permissions: - actions: read - pull-requests: write - timeout-minutes: 1 - steps: - - name: Download Artifact - uses: actions/download-artifact@v8 - with: - run-id: ${{ github.event.workflow_run.id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Get JSDoc stats - id: stats - run: | - { - echo 'stats<> $GITHUB_OUTPUT - # https://github.com/orgs/community/discussions/25220#discussioncomment-11300118 - - name: Get PR number - id: pr-context - env: - GH_TOKEN: ${{ github.token }} - PR_TARGET_REPO: ${{ github.repository }} - PR_BRANCH: |- - ${{ - (github.event.workflow_run.head_repository.owner.login != github.event.workflow_run.repository.owner.login) - && format('{0}:{1}', github.event.workflow_run.head_repository.owner.login, github.event.workflow_run.head_branch) - || github.event.workflow_run.head_branch - }} - run: gh pr view --repo "${PR_TARGET_REPO}" "${PR_BRANCH}" --json 'number' --jq '"number=\(.number)"' >> "${GITHUB_OUTPUT}" - - name: Find Comment - id: find-comment - uses: peter-evans/find-comment@v4 - with: - issue-number: ${{ steps.pr-context.outputs.number }} - comment-author: "github-actions[bot]" - body-includes: - - name: Create Comment - id: comment - uses: peter-evans/create-or-update-comment@v5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - JSDOC_STATS: "${{ steps.stats.outputs.stats }}" - with: - comment-id: ${{ steps.find-comment.outputs.comment-id }} - issue-number: ${{ steps.pr-context.outputs.number }} - edit-mode: replace - body: | - - ## šŸ“Š JSDoc Documentation Analysis - -
- šŸ“ˆ Current Analysis Results - - ${{ env.JSDOC_STATS }} - -
- - --- - *This comment is automatically updated on each push. View the [analysis script](https://github.com/Effect-TS/effect-smol/blob/main/scripts/analyze-jsdoc.mjs) for details.* diff --git a/.gitignore b/.gitignore index 59fa7214c4..a1742005e5 100644 --- a/.gitignore +++ b/.gitignore @@ -15,9 +15,6 @@ node_modules/ coverage/ docs/ tmp/ -stats.html -jsdoc-analysis-results.json -jsdoc-stats.md # Generated by MacOS .DS_Store diff --git a/.specs/README.md b/.specs/README.md index 0bf40b7564..573160801e 100644 --- a/.specs/README.md +++ b/.specs/README.md @@ -1,3 +1,4 @@ # Specifications - [Effect SQL UniqueViolation SqlError Reason](./effect-sql-unique-violation.md) — Adds a `UniqueViolation` SQL error reason with a `constraint` property and updates driver classification for UNIQUE constraint violations. +- [Effect Platform Crypto Service](./effect-platform-crypto.md) — Adds a platform-agnostic `Crypto` service for cryptographic random bytes, native UUIDv4 generation, and digest operations. diff --git a/.specs/effect-platform-crypto.md b/.specs/effect-platform-crypto.md new file mode 100644 index 0000000000..04a7ba2183 --- /dev/null +++ b/.specs/effect-platform-crypto.md @@ -0,0 +1,338 @@ +# Effect Platform Crypto Service Specification + +## Summary + +Add a platform-agnostic `Crypto` service to `effect`. The service provides cryptographic random bytes, cryptographically secure random generators, UUIDv4 / UUIDv7 generation, and digest operations through an Effect service interface, with platform-specific implementations supplied by runtime packages such as `@effect/platform-node`, `@effect/platform-bun`, and `@effect/platform-browser`. + +The service replaces `Random.nextUUIDv4`. UUIDv4 and UUIDv7 generation are exposed on the `Crypto` service and derived by the `make` constructor from the primitive `randomBytes` implementation. UUIDv7 also uses the `Clock` service for the Unix millisecond timestamp. + +## Background and Research + +Platform services in this repository follow the pattern used by `FileSystem`: + +- Core service abstraction: `packages/effect/src/FileSystem.ts`. +- Node wrapper: `packages/platform-node/src/NodeFileSystem.ts`. +- Bun wrapper: `packages/platform-bun/src/BunFileSystem.ts`. +- Shared Node/Bun implementation: `packages/platform-node-shared/src/NodeFileSystem.ts`. + +Important conventions from that implementation: + +- The service interface lives in `effect` and carries a unique `TypeId` property. +- The service tag is created with `Context.Service`. +- Platform-specific packages expose `layer` values that provide the service. +- Node and Bun can share implementation code when Bun supports the relevant Node API. +- Generated `index.ts` barrel files must not be edited manually; run `pnpm codegen` after adding modules. +- Platform errors should use `PlatformError` where possible. +- Library implementation code should avoid `async` / `await` and `try` / `catch`, using Effect APIs instead. + +The current base `Random` service is not cryptographically secure because its default implementation is based on `Math.random`. `Crypto` extends the `Random` service shape so platform crypto implementations can expose cryptographically secure random number generation through `Crypto` service methods. + +## User Requirements and Clarifications + +- Add a platform-agnostic `Crypto` Effect service. +- `Crypto` must extend the `Random` service. +- Include UUIDv4 and UUIDv7 generators so users can stop using `Random.nextUUIDv4`. +- `randomUUIDv4` must be a service method derived by `make`, not a primitive required from platform implementations. +- `randomUUIDv4` must format bytes from the service's `randomBytes(16)` and follow UUIDv4 version/variant requirements. +- `randomUUIDv7` must be a service method derived by `make`, format bytes from `randomBytes(16)`, use `Clock.currentTimeMillis` for the 48-bit Unix millisecond timestamp, and follow UUIDv7 version/variant requirements. +- Define `DigestAlgorithm` as a string literal union. +- Add cryptographically secure counterparts to the `Random` module generators as service methods with clearer names: `random`, `randomBoolean`, `randomInt`, `randomBetween`, `randomIntBetween`, `randomShuffle`, `randomUUIDv4`, and `randomUUIDv7`. +- Keep the initial `Crypto` surface limited to random bytes, random generators, UUID generation, and digests. + +## Proposed Public API + +Add `packages/effect/src/Crypto.ts`. + +```ts +import * as Context from "./Context.ts" +import * as Clock from "./Clock.ts" +import type * as Effect from "./Effect.ts" +import type { PlatformError } from "./PlatformError.ts" +import type * as Random from "./Random.ts" + +const TypeId = "~effect/platform/Crypto" + +export type DigestAlgorithm = "SHA-1" | "SHA-256" | "SHA-384" | "SHA-512" + +export interface Crypto extends Random.Random { + readonly [TypeId]: typeof TypeId + + readonly randomBytes: ( + size: number + ) => Effect.Effect + + readonly digest: ( + algorithm: DigestAlgorithm, + data: Uint8Array + ) => Effect.Effect + + readonly random: Effect.Effect + readonly randomBoolean: Effect.Effect + readonly randomInt: Effect.Effect + readonly randomBetween: (min: number, max: number) => Effect.Effect + readonly randomIntBetween: ( + min: number, + max: number, + options?: { readonly halfOpen?: boolean | undefined } + ) => Effect.Effect + readonly randomShuffle: (elements: Iterable) => Effect.Effect> + readonly randomUUIDv4: Effect.Effect + readonly randomUUIDv7: Effect.Effect +} + +export const Crypto: Context.Service = Context.Service("effect/platform/Crypto") + +export const make: (impl: Omit) => Crypto +``` + +## Functional Requirements + +### Core `Crypto` Module + +1. Add `packages/effect/src/Crypto.ts`. +2. Define `TypeId` as `"~effect/platform/Crypto"`. +3. Define `DigestAlgorithm` as a top-level string literal union. +4. Use Web Crypto algorithm names `"SHA-1"`, `"SHA-256"`, `"SHA-384"`, and `"SHA-512"`. +5. Do not require platform implementations to provide derived random generator helpers. +6. Define `Crypto` as an extension of `Random.Random` with `randomBytes` and `digest`. +7. Include `randomUUIDv4`, `randomUUIDv7`, and the random generator helpers on the `Crypto` service interface. +8. Define the service tag with `Context.Service("effect/platform/Crypto")`. +9. Do not add top-level accessors; users should retrieve the service and call its methods. +10. Add cryptographically secure random generator helpers matching the `Random` module capabilities with clearer names to the service interface. +11. Derive service `randomUUIDv4` from `randomBytes(16)`. +12. Derive service `randomUUIDv7` from `Clock.currentTimeMillis` and `randomBytes(16)`. +13. Keep the core module platform-agnostic; it must not import `node:crypto` or rely directly on `globalThis.crypto`. +14. The `make` helper should accept the primitive implementation and derive random helper methods, similar to `ChildProcessSpawner.make`. + +### Random Generator Requirements + +1. `random` must use `Crypto.nextDoubleUnsafe`. +2. `randomBoolean` must use `Crypto.nextDoubleUnsafe() > 0.5`. +3. `randomInt` must use `Crypto.nextIntUnsafe`. +4. `randomBetween` must follow `Random.nextBetween` semantics. +5. `randomIntBetween` must follow `Random.nextIntBetween` semantics, including `halfOpen`. +6. `randomShuffle` must follow `Random.shuffle` semantics. +7. The random generator helper names must be more descriptive than the base `Random` names. + +### UUIDv4 Requirements + +1. `randomUUIDv4` must return a lowercase UUIDv4 string. +2. It must satisfy the standard UUIDv4 shape: `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`, where `y` is one of `8`, `9`, `a`, or `b`. +3. It must call `randomBytes(16)` and format those bytes according to UUIDv4 rules. +4. It must set the version bits to `0100` in byte 6. +5. It must set the variant bits to `10` in byte 8. +6. It must not use `Random`, `Math.random`, `Date.now`, `new Date`, or platform `crypto.randomUUID`. + +### UUIDv7 Requirements + +1. `randomUUIDv7` must return a lowercase UUIDv7 string. +2. It must satisfy the standard UUIDv7 shape: `xxxxxxxx-xxxx-7xxx-yxxx-xxxxxxxxxxxx`, where `y` is one of `8`, `9`, `a`, or `b`. +3. Bytes 0 through 5 must encode the current Unix timestamp in milliseconds from `Clock.currentTimeMillis` as a big-endian 48-bit integer. +4. Remaining UUID bits must come from `randomBytes(16)` except for overwritten timestamp, version, and variant bits. +5. It must set the version bits to `0111` in byte 6. +6. It must set the variant bits to `10` in byte 8. +7. It must not use `Random`, `Math.random`, `Date.now`, `new Date`, or platform `crypto.randomUUID`. + +### Random Bytes Requirements + +1. `randomBytes(size)` must generate cryptographically secure random bytes. +2. `size` must be a safe non-negative integer. +3. Invalid sizes must fail with `PlatformError.badArgument`. +4. `randomBytes(0)` should succeed with an empty `Uint8Array`. +5. Browser implementations must respect the `crypto.getRandomValues` per-call limit by chunking large requests, typically at `65_536` bytes. +6. Returned byte arrays should be fresh arrays owned by the caller. + +### Digest Requirements + +1. `digest(algorithm, data)` must compute a cryptographic digest of `data`. +2. The returned value must be a `Uint8Array`. +3. The input `data` must not be mutated. +4. Platform failures must be represented as `PlatformError` values. +5. SHA-1 is included for compatibility with Web Crypto and existing protocols, but documentation must state that SHA-1 should not be used for new security-sensitive designs. + +## Platform Implementation Plan + +### Node and Bun Shared Implementation + +Add `packages/platform-node-shared/src/NodeCrypto.ts`. + +Requirements: + +1. Import `node:crypto`. +2. Implement `randomBytes` with `crypto.randomBytes`. +3. Implement `nextIntUnsafe` and `nextDoubleUnsafe` with synchronous cryptographically secure random bytes. +4. Implement `digest` with `crypto.webcrypto.subtle.digest` or equivalent Node crypto APIs. +5. Convert backend exceptions and promise rejections to `PlatformError`. +6. Export `layer: Layer.Layer`. + +Add thin runtime wrappers: + +- `packages/platform-node/src/NodeCrypto.ts` re-exports `NodeCrypto.layer`. +- `packages/platform-bun/src/BunCrypto.ts` re-exports the shared layer if Bun compatibility is sufficient. + +### Browser Implementation + +Add `packages/platform-browser/src/BrowserCrypto.ts`. + +Requirements: + +1. Use `globalThis.crypto` as the backend. +2. Implement `randomBytes` with `crypto.getRandomValues`, chunking large requests. +3. Implement `nextIntUnsafe` and `nextDoubleUnsafe` with synchronous `crypto.getRandomValues`. +4. Implement `digest` with `crypto.subtle.digest`. +5. Fail with a clear `PlatformError.systemError({ _tag: "Unknown" })` when required crypto capabilities are unavailable. +6. Do not require Node types or Node imports. + +## Service Aggregation + +Update Node service aggregation: + +- Add `Crypto` to `packages/platform-node/src/NodeServices.ts` imports and `NodeServices` union. +- Merge `NodeCrypto.layer` into `NodeServices.layer`. + +Update Bun service aggregation: + +- Add `Crypto` to `packages/platform-bun/src/BunServices.ts` imports and `BunServices` union. +- Merge `BunCrypto.layer` into `BunServices.layer`. + +No browser aggregate service module currently exists. Do not introduce one solely for this task unless maintainers request it. + +## Random Migration Plan + +1. Remove `Random.nextUUIDv4`. +2. Update `Random` module documentation to remove UUID examples. +3. Update `packages/effect/test/Random.test.ts` to remove UUID-specific tests. +4. Add tests and documentation showing `Crypto.Crypto` service `randomUUIDv4` as the replacement. +5. Document that the base `Random` service is not cryptographically secure and should not be used for security-sensitive values. + +## Documentation Requirements + +Add detailed JSDoc to `packages/effect/src/Crypto.ts` and update `packages/effect/src/Random.ts`. + +The documentation must explain: + +1. `Crypto` is for cryptographic randomness and cryptographic operations. +2. The base `Random` service is not cryptographically secure. +3. `Random.withSeed` provides deterministic replacement for repeatability; predictable seeds are not cryptographically secure. +4. UUID generation should use the `Crypto` service's `randomUUIDv4` or `randomUUIDv7`, not `Random.nextUUIDv4`. +5. Platform implementations must be provided through layers. +6. SHA-1 is available only for compatibility and should be avoided for new security-sensitive designs. + +## Testing Requirements + +### Core Tests + +Add `packages/effect/test/Crypto.test.ts`. + +Test cases: + +1. `DigestAlgorithm` supports the expected string literal values. +2. `randomBytes` delegates to the provided service. +3. Random generator methods derive from the `Random` methods on the provided `Crypto` service. +4. `randomUUIDv4` formats bytes from the provided service's `randomBytes` method. +5. `randomUUIDv7` formats bytes from the provided service's `randomBytes` method and the `Clock` timestamp. +6. `digest` delegates to the service. +7. A custom `Crypto` service can be provided via `Effect.provideService`. + +### Node Tests + +Add `packages/platform-node/test/NodeCrypto.test.ts`. + +Test cases: + +1. `randomBytes(0)` returns an empty `Uint8Array`. +2. `randomBytes(32)` returns 32 bytes. +3. Invalid sizes fail with `PlatformError`. +4. Service `randomUUIDv4` returns a valid UUIDv4 string when provided with the Node layer. +5. Service `randomUUIDv7` returns a valid UUIDv7 string with the current `Clock` timestamp when provided with the Node layer. +6. Two UUIDs generated from Node cryptographic random bytes are not equal in a basic smoke test. +7. SHA-256 digest of a known input matches the known vector. + +### Browser Tests + +Add `packages/platform-browser/test/BrowserCrypto.test.ts`. + +Test cases: + +1. `randomBytes` delegates to `getRandomValues` and handles chunking. +2. Service `randomUUIDv4` formats bytes from browser `getRandomValues`. +3. Service `randomUUIDv7` formats bytes from browser `getRandomValues` and the `Clock` timestamp. +4. Missing crypto capabilities fail with `PlatformError`. +5. SHA-256 digest matches a known vector if `crypto.subtle` is available. + +## Generated Files + +After adding modules, run `pnpm codegen`. + +Expected generated barrel updates: + +- `packages/effect/src/index.ts`. +- `packages/platform-node/src/index.ts`. +- `packages/platform-bun/src/index.ts`. +- `packages/platform-browser/src/index.ts`. + +Do not manually edit generated barrel files. + +## Changeset Requirements + +Add a changeset covering at least: + +- `effect`. +- `@effect/platform-node`. +- `@effect/platform-node-shared`. +- `@effect/platform-bun`. +- `@effect/platform-browser`. + +The changeset must state: + +- A new platform-agnostic `Crypto` service was added. +- `Crypto` extends `Random` and exposes cryptographically secure random generator helpers. +- `Crypto` service `randomUUIDv4` formats bytes from the platform `Crypto` service. +- `Crypto` service `randomUUIDv7` formats bytes from the platform `Crypto` service and uses the `Clock` service timestamp. +- `DigestAlgorithm` is represented as a string literal union. +- Users should migrate away from `Random.nextUUIDv4` for UUID generation. + +## Validation Plan + +Run validation in this order: + +1. `pnpm lint-fix`. +2. `pnpm codegen`. +3. `pnpm test packages/effect/test/Crypto.test.ts`. +4. `pnpm test packages/platform-node/test/NodeCrypto.test.ts`. +5. `pnpm test packages/platform-browser/test/BrowserCrypto.test.ts`. +6. `pnpm test packages/effect/test/Random.test.ts`. +7. `pnpm check:tsgo`. +8. If `pnpm check:tsgo` repeatedly fails due to stale caches, run `pnpm clean` and rerun `pnpm check:tsgo`. +9. For localized `effect` package documentation changes, run `cd packages/effect && pnpm docgen`. + +## Acceptance Criteria + +1. `Crypto` is available from `effect/Crypto`. +2. `DigestAlgorithm` is a top-level string literal union. +3. `Crypto` extends the base `Random` service type. +4. Service `randomUUIDv4` derives UUIDs from service `randomBytes(16)` and formats UUIDv4 version/variant bits correctly. +5. The service interface contains `randomUUIDv4` and `randomUUIDv7` as derived methods. +6. `randomBytes` validates sizes and returns cryptographically secure random bytes. +7. `digest` supports SHA-1, SHA-256, SHA-384, and SHA-512 through `DigestAlgorithm` values. +8. The `Crypto` service exposes `random`, `randomBoolean`, `randomInt`, `randomBetween`, `randomIntBetween`, `randomShuffle`, `randomUUIDv4`, and `randomUUIDv7`. +9. Platform failures are represented as `PlatformError` values. +10. Core and platform tests pass. +11. JSDoc examples compile with docgen. +12. Generated barrel files are regenerated with `pnpm codegen`. +13. A changeset documents the new service and UUID migration guidance. + +## Risks and Mitigations + +1. Risk: Formatting UUIDv4 in the core module could set version or variant bits incorrectly. + - Mitigation: Add tests with deterministic bytes and validate the UUIDv4 shape. +2. Risk: Browser `getRandomValues` is unavailable in some environments. + - Mitigation: Fail with a structured `PlatformError` for effectful operations. +3. Risk: `DigestAlgorithm` string inputs invite typos or inconsistent algorithm spelling. + - Mitigation: Use a top-level string literal union with Web Crypto algorithm names and centralize platform mapping. +4. Risk: Existing users rely on deterministic UUIDs from `Random.nextUUIDv4` in tests. + - Mitigation: Document that deterministic IDs should be provided through a fake `Crypto` service or an explicit test service. + +## Open Questions + +None. The user clarified that `Random.nextUUIDv4` should be removed and the initial crypto surface should remain limited. PR review clarified that `DigestAlgorithm` should be a string literal union and that random helpers should live on the service interface. diff --git a/MIGRATION.md b/MIGRATION.md index ff59832f4a..902888a685 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -60,9 +60,9 @@ minimal Effect program bundles to ~6.3 KB (minified + gzipped). With Schema, ## Migration Guides -### Imports +### Import and API Rename Maps -- [v3 to v4 Import Map](./migration/v3-to-v4-import-map.md) +- [v3 to v4 Import and API Rename Maps](./migration/v3-to-v4.md) ### Core diff --git a/migration/schema.md b/migration/schema.md index fc20fddd53..da26e692be 100644 --- a/migration/schema.md +++ b/migration/schema.md @@ -38,6 +38,7 @@ This document maps v3 Schema APIs to their v4 equivalents. Simple renames and ar | `encode` | `encodeEffect` | rename | | `encodeUnknownEither` | `encodeUnknownExit` | rename | | `encodeEither` | `encodeExit` | rename | +| `asserts(schema)(input)` | `asserts(schema, input)` | semi-auto | | `Literal(null)` | `Null` | restructure | | `Literal("a", "b")` | `Literals(["a", "b"])` | variadic-to-array | | `pickLiteral("a", "b")` | `Literals(...).pick(["a", "b"])` | restructure | @@ -99,6 +100,29 @@ Note: `positive`, `negative`, `nonNegative`, `nonPositive` have been removed in ## Detailed migrations +### asserts signature + +**Migration: semi-auto** + +`Schema.asserts` now asserts an input directly instead of returning an assertion function. + +v3 + +```ts +import { Schema } from "effect" + +const assertString = Schema.asserts(Schema.String) +assertString(input) +``` + +v4 + +```ts +import { Schema } from "effect" + +Schema.asserts(Schema.String, input) +``` + ### validate* removal **Migration: removed** diff --git a/migration/v3-to-v4-import-map.md b/migration/v3-to-v4.md similarity index 91% rename from migration/v3-to-v4-import-map.md rename to migration/v3-to-v4.md index d5db37d334..5e3f3c7718 100644 --- a/migration/v3-to-v4-import-map.md +++ b/migration/v3-to-v4.md @@ -1,13 +1,14 @@ -# v3 to v4 Import Map +# v3 to v4 Import and API Rename Maps Mapped modules: 290 No counterpart: 43 +API renames: 53 -This file is intended for migration agents. The primary map is expressed in -terms of user-facing import specifiers, not repository file paths. +This file is intended for migration agents. It contains user-facing import +specifier mappings and API rename mappings. -Use the import map when rewriting user code. The map intentionally avoids -repository file paths so it can be used directly against import declarations. +Use the import map when rewriting import declarations. Use the API renames when +rewriting renamed symbols. ## Import Map @@ -357,3 +358,64 @@ effect/unstable/reactivity/AtomRpc (barrel: effect/unstable/reactivity) effect/unstable/reactivity/Hydration (barrel: effect/unstable/reactivity) effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) ``` + +## API Renames + +Each line is `v3 API -> v4 API`. Use these mappings when rewriting renamed +symbols from v3 source code to v4. + +```text +Effect.async -> Effect.callback +Effect.zipRight -> Effect.andThen +Effect.zipLeft -> Effect.tap +Effect.either -> Effect.result +Effect.catchAll -> Effect.catch +Effect.catchAllCause -> Effect.catchCause +Effect.catchAllDefect -> Effect.catchDefect +Effect.catchSome -> Effect.catchIf +Effect.catchIf -> Effect.catchIf +Effect.optionFromOptional -> Effect.catchNoSuchElement +Effect.catchSomeCause -> Effect.catchCauseIf +Effect.tapErrorCause -> Effect.tapCause +Effect.ignoreLogged -> Effect.ignore +Effect.makeLatchUnsafe -> Latch.makeUnsafe +Effect.makeLatch -> Latch.make +Layer.scoped -> Layer.effect +Layer.scopedDiscard -> Layer.effectDiscard +Layer.tapErrorCause -> Layer.tapCause +Mailbox -> Queue.Queue +Mailbox.make -> Queue.make +Either -> Result.Result +Either.right -> Result.succeed +Either.left -> Result.fail +Scope.extend -> Scope.provide +Effect.makeSemaphoreUnsafe -> Semaphore.makeUnsafe +Effect.makeSemaphore -> Semaphore.make +Stream.Context -> Stream.Services +StreamHaltStrategy.HaltStrategy -> Stream.HaltStrategy +Stream.repeatEffect -> Stream.fromEffectRepeat +Stream.repeatEffectWithSchedule -> Stream.fromEffectSchedule +Stream.async -> Stream.callback +Stream.asyncEffect -> Stream.callback +Stream.asyncPush -> Stream.callback +Stream.asyncScoped -> Stream.callback +Stream.repeatEffectChunk -> Stream.fromIterableEffectRepeat +Stream.fromChunk -> Stream.fromArray +Stream.fromChunks -> Stream.fromArrays +Stream.mapChunks -> Stream.mapArray +Stream.mapChunksEffect -> Stream.mapArrayEffect +Stream.either -> Stream.result +Stream.flattenChunks -> Stream.flattenArray +Stream.flattenIterables -> Stream.flattenIterable +Stream.mergeEither -> Stream.mergeResult +Stream.zipWithChunks -> Stream.zipWithArray +Stream.bufferChunks -> Stream.bufferArray +Stream.catchAllCause -> Stream.catchCause +Stream.tapErrorCause -> Stream.tapCause +Stream.catchAll -> Stream.catch +Stream.catchSome -> Stream.catchIf +Stream.catchSomeCause -> Stream.catchCauseIf +Stream.combineChunks -> Stream.combineArray +provideSomeLayer -> Stream.provide +provideSomeContext -> Stream.provide +``` diff --git a/package.json b/package.json index 11b0275ff0..9edfe6f0e8 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "docgen": "pnpm --recursive --filter \"./packages/**/*\" exec docgen && node scripts/docs.mjs", "ai-docgen": "effect-ai-docgen ai-docs/src -o LLMS.md", "ai-docgen:watch": "pnpm ai-docgen --watch", - "analyze-jsdoc": "node scripts/analyze-jsdoc.mjs", "test-types": "tstyche", "changeset-version": "changeset version", "changeset-publish": "pnpm codemod && pnpm build && changeset publish", diff --git a/packages/ai/anthropic/CHANGELOG.md b/packages/ai/anthropic/CHANGELOG.md index 920608ddf4..33d81cf939 100644 --- a/packages/ai/anthropic/CHANGELOG.md +++ b/packages/ai/anthropic/CHANGELOG.md @@ -1,5 +1,12 @@ # @effect/ai-anthropic +## 4.0.0-beta.68 + +### Patch Changes + +- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: + - effect@4.0.0-beta.68 + ## 4.0.0-beta.67 ### Patch Changes diff --git a/packages/ai/anthropic/package.json b/packages/ai/anthropic/package.json index 884ce537e2..02abc27130 100644 --- a/packages/ai/anthropic/package.json +++ b/packages/ai/anthropic/package.json @@ -1,6 +1,6 @@ { "name": "@effect/ai-anthropic", - "version": "4.0.0-beta.67", + "version": "4.0.0-beta.68", "type": "module", "license": "MIT", "description": "An Anthropic provider integration for Effect AI SDK", diff --git a/packages/ai/anthropic/src/index.ts b/packages/ai/anthropic/src/index.ts index c4bd0be5b2..23e2a21150 100644 --- a/packages/ai/anthropic/src/index.ts +++ b/packages/ai/anthropic/src/index.ts @@ -15,6 +15,24 @@ export * as AnthropicClient from "./AnthropicClient.ts" /** + * The `AnthropicConfig` module provides contextual configuration for the + * Anthropic AI provider integration. It is used to customize the generated + * Anthropic HTTP client without changing individual request code. + * + * **Common tasks** + * + * - Provide a shared `HttpClient` transformation for Anthropic requests + * - Add provider-specific concerns such as request instrumentation, proxying, + * retries, or header manipulation + * - Scope a client transformation to a single effect with {@link withClientTransform} + * + * **Gotchas** + * + * - Configuration is read from the Effect context, so overrides only apply to + * effects run inside the configured scope + * - `withClientTransform` replaces the current `transformClient` value while + * preserving any other Anthropic configuration fields + * * @since 4.0.0 */ export * as AnthropicConfig from "./AnthropicConfig.ts" @@ -30,6 +48,32 @@ export * as AnthropicConfig from "./AnthropicConfig.ts" export * as AnthropicError from "./AnthropicError.ts" /** + * The `AnthropicLanguageModel` module provides the Anthropic implementation of + * Effect AI's `LanguageModel` service. It turns Effect AI prompts, tools, files, + * reasoning parts, and provider options into Anthropic Messages API requests, + * and converts Anthropic responses and streams back into Effect AI response + * parts with Anthropic-specific metadata. + * + * **Common tasks** + * + * - Create an Anthropic-backed model with {@link model} + * - Build or provide a `LanguageModel.LanguageModel` layer with {@link layer} + * or {@link make} + * - Supply default request options through {@link Config} + * - Override configuration for a scoped operation with {@link withConfigOverride} + * - Attach Anthropic provider options for prompt caching, document citations, + * reasoning signatures, MCP metadata, and server-side tools + * + * **Gotchas** + * + * - Prompt files are translated to Anthropic image or document blocks; only the + * supported media types can be sent to the provider. + * - Structured output support depends on the selected Claude model, so this + * module may use Anthropic's native structured output or fall back to a JSON + * response tool. + * - Some features require Anthropic beta headers, which are added + * automatically from the selected tools, files, and model capabilities. + * * @since 4.0.0 */ export * as AnthropicLanguageModel from "./AnthropicLanguageModel.ts" diff --git a/packages/ai/openai-compat/CHANGELOG.md b/packages/ai/openai-compat/CHANGELOG.md index a33110b1e7..172170141e 100644 --- a/packages/ai/openai-compat/CHANGELOG.md +++ b/packages/ai/openai-compat/CHANGELOG.md @@ -1,5 +1,12 @@ # @effect/ai-openai-compat +## 4.0.0-beta.68 + +### Patch Changes + +- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: + - effect@4.0.0-beta.68 + ## 4.0.0-beta.67 ### Patch Changes diff --git a/packages/ai/openai-compat/package.json b/packages/ai/openai-compat/package.json index b922c2476c..6199cb3add 100644 --- a/packages/ai/openai-compat/package.json +++ b/packages/ai/openai-compat/package.json @@ -1,6 +1,6 @@ { "name": "@effect/ai-openai-compat", - "version": "4.0.0-beta.67", + "version": "4.0.0-beta.68", "type": "module", "license": "MIT", "description": "An OpenAI compat integration for Effect", diff --git a/packages/ai/openai-compat/src/index.ts b/packages/ai/openai-compat/src/index.ts index 794e9b66bc..33f715b744 100644 --- a/packages/ai/openai-compat/src/index.ts +++ b/packages/ai/openai-compat/src/index.ts @@ -5,11 +5,54 @@ // @barrel: Auto-generated exports. Do not edit manually. /** + * The `OpenAiClient` module provides an Effect service for calling + * OpenAI-compatible chat completions and embeddings APIs. It builds on the + * Effect HTTP client, adds authentication and OpenAI header handling, and + * exposes typed helpers for regular responses, server-sent event streaming, and + * embedding requests. + * + * **Common tasks** + * + * - Create a client service directly with {@link make} + * - Provide the service as a layer with {@link layer} or {@link layerConfig} + * - Send non-streaming chat completion requests with `createResponse` + * - Send streaming chat completion requests with `createResponseStream` + * - Generate embeddings with `createEmbedding` + * - Reuse the exported request and response types when integrating compatible providers + * + * **Gotchas** + * + * - The default base URL is `https://api.openai.com/v1`; set `apiUrl` for other + * OpenAI-compatible providers. + * - `createResponseStream` forces `stream: true` and requests usage events with + * `stream_options.include_usage`. + * - HTTP and schema decoding failures are mapped into `AiError`. + * * @since 4.0.0 */ export * as OpenAiClient from "./OpenAiClient.ts" /** + * The `OpenAiConfig` module provides shared configuration for clients that + * talk to OpenAI-compatible APIs. It is used to customize the HTTP client + * wiring around a provider without changing the higher-level model, + * embeddings, or tool-calling APIs that consume the client. + * + * **Common tasks** + * + * - Install a client transform with {@link withClientTransform} + * - Add provider-specific HTTP behavior, such as headers, retries, proxies, or + * instrumentation + * - Read the active configuration from the Effect context when implementing + * OpenAI-compatible integrations + * + * **Gotchas** + * + * - The transform receives and returns an `HttpClient`, so it should preserve + * the existing client behavior unless it intentionally replaces it + * - Configuration is provided through Effect context and is scoped to the + * effect that receives the service + * * @since 4.0.0 */ export * as OpenAiConfig from "./OpenAiConfig.ts" @@ -24,6 +67,19 @@ export * as OpenAiConfig from "./OpenAiConfig.ts" export * as OpenAiEmbeddingModel from "./OpenAiEmbeddingModel.ts" /** + * The `OpenAiError` module defines OpenAI-specific metadata that can be + * attached to the shared `AiError` error types used by the AI packages. It is + * primarily used by OpenAI-compatible clients to preserve provider details + * such as error codes, error types, request IDs, and rate limit headers while + * still exposing errors through the provider-neutral Effect AI error model. + * + * Use this module when mapping OpenAI API failures into `AiError` values and + * when consumers need enough structured metadata to debug failed requests, + * inspect quota or rate limit responses, or correlate an error with OpenAI + * support. The exported types are metadata shapes only; the module augmentation + * makes those shapes available on the corresponding shared AI error metadata + * interfaces without defining new runtime error classes. + * * @since 4.0.0 */ export * as OpenAiError from "./OpenAiError.ts" diff --git a/packages/ai/openai/CHANGELOG.md b/packages/ai/openai/CHANGELOG.md index 741251b12d..09c8eb02e6 100644 --- a/packages/ai/openai/CHANGELOG.md +++ b/packages/ai/openai/CHANGELOG.md @@ -1,5 +1,12 @@ # @effect/ai-openai +## 4.0.0-beta.68 + +### Patch Changes + +- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: + - effect@4.0.0-beta.68 + ## 4.0.0-beta.67 ### Patch Changes diff --git a/packages/ai/openai/package.json b/packages/ai/openai/package.json index a30448c8f4..8afe8d5f6a 100644 --- a/packages/ai/openai/package.json +++ b/packages/ai/openai/package.json @@ -1,6 +1,6 @@ { "name": "@effect/ai-openai", - "version": "4.0.0-beta.67", + "version": "4.0.0-beta.68", "type": "module", "license": "MIT", "description": "An OpenAI provider integration for Effect AI SDK", diff --git a/packages/ai/openai/src/index.ts b/packages/ai/openai/src/index.ts index be39a81121..0b0b758cff 100644 --- a/packages/ai/openai/src/index.ts +++ b/packages/ai/openai/src/index.ts @@ -25,6 +25,22 @@ export * as OpenAiClient from "./OpenAiClient.ts" export * as OpenAiClientGenerated from "./OpenAiClientGenerated.ts" /** + * The `OpenAiConfig` module provides contextual configuration for the + * `@effect/ai-openai` integration. It is used to customize how OpenAI clients + * are built and interpreted without threading configuration through every API + * call manually. + * + * The primary use case is installing an HTTP client transform with + * {@link withClientTransform}. This lets applications adapt the underlying + * OpenAI HTTP client for cross-cutting concerns such as custom middleware, + * instrumentation, proxying, or request policy changes while keeping the + * OpenAI service APIs unchanged. + * + * Configuration is scoped through Effect's context, so transforms only apply to + * the effect they are provided to and anything evaluated inside that scope. + * When multiple transforms are needed, compose them into a single + * `HttpClient => HttpClient` function before providing the configuration. + * * @since 4.0.0 */ export * as OpenAiConfig from "./OpenAiConfig.ts" diff --git a/packages/ai/openrouter/CHANGELOG.md b/packages/ai/openrouter/CHANGELOG.md index d89acda3dc..c5b91d594c 100644 --- a/packages/ai/openrouter/CHANGELOG.md +++ b/packages/ai/openrouter/CHANGELOG.md @@ -1,5 +1,12 @@ # @effect/ai-openrouter +## 4.0.0-beta.68 + +### Patch Changes + +- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: + - effect@4.0.0-beta.68 + ## 4.0.0-beta.67 ### Patch Changes diff --git a/packages/ai/openrouter/package.json b/packages/ai/openrouter/package.json index 276bb972c1..b5dc220906 100644 --- a/packages/ai/openrouter/package.json +++ b/packages/ai/openrouter/package.json @@ -1,6 +1,6 @@ { "name": "@effect/ai-openrouter", - "version": "4.0.0-beta.67", + "version": "4.0.0-beta.68", "type": "module", "license": "MIT", "description": "An OpenRouter provider integration for Effect AI SDK", diff --git a/packages/ai/openrouter/src/index.ts b/packages/ai/openrouter/src/index.ts index cbac6769f5..6b06b47d93 100644 --- a/packages/ai/openrouter/src/index.ts +++ b/packages/ai/openrouter/src/index.ts @@ -10,11 +10,51 @@ export * as Generated from "./Generated.ts" /** + * The `OpenRouterClient` module provides an Effect service for calling + * OpenRouter's chat completions API. It wraps the generated OpenRouter HTTP + * client with Effect-native constructors, layers, typed errors, and streaming + * support. + * + * **Common tasks** + * + * - Build a client from explicit options with {@link make} + * - Provide the client to an application with {@link layer} or {@link layerConfig} + * - Create non-streaming chat completions with {@link Service.createChatCompletion} + * - Create server-sent event chat completion streams with + * {@link Service.createChatCompletionStream} + * - Customize authentication, base URL, OpenRouter ranking headers, or the + * underlying HTTP client through {@link Options} + * + * **Gotchas** + * + * - Streaming requests are sent directly to `/chat/completions` with `stream` + * and `stream_options.include_usage` enabled by this module. + * - OpenRouter API failures, HTTP client failures, and schema decoding failures + * are mapped into `AiError` values for the exported service methods. + * * @since 4.0.0 */ export * as OpenRouterClient from "./OpenRouterClient.ts" /** + * The `OpenRouterConfig` module provides contextual configuration for the + * OpenRouter provider integration. It is used to customize the HTTP client that + * backs OpenRouter requests without rebuilding the provider layer itself. + * + * Use {@link withClientTransform} when a single effect, workflow, or scoped + * portion of an application needs to add cross-cutting HTTP client behavior + * such as request logging, retries, proxy routing, additional headers, or test + * doubles. The configuration is read from the current Effect context, so the + * transform only applies where the returned effect is run with that context. + * + * **Gotchas** + * + * - Each call to {@link withClientTransform} replaces the current client + * transform for the provided effect; compose transforms manually when both + * behaviors should apply. + * - The transform receives and returns an `HttpClient`, so it should preserve + * the OpenRouter client contract while adding behavior around it. + * * @since 4.0.0 */ export * as OpenRouterConfig from "./OpenRouterConfig.ts" @@ -30,6 +70,26 @@ export * as OpenRouterConfig from "./OpenRouterConfig.ts" export * as OpenRouterError from "./OpenRouterError.ts" /** + * The `OpenRouterLanguageModel` module provides constructors for using + * OpenRouter chat completion models through the Effect AI `LanguageModel` + * interface. It adapts Effect prompts, tools, structured output schemas, file + * parts, reasoning details, cache-control hints, and telemetry annotations into + * the OpenRouter request and response formats. + * + * Use this module when an application wants to select an OpenRouter model by + * name while keeping the rest of its AI workflow provider-agnostic. The + * exported layer and model constructors install a `LanguageModel` service backed + * by `OpenRouterClient`, and `withConfigOverride` can scope per-request + * OpenRouter options such as sampling, routing, tool use, or JSON schema + * behavior. + * + * OpenRouter routes requests to many underlying providers, so model support for + * images, files, tools, structured outputs, caching, and reasoning metadata can + * vary. Provider-specific prompt and response metadata is preserved under the + * `openrouter` option namespace so multi-turn conversations can round-trip + * details such as reasoning blocks and file annotations when the selected model + * supports them. + * * @since 4.0.0 */ export * as OpenRouterLanguageModel from "./OpenRouterLanguageModel.ts" diff --git a/packages/atom/react/CHANGELOG.md b/packages/atom/react/CHANGELOG.md index 828b3b83dc..d0019a54d2 100644 --- a/packages/atom/react/CHANGELOG.md +++ b/packages/atom/react/CHANGELOG.md @@ -1,5 +1,12 @@ # @effect/atom-react +## 4.0.0-beta.68 + +### Patch Changes + +- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: + - effect@4.0.0-beta.68 + ## 4.0.0-beta.67 ### Patch Changes diff --git a/packages/atom/react/package.json b/packages/atom/react/package.json index e613bfcc7d..d239670a3b 100644 --- a/packages/atom/react/package.json +++ b/packages/atom/react/package.json @@ -1,6 +1,6 @@ { "name": "@effect/atom-react", - "version": "4.0.0-beta.67", + "version": "4.0.0-beta.68", "type": "module", "license": "MIT", "description": "React bindings for the Effect Atom modules", diff --git a/packages/atom/solid/CHANGELOG.md b/packages/atom/solid/CHANGELOG.md index f7395ca03a..3ce20a8bdf 100644 --- a/packages/atom/solid/CHANGELOG.md +++ b/packages/atom/solid/CHANGELOG.md @@ -1,5 +1,12 @@ # @effect/atom-solid +## 4.0.0-beta.68 + +### Patch Changes + +- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: + - effect@4.0.0-beta.68 + ## 4.0.0-beta.67 ### Patch Changes diff --git a/packages/atom/solid/package.json b/packages/atom/solid/package.json index 508041ecff..95cab8ec56 100644 --- a/packages/atom/solid/package.json +++ b/packages/atom/solid/package.json @@ -1,6 +1,6 @@ { "name": "@effect/atom-solid", - "version": "4.0.0-beta.67", + "version": "4.0.0-beta.68", "type": "module", "license": "MIT", "description": "SolidJS bindings for the Effect Atom modules", diff --git a/packages/atom/vue/CHANGELOG.md b/packages/atom/vue/CHANGELOG.md index c594d05de7..69113d7983 100644 --- a/packages/atom/vue/CHANGELOG.md +++ b/packages/atom/vue/CHANGELOG.md @@ -1,5 +1,12 @@ # @effect/atom-vue +## 4.0.0-beta.68 + +### Patch Changes + +- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: + - effect@4.0.0-beta.68 + ## 4.0.0-beta.67 ### Patch Changes diff --git a/packages/atom/vue/package.json b/packages/atom/vue/package.json index 3b45386cc8..83f694b2e1 100644 --- a/packages/atom/vue/package.json +++ b/packages/atom/vue/package.json @@ -1,6 +1,6 @@ { "name": "@effect/atom-vue", - "version": "4.0.0-beta.67", + "version": "4.0.0-beta.68", "type": "module", "license": "MIT", "description": "Vue bindings for the Effect Atom modules", diff --git a/packages/effect/CHANGELOG.md b/packages/effect/CHANGELOG.md index 6ad0651f2f..2e2a3e435c 100644 --- a/packages/effect/CHANGELOG.md +++ b/packages/effect/CHANGELOG.md @@ -1,5 +1,21 @@ # effect +## 4.0.0-beta.68 + +### Patch Changes + +- [#2210](https://github.com/Effect-TS/effect-smol/pull/2210) [`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217) Thanks @tim-smart! - Add Stream.broadcastN for fixed-size stream broadcasts. + +- [#2180](https://github.com/Effect-TS/effect-smol/pull/2180) [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5) Thanks @IMax153! - update Model uuid helpers + +- [#2180](https://github.com/Effect-TS/effect-smol/pull/2180) [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5) Thanks @IMax153! - Add a platform-agnostic `Crypto` service for cryptographic random bytes, secure random generators, UUIDv4 / UUIDv7 generation, and digest operations. UUID generation should now use the `Crypto` service's `randomUUIDv4` or `randomUUIDv7`, which format bytes from the platform `Crypto` service; UUIDv7 also uses the `Clock` service timestamp. `Random.nextUUIDv4` has been removed because the base `Random` service is not cryptographically secure. + +- [#2221](https://github.com/Effect-TS/effect-smol/pull/2221) [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa) Thanks @gcanti! - Change `Schema.asserts` and `SchemaParser.asserts` to assert a value directly with `asserts(schema, input)` and remove `Schema.Codec.ToAsserts`. + +- [#2209](https://github.com/Effect-TS/effect-smol/pull/2209) [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557) Thanks @tim-smart! - Fix Channel.decodeText corrupting UTF-8 characters split across chunk boundaries. + +- [#2207](https://github.com/Effect-TS/effect-smol/pull/2207) [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6) Thanks @tim-smart! - rename Model.Generated to Model.GeneratedByDb + ## 4.0.0-beta.67 ### Patch Changes diff --git a/packages/effect/package.json b/packages/effect/package.json index 520942f674..8141639c42 100644 --- a/packages/effect/package.json +++ b/packages/effect/package.json @@ -1,7 +1,7 @@ { "name": "effect", "type": "module", - "version": "4.0.0-beta.67", + "version": "4.0.0-beta.68", "license": "MIT", "description": "The missing standard library for TypeScript, for writing production-grade software.", "homepage": "https://effect.website", diff --git a/packages/effect/src/Crypto.ts b/packages/effect/src/Crypto.ts new file mode 100644 index 0000000000..18bb477e75 --- /dev/null +++ b/packages/effect/src/Crypto.ts @@ -0,0 +1,327 @@ +/** + * The `Crypto` module provides a platform-agnostic service for cryptographic + * operations. Runtime packages such as `@effect/platform-node`, + * `@effect/platform-bun`, and `@effect/platform-browser` provide concrete + * implementations backed by the host platform's cryptography APIs. + * + * Use `Crypto` for cryptographic randomness, UUID generation, random values, + * and message digests. The base `Random` service is not cryptographically + * secure unless you replace it with a cryptographically secure implementation. + * + * **Example** (Providing a test Crypto service) + * + * ```ts + * import { Console, Crypto, Effect, Layer } from "effect" + * + * const TestCrypto = Layer.succeed( + * Crypto.Crypto, + * Crypto.make({ + * randomBytes: (size) => new Uint8Array(size), + * digest: (_algorithm, data) => Effect.succeed(data) + * }) + * ) + * + * const program = Effect.gen(function*() { + * const crypto = yield* Crypto.Crypto + * const id = yield* crypto.randomUUIDv4 + * yield* Console.log(`Created id: ${id}`) + * }) + * + * Effect.runPromise(Effect.provide(program, TestCrypto)) + * ``` + * + * **Example** (Generating random bytes) + * + * ```ts + * import { Crypto, Effect, Layer } from "effect" + * + * const TestCrypto = Layer.succeed( + * Crypto.Crypto, + * Crypto.make({ + * randomBytes: (size) => new Uint8Array(size), + * digest: (_algorithm, data) => Effect.succeed(data) + * }) + * ) + * + * const program = Effect.gen(function*() { + * const crypto = yield* Crypto.Crypto + * return yield* crypto.randomBytes(32) + * }) + * + * Effect.runPromise(Effect.provide(program, TestCrypto)) + * ``` + * + * @since 4.0.0 + */ +import * as Context from "./Context.ts" +import * as Effect from "./Effect.ts" +import * as PlatformError from "./PlatformError.ts" + +const TypeId = "~effect/platform/Crypto" + +/** + * Digest algorithms supported by the platform `Crypto` service. + * + * SHA-1 is included for interoperability with existing protocols. Do not use + * SHA-1 for new security-sensitive designs. + * + * **Example** (Using a digest algorithm) + * + * ```ts + * import { Crypto } from "effect" + * + * const algorithm: Crypto.DigestAlgorithm = "SHA-256" + * ``` + * + * @category models + * @since 4.0.0 + */ +export type DigestAlgorithm = "SHA-1" | "SHA-256" | "SHA-384" | "SHA-512" + +/** + * Platform-agnostic cryptographic operations. + * + * `Crypto` implementations must use cryptographically secure platform APIs. + * The random generator helpers are derived by the `make` constructor from + * the random methods on this service. + * + * **Example** (Using cryptographic operations) + * + * ```ts + * import { Crypto, Effect, Layer } from "effect" + * + * const TestCrypto = Layer.succeed( + * Crypto.Crypto, + * Crypto.make({ + * randomBytes: (size) => new Uint8Array(size), + * digest: (_algorithm, data) => Effect.succeed(data) + * }) + * ) + * + * const program = Effect.gen(function*() { + * const crypto = yield* Crypto.Crypto + * const bytes = yield* crypto.randomBytes(16) + * const uuidv4 = yield* crypto.randomUUIDv4 + * const uuidv7 = yield* crypto.randomUUIDv7 + * const hash = yield* crypto.digest("SHA-256", bytes) + * return { uuidv4, uuidv7, hash } + * }) + * + * Effect.runPromise(Effect.provide(program, TestCrypto)) + * ``` + * + * @category models + * @since 4.0.0 + */ +export interface Crypto { + readonly [TypeId]: typeof TypeId + + /** + * Generates a random integer in the range Number.MIN_SAFE_INTEGER to + * Number.MAX_SAFE_INTEGER (both inclusive). + */ + nextIntUnsafe(): number + + /** + * Generates a random number in the range 0 (inclusive) to 1 (exclusive). + */ + nextDoubleUnsafe(): number + + /** + * Generates cryptographically secure random bytes. + */ + randomBytes(size: number): Effect.Effect + + /** + * Computes a cryptographic digest for the supplied data. + */ + digest( + algorithm: DigestAlgorithm, + data: Uint8Array + ): Effect.Effect + + /** + * Generates a cryptographically secure random number between 0 (inclusive) + * and 1 (exclusive). + */ + readonly random: Effect.Effect + + /** + * Generates a cryptographically secure random boolean. + */ + readonly randomBoolean: Effect.Effect + + /** + * Generates a cryptographically secure random integer between + * `Number.MIN_SAFE_INTEGER` and `Number.MAX_SAFE_INTEGER` (both inclusive). + */ + readonly randomInt: Effect.Effect + + /** + * Generates a cryptographically secure random number between `min` + * (inclusive) and `max` (exclusive). + */ + randomBetween(min: number, max: number): Effect.Effect + + /** + * Generates a cryptographically secure random integer between `min` and `max`. + * + * The lower bound is rounded up with `Math.ceil` and the upper bound is + * rounded down with `Math.floor`. By default the range is inclusive; set + * `options.halfOpen: true` to exclude the upper bound. + */ + randomIntBetween(min: number, max: number, options?: { + readonly halfOpen?: boolean | undefined + }): Effect.Effect + + /** + * Uses the cryptographically secure random generator to shuffle the supplied + * iterable. + */ + randomShuffle(elements: Iterable): Effect.Effect> + + /** + * Generates a cryptographically secure UUIDv4 string. + */ + readonly randomUUIDv4: Effect.Effect + + /** + * Generates a cryptographically secure UUIDv7 string. + */ + readonly randomUUIDv7: Effect.Effect +} + +/** + * The service identifier for the platform `Crypto` service. + * + * @category services + * @since 4.0.0 + */ +export const Crypto: Context.Service = Context.Service("effect/Crypto") + +/** + * Creates a `Crypto` service from the primitive implementation, deriving the + * random generator helpers and UUID generation from those primitives. + * + * **Example** (Creating a Crypto service) + * + * ```ts + * import { Crypto, Effect, Layer } from "effect" + * + * const TestCrypto = Layer.succeed( + * Crypto.Crypto, + * Crypto.make({ + * randomBytes: (size) => new Uint8Array(size), + * digest: (_algorithm, data) => Effect.succeed(data) + * }) + * ) + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const make = ( + impl: { + readonly randomBytes: (size: number) => Uint8Array + readonly digest: ( + algorithm: DigestAlgorithm, + data: Uint8Array + ) => Effect.Effect + } +): Crypto => { + const randomBytesUnsafe = impl.randomBytes + + const randomBytes: Crypto["randomBytes"] = (size) => Effect.map(validateSize("randomBytes", size), randomBytesUnsafe) + + const nextDoubleUnsafe = (): number => { + const bytes = randomBytesUnsafe(7) + const value = ((bytes[0] & 0x1f) * 2 ** 48) + (bytes[1] * 2 ** 40) + (bytes[2] * 2 ** 32) + + (bytes[3] * 2 ** 24) + (bytes[4] * 2 ** 16) + (bytes[5] * 2 ** 8) + bytes[6] + return value / 2 ** 53 + } + + const nextIntUnsafe = (): number => + Math.floor(nextDoubleUnsafe() * (Number.MAX_SAFE_INTEGER - Number.MIN_SAFE_INTEGER + 1)) + Number.MIN_SAFE_INTEGER + + return Crypto.of({ + [TypeId]: TypeId, + randomBytes, + nextDoubleUnsafe, + nextIntUnsafe, + digest: impl.digest, + random: Effect.sync(() => nextDoubleUnsafe()), + randomBoolean: Effect.sync(() => nextDoubleUnsafe() > 0.5), + randomInt: Effect.sync(() => nextIntUnsafe()), + randomBetween: (min, max) => Effect.sync(() => nextDoubleUnsafe() * (max - min) + min), + randomIntBetween(min, max, options) { + const extra = options?.halfOpen === true ? 0 : 1 + return Effect.sync(() => { + const minInt = Math.ceil(min) + const maxInt = Math.floor(max) + return Math.floor(nextDoubleUnsafe() * (maxInt - minInt + extra)) + minInt + }) + }, + randomShuffle: (elements) => + Effect.sync(() => { + const buffer = Array.from(elements) + for (let i = buffer.length - 1; i >= 1; i = i - 1) { + const index = Math.min(i, Math.floor(nextDoubleUnsafe() * (i + 1))) + const value = buffer[i]! + buffer[i] = buffer[index]! + buffer[index] = value + } + return buffer + }), + randomUUIDv4: Effect.sync(() => formatUUIDv4(randomBytesUnsafe(16))), + randomUUIDv7: Effect.clockWith((clock) => + Effect.succeed(formatUUIDv7(clock.currentTimeMillisUnsafe(), randomBytesUnsafe(16))) + ) + }) +} + +const validateSize = (method: string, size: number): Effect.Effect => + Number.isSafeInteger(size) && size >= 0 + ? Effect.succeed(size) + : Effect.fail(PlatformError.badArgument({ + module: "Crypto", + method, + description: "size must be a non-negative safe integer" + })) + +const hex = (byte: number): string => byte.toString(16).padStart(2, "0") + +const formatUUID = (bytes: Uint8Array): string => { + const segments = [ + bytes.subarray(0, 4), + bytes.subarray(4, 6), + bytes.subarray(6, 8), + bytes.subarray(8, 10), + bytes.subarray(10, 16) + ] + + return segments.map((segment) => Array.from(segment, hex).join("")).join("-") +} + +const formatUUIDv4 = (bytes: Uint8Array): string => { + bytes[6] = (bytes[6] & 0x0f) | 0x40 + bytes[8] = (bytes[8] & 0x3f) | 0x80 + + return formatUUID(bytes) +} + +const maxUUIDv7Timestamp = 2 ** 48 - 1 + +const formatUUIDv7 = (timestampMillis: number, bytes: Uint8Array): string => { + const timestamp = Math.min(Math.max(0, Math.trunc(timestampMillis)), maxUUIDv7Timestamp) + + bytes[0] = Math.floor(timestamp / 2 ** 40) + bytes[1] = Math.floor(timestamp / 2 ** 32) & 0xff + bytes[2] = Math.floor(timestamp / 2 ** 24) & 0xff + bytes[3] = Math.floor(timestamp / 2 ** 16) & 0xff + bytes[4] = Math.floor(timestamp / 2 ** 8) & 0xff + bytes[5] = timestamp & 0xff + bytes[6] = (bytes[6] & 0x0f) | 0x70 + bytes[8] = (bytes[8] & 0x3f) | 0x80 + + return formatUUID(bytes) +} diff --git a/packages/effect/src/Effect.ts b/packages/effect/src/Effect.ts index 4351d869bd..402a90ae0e 100644 --- a/packages/effect/src/Effect.ts +++ b/packages/effect/src/Effect.ts @@ -1168,12 +1168,6 @@ export { * Use `Effect.callback` when integrating APIs that complete through callbacks * instead of returning a `Promise`. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Effect.async` - * * **Example** (Usage) * * ```ts @@ -1899,12 +1893,6 @@ export const flatten: (self: Effect, E2, R2>) = * Failures or requirements from either effect are preserved in the returned * effect. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Effect.zipRight` - * * **Example** (Applying a Discount Based on Fetched Amount) * * ```ts @@ -1982,12 +1970,6 @@ export const andThen: { * next part of the chain. Note that if the side effect fails, the entire chain * will fail too. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Effect.zipLeft` - * * **Example** (Logging a step in a pipeline) * * ```ts @@ -2064,12 +2046,6 @@ export const tap: { * The resulting effect cannot fail directly because all recoverable failures * are represented inside the `Result` type. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Effect.either` - * * **Example** (Usage) * * ```ts @@ -2494,12 +2470,6 @@ export { * * @see {@link catchCause} for a version that can recover from both recoverable and unrecoverable errors. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Effect.catchAll` - * * @category error handling * @since 4.0.0 */ @@ -2973,12 +2943,6 @@ export const unwrapReason: { * they often indicate serious issues. However, in some cases, such as * dynamically loaded plugins, controlled recovery might be needed. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Effect.catchAllCause` - * * **Example** (Usage) * * ```ts @@ -3030,12 +2994,6 @@ export const catchCause: { * they often indicate serious issues. In some cases, such as dynamically loaded * plugins, controlled recovery may be needed. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Effect.catchAllDefect` - * * **Example** (Usage) * * ```ts @@ -3077,13 +3035,6 @@ export const catchDefect: { * matching. Non-matching errors re-fail with the original cause. Defects and * interrupts are not caught. * - * **Previously Known As** - * - * This API replaces the following: - * - * - `Effect.catchSome` (Effect 3.x) - * - `Effect.catchIf` - * * **Example** (Usage) * * ```ts @@ -3176,12 +3127,6 @@ export const catchFilter: { * Effect.runPromise(none).then(console.log) // { _id: 'Option', _tag: 'None' } * ``` * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Effect.optionFromOptional` - * * @category error handling * @since 4.0.0 */ @@ -3196,12 +3141,6 @@ export const catchNoSuchElement: ( * that match a specific predicate. This is useful when you want to handle * only certain types of errors while letting others propagate. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Effect.catchSomeCause` - * * **Example** (Usage) * * ```ts @@ -3488,12 +3427,6 @@ export const tapErrorTag: { * the operation succeeds, the original cause is preserved. If the operation * fails, its error is also represented in the returned effect. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Effect.tapErrorCause` - * * **Example** (Usage) * * ```ts @@ -3935,12 +3868,6 @@ export const sandbox: ( * const programWarn = task.pipe(Effect.ignore({ log: "Warn", message: "Ignoring task failure" })) * ``` * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Effect.ignoreLogged` - * * @category error handling * @since 2.0.0 */ diff --git a/packages/effect/src/Latch.ts b/packages/effect/src/Latch.ts index f6147642ac..6ec09602b1 100644 --- a/packages/effect/src/Latch.ts +++ b/packages/effect/src/Latch.ts @@ -87,12 +87,6 @@ export interface Latch { * The latch starts closed by default; pass `true` to create it open. Use this * only when synchronous allocation is required, otherwise prefer `make`. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Effect.makeLatchUnsafe` - * * **Example** (Creating a latch unsafely) * * ```ts @@ -125,12 +119,6 @@ export const makeUnsafe: (open?: boolean | undefined) => Latch = internal.makeLa * * The latch starts closed by default; pass `true` to create it open. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Effect.makeLatch` - * * **Example** (Creating a latch) * * ```ts diff --git a/packages/effect/src/Layer.ts b/packages/effect/src/Layer.ts index e12e15b9ac..76b2ca6965 100644 --- a/packages/effect/src/Layer.ts +++ b/packages/effect/src/Layer.ts @@ -841,12 +841,6 @@ export const syncContext = (evaluate: LazyArg>): Layer * The Effect is executed in the scope of the layer, allowing for proper * resource management. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Layer.scoped` - * * **Example** (Creating a layer from an effect) * * ```ts @@ -924,12 +918,6 @@ export const effectContext = ( * This is useful when you want to run an Effect for its side effects during * layer construction, but don't need to provide any services. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Layer.scopedDiscard` - * * **Example** (Running an effect during layer construction) * * ```ts @@ -1509,12 +1497,6 @@ export const tapError: { * fails again with the original cause; if the callback fails, that failure is * added to the layer's error type. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Layer.tapErrorCause` - * * @category sequencing * @since 4.0.0 */ diff --git a/packages/effect/src/Queue.ts b/packages/effect/src/Queue.ts index 46cad2ec52..54c00f7c7d 100644 --- a/packages/effect/src/Queue.ts +++ b/packages/effect/src/Queue.ts @@ -245,12 +245,6 @@ export declare namespace Dequeue { * * It also supports signaling that it is done or failed. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Mailbox` - * * **Example** (Offering and taking queue values) * * ```ts @@ -381,12 +375,6 @@ const QueueProto = { * `"sliding"` to control what happens when the queue is full. The returned * queue can be offered to, taken from, failed, ended, interrupted, or shut down. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Mailbox.make` - * * **Example** (Creating queues) * * ```ts diff --git a/packages/effect/src/Random.ts b/packages/effect/src/Random.ts index cd9ad7c626..c16637fb4c 100644 --- a/packages/effect/src/Random.ts +++ b/packages/effect/src/Random.ts @@ -1,7 +1,15 @@ /** - * The Random module provides a service for generating random numbers in Effect - * programs. It offers a testable and composable way to work with randomness, - * supporting integers, floating-point numbers, and range-based generation. + * The `Random` module provides a service for generating pseudo-random numbers + * in Effect programs. It offers a testable and composable way to work with + * randomness, supporting integers, floating-point numbers, and range-based + * generation. + * + * The default `Random` service is not cryptographically secure. Do not use it + * for secrets, tokens, UUIDs, session identifiers, or other security-sensitive + * values. For cryptographically secure random generation, replace the service + * with a cryptographically secure implementation such as the platform `Crypto` + * service. `Random.withSeed` also replaces the service, but predictable seeds + * remain deterministic and must not be treated as cryptographically secure. * * **Example** (Generating random values) * @@ -29,7 +37,11 @@ import * as random from "./internal/random.ts" import * as Predicate from "./Predicate.ts" /** - * Represents a service for generating random numbers. + * Represents a service for generating pseudo-random numbers. + * + * The default implementation is based on `Math.random` and is not + * cryptographically secure. Replace the service with a cryptographically secure + * implementation before using these generators for security-sensitive values. * * **Example** (Accessing the random service) * @@ -40,12 +52,10 @@ import * as Predicate from "./Predicate.ts" * const float = yield* Random.next * const integer = yield* Random.nextInt * const inRange = yield* Random.nextIntBetween(1, 100) - * const uuid = yield* Random.nextUUIDv4 * * console.log("Float:", float) * console.log("Integer:", integer) * console.log("In range:", inRange) - * console.log("UUID:", uuid) * }) * ``` * @@ -61,7 +71,7 @@ const randomWith = (f: (random: typeof Random["Service"]) => A): Effect.Effec Effect.withFiber((fiber) => Effect.succeed(f(fiber.getRef(Random)))) /** - * Generates a random number between 0 (inclusive) and 1 (inclusive). + * Generates a random number between 0 (inclusive) and 1 (exclusive). * * **Example** (Generating a random number) * @@ -119,7 +129,7 @@ export const nextBoolean: Effect.Effect = randomWith((r) => r.nextDoubl export const nextInt: Effect.Effect = randomWith((r) => r.nextIntUnsafe()) /** - * Generates a random number between `min` (inclusive) and `max` (inclusive). + * Generates a random number between `min` (inclusive) and `max` (exclusive). * * **Example** (Generating a bounded random number) * @@ -203,51 +213,7 @@ export const shuffle = (elements: Iterable): Effect.Effect> => }) /** - * Generates a random UUID (v4) string. - * - * **Example** (Generating a UUID) - * - * ```ts - * import { Effect, Random } from "effect" - * - * const program = Effect.gen(function*() { - * const uuid = yield* Random.nextUUIDv4 - * console.log("UUID:", uuid) - * }) - * ``` - * - * @category Random Number Generators - * @since 4.0.0 - */ -export const nextUUIDv4: Effect.Effect = randomWith((r) => { - // Generate 16 random bytes (128 bits) for UUID - const bytes: Array = [] - for (let i = 0; i < 16; i++) { - // Get unsigned byte [0, 255] from nextInt (signed 32-bit) - bytes.push((r.nextIntUnsafe() >>> 0) & 0xFF) - } - - // Set version to 4 (bits 12-15 of time_hi_and_version) - bytes[6] = (bytes[6] & 0x0F) | 0x40 - - // Set variant to RFC 4122 (bits 6-7 of clock_seq_hi_and_reserved) - bytes[8] = (bytes[8] & 0x3F) | 0x80 - - // Format as UUID string: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx - const hex = (n: number) => n.toString(16).padStart(2, "0") - - return [ - bytes.slice(0, 4).map(hex).join(""), - bytes.slice(4, 6).map(hex).join(""), - bytes.slice(6, 8).map(hex).join(""), - bytes.slice(8, 10).map(hex).join(""), - bytes.slice(10, 16).map(hex).join("") - ].join("-") -}) - -/** - * Runs an effect with a pseudorandom number generator initialized from the - * specified seed. + * Seeds the pseudo-random number generator with the specified value. * * Using the same seed produces the same random sequence, which is useful for * tests and reproducible simulations. Use an unpredictable seed when uniqueness @@ -488,7 +454,7 @@ function ISAAC_CSPRNG(userSeed?: string | number) { } /** - * Returns a signed, random integer in the range [-2^31, 2^31]. + * Returns a signed, random integer in the range [-2^31, 2^31). */ function nextInt32(): number { if (!generation--) { @@ -499,15 +465,8 @@ function ISAAC_CSPRNG(userSeed?: string | number) { } function nextIntUnsafe(): number { - // Get 32 bits (unsigned) - const low = nextInt32() >>> 0 // [0, 2^32-1] - - // Get 21 more bits for a total of 53 - const high = nextInt32() & 0x1FFFFF // [0, 2^21-1] - - // Combine: high bits * 2^32 + low bits, then shift to signed range - // This gives [0, 2^53-1], subtract 2^52 to center around 0 - return (high * 0x100000000) + low - 0x10000000000000 + return Math.floor(nextDoubleUnsafe() * (Number.MAX_SAFE_INTEGER - Number.MIN_SAFE_INTEGER + 1)) + + Number.MIN_SAFE_INTEGER } /** @@ -520,7 +479,7 @@ function ISAAC_CSPRNG(userSeed?: string | number) { // 53-bit integer const combined = hi * 4294967296 + lo - return combined / 9007199254740991 // [0, 1) + return combined / 0x20000000000000 // [0, 1) } return { nextIntUnsafe, nextDoubleUnsafe } diff --git a/packages/effect/src/Result.ts b/packages/effect/src/Result.ts index 910074079a..636e5e3edd 100644 --- a/packages/effect/src/Result.ts +++ b/packages/effect/src/Result.ts @@ -95,12 +95,6 @@ const TypeId = "~effect/data/Result" * * `E` defaults to `never`, so `Result` means a result that cannot fail. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Either` - * * **Example** (Creating and matching a Result) * * ```ts @@ -303,12 +297,6 @@ export declare namespace Result { * - The error type `E` defaults to `never` * - Does not mutate input; allocates a new `Success` wrapper * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Either.right` - * * **Example** (Wrapping a value) * * ```ts @@ -335,12 +323,6 @@ export const succeed: (right: A) => Result = result.succeed * - The success type `A` defaults to `never` * - Does not mutate input; allocates a new `Failure` wrapper * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Either.left` - * * **Example** (Creating a failure) * * ```ts diff --git a/packages/effect/src/Schema.ts b/packages/effect/src/Schema.ts index fa43d4e4c0..281858f8b6 100644 --- a/packages/effect/src/Schema.ts +++ b/packages/effect/src/Schema.ts @@ -702,17 +702,6 @@ export declare namespace Codec { * @since 4.0.0 */ export type EncodingServices = S extends Top ? S["EncodingServices"] : never - /** - * Converts a schema type into an assertion function signature. The resulting - * function narrows its argument to `I & S["Type"]`. Only schemas with - * `DecodingServices: never` (i.e. no required services) can be used here. - * - * Produced by {@link asserts}. - * - * @category utility types - * @since 3.10.0 - */ - export type ToAsserts = (input: I) => asserts input is I & S["Type"] } /** @@ -1122,21 +1111,16 @@ export const is = Parser.is * ```ts * import { Schema } from "effect" * - * const assertString: (u: unknown) => asserts u is string = Schema.asserts( - * Schema.String - * ) + * const input: unknown = "hello" * - * // This will pass silently (no return value) - * try { - * assertString("hello") - * console.log("String assertion passed") - * } catch (error) { - * console.log("String assertion failed") - * } + * // This will pass silently (no return value) and narrow input to string + * Schema.asserts(Schema.String, input) + * console.log(input.toUpperCase()) * * // This will throw an error * try { - * assertString(123) + * const invalid: unknown = 123 + * Schema.asserts(Schema.String, invalid) * } catch (error) { * console.log("Non-string assertion failed as expected") * } @@ -1145,7 +1129,7 @@ export const is = Parser.is * @category guards * @since 4.0.0 */ -export const asserts = Parser.asserts +export const asserts: (schema: S, input: I) => asserts input is I & S["Type"] = Parser.asserts /** * Decodes an `unknown` input against a schema, returning an `Effect` that diff --git a/packages/effect/src/SchemaParser.ts b/packages/effect/src/SchemaParser.ts index a1f67dcf82..6d53303385 100644 --- a/packages/effect/src/SchemaParser.ts +++ b/packages/effect/src/SchemaParser.ts @@ -155,26 +155,23 @@ export function _issue(ast: AST.AST) { } /** - * Creates an assertion function that narrows an input to the schema's decoded type - * side. + * Asserts that an input satisfies the schema's decoded type side. * * The assertion returns normally when validation succeeds and throws when the * input does not satisfy the schema. * * @category Asserting - * @since 3.10.0 + * @since 4.0.0 */ -export function asserts(schema: Schema.Schema) { - const parser = asExit(run(AST.toType(schema.ast))) - return (input: I): asserts input is I & T => { - const exit = parser(input, AST.defaultParseOptions) - if (Exit.isFailure(exit)) { - const issue = Cause.findError(exit.cause) - if (Result.isFailure(issue)) { - throw Cause.squash(issue.failure) - } - throw new Error(issue.success.toString(), { cause: issue.success }) +export function asserts(schema: S, input: I): asserts input is I & S["Type"] { + const parser = asExit(run(AST.toType(schema.ast))) + const exit = parser(input, AST.defaultParseOptions) + if (Exit.isFailure(exit)) { + const issue = Cause.findError(exit.cause) + if (Result.isFailure(issue)) { + throw Cause.squash(issue.failure) } + throw new Error(issue.success.toString(), { cause: issue.success }) } } diff --git a/packages/effect/src/Scope.ts b/packages/effect/src/Scope.ts index 1dbe5fc029..6cf32bf940 100644 --- a/packages/effect/src/Scope.ts +++ b/packages/effect/src/Scope.ts @@ -276,12 +276,6 @@ export const makeUnsafe: (finalizerStrategy?: "sequential" | "parallel") => Clos * Provides a `Scope` to an `Effect`, removing the `Scope` requirement from its context. * This allows you to run effects that require a scope by explicitly providing one. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Scope.extend` - * * **Example** (Providing a scope) * * ```ts diff --git a/packages/effect/src/Semaphore.ts b/packages/effect/src/Semaphore.ts index fb4ba5b915..f02926421c 100644 --- a/packages/effect/src/Semaphore.ts +++ b/packages/effect/src/Semaphore.ts @@ -126,12 +126,6 @@ export interface Semaphore { * Use this low-level constructor when an immediate semaphore value is required; * otherwise prefer the effectful `make` constructor. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Effect.makeSemaphoreUnsafe` - * * **Example** (Creating an unsafe semaphore) * * ```ts @@ -273,12 +267,6 @@ class SemaphoreImpl implements Semaphore { * Use the returned semaphore to limit concurrency with `withPermit` or * `withPermits`, or to manually `take` and `release` permits. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Effect.makeSemaphore` - * * **Example** (Creating a semaphore) * * ```ts diff --git a/packages/effect/src/Stream.ts b/packages/effect/src/Stream.ts index b9efa32a43..026ca514cb 100644 --- a/packages/effect/src/Stream.ts +++ b/packages/effect/src/Stream.ts @@ -256,10 +256,6 @@ export type Error> = [T] extends [Stream(effect: Effect.Effect): Stream /** * Creates a stream from an effect producing a value of type `A` which repeats forever. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Stream.repeatEffect` - * * **Example** (Repeating an effect forever) * * ```ts @@ -525,12 +509,6 @@ export const fromEffectRepeat = (effect: Effect.Effect): Strea * Creates a stream from an effect producing a value of type `A`, which is * repeated using the specified schedule. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Stream.repeatEffectWithSchedule` - * * **Example** (Repeating an effect with a schedule) * * ```ts @@ -773,15 +751,6 @@ export const toChannel = ( * You can customize the buffer size and strategy by passing an object as the * second argument with the `bufferSize` and `strategy` fields. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Stream.async` - * - `Stream.asyncEffect` - * - `Stream.asyncPush` - * - `Stream.asyncScoped` - * * **Example** (Creating a stream from a callback that can emit values into a queue) * * ```ts @@ -1180,12 +1149,6 @@ export const fromIterableEffect = (iterable: Effect.Effect, /** * Creates a stream by repeatedly running an effect that yields an iterable of values. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Stream.repeatEffectChunk` - * * **Example** (Repeating an iterable effect) * * ```ts @@ -1213,12 +1176,6 @@ export const fromIterableEffectRepeat = ( /** * Creates a stream from an array of values. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Stream.fromChunk` - * * **Example** (Creating a stream from an array of values) * * ```ts @@ -1268,12 +1225,6 @@ export const fromArrayEffect = ( /** * Creates a stream from an arbitrary number of arrays. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Stream.fromChunks` - * * **Example** (Creating a stream from an arbitrary number of arrays) * * ```ts @@ -1959,12 +1910,6 @@ export const mapBoth: { /** * Transforms each emitted chunk using the provided function, which receives the chunk and its index. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Stream.mapChunks` - * * **Example** (Mapping stream chunks) * * ```ts @@ -2117,12 +2062,6 @@ export const flattenEffect: < /** * Effectfully maps over non-empty array chunks emitted by the stream. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Stream.mapChunksEffect` - * * **Example** (Effectfully mapping stream chunks) * * ```ts @@ -2164,12 +2103,6 @@ export const mapArrayEffect: { * * The stream ends after the first failure, emitting a `Result.fail` value. * - * **Previously Known As:** - * - * This API replaces the following from Effect 3.x: - * - * - `Stream.either` - * * **Example** (Converting failures to results) * * ```ts @@ -2593,12 +2526,6 @@ export const flatten: < /** * Flattens a stream of non-empty arrays into a stream of elements. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Stream.flattenChunks` - * * **Example** (Flattening a stream of non-empty arrays into a stream of elements) * * ```ts @@ -2987,12 +2914,6 @@ export const forever = (self: Stream): Stream => from /** * Submerges the iterables emitted by this stream into the stream's structure. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Stream.flattenIterables` - * * **Example** (Flattening iterable values) * * ```ts @@ -3205,12 +3126,6 @@ export const mergeEffect: { * Merges this stream and the specified stream together, tagging values from the * left stream as `Result.succeed` and values from the right stream as `Result.fail`. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Stream.mergeEither` - * * **Example** (Merging streams into results) * * ```ts @@ -3521,12 +3436,6 @@ const zipArrays = ( * * The function returns output plus leftover arrays that carry into the next pull. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Stream.zipWithChunks` - * * **Example** (Zipping stream chunks) * * ```ts @@ -4765,12 +4674,6 @@ export const buffer: { * buffer is full. This combinator preserves chunking and is best with * power-of-2 capacities. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Stream.bufferChunks` - * * **Example** (Buffering stream chunks) * * ```ts @@ -4817,12 +4720,6 @@ export const bufferArray: { * one fails. Allows recovery from all causes of failure, including * interruption if the stream is uninterruptible. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Stream.catchAllCause` - * * **Example** (Catching stream causes) * * ```ts @@ -4870,12 +4767,6 @@ export const catchCause: { * Runs an effect when the stream fails without changing its values or error, * unless the tap effect itself fails. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Stream.tapErrorCause` - * * **Example** (Tapping stream causes) * * ```ts @@ -4934,12 +4825,6 @@ export { /** * Switches over to the stream produced by the provided function if this one fails. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Stream.catchAll` - * * **Example** (Catching stream failures) * * ```ts @@ -5017,12 +4902,6 @@ export const tapError: { * stream. Non-matching failures propagate downstream, so the error type is * preserved unless the filter narrows it. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Stream.catchSome` - * * **Example** (Catching matching failures) * * ```ts @@ -5660,12 +5539,6 @@ export const mapError: { * Recovers from stream failures by filtering the `Cause` and switching to a recovery stream. * Non-matching causes are re-emitted as failures. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Stream.catchSomeCause` - * * **Example** (Catching matching causes) * * ```ts @@ -7115,12 +6988,6 @@ export const combine: { * internal state to control the combining process, with the initial state * being specified by `s`. * - * **Previously Known As** - * - * This API replaces the following from Effect 3.x: - * - * - `Stream.combineChunks` - * * **Example** (Combining stream chunks with state) * * ```ts @@ -9708,8 +9575,6 @@ export const ensuring: { * service requirements. Use `options.local` to build the layer every time; by * default, layers are shared between provide calls. * - * **Previously Known As:** `provideSomeLayer`, `provideSomeContext`. - * * **Example** (Providing stream requirements) * * ```ts diff --git a/packages/effect/src/index.ts b/packages/effect/src/index.ts index 5337cda7cd..cb7cf191e8 100644 --- a/packages/effect/src/index.ts +++ b/packages/effect/src/index.ts @@ -161,6 +161,41 @@ export * as Boolean from "./Boolean.ts" export * as Brand from "./Brand.ts" /** + * The `Cache` module provides an effectful, mutable key-value cache for values + * that are computed by a lookup function. A `Cache` stores lookup + * results for keys, shares concurrent lookups for the same key, and manages + * entry lifetime with capacity limits and optional time-to-live policies. + * + * **Mental model** + * + * - A cache is created from a lookup function and a maximum capacity + * - {@link get} returns a cached value when present, or runs the lookup on a miss + * - Concurrent misses for the same key share one pending lookup + * - Lookup failures are cached as failures until the entry expires, is invalidated, or is refreshed + * - Entries can live forever, expire after a fixed duration, or use a dynamic TTL based on the lookup `Exit` + * - Capacity is enforced by removing the oldest stored entries when new entries are added + * + * **Common tasks** + * + * - Create a cache: {@link make}, {@link makeWith} + * - Read values: {@link get}, {@link getOption}, {@link getSuccess} + * - Seed or overwrite values: {@link set} + * - Refresh values: {@link refresh} + * - Remove entries: {@link invalidate}, {@link invalidateWhen}, {@link invalidateAll} + * - Inspect contents: {@link has}, {@link size}, {@link keys}, {@link values}, {@link entries} + * + * **Gotchas** + * + * - {@link getOption} does not run the lookup; it only reads an existing non-expired entry + * - {@link size} may include expired entries until they are observed and removed + * - {@link values} and {@link entries} include only successfully resolved entries + * - Use `Data` or another `Equal`-compatible key type when keys need structural equality + * + * **See also** + * + * - {@link Duration} for configuring fixed or dynamic time-to-live values + * - {@link Effect} for the lookup effects used to compute cached values + * * @since 4.0.0 */ export * as Cache from "./Cache.ts" @@ -309,6 +344,48 @@ export * as Cause from "./Cause.ts" export * as Channel from "./Channel.ts" /** + * The `ChannelSchema` module provides helpers for applying `Schema` encoding + * and decoding at `Channel` boundaries. It is useful when a channel should + * expose typed values to application code while communicating with an upstream + * or downstream component through an encoded representation such as JSON-ready + * data, wire protocol values, or any other schema-defined format. + * + * **Mental model** + * + * - A channel schema adapter is a streaming boundary: chunks flow through a + * `Channel`, and each non-empty chunk is validated and transformed with a + * `Schema` + * - `encode` turns typed schema values into their encoded representation before + * they leave a typed part of a pipeline + * - `decode` turns encoded input into typed schema values before application + * code consumes them + * - `duplex` wraps a bidirectional channel so callers work with typed input and + * output while the wrapped channel continues to operate on encoded chunks + * - Schema failures are surfaced through the channel error type as + * `SchemaError`, and schema services are reflected in the channel + * requirements + * + * **Common tasks** + * + * - Encode typed channel input before sending it to an encoded transport: + * {@link encode} + * - Decode encoded channel output before handling it as domain data: + * {@link decode} + * - Use unknown encoded boundaries when static encoded types are intentionally + * erased: {@link encodeUnknown} and {@link decodeUnknown} + * - Wrap a bidirectional encoded channel with typed input and output schemas: + * {@link duplex} or {@link duplexUnknown} + * + * **Gotchas** + * + * - These helpers operate on `NonEmptyReadonlyArray` chunks, so schemas are + * applied to non-empty batches rather than individual scalar values + * - Encoding and decoding can require services from the schema; those + * requirements become part of the resulting channel type + * - `duplex` encodes values flowing into the wrapped channel and decodes values + * emitted by it, so choose `inputSchema` and `outputSchema` from the + * perspective of the typed caller + * * @since 4.0.0 */ export * as ChannelSchema from "./ChannelSchema.ts" @@ -376,12 +453,11 @@ export * as ChannelSchema from "./ChannelSchema.ts" * import { Chunk, Effect } from "effect" * * // Working with Effects - * const processChunk = (chunk: Chunk.Chunk) => - * Effect.gen(function*() { - * const mapped = Chunk.map(chunk, (n) => n * 2) - * const filtered = Chunk.filter(mapped, (n) => n > 5) - * return Chunk.toReadonlyArray(filtered) - * }) + * const processChunk = Effect.fnUntraced(function*(chunk: Chunk.Chunk) { + * const mapped = Chunk.map(chunk, (n) => n * 2) + * const filtered = Chunk.filter(mapped, (n) => n > 5) + * return Chunk.toReadonlyArray(filtered) + * }) * ``` * * @since 2.0.0 @@ -762,10 +838,107 @@ export * as Console from "./Console.ts" export * as Context from "./Context.ts" /** + * The `Cron` module provides utilities for representing recurring calendar + * schedules with cron expressions. A `Cron` value stores allowed seconds, + * minutes, hours, days of month, months, weekdays, and an optional time zone, + * then uses those constraints to test dates and find scheduled occurrences. + * + * **Mental model** + * + * - A cron schedule is a set of allowed values for each time field + * - Expressions may use five fields (`minute hour day month weekday`) or six + * fields (`second minute hour day month weekday`); five-field expressions + * default seconds to `0` + * - Each field supports `*`, comma-separated values, ranges, and step syntax + * - Month and weekday fields support aliases such as `JAN`, `DEC`, `SUN`, and + * `MON` + * - Empty internal field sets represent an unconstrained field, the same idea + * as `*` + * - When both day-of-month and weekday are constrained, matching uses cron's + * inclusive behavior: either field may match + * + * **Common tasks** + * + * - Build directly from field constraints: {@link make} + * - Parse expressions safely: {@link parse} + * - Parse expressions and throw on invalid input: {@link parseUnsafe} + * - Check whether a date satisfies a schedule: {@link match} + * - Find adjacent scheduled dates: {@link next}, {@link prev} + * - Iterate future scheduled dates: {@link sequence} + * - Compare schedule constraints: {@link equals}, {@link Equivalence} + * - Detect parse failures: {@link CronParseError}, {@link isCronParseError} + * + * **Gotchas** + * + * - Weekdays are numbered `0` through `6`, with `0` representing Sunday + * - Months are numbered `1` through `12`, while JavaScript `Date` months are + * zero-based + * - `*` normalizes to an empty set internally, so inspect schedules with the + * public helpers instead of assuming every allowed value is stored + * - `next` and `prev` search strictly after or before the provided instant + * - Time-zone-aware schedules account for daylight saving transitions; during + * a fall-back transition, repeated local times are emitted once when moving + * forward + * * @since 2.0.0 */ export * as Cron from "./Cron.ts" +/** + * The `Crypto` module provides a platform-agnostic service for cryptographic + * operations. Runtime packages such as `@effect/platform-node`, + * `@effect/platform-bun`, and `@effect/platform-browser` provide concrete + * implementations backed by the host platform's cryptography APIs. + * + * Use `Crypto` for cryptographic randomness, UUID generation, random values, + * and message digests. The base `Random` service is not cryptographically + * secure unless you replace it with a cryptographically secure implementation. + * + * @example + * ```ts + * import { Console, Crypto, Effect, Layer } from "effect" + * + * const TestCrypto = Layer.succeed( + * Crypto.Crypto, + * Crypto.make({ + * randomBytes: (size) => new Uint8Array(size), + * digest: (_algorithm, data) => Effect.succeed(data) + * }) + * ) + * + * const program = Effect.gen(function*() { + * const crypto = yield* Crypto.Crypto + * const id = yield* crypto.randomUUIDv4 + * yield* Console.log(`Created id: ${id}`) + * }) + * + * Effect.runPromise(Effect.provide(program, TestCrypto)) + * ``` + * + * @example + * ```ts + * import { Crypto, Effect, Layer } from "effect" + * + * const TestCrypto = Layer.succeed( + * Crypto.Crypto, + * Crypto.make({ + * randomBytes: (size) => new Uint8Array(size), + * digest: (_algorithm, data) => Effect.succeed(data) + * }) + * ) + * + * const program = Effect.gen(function*() { + * const crypto = yield* Crypto.Crypto + * return yield* crypto.randomBytes(32) + * }) + * + * Effect.runPromise(Effect.provide(program, TestCrypto)) + * ``` + * + * @since 4.0.0 + */ +export * as Crypto from "./Crypto.ts" + /** * Immutable data constructors with discriminated-union support. * @@ -842,6 +1015,55 @@ export * as Cron from "./Cron.ts" export * as Data from "./Data.ts" /** + * The `DateTime` module provides immutable data types and utilities for working + * with instants, UTC date-times, zoned date-times, and time zones. A + * `DateTime` is always an absolute point in time, represented internally by + * epoch milliseconds, and may also carry a `TimeZone` for zone-aware calendar + * parts and formatting. + * + * **Mental model** + * + * - `DateTime` is a discriminated union: `Utc | Zoned` + * - `Utc` stores an absolute instant without an associated time zone + * - `Zoned` stores the same kind of absolute instant plus a `TimeZone` + * - Time zones can be fixed offsets or named IANA zones such as `"Europe/Rome"` + * - Comparison and ordering use the instant, so two values in different zones + * can still be equivalent + * - Calendar parts and formatted output depend on whether you ask for UTC parts + * or zone-adjusted parts + * + * **Common tasks** + * + * - Construct values: {@link make}, {@link makeUnsafe}, {@link makeZoned}, {@link makeZonedUnsafe} + * - Get the current instant: {@link now}, {@link nowInCurrentZone} + * - Create time zones: {@link zoneMakeOffset}, {@link zoneMakeNamed}, {@link zoneFromString} + * - Attach or change zones: {@link setZone}, {@link setZoneNamed}, {@link setZoneCurrent}, {@link toUtc} + * - Convert to platform values or parts: {@link toDate}, {@link toDateUtc}, {@link toEpochMillis}, {@link toParts}, {@link toPartsUtc} + * - Compare and bound values: {@link Equivalence}, {@link Order}, {@link distance}, {@link min}, {@link max}, {@link clamp}, {@link between} + * - Transform values: {@link add}, {@link subtract}, {@link startOf}, {@link endOf}, {@link nearest}, {@link setParts}, {@link mutate} + * - Format values: {@link format}, {@link formatUtc}, {@link formatLocal}, {@link formatIntl}, {@link formatIso}, {@link formatIsoZoned} + * - Provide an application time zone: {@link CurrentTimeZone}, {@link withCurrentZone}, {@link layerCurrentZone} + * + * **Gotchas** + * + * - `make` and `makeZoned` return `Option`; unsafe constructors throw on invalid + * input + * - `DateTime` equality is instant-based, not display-time-based + * - `setZone` changes the zone used for local parts and formatting without + * changing the represented instant + * - Use `adjustForTimeZone` with {@link makeZoned} when input parts should be + * interpreted as wall-clock time in the target zone + * - Daylight-saving gaps and repeated local times are resolved with + * `Disambiguation` + * - Prefer the Clock-backed {@link now} and `CurrentTimeZone` services in + * Effect workflows; unsafe helpers read from the host environment directly + * + * **See also** + * + * - {@link DateTime} for the UTC/zoned data model + * - {@link TimeZone} for offset and named time-zone values + * - {@link Disambiguation} for daylight-saving ambiguity handling + * * @since 3.6.0 */ export * as DateTime from "./DateTime.ts" @@ -918,6 +1140,42 @@ export * as DateTime from "./DateTime.ts" export * as Deferred from "./Deferred.ts" /** + * The `Differ` module defines the core abstraction for describing changes to a + * value. A `Differ` knows how to compare two `T` values, produce a + * patch that represents the difference, combine multiple patches, and apply a + * patch to an old value to obtain the updated value. + * + * **Mental model** + * + * - A differ separates "what changed" from "the value after the change" + * - `diff(oldValue, newValue)` produces a `Patch` that can later be applied + * - `patch(oldValue, patch)` replays a patch against a value of the same domain + * - `empty` is the identity patch: applying it should leave the value unchanged + * - `combine(first, second)` composes patches in sequence, where `second` + * represents changes that happen after `first` + * - Patch types are chosen by the differ implementation and may be compact, + * domain-specific, or compatible with a serialization format such as JSON + * Patch + * + * **Common tasks** + * + * - Construct a differ by providing the four operations of the {@link Differ} + * interface + * - Compute a patch with `diff` when you have an old value and a new value + * - Store, transmit, or aggregate patches instead of storing full replacement + * values + * - Combine incremental updates with `combine` before applying them + * - Apply updates with `patch` to reconstruct the next value from a previous + * value and a patch + * + * **Gotchas** + * + * - `combine` is order-sensitive for most patch formats + * - A patch is generally meaningful only for values that belong to the same + * domain and assumptions used by the differ that created it + * - Differs should make `empty` a true identity and should make combined + * patches behave the same as applying the original patches in order + * * @since 4.0.0 */ export * as Differ from "./Differ.ts" @@ -1014,6 +1272,41 @@ export * as Duration from "./Duration.ts" export * as Effect from "./Effect.ts" /** + * The `Effectable` module provides low-level building blocks for defining + * custom values that behave like `Effect`s. It is primarily used by library + * authors who need domain-specific effect-like data types, such as service + * keys, configuration descriptions, prompts, or other declarative programs + * that can be yielded inside `Effect.gen`. + * + * **Mental model** + * + * - `Effectable` does not run effects by itself; it provides prototypes that + * implement the internal Effect protocol. + * - {@link Prototype} creates a primitive Effect prototype with a custom + * evaluation function that receives the current `Fiber`. + * - {@link Class} is an abstract base class for defining custom classes whose + * instances are also `Effect` values. + * - The success, error, and service requirements of the custom type are + * preserved through the `Effect.Effect` type parameters. + * + * **Common tasks** + * + * - Build an effect-like interface around a declarative data structure. + * - Implement a custom `evaluate` hook that interprets the value in terms of + * the current fiber and returns the underlying `Effect`. + * - Extend {@link Class} when a nominal class-based API is more convenient + * than manually wiring a prototype. + * + * **Gotchas** + * + * - This module is intentionally low-level; most application code should use + * `Effect` constructors and combinators instead. + * - `evaluate` must return an `Effect` with the same success, error, and + * service types as the custom value. + * - Because these APIs participate in the internal Effect protocol, keep + * implementations small and follow existing modules such as `Config` and + * `Context` when adding new effect-like types. + * * @since 4.0.0 */ export * as Effectable from "./Effectable.ts" @@ -1207,6 +1500,43 @@ export * as Equivalence from "./Equivalence.ts" export * as ErrorReporter from "./ErrorReporter.ts" /** + * The `ExecutionPlan` module provides a way to describe ordered fallback + * strategies for effects and streams that need different resources across + * repeated attempts. An `ExecutionPlan` is a non-empty list of steps, where + * each step supplies a `Context` or `Layer` and may control retries with an + * attempt limit, a `Schedule`, or a `while` predicate. + * + * **Mental model** + * + * - A plan is evaluated step by step until the wrapped effect or stream + * succeeds, or until every step has been exhausted + * - Each step provides the services used while that step is active + * - `attempts` limits how many times a step may be tried + * - `schedule` controls retry timing and receives the failure input + * - `while` can stop retrying a step based on the failure input + * - `CurrentMetadata` exposes the current 1-based attempt and 0-based step + * index to code running under a plan + * + * **Common tasks** + * + * - Build a plan with {@link make} + * - Run an effect with a plan using `Effect.withExecutionPlan` + * - Run a stream with a plan using `Stream.withExecutionPlan` + * - Combine plans in order with {@link merge} + * - Capture required services up front with `captureRequirements` + * - Inspect the current attempt and step with {@link CurrentMetadata} + * + * **Gotchas** + * + * - Plans must contain at least one step + * - `attempts` must be greater than zero when provided + * - If `attempts` is omitted, a step is attempted once unless a `schedule` is + * provided + * - A `while` predicate returning `false` skips the remaining retries for that + * step and moves the plan forward + * - Layer, schedule, and predicate requirements are tracked in the plan type + * until they are provided or captured + * * @since 3.16.0 */ export * as ExecutionPlan from "./ExecutionPlan.ts" @@ -1349,16 +1679,127 @@ export * as Exit from "./Exit.ts" export * as Fiber from "./Fiber.ts" /** + * The `FiberHandle` module provides a scoped handle for managing the lifecycle + * of at most one fiber at a time. A `FiberHandle` can hold one + * `Fiber`; when a new fiber is installed, the previous fiber is + * interrupted unless the operation is configured with `onlyIfMissing`. + * + * **Mental model** + * + * - A handle is either open with zero or one current fiber, or closed by its + * surrounding `Scope` + * - Closing the scope interrupts the current fiber and prevents new work from + * being accepted + * - Completed fibers remove themselves from the handle, so the handle can be + * reused for later work + * - Replacing a fiber uses the handle's internal interruption id, allowing + * expected replacement interruptions to be distinguished from real failures + * + * **Common tasks** + * + * - Create a scoped handle: {@link make} + * - Fork an effect into the handle: {@link run} + * - Store an existing fiber: {@link set} + * - Read or clear the current fiber: {@link get}, {@link clear} + * - Capture runtime-specific runners: {@link makeRuntime}, {@link runtime} + * - Run handled effects as Promises: {@link makeRuntimePromise}, + * {@link runtimePromise} + * - Wait for failure or closure: {@link join} + * - Wait until the current fiber is gone: {@link awaitEmpty} + * + * **Gotchas** + * + * - The handle never contains more than one live fiber; starting or setting + * another fiber interrupts the previous one by default + * - Use `onlyIfMissing` when a call should leave an already running fiber in + * place instead of replacing it + * - `join` observes the handle's failure/close signal; successful fiber + * completion only empties the handle + * - `awaitEmpty` waits for the fiber that is current when it starts; later + * calls to {@link run} or {@link set} can install new work + * * @since 2.0.0 */ export * as FiberHandle from "./FiberHandle.ts" /** + * The `FiberMap` module provides a scoped, mutable collection for managing + * fibers by key. A `FiberMap` owns a set of running fibers, interrupts + * them when its scope closes, and automatically removes each entry when the + * corresponding fiber completes. + * + * **Mental model** + * + * - A `FiberMap` is a keyed registry of fibers with lifecycle management + * - Keys identify the currently active fiber for a logical task or resource + * - Adding a fiber under an existing key interrupts the previous fiber by default + * - Completed fibers remove themselves from the map if they are still current + * - Closing the map's scope interrupts every fiber that remains in the map + * - The map can surface the first non-ignored managed fiber failure via {@link join} + * + * **Common tasks** + * + * - Create a scoped map: {@link make} + * - Fork effects into the map: {@link run} + * - Add existing fibers: {@link set} + * - Create captured runners: {@link makeRuntime}, {@link runtime} + * - Bridge to Promise-based callers: {@link makeRuntimePromise}, {@link runtimePromise} + * - Inspect entries: {@link get}, {@link has}, {@link size} + * - Stop work: {@link remove}, {@link clear} + * - Coordinate completion or failure: {@link awaitEmpty}, {@link join} + * + * **Gotchas** + * + * - `FiberMap` is scoped; use it with `Effect.scoped` or another scope owner so + * managed fibers are interrupted when the scope closes + * - Reusing a key is a replacement operation unless `onlyIfMissing` is enabled + * - `join` waits for the map to fail or close; use {@link awaitEmpty} to wait + * until all currently managed fibers have completed + * - The `Unsafe` variants mutate synchronously and should only be used when the + * caller already controls the surrounding execution context + * * @since 2.0.0 */ export * as FiberMap from "./FiberMap.ts" /** + * The `FiberSet` module provides a scoped container for managing many fibers as + * one lifecycle. A `FiberSet` tracks fibers whose successful values are + * compatible with `A` and whose failures are compatible with `E`, removes each + * fiber when it completes, and interrupts all still-running fibers when the + * owning `Scope` closes. + * + * **Mental model** + * + * - A `FiberSet` is an owned, scoped collection of fibers + * - Fibers can be added directly with {@link add} / {@link addUnsafe} + * - Effects can be forked into the set with {@link run}, {@link runtime}, or + * {@link runtimePromise} + * - Completed fibers are automatically removed from the set + * - Closing the scope or calling {@link clear} interrupts the currently tracked + * fibers + * - {@link join} waits for the set's first non-ignored failure, while + * {@link awaitEmpty} waits until all tracked fibers have completed + * + * **Common tasks** + * + * - Create a scoped set: {@link make} + * - Create scoped runners: {@link makeRuntime}, {@link makeRuntimePromise} + * - Add an existing fiber: {@link add} + * - Fork an effect into the set: {@link run} + * - Interrupt tracked fibers: {@link clear} + * - Observe the set: {@link size}, {@link awaitEmpty}, {@link join} + * - Check a value: {@link isFiberSet} + * + * **Gotchas** + * + * - `FiberSet` values are scoped; use them inside `Effect.scoped` or another + * scope owner so their fibers are interrupted reliably + * - Adding or running into a closed set interrupts the fiber immediately + * - By default, interruptions are not treated as failures for {@link join}; + * use the `propagateInterruption` option when interruption should be + * propagated + * * @since 2.0.0 */ export * as FiberSet from "./FiberSet.ts" @@ -1404,6 +1845,40 @@ export * as FiberSet from "./FiberSet.ts" export * as FileSystem from "./FileSystem.ts" /** + * The `Filter` module provides composable functions for accepting, rejecting, + * narrowing, and transforming values. A `Filter` receives an + * input and returns a `Result`: success means the value passed the filter, while + * failure means the value was filtered out. + * + * **Mental model** + * + * - A filter is a typed predicate that can also transform the successful value + * - Predicate-based filters pass the original input when the predicate returns `true` + * - Refinement-based filters narrow the successful type, for example from `unknown` to `string` + * - Custom filters return `Result.succeed(pass)` or `Result.fail(fail)` directly + * - Filters compose with logical and sequential combinators instead of throwing exceptions + * - `FilterEffect` is the effectful form for filters that need asynchronous work, errors, or services + * + * **Common tasks** + * + * - Build filters: {@link make}, {@link makeEffect}, {@link fromPredicate}, {@link fromPredicateOption} + * - Narrow unknown values: {@link string}, {@link number}, {@link boolean}, {@link bigint}, {@link symbol}, {@link date} + * - Match shapes and variants: {@link instanceOf}, {@link tagged}, {@link reason}, {@link has} + * - Match exact values: {@link equals}, {@link equalsStrict} + * - Combine alternatives: {@link or} + * - Require multiple filters: {@link zip}, {@link zipWith}, {@link andLeft}, {@link andRight} + * - Run filters in sequence: {@link compose}, {@link composePassthrough} + * - Convert results: {@link toPredicate}, {@link toOption}, {@link toResult} + * - Adjust failure values: {@link mapFail} + * + * **Gotchas** + * + * - A failed filter is data in the `Result` failure channel; it is not an exception + * - `compose` preserves intermediate failure values, while {@link composePassthrough} fails with the original input + * - `equalsStrict` uses JavaScript `===`; use {@link equals} for structural equality + * - `fromPredicateOption` fails with the original input when the returned `Option` is `None` + * - Prefer refinement predicates when you want TypeScript to narrow the successful value type + * * @since 4.0.0 */ export * as Filter from "./Filter.ts" @@ -1465,11 +1940,106 @@ export * as Filter from "./Filter.ts" export * as Formatter from "./Formatter.ts" /** + * The `Function` module provides small, pure helpers for defining, composing, + * adapting, and reusing TypeScript functions. It is the foundation for the + * data-first and data-last APIs used throughout Effect, and it includes the + * core pipeline utilities that make those APIs ergonomic. + * + * **Mental model** + * + * - {@link pipe} starts with a value and passes it through one unary function at + * a time + * - {@link flow} composes unary functions into a reusable function + * - {@link dual} builds APIs that support both direct calls and `pipe`-friendly + * data-last calls + * - {@link identity}, {@link constant}, and the `const*` helpers model common + * identity and thunk patterns without allocating ad hoc callbacks + * - {@link tupled}, {@link untupled}, {@link flip}, and {@link apply} adapt + * call shapes without changing the underlying behavior + * - Type helpers such as {@link LazyArg}, {@link FunctionN}, {@link satisfies}, + * and {@link cast} describe or constrain functions at the type level + * + * **Common tasks** + * + * - Build readable transformation pipelines: {@link pipe} + * - Create reusable composed functions: {@link flow}, {@link compose} + * - Define functions callable in both data-first and data-last style: {@link dual} + * - Return a value unchanged: {@link identity} + * - Create thunks and common constant functions: {@link constant}, + * {@link constTrue}, {@link constFalse}, {@link constNull}, + * {@link constUndefined}, {@link constVoid} + * - Convert between rest-argument and tuple-argument functions: {@link tupled}, + * {@link untupled} + * - Express impossible branches: {@link absurd} + * - Cache results for object keys: {@link memoize} + * + * **Gotchas** + * + * - Functions passed to {@link pipe} and {@link flow} are applied left-to-right + * and should be unary at each step + * - {@link dual} uses either an arity or a predicate to decide whether a call is + * data-first or data-last; use a predicate when optional arguments make arity + * ambiguous + * - {@link cast} changes only the static TypeScript type and performs no runtime + * validation + * - {@link memoize} is intended for object keys and stores cached values in a + * `WeakMap` + * * @since 2.0.0 */ export * as Function from "./Function.ts" /** + * The `Graph` module provides immutable and scoped-mutable graph data + * structures for modeling relationships between indexed nodes and edges. A + * graph can be directed or undirected, stores user-defined data on both nodes + * and edges, and exposes traversal, analysis, path finding, transformation, and + * diagram export utilities. + * + * **Mental model** + * + * - Nodes and edges are addressed by stable numeric indices: {@link NodeIndex} + * and {@link EdgeIndex} + * - Node data has type `N`; edge data has type `E` + * - {@link Graph} values are immutable snapshots; use {@link MutableGraph} + * through {@link mutate}, {@link beginMutation}, or constructor callbacks to + * add, remove, or update nodes and edges + * - Directed graphs follow edge direction for neighbors and traversals, while + * undirected graphs treat each edge as connecting both endpoints + * - Missing lookups return `Option`, while structurally invalid operations such + * as adding an edge to a missing node throw {@link GraphError} + * + * **Common tasks** + * + * - Create graphs: {@link directed}, {@link undirected} + * - Mutate safely: {@link mutate}, {@link addNode}, {@link addEdge}, + * {@link removeNode}, {@link removeEdge} + * - Query contents: {@link getNode}, {@link getEdge}, {@link hasNode}, + * {@link hasEdge}, {@link nodeCount}, {@link edgeCount}, {@link neighbors} + * - Transform data: {@link updateNode}, {@link updateEdge}, {@link mapNodes}, + * {@link mapEdges}, {@link filterNodes}, {@link filterEdges}, + * {@link filterMapNodes}, {@link filterMapEdges} + * - Traverse lazily: {@link dfs}, {@link bfs}, {@link topo}, + * {@link dfsPostOrder}, {@link nodes}, {@link edges}, {@link Walker} + * - Analyze structure: {@link isAcyclic}, {@link isBipartite}, + * {@link connectedComponents}, {@link stronglyConnectedComponents}, + * {@link externals} + * - Find paths: {@link dijkstra}, {@link astar}, {@link bellmanFord}, + * {@link floydWarshall} + * - Export diagrams: {@link toGraphViz}, {@link toMermaid} + * + * **Gotchas** + * + * - Only mutable graphs can be changed. Create one with {@link mutate} or by + * passing a callback to {@link directed} / {@link undirected}. + * - Traversal APIs return lazy {@link Walker} values. Use {@link indices}, + * {@link values}, or {@link entries} to choose what each iteration yields. + * - `NodeIndex` and `EdgeIndex` values are identifiers, not array offsets. They + * are not reused after removals. + * - Shortest-path algorithms require a cost function. {@link dijkstra} and + * {@link astar} reject negative weights; use {@link bellmanFord} or + * {@link floydWarshall} when negative weights are part of the model. + * * @since 4.0.0 */ export * as Graph from "./Graph.ts" @@ -1486,16 +2056,184 @@ export * as Graph from "./Graph.ts" export * as Hash from "./Hash.ts" /** + * The `HashMap` module provides an immutable key-value data structure with + * efficient lookup, insertion, removal, and transformation operations. A + * `HashMap` stores entries by hashing keys and resolving matches + * with Effect's structural equality semantics. + * + * **Mental model** + * + * - A `HashMap` is an immutable collection of key-value pairs + * - Operations such as {@link set}, {@link remove}, and {@link modifyAt} return + * new maps; existing maps are not mutated + * - Keys are compared using the `Equal` protocol and are grouped by hashes from + * the `Hash` protocol + * - Plain JavaScript primitives work as keys, and custom objects can define + * `Equal` / `Hash` behavior for structural lookup + * - Lookups with {@link get} return an `Option`, making missing keys explicit + * - Iteration order is based on the map's internal hash structure and should + * not be treated as insertion order + * + * **Common tasks** + * + * - Create maps: {@link empty}, {@link make}, {@link fromIterable} + * - Read values: {@link get}, {@link getUnsafe}, {@link has}, {@link hasBy} + * - Add or update entries: {@link set}, {@link modify}, {@link modifyAt}, {@link setMany} + * - Remove entries: {@link remove}, {@link removeMany} + * - Combine maps: {@link union} + * - Iterate or convert: {@link keys}, {@link values}, {@link entries}, {@link toValues}, {@link toEntries} + * - Transform values: {@link map}, {@link flatMap}, {@link filter}, {@link filterMap}, {@link compact} + * - Fold and search: {@link reduce}, {@link findFirst}, {@link some}, {@link every} + * - Batch updates efficiently: {@link mutate}, {@link beginMutation}, {@link endMutation} + * + * **Gotchas** + * + * - {@link getUnsafe} throws when the key is absent; prefer {@link get} unless + * absence is impossible by construction + * - Mutating a key object after insertion can make future lookups fail if its + * equality or hash changes + * - Hash collisions are handled by equality checks, so matching hashes alone do + * not make two keys equal + * - Use {@link getHash} and {@link hasHash} only when you already have the + * correct hash for the same key + * - Convert entries to an array and sort them when deterministic presentation is + * required + * + * **Quickstart** + * + * **Example** (Working with immutable maps) + * + * ```ts + * import { HashMap, Option } from "effect" + * + * const scores = HashMap.make(["alice", 10], ["bob", 15]) + * + * const updated = scores.pipe( + * HashMap.set("carol", 20), + * HashMap.modify("alice", (score) => score + 1) + * ) + * + * console.log(HashMap.get(updated, "alice")) + * // Output: Option.some(11) + * + * console.log(HashMap.get(scores, "carol")) + * // Output: Option.none() + * + * console.log(Option.getOrElse(HashMap.get(updated, "dave"), () => 0)) + * // Output: 0 + * ``` + * + * **See also** + * + * - {@link HashSet} for immutable sets backed by hash semantics + * - {@link Equal} for structural equality + * - {@link Hash} for hash implementations used by hashed collections + * * @since 2.0.0 */ export * as HashMap from "./HashMap.ts" /** + * The `HashRing` module provides a weighted consistent-hashing data structure + * for assigning arbitrary string inputs to a changing set of nodes. A hash ring + * minimizes remapping when nodes are added, removed, or reweighted, which makes + * it useful for routing requests, partitioning keys, and distributing shards + * across service instances or storage backends. + * + * **Mental model** + * + * - Each node is identified by its {@link PrimaryKey.PrimaryKey} value + * - {@link add} and {@link addMany} place weighted virtual points on the ring + * - {@link get} hashes an input string and returns the nearest node on the ring + * - {@link getShards} assigns a fixed number of shard indexes across the nodes + * - Higher weights receive proportionally more virtual points and shard + * allocations + * - Operations mutate and return the same ring instance + * + * **Common tasks** + * + * - Create an empty ring: {@link make} + * - Add or update nodes: {@link add}, {@link addMany} + * - Remove nodes: {@link remove} + * - Check membership by primary key: {@link has} + * - Route an input key to a node: {@link get} + * - Precompute shard ownership: {@link getShards} + * - Guard unknown values: {@link isHashRing} + * + * **Gotchas** + * + * - Empty rings return `undefined` from {@link get} and {@link getShards} + * - Nodes with the same primary key represent the same ring member + * - Weights are clamped to a positive minimum so a node remains represented + * - Mutating a ring in place is intentional; create a new ring when independent + * snapshots are required + * + * **Quickstart** + * + * **Example** (Routing keys across nodes) + * + * ```ts + * import { HashRing, PrimaryKey } from "effect" + * + * class Node implements PrimaryKey.PrimaryKey { + * constructor(readonly id: string) {} + * + * [PrimaryKey.symbol](): string { + * return this.id + * } + * } + * + * const ring = HashRing.make().pipe( + * HashRing.add(new Node("node-a")), + * HashRing.add(new Node("node-b"), { weight: 2 }) + * ) + * + * const owner = HashRing.get(ring, "user:123") + * console.log(owner ? PrimaryKey.value(owner) : undefined) + * ``` + * * @since 4.0.0 */ export * as HashRing from "./HashRing.ts" /** + * The `HashSet` module provides an immutable set data structure for storing + * unique values with efficient membership checks, additions, removals, and set + * operations. A `HashSet` contains at most one value for each equality class + * as determined by Effect's `Equal` / `Hash` semantics. + * + * **Mental model** + * + * - `HashSet` is an immutable collection of unique values of type `A` + * - Operations such as {@link add}, {@link remove}, {@link union}, and + * {@link difference} return new sets; the input set is never mutated + * - Membership is checked with {@link has}, using Effect equality and hashing + * rather than array-style linear scanning + * - Duplicate values are collapsed when using {@link make}, {@link fromIterable}, + * {@link add}, or {@link map} + * - `HashSet` is iterable, but iteration order is not a sorting guarantee + * + * **Common tasks** + * + * - Create sets: {@link empty}, {@link make}, {@link fromIterable} + * - Check membership and size: {@link has}, {@link size}, {@link isEmpty} + * - Add or remove values: {@link add}, {@link remove} + * - Combine sets: {@link union}, {@link intersection}, {@link difference} + * - Compare sets: {@link isSubset} + * - Transform or select values: {@link map}, {@link filter} + * - Test values: {@link some}, {@link every} + * - Fold values: {@link reduce} + * + * **Gotchas** + * + * - Values that should compare structurally should implement compatible + * `Equal` and `Hash` behavior; otherwise object identity may affect whether + * values are considered distinct + * - {@link map} may reduce the set size when multiple input values map to the + * same output value + * - Do not rely on iteration order for deterministic presentation; sort the + * values after converting to an array when order matters + * * @since 2.0.0 */ export * as HashSet from "./HashSet.ts" @@ -1815,6 +2553,38 @@ export * as JsonPointer from "./JsonPointer.ts" export * as JsonSchema from "./JsonSchema.ts" /** + * The `Latch` module provides a reusable synchronization primitive for + * coordinating fibers. A `Latch` is either open or closed: when it is closed, + * fibers that use {@link await} or {@link whenOpen} suspend until the latch is + * opened or the current waiters are released. + * + * **Mental model** + * + * - An open latch lets current and future waiters continue immediately + * - A closed latch causes `await` and `whenOpen` to suspend + * - {@link open} permanently opens the latch until it is closed again + * - {@link release} wakes only the fibers currently waiting and leaves the + * latch closed for future waiters + * - {@link close} resets the latch so later waiters suspend again + * + * **Common tasks** + * + * - Create a latch inside `Effect`: {@link make} + * - Create a latch synchronously: {@link makeUnsafe} + * - Wait for a signal before continuing: {@link await} + * - Guard an effect so it runs only after the latch is open: {@link whenOpen} + * - Let all current and future waiters proceed: {@link open} + * - Let only the current waiters proceed: {@link release} + * - Re-enable waiting after opening: {@link close} + * + * **Gotchas** + * + * - `release` is not the same as `open`; new waiters still suspend after the + * current waiters are released + * - `open` and `close` report whether they changed the latch state + * - Prefer the effectful APIs unless synchronous allocation or mutation is + * required + * * @since 3.8.0 */ export * as Latch from "./Latch.ts" @@ -1841,231 +2611,210 @@ export * as Latch from "./Latch.ts" export * as Layer from "./Layer.ts" /** - * @since 3.14.0 - */ -export * as LayerMap from "./LayerMap.ts" - -/** - * @since 2.0.0 - * - * The `Logger` module provides a robust and flexible logging system for Effect applications. - * It offers structured logging, multiple output formats, and seamless integration with the - * Effect runtime's tracing and context management. + * The `LayerMap` module provides utilities for managing scoped resources that + * are selected by key and built from `Layer` values. A `LayerMap` turns + * a key into a cached service `Context`, so applications can lazily acquire + * and reuse different resource instances such as tenant clients, regional + * connections, environment-specific services, or other keyed infrastructure. * - * ## Key Features - * - * - **Structured Logging**: Built-in support for structured log messages with metadata - * - **Multiple Formats**: JSON, LogFmt, Pretty, and custom formatting options - * - **Context Integration**: Automatic capture of fiber context, spans, and annotations - * - **Batching**: Efficient log aggregation and batch processing - * - **File Output**: Direct file writing with configurable batch windows - * - **Composable**: Transform and compose loggers using functional patterns + * **Mental model** * - * ## Basic Usage + * - A `LayerMap` is a scoped, reference-counted cache of contexts produced by layers + * - Keys identify which layer-backed resource set should be acquired + * - Resources are acquired on demand when a key is requested + * - The same key reuses the cached context while it remains live + * - Cached resources are finalized when invalidated, when their scope closes, or after idle expiration + * - The layers built by a `LayerMap` share the current layer memoization map * - * **Example** (Logging messages with structured data) + * **Common tasks** * - * ```ts - * import { Effect } from "effect" + * - Create from a lookup function: {@link make} + * - Create from a fixed record of layers: {@link fromRecord} + * - Define a service wrapper with accessor helpers: {@link Service} + * - Retrieve a layer for a key: {@link LayerMap.get} + * - Retrieve a scoped context directly: {@link LayerMap.contextEffect} + * - Force a cached entry to be rebuilt later: {@link LayerMap.invalidate} + * - Remove idle entries automatically with the `idleTimeToLive` option + * - Eagerly build known entries with `preloadKeys` or `preload` * - * // Basic logging - * const program = Effect.gen(function*() { - * yield* Effect.log("Application started") - * yield* Effect.logInfo("Processing user request") - * yield* Effect.logWarning("Resource limit approaching") - * yield* Effect.logError("Database connection failed") - * }) + * **Gotchas** * - * // With structured data - * const structuredLog = Effect.gen(function*() { - * yield* Effect.log("User action", { userId: 123, action: "login" }) - * yield* Effect.logInfo("Request processed", { duration: 150, statusCode: 200 }) - * }) - * ``` + * - `contextEffect` requires a `Scope.Scope` because it exposes the acquired context directly + * - `get` returns a `Layer` that can be provided to programs expecting the keyed services + * - Invalidating a key finalizes the current cached resources for that key; the next access rebuilds them + * - Preloading moves layer construction errors to `LayerMap` creation instead of first use * - * ## Custom Loggers + * @since 3.14.0 + */ +export * as LayerMap from "./LayerMap.ts" + +/** + * The `Logger` module defines the logging model used by the Effect runtime and + * provides constructors for formatting, routing, batching, and installing + * loggers. A `Logger` receives each runtime log event as an + * {@link Options} value and transforms it into an output such as a string, + * structured object, JSON line, console write, file write, or trace span event. * - * **Example** (Creating and providing custom loggers) + * **Mental model** * - * ```ts - * import { Effect, Logger } from "effect" + * - Effect programs emit log events with APIs such as `Effect.log`, + * `Effect.logInfo`, `Effect.logWarning`, and `Effect.logError` + * - Each event contains a message, log level, cause, fiber, and timestamp + * - Loggers are ordinary values created with {@link make} and installed with + * {@link layer} + * - Multiple loggers can be active at once by providing a layer with several + * logger values + * - Formatter loggers such as {@link formatLogFmt}, {@link formatStructured}, + * and {@link formatJson} return formatted data without writing it anywhere + * - Console loggers such as {@link consolePretty}, {@link consoleLogFmt}, + * {@link consoleStructured}, and {@link consoleJson} write formatted output + * to the active Effect console + * + * **Log output structure** + * + * Built-in formatters include the log level, timestamp, fiber identifier, and + * logged message. When present, they also include the pretty-printed cause, + * active log annotations, and active log spans. Structured and JSON loggers keep + * these fields as machine-readable data, while logfmt and pretty loggers render + * them as human-readable text. * - * // Create a custom logger - * const customLogger = Logger.make((options) => { - * console.log(`[${options.logLevel}] ${options.message}`) - * }) + * **Common tasks** * - * // Use JSON format for production - * const jsonLogger = Logger.consoleJson + * - Create a custom logger: {@link make} + * - Transform logger output: {@link map} + * - Write formatter output to the console: {@link withConsoleLog}, + * {@link withConsoleError}, {@link withLeveledConsole} + * - Use built-in console loggers: {@link consolePretty}, {@link consoleLogFmt}, + * {@link consoleStructured}, {@link consoleJson} + * - Use built-in formatter loggers: {@link formatSimple}, {@link formatLogFmt}, + * {@link formatStructured}, {@link formatJson} + * - Batch logger output before flushing to a sink: {@link batched} + * - Write string logger output to a file: {@link toFile} + * - Preserve trace correlation by including {@link tracerLogger} + * - Install or replace loggers for an effect: {@link layer} * - * // Pretty format for development - * const prettyLogger = Logger.consolePretty() + * **Gotchas** * - * const program = Effect.log("Hello World").pipe( - * Effect.provide(Logger.layer([jsonLogger])) - * ) - * ``` + * - {@link layer} replaces the current logger set by default; pass + * `mergeWithExisting: true` when adding loggers to the existing runtime + * loggers + * - Formatter loggers only produce values; wrap them with console, file, batch, + * or custom sink loggers when output should be written somewhere + * - {@link batched} and {@link toFile} are scoped; keep their scope open while + * logs are being emitted so buffered entries can flush reliably + * - {@link toFile} accepts only loggers that output strings, so pair it with + * string formatters such as {@link formatJson} or {@link formatLogFmt} + * - The default runtime logger set includes {@link tracerLogger}; replacing + * loggers without merging may remove automatic log-to-trace-span recording * - * ## Multiple Loggers + * **Quickstart** * - * **Example** (Combining multiple loggers) + * **Example** (Installing a JSON console logger) * * ```ts * import { Effect, Logger } from "effect" * - * // Combine multiple loggers - * const CombinedLoggerLive = Logger.layer([ - * Logger.consoleJson, - * Logger.consolePretty() - * ]) - * - * const program = Effect.log("Application event").pipe( - * Effect.provide(CombinedLoggerLive) + * const program = Effect.gen(function*() { + * yield* Effect.logInfo("request started", { method: "GET", path: "/users" }) + * yield* Effect.logError("request failed", { status: 500 }) + * }).pipe( + * Effect.annotateLogs("service", "users-api"), + * Effect.withLogSpan("http.request"), + * Effect.provide(Logger.layer([Logger.consoleJson])) * ) * ``` * - * ## Batched Logging - * - * **Example** (Batching log messages) + * **See also** * - * ```ts - * import { Duration, Effect, Logger } from "effect" - * - * const batchedLogger = Logger.batched(Logger.formatJson, { - * window: Duration.seconds(5), - * flush: (messages) => - * Effect.sync(() => { - * // Process batch of log messages - * console.log("Flushing", messages.length, "log entries") - * }) - * }) + * - {@link make} for defining custom loggers + * - {@link layer} for installing loggers + * - {@link formatJson} and {@link consoleJson} for structured production logs + * - {@link consolePretty} for readable local logs * - * const program = Effect.gen(function*() { - * const logger = yield* batchedLogger - * yield* Effect.provide( - * Effect.all([ - * Effect.log("Event 1"), - * Effect.log("Event 2"), - * Effect.log("Event 3") - * ]), - * Logger.layer([logger]) - * ) - * }) - * ``` + * @since 2.0.0 */ export * as Logger from "./Logger.ts" /** - * @since 2.0.0 - * - * The `LogLevel` module provides utilities for managing log levels in Effect applications. - * It defines a hierarchy of log levels and provides functions for comparing and filtering logs - * based on their severity. - * - * ## Log Level Hierarchy - * - * The log levels are ordered from most severe to least severe: - * - * 1. **All** - Special level that allows all messages - * 2. **Fatal** - System is unusable, immediate attention required - * 3. **Error** - Error conditions that should be investigated - * 4. **Warn** - Warning conditions that may indicate problems - * 5. **Info** - Informational messages about normal operation - * 6. **Debug** - Debug information useful during development - * 7. **Trace** - Very detailed trace information - * 8. **None** - Special level that suppresses all messages - * - * ## Basic Usage - * - * **Example** (Logging at different levels) - * - * ```ts - * import { Effect } from "effect" - * - * // Basic log level usage - * const program = Effect.gen(function*() { - * yield* Effect.logFatal("System is shutting down") - * yield* Effect.logError("Database connection failed") - * yield* Effect.logWarning("Memory usage is high") - * yield* Effect.logInfo("User logged in") - * yield* Effect.logDebug("Processing request") - * yield* Effect.logTrace("Variable value: xyz") - * }) - * ``` + * The `LogLevel` module defines the levels used by Effect logging and the + * ordering operations used to compare, filter, and enable log output. * - * ## Level Comparison - * - * **Example** (Comparing log levels) - * - * ```ts - * import { LogLevel } from "effect" - * - * // Check if one level is more severe than another - * console.log(LogLevel.isGreaterThan("Error", "Info")) // true - * console.log(LogLevel.isGreaterThan("Debug", "Error")) // false + * **Mental model** * - * // Check if level meets minimum threshold - * console.log(LogLevel.isGreaterThanOrEqualTo("Info", "Debug")) // true - * console.log(LogLevel.isLessThan("Trace", "Info")) // true - * ``` + * - A `LogLevel` is one of `All`, `Fatal`, `Error`, `Warn`, `Info`, `Debug`, + * `Trace`, or `None` + * - `Fatal` is the most severe concrete level and `Trace` is the least severe + * - `All` and `None` are sentinel levels: `All` enables every message and + * `None` disables every message + * - Ordering follows logging severity, so higher levels are more important and + * lower levels are more verbose + * - Filtering is usually expressed as "log this message when its level is + * greater than or equal to the configured minimum" * - * ## Filtering by Level + * **Common tasks** * - * **Example** (Filtering logger output) + * - Enumerate levels with {@link values} + * - Compare exact levels with {@link Equivalence} + * - Sort or compare by severity with {@link Order} and {@link getOrdinal} + * - Check thresholds with {@link isGreaterThanOrEqualTo} and + * {@link isLessThanOrEqualTo} + * - Test whether a level is enabled for the current fiber with + * {@link isEnabled} * - * ```ts - * import { Logger, LogLevel } from "effect" + * **Gotchas** * - * // Create a logger that only logs Error and above - * const errorLogger = Logger.make((options) => { - * if (LogLevel.isGreaterThanOrEqualTo(options.logLevel, "Error")) { - * console.log(`[${options.logLevel}] ${options.message}`) - * } - * }) + * - `All` and `None` are useful for configuration boundaries, but they are not + * concrete message severities; use {@link Severity} when only emitted message + * levels are valid + * - The comparison helpers compare severity, not declaration position in source + * code or alphabetical order + * - `isEnabled` reads the current fiber's `MinimumLogLevel` reference, so it is + * context-sensitive; use the pure comparison helpers when checking an + * explicit threshold * - * // Production logger - Info and above - * const productionLogger = Logger.make((options) => { - * if (LogLevel.isGreaterThanOrEqualTo(options.logLevel, "Info")) { - * console.log( - * `${options.date.toISOString()} [${options.logLevel}] ${options.message}` - * ) - * } - * }) + * @since 2.0.0 + */ +export * as LogLevel from "./LogLevel.ts" + +/** + * The `ManagedRuntime` module provides a way to build a reusable runtime from + * a `Layer` and use it to run effects that require the services produced by + * that layer. A `ManagedRuntime` owns the lifecycle of the layer-built + * resources, caches the resulting `Context`, and exposes runners for + * integrating Effect programs with JavaScript entry points. * - * // Development logger - Debug and above - * const devLogger = Logger.make((options) => { - * if (LogLevel.isGreaterThanOrEqualTo(options.logLevel, "Debug")) { - * console.log(`[${options.logLevel}] ${options.message}`) - * } - * }) - * ``` + * **Mental model** * - * ## Runtime Configuration + * - A managed runtime is created from a `Layer` with {@link make} + * - The layer is built lazily the first time the runtime is used + * - The built context is cached and reused for subsequent effect executions + * - Resources acquired by the layer are owned by the runtime's internal scope + * - Disposing the runtime closes that scope and releases all managed resources + * - Effects run through the runtime receive the layer's services automatically * - * **Example** (Configuring log level from the environment) + * **Common tasks** * - * ```ts - * import { Config, Effect, Logger, LogLevel } from "effect" + * - Create a runtime from application services: {@link make} + * - Run an effect as a `Promise`: {@link ManagedRuntime.runPromise} + * - Run an effect and keep its `Exit`: {@link ManagedRuntime.runPromiseExit} + * - Fork an effect into a `Fiber`: {@link ManagedRuntime.runFork} + * - Bridge callback-style APIs: {@link ManagedRuntime.runCallback} + * - Run synchronous effects at program boundaries: {@link ManagedRuntime.runSync}, + * {@link ManagedRuntime.runSyncExit} + * - Access the cached service context: {@link ManagedRuntime.context} + * - Release layer resources: {@link ManagedRuntime.dispose}, + * {@link ManagedRuntime.disposeEffect} * - * // Configure log level from environment - * const logLevelConfig = Config.string("LOG_LEVEL").pipe( - * Config.withDefault("Info") - * ) + * **Gotchas** * - * const configurableLogger = Effect.gen(function*() { - * const minLevel = yield* logLevelConfig + * - Always dispose a managed runtime when it is no longer needed, especially + * when the layer acquires resources such as connections, servers, or files + * - Layer construction errors are included in the error channel of runtime + * runners, so `ER` is combined with the effect's own error type + * - `runSync` can only execute effects without asynchronous boundaries; use + * `runPromise` for asynchronous programs + * - After disposal, the runtime cannot be reused * - * return Logger.make((options) => { - * if (LogLevel.isGreaterThanOrEqualTo(options.logLevel, minLevel)) { - * console.log(`[${options.logLevel}] ${options.message}`) - * } - * }) - * }) - * ``` - */ -export * as LogLevel from "./LogLevel.ts" - -/** * @since 2.0.0 */ export * as ManagedRuntime from "./ManagedRuntime.ts" @@ -2101,51 +2850,56 @@ export * as ManagedRuntime from "./ManagedRuntime.ts" export * as Match from "./Match.ts" /** - * @since 2.0.0 - * - * The `Metric` module provides a comprehensive system for collecting, aggregating, and observing - * application metrics in Effect applications. It offers type-safe, concurrent metrics that can - * be used to monitor performance, track business metrics, and gain insights into application behavior. - * - * ## Key Features - * - * - **Five Metric Types**: Counters, Gauges, Frequencies, Histograms, and Summaries - * - **Type Safety**: Fully typed metrics with compile-time guarantees - * - **Concurrency Safe**: Thread-safe metrics that work with Effect's concurrency model - * - **Attributes**: Tag metrics with key-value attributes for filtering and grouping - * - **Snapshots**: Take point-in-time snapshots of all metrics for reporting - * - **Runtime Integration**: Automatic fiber runtime metrics collection + * The `Metric` module provides tools for defining, updating, tagging, and + * reading application metrics from Effect programs. A `Metric` + * accepts typed input values and aggregates them into a typed state that can be + * read directly or exported from a snapshot. * - * ## Metric Types + * **Mental model** * - * ### Counter - * Tracks cumulative values that only increase or can be reset to zero. - * Perfect for counting events, requests, errors, etc. + * - A metric has an identifier, a type, an optional description, optional attributes, and mutable aggregate state + * - Use counters for cumulative values such as requests, errors, retries, or bytes processed + * - Use gauges for point-in-time values that can rise or fall, such as active connections or queue size + * - Use frequencies to count occurrences of discrete string values, such as status codes or action names + * - Use histograms to bucket numeric observations and inspect count, min, max, and sum + * - Use summaries to calculate quantiles over a bounded, time-based observation window + * - Metrics are updated from effects with {@link update} and {@link modify}, and read with {@link value} + * - Attributes tag metrics with key-value dimensions so the same logical metric can be grouped by service, endpoint, method, or other labels + * - Snapshots capture the currently registered metrics and their aggregate states for reporting or export * - * ### Gauge - * Represents a single numerical value that can go up or down. - * Ideal for current resource usage, temperature, queue sizes, etc. + * **Common tasks** * - * ### Frequency - * Counts occurrences of discrete string values. - * Useful for tracking categorical data like HTTP status codes, user actions, etc. + * - Create counters: {@link counter} + * - Create gauges: {@link gauge} + * - Create frequencies: {@link frequency} + * - Create histograms: {@link histogram}, {@link linearBoundaries}, {@link exponentialBoundaries} + * - Create summaries: {@link summary}, {@link summaryWithTimestamp} + * - Measure effect duration: {@link timer} + * - Update a metric: {@link update} + * - Apply relative updates where supported: {@link modify} + * - Read one metric: {@link value} + * - Tag a metric: {@link withAttributes} + * - Transform accepted input values: {@link mapInput} + * - Record a constant input for repeated events: {@link withConstantInput} + * - Inspect all registered metrics: {@link snapshot}, {@link dump} + * - Enable fiber runtime metrics: {@link enableRuntimeMetrics} * - * ### Histogram - * Records observations in configurable buckets to analyze distribution. - * Great for response times, request sizes, and other measured values. + * **Gotchas** * - * ### Summary - * Calculates quantiles over a sliding time window. - * Provides statistical insights into value distributions over time. + * - Counter and gauge metrics can use `number` inputs by default or `bigint` inputs with the `bigint` option + * - Incremental counters ignore negative updates; use non-incremental counters only when decreases are meaningful + * - {@link update} sets a gauge to an absolute value, while {@link modify} changes it relative to its current value + * - Histogram buckets are cumulative and depend on the boundaries supplied when the metric is created + * - Summary quantiles are calculated from the configured sliding window, so old observations expire + * - Prefer low-cardinality attributes; using unbounded values such as request IDs can create too many metric series * - * ## Basic Usage + * **Quickstart** * * **Example** (Creating and updating metrics) * * ```ts * import { Effect, Metric } from "effect" * - * // Create metrics * const requestCount = Metric.counter("http_requests_total", { * description: "Total number of HTTP requests" * }) @@ -2155,33 +2909,7 @@ export * as Match from "./Match.ts" * boundaries: Metric.linearBoundaries({ start: 0, width: 50, count: 20 }) * }) * - * // Use metrics in your application * const handleRequest = Effect.gen(function*() { - * yield* Metric.update(requestCount, 1) - * - * const startTime = yield* Effect.clockWith((clock) => clock.currentTimeMillis) - * - * // Process request... - * yield* Effect.sleep("100 millis") - * - * const endTime = yield* Effect.clockWith((clock) => clock.currentTimeMillis) - * yield* Metric.update(responseTime, endTime - startTime) - * }) - * ``` - * - * ## Attributes and Tagging - * - * **Example** (Tagging metrics with attributes) - * - * ```ts - * import { Effect, Metric } from "effect" - * - * const requestCount = Metric.counter("requests", { - * description: "Number of requests by endpoint and method" - * }) - * - * const program = Effect.gen(function*() { - * // Add attributes to metrics * yield* Metric.update( * Metric.withAttributes(requestCount, { * endpoint: "/api/users", @@ -2190,59 +2918,21 @@ export * as Match from "./Match.ts" * 1 * ) * - * // Or use withAttributes for compile-time attributes - * const taggedCounter = Metric.withAttributes(requestCount, { - * endpoint: "/api/posts", - * method: "POST" - * }) - * yield* Metric.update(taggedCounter, 1) - * }) - * ``` - * - * ## Advanced Examples - * - * **Example** (Recording business and performance metrics) - * - * ```ts - * import { Effect, Metric } from "effect" - * - * // Business metrics - * const userSignups = Metric.counter("user_signups_total") - * const activeUsers = Metric.gauge("active_users_current") - * const featureUsage = Metric.frequency("feature_usage") + * yield* Metric.update(responseTime, 125) * - * // Performance metrics - * const dbQueryTime = Metric.summary("db_query_duration", { - * maxAge: "5 minutes", - * maxSize: 1000, - * quantiles: [0.5, 0.9, 0.95, 0.99] + * return yield* Metric.value(requestCount) * }) + * ``` * - * const program = Effect.gen(function*() { - * // Track user signup - * yield* Metric.update(userSignups, 1) - * - * // Update active user count - * yield* Metric.update(activeUsers, 1250) - * - * // Record feature usage - * yield* Metric.update(featureUsage, "dashboard_view") - * - * // Measure database query time - * yield* Effect.timed(performDatabaseQuery).pipe( - * Effect.tap(([duration]) => Metric.update(dbQueryTime, duration)) - * ) - * }) + * **See also** * - * // Get metric snapshots - * const getMetrics = Effect.gen(function*() { - * const snapshots = yield* Metric.snapshot + * - {@link counter} / {@link gauge} / {@link frequency} for common metric types + * - {@link histogram} / {@link summary} for distribution metrics + * - {@link update} / {@link modify} / {@link value} for working with metric state + * - {@link withAttributes} for adding dimensions + * - {@link snapshot} for exporting all registered metric values * - * for (const metric of snapshots) { - * console.log(`${metric.id}: ${JSON.stringify(metric.state)}`) - * } - * }) - * ``` + * @since 2.0.0 */ export * as Metric from "./Metric.ts" @@ -2275,99 +2965,166 @@ export * as Metric from "./Metric.ts" export * as MutableHashMap from "./MutableHashMap.ts" /** - * @fileoverview - * MutableHashSet is a high-performance, mutable set implementation that provides efficient storage - * and retrieval of unique values. Built on top of MutableHashMap, it inherits the same performance - * characteristics and support for both structural and referential equality. + * The `MutableHashSet` module provides a mutable hash set for storing unique + * values with efficient membership checks, insertion, removal, and iteration. + * It is built on {@link MutableHashMap}: each set value is stored as a map key, + * so uniqueness follows the same hashing and equality rules as the underlying + * mutable hash map. + * + * **Mental model** + * + * - `MutableHashSet` is a mutable collection of unique values of type `V` + * - Operations such as {@link add}, {@link remove}, and {@link clear} mutate + * the set in place + * - Duplicate values are ignored according to Effect equality and hashing semantics + * - Values that implement `Equal` / `Hash` are compared structurally + * - Primitive values and references that do not implement Effect equality use + * the normal hash map behavior + * - The set is iterable, so `Array.from(set)` or `for...of` can be used to + * inspect its values + * + * **Common tasks** + * + * - Create an empty set: {@link empty} + * - Create from values: {@link make} + * - Create from any iterable: {@link fromIterable} + * - Add a value: {@link add} + * - Check membership: {@link has} + * - Remove a value: {@link remove} + * - Remove all values: {@link clear} + * - Count unique values: {@link size} + * - Narrow unknown values: {@link isMutableHashSet} + * + * **Gotchas** + * + * - This data structure is intentionally mutable; keep ownership clear when + * sharing it between callers + * - Mutating operations return the same set instance for convenient piping, not + * a copy + * - Iteration order should not be used as a stable sorting mechanism + * - For immutable set operations, use Effect's immutable collection modules + * instead + * + * **Performance** + * + * - Add, membership checks, and removal are O(1) on average and O(n) in the + * presence of hash collisions + * - Clearing and reading the size are O(1) + * - Iteration is O(n) + * + * **Quickstart** + * + * **Example** (Tracking unique values) + * + * ```ts + * import { MutableHashSet } from "effect" * - * The implementation uses a MutableHashMap internally where each value is stored as a key with a - * boolean flag, providing O(1) average-case performance for all operations. + * const set = MutableHashSet.make("apple", "banana", "apple") * - * Key Features: - * - Mutable operations for performance-critical scenarios - * - Supports both structural and referential equality - * - Efficient duplicate detection and removal - * - Iterable interface for easy traversal - * - Memory-efficient storage with automatic deduplication - * - Seamless integration with Effect's Equal and Hash interfaces + * MutableHashSet.add(set, "cherry") + * MutableHashSet.remove(set, "banana") * - * Performance Characteristics: - * - Add/Has/Remove: O(1) average, O(n) worst case (hash collisions) - * - Clear: O(1) - * - Size: O(1) - * - Iteration: O(n) + * console.log(MutableHashSet.has(set, "apple")) + * // Output: true + * + * console.log(MutableHashSet.size(set)) + * // Output: 2 + * + * console.log(Array.from(set)) + * // Output: ["apple", "cherry"] + * ``` * + * @fileoverview * @category data-structures * @since 2.0.0 */ export * as MutableHashSet from "./MutableHashSet.ts" /** - * @fileoverview - * MutableList is an efficient, mutable linked list implementation optimized for high-throughput - * scenarios like logging, queuing, and streaming. It uses a bucket-based architecture where - * elements are stored in arrays (buckets) linked together, providing optimal performance for - * both append and prepend operations. + * The `MutableList` module provides a mutable linked list for accumulating, + * ordering, inspecting, and draining values with efficient operations at both + * ends of the list. * - * The implementation uses a sophisticated bucket system: - * - Each bucket contains an array of elements with an offset pointer - * - Buckets can be marked as mutable or immutable for optimization - * - Elements are taken from the head and added to the tail - * - Memory is efficiently managed through bucket reuse and cleanup + * A `MutableList` stores values in linked buckets of arrays. Appending adds + * values to the tail, prepending adds values to the head, and taking removes + * values from the head. Unlike persistent collections, every mutation updates + * the list object in place: operations such as {@link append}, {@link prepend}, + * {@link take}, {@link takeN}, {@link clear}, {@link filter}, and {@link remove} + * change the same `MutableList` instance and update its `length`. * - * Key Features: - * - Highly optimized for high-frequency append/prepend operations - * - Memory efficient with automatic cleanup of consumed elements - * - Support for bulk operations (appendAll, prependAll, takeN) - * - Filtering and removal operations - * - Zero-copy optimizations for certain scenarios + * **Mental model** * - * Performance Characteristics: - * - Append/Prepend: O(1) amortized - * - Take/TakeN: O(1) per element taken - * - Length: O(1) - * - Clear: O(1) - * - Filter: O(n) + * - `MutableList` is a stateful container with `head`, `tail`, and `length` + * - Values are consumed from the head with {@link take}, {@link takeN}, or + * {@link takeAll} + * - {@link append} and {@link appendAll} preserve FIFO queue order for normal + * producer-consumer use cases + * - {@link prepend} and {@link prependAll} place values before the current + * contents, which is useful for priority work or restoring items to the front + * - {@link toArray} and {@link toArrayN} copy values without modifying the list + * - The `head` and `tail` bucket fields are exposed for advanced use, but most + * code should treat them as implementation details + * + * **Common tasks** * - * Ideal Use Cases: - * - High-throughput logging systems - * - Producer-consumer queues - * - Streaming data buffers - * - Real-time data processing pipelines + * - Create an empty list: {@link make} + * - Add one value: {@link append}, {@link prepend} + * - Add many values: {@link appendAll}, {@link prependAll} + * - Drain one value: {@link take} + * - Drain many values: {@link takeN}, {@link takeAll} + * - Inspect without draining: {@link toArrayN}, {@link toArray} + * - Reset the list: {@link clear} + * - Mutate contents in place: {@link filter}, {@link remove} * + * **Gotchas** + * + * - `MutableList` is intentionally mutable; sharing a list means sharing its + * changing state + * - {@link take} returns the {@link Empty} symbol when the list has no value, so + * compare with `MutableList.Empty` instead of relying on falsy checks + * - {@link appendAllUnsafe} and {@link prependAllUnsafe} may reuse the provided + * array when `mutable` is `true`; only enable that optimization when callers + * will not keep using the array independently + * - {@link remove} uses JavaScript strict equality semantics, not structural + * equality + * + * @fileoverview * @category data-structures * @since 4.0.0 */ export * as MutableList from "./MutableList.ts" /** - * @fileoverview - * MutableRef provides a mutable reference container that allows safe mutation of values - * in functional programming contexts. It serves as a bridge between functional and imperative - * programming paradigms, offering atomic operations for state management. + * The `MutableRef` module provides a small synchronous container for mutable + * state. A `MutableRef` stores one current value of type `A`, exposes that + * value through `.current`, and offers pipeable helpers for reading, replacing, + * and transforming the value in place. * - * Unlike regular variables, MutableRef encapsulates mutable state and provides controlled - * access through a standardized API. It supports atomic compare-and-set operations for - * thread-safe updates and integrates seamlessly with Effect's ecosystem. + * **Mental model** * - * Key Features: - * - Mutable reference semantics with functional API - * - Atomic compare-and-set operations for safe concurrent updates - * - Specialized operations for numeric and boolean values - * - Chainable operations that return the reference or the value - * - Integration with Effect's Equal interface for value comparison - * - * Common Use Cases: - * - State containers in functional applications - * - Counters and accumulators - * - Configuration that needs to be updated at runtime - * - Caching and memoization scenarios - * - Inter-module communication via shared references + * - `MutableRef` is a stable reference whose `.current` field may change over time + * - Reads and writes are synchronous and return immediately + * - `set`, `update`, `increment`, `decrement`, and `toggle` mutate the same reference in place + * - `getAnd*` helpers return the previous value, while `*AndGet` helpers return the new value + * - `compareAndSet` updates only when the current value is equal to the expected value using `Equal.equals` + * - A `MutableRef` is useful for local mutable state, but it does not make updates transactional or effectful * - * Performance Characteristics: - * - Get/Set: O(1) - * - Compare-and-set: O(1) - * - All operations: O(1) + * **Common tasks** + * + * - Create a reference: {@link make} + * - Read the current value: {@link get} or `.current` + * - Replace the current value: {@link set}, {@link setAndGet}, {@link getAndSet} + * - Transform the current value: {@link update}, {@link updateAndGet}, {@link getAndUpdate} + * - Coordinate conditional replacement: {@link compareAndSet} + * - Work with counters: {@link increment}, {@link decrement}, {@link incrementAndGet}, {@link decrementAndGet} + * - Work with boolean flags: {@link toggle} + * + * **Gotchas** + * + * - All updates are imperative mutations; aliases to the same `MutableRef` observe the same changing value + * - Updating object or array values does not clone them unless the update function creates a new value + * - `compareAndSet` compares with Effect equality semantics, not only JavaScript reference equality + * - For state that must participate in `Effect` workflows, interruption, or fiber coordination, prefer higher-level Effect data types * * @category data-structures * @since 2.0.0 @@ -2442,28 +3199,34 @@ export * as MutableRef from "./MutableRef.ts" export * as Newtype from "./Newtype.ts" /** - * @since 2.0.0 + * The `NonEmptyIterable` module provides a type-level representation of any + * JavaScript `Iterable` that is known to contain at least one element. A + * `NonEmptyIterable` can be consumed anywhere an `Iterable` is expected, + * while also carrying the guarantee that reading the first element is safe. * - * The `NonEmptyIterable` module provides types and utilities for working with iterables - * that are guaranteed to contain at least one element. This provides compile-time - * safety when working with collections that must not be empty. + * **Mental model** * - * ## Key Features + * - `NonEmptyIterable` is an `Iterable` branded with a non-empty guarantee + * - The guarantee is static: values should only be typed this way when construction or validation proves at least one element exists + * - The iterable can be an array, string, set, map, generator, or any custom iterable + * - `unprepend` safely separates the first element from an iterator for the remaining elements + * - Operations that may remove elements, such as filtering, usually return ordinary collections because they can become empty * - * - **Type Safety**: Compile-time guarantee that the iterable contains at least one element - * - **Iterator Protocol**: Fully compatible with JavaScript's built-in iteration protocol - * - **Functional Operations**: Safe operations that preserve the non-empty property - * - **Lightweight**: Minimal overhead with maximum type safety + * **Common tasks** + * + * - Accept inputs that must contain at least one value + * - Extract a head element and process the remaining iterator with {@link unprepend} + * - Model APIs such as reductions, comparisons, or aggregation that are undefined for empty inputs + * - Preserve compatibility with the JavaScript iteration protocol while documenting the stronger invariant * - * ## Why NonEmptyIterable? + * **Gotchas** * - * Many operations require non-empty collections to be meaningful: - * - Finding the maximum or minimum value - * - Getting the first or last element - * - Reducing without an initial value - * - Operations that would otherwise need runtime checks + * - A type assertion does not make an empty iterable non-empty; only assert after a trusted check or constructor + * - Iterators are stateful, so calling {@link unprepend} consumes the first yielded value from that iterator + * - The order of the first element follows the source iterable's iteration order, for example insertion order for `Map` and `Set` + * - Some transformations preserve non-emptiness, but transformations that can discard elements must account for the empty case * - * ## Basic Usage + * **Quickstart** * * **Example** (Requiring a non-empty iterable) * @@ -2575,6 +3338,8 @@ export * as Newtype from "./Newtype.ts" * // This would still be non-empty if the source was non-empty * ) * ``` + * + * @since 2.0.0 */ export * as NonEmptyIterable from "./NonEmptyIterable.ts" @@ -2812,55 +3577,207 @@ export * as Option from "./Option.ts" export * as Order from "./Order.ts" /** - * @fileoverview - * The Ordering module provides utilities for working with comparison results and ordering operations. - * An Ordering represents the result of comparing two values, expressing whether the first value is - * less than (-1), equal to (0), or greater than (1) the second value. + * The `Ordering` module provides the standard representation for the result of + * comparing two values. An `Ordering` is one of three numeric literals: `-1` + * when the first value is less than the second, `0` when both values compare as + * equal, and `1` when the first value is greater than the second. + * + * **Mental model** * - * This module is fundamental for building comparison functions, sorting algorithms, and implementing - * ordered data structures. It provides composable operations for combining multiple comparison results - * and pattern matching on ordering outcomes. + * - `Ordering` describes the relationship between two compared values, not the + * values themselves + * - Negative means "less than", zero means "equal", and positive means "greater + * than" + * - Unlike JavaScript comparators, this type is normalized to exactly `-1`, `0`, + * or `1` + * - `0` is neutral when combining comparisons; the first non-zero ordering + * determines the result * - * Key Features: - * - Type-safe representation of comparison results (-1, 0, 1) - * - Composable operations for combining multiple orderings - * - Pattern matching utilities for handling different ordering cases - * - Ordering reversal and combination functions - * - Integration with Effect's functional programming patterns - * - * Common Use Cases: - * - Implementing custom comparison functions - * - Building complex sorting criteria - * - Combining multiple comparison results - * - Creating ordered data structures - * - Pattern matching on comparison outcomes + * **Common tasks** + * + * - Interpret a comparison result with {@link match} + * - Reverse ascending and descending order with {@link reverse} + * - Combine multiple comparison criteria with {@link Reducer} + * - Build custom comparison functions for sorting, ordered collections, and + * domain-specific ordering rules * + * **Gotchas** + * + * - Do not cast arbitrary comparator results such as `a.localeCompare(b)` + * directly unless they have been normalized to `-1`, `0`, or `1` + * - In comparator-style APIs, `-1` means the left value should come before the + * right value, while `1` means it should come after + * - Reversing an `Ordering` swaps `-1` and `1`, but leaves `0` unchanged + * + * @fileoverview * @category utilities * @since 2.0.0 */ export * as Ordering from "./Ordering.ts" /** + * The `PartitionedSemaphore` module provides a semaphore for limiting + * concurrency across a shared permit pool while keeping waiters grouped by + * partition key. A `PartitionedSemaphore` is useful when many independent + * groups of work compete for the same bounded resource and each group should + * make progress without one busy group monopolizing released permits. + * + * **Mental model** + * + * - The semaphore has a fixed shared capacity measured in permits + * - Work acquires permits with a partition key of type `K` + * - Waiting acquisitions are tracked per partition + * - Released permits are assigned to waiting partitions in round-robin order + * - `withPermit` and `withPermits` acquire permits around an effect and + * release them when the effect exits, fails, or is interrupted + * + * **Common tasks** + * + * - Create a semaphore: {@link make}, {@link makeUnsafe} + * - Inspect capacity and availability: {@link capacity}, {@link available} + * - Acquire and release manually: {@link take}, {@link release} + * - Limit a single operation per partition: {@link withPermit} + * - Limit weighted work per partition: {@link withPermits} + * - Run only when permits are immediately available: + * {@link withPermitsIfAvailable} + * + * **Gotchas** + * + * - `withPermitsIfAvailable` does not use a partition key; it only succeeds + * when the shared pool has enough permits immediately + * - Acquiring more permits than the semaphore capacity never completes + * - Requests for zero or negative permits complete without acquiring anything + * - Non-finite capacities create an unbounded semaphore whose acquire and + * release operations complete immediately + * * @since 4.0.0 */ export * as PartitionedSemaphore from "./PartitionedSemaphore.ts" /** + * The `Path` module provides a platform path service for manipulating file + * system paths through Effect's environment. It models path operations as a + * replaceable service so programs can depend on path behavior without directly + * coupling to a particular runtime implementation. + * + * **Mental model** + * + * - `Path.Path` is a `Context.Service` tag used to access the current path implementation + * - The service offers familiar path operations such as joining, resolving, parsing, and formatting + * - Most operations are pure string transformations and follow POSIX-style path semantics + * - File URL conversions return `Effect`s because invalid paths or URLs can fail with `BadArgument` + * - Custom implementations can be provided with `Layer.succeed` for alternate platforms or tests + * + * **Common tasks** + * + * - Combine path segments with `join` or turn segments into an absolute path with `resolve` + * - Normalize `.` and `..` segments with `normalize` + * - Inspect paths with `basename`, `dirname`, `extname`, and `isAbsolute` + * - Convert between structured path parts and strings with `parse` and `format` + * - Compute relative paths with `relative` + * - Convert between file paths and `file:` URLs with `toFileUrl` and `fromFileUrl` + * + * **Gotchas** + * + * - Path strings are not checked against the file system; these operations only manipulate syntax + * - `resolve` may consult the host current working directory when no absolute segment is supplied + * - `fromFileUrl` only accepts valid `file:` URLs and rejects encoded path separators + * - Use the service from the environment when writing portable Effect code instead of importing + * host-specific path APIs directly + * * @since 4.0.0 */ export * as Path from "./Path.ts" /** + * The `Pipeable` module defines the shared interface and implementation helpers + * for values that support Effect-style method chaining with `.pipe(...)`. + * + * A `Pipeable` value can pass itself through a sequence of unary functions from + * left to right, so code can be written as `value.pipe(f, g, h)` instead of + * deeply nesting calls. This is the method form used by many Effect data types + * to compose transformations, validations, and effectful operations while + * keeping the original value as the starting point of the pipeline. + * + * **Common tasks** + * + * - Type values that expose a `.pipe(...)` method with the {@link Pipeable} interface + * - Implement a custom `.pipe(...)` method with {@link pipeArguments} + * - Reuse the standard implementation through {@link Prototype}, {@link Class}, or {@link Mixin} + * + * **Gotchas** + * + * - Each function receives the result of the previous function, not the original value + * - The overloads preserve precise types for long pipelines, but very long chains may be easier to read when split + * * @since 2.0.0 */ export * as Pipeable from "./Pipeable.ts" /** + * The `PlatformError` module defines the normalized error model used by + * platform APIs when adapting host operations into Effect programs. It gives + * callers a stable `PlatformError` wrapper whose `reason` is either a + * `BadArgument`, for invalid inputs rejected before an operation runs, or a + * `SystemError`, for failures reported by the host platform or operating + * system. + * + * Use this module when implementing or consuming platform services such as + * file systems, terminal access, sockets, or other environment-specific APIs. + * `SystemError` intentionally groups many low-level failures into a small set + * of portable tags like `NotFound`, `PermissionDenied`, and `TimedOut`, while + * still preserving operation details such as the module, method, syscall, path + * or descriptor, description, and original cause when available. + * + * **Common tasks** + * + * - Create platform failures from system operations with {@link systemError} + * - Report rejected caller input with {@link badArgument} + * - Inspect the underlying reason via {@link PlatformError.reason} + * - Match normalized system failures with {@link SystemErrorTag} + * + * **Gotchas** + * + * - `PlatformError` is a wrapper; inspect `reason` to distinguish + * `BadArgument` from `SystemError` + * - `SystemErrorTag` values are normalized categories, not necessarily raw + * platform error codes + * - The original cause is preserved when provided, but portable handling + * should rely on the normalized fields + * * @since 4.0.0 */ export * as PlatformError from "./PlatformError.ts" /** + * The `Pool` module provides scoped resource pools for sharing expensive or + * limited resources across fibers. A `Pool` manages values of type `A` + * acquired by an effect that may fail with `E`, automatically releasing all + * allocated resources when the surrounding `Scope` closes. + * + * **Mental model** + * + * - A pool owns a bounded set of acquired items and hands them out with {@link get} + * - Each checkout is scoped; leaving the scope returns the item to the pool + * - `concurrency` controls how many fibers may use the same item at once + * - `targetUtilization` controls when the pool grows between its minimum and maximum sizes + * - {@link invalidate} removes a specific item so it can be replaced lazily + * + * **Common tasks** + * + * - Create a fixed-size pool with {@link make} + * - Create an elastic pool with time-to-live reclamation using {@link makeWithTTL} + * - Implement custom resizing and reclamation behavior with {@link makeWithStrategy} + * - Borrow resources safely in scoped effects with {@link get} + * + * **Gotchas** + * + * - Pool construction and item checkout require `Scope`; closing the scope shuts + * down the pool or returns the borrowed item + * - Failed acquisitions are represented by the `get` effect failing with the + * acquisition error, and retrying `get` can retry acquisition + * - Resource finalization order during shutdown is unspecified + * * @since 2.0.0 */ export * as Pool from "./Pool.ts" @@ -2936,13 +3853,14 @@ export * as PrimaryKey from "./PrimaryKey.ts" * const program = Effect.gen(function*() { * const pubsub = yield* PubSub.bounded(10) * - * // Publisher - * yield* PubSub.publish(pubsub, "Hello") - * yield* PubSub.publish(pubsub, "World") - * - * // Subscriber * yield* Effect.scoped(Effect.gen(function*() { * const subscription = yield* PubSub.subscribe(pubsub) + * + * // Publisher + * yield* PubSub.publish(pubsub, "Hello") + * yield* PubSub.publish(pubsub, "World") + * + * // Subscriber * const message1 = yield* PubSub.take(subscription) * const message2 = yield* PubSub.take(subscription) * console.log(message1, message2) // "Hello", "World" @@ -2955,19 +3873,105 @@ export * as PrimaryKey from "./PrimaryKey.ts" export * as PubSub from "./PubSub.ts" /** + * The `Pull` module provides the low-level pull-step abstraction used by + * stream-like consumers. A `Pull` is an `Effect` that can + * produce one value of type `A`, fail with an ordinary error `E`, or signal + * end-of-input with a `Cause.Done` value. + * + * **Mental model** + * + * - `Pull` is an `Effect` with a distinguished completion signal in the error channel + * - ordinary failures and completion are both represented by `Cause`, but can be separated with the helpers in this module + * - the `Done` value can carry leftover state or a final value needed by a downstream consumer + * - `Pull` is useful when repeatedly evaluating an effect until it either produces values, fails, or reports that no more input is available + * + * **Common tasks** + * + * - Extract type parameters from a pull: {@link Success}, {@link Error}, {@link Leftover}, {@link Services} + * - Detect and filter completion: {@link isDoneCause}, {@link filterDone}, {@link filterNoDone} + * - Recover from completion while preserving ordinary failures: {@link catchDone} + * - Convert done causes to successful exits: {@link doneExitFromCause} + * - Handle all outcomes explicitly: {@link matchEffect} + * + * **Gotchas** + * + * - `Cause.Done` is not an ordinary failure; use this module's helpers before treating a pull failure as an error + * - `Done` lives in the error channel, so generic `Effect` error handling can catch it unless you filter it deliberately + * - `Pull` is a low-level primitive; most user-facing stream workflows should prefer higher-level stream APIs when available + * * @since 4.0.0 */ export * as Pull from "./Pull.ts" /** + * The `Queue` module provides asynchronous queues for communicating between + * fibers. A `Queue` can receive values of type `A`, deliver them to + * consumers in order, and eventually complete or fail with an error of type + * `E`. + * + * **Mental model** + * + * - A queue is a fiber-aware channel with one write side ({@link Enqueue}) and + * one read side ({@link Dequeue}) + * - Producers add values with {@link offer} or {@link offerAll}; consumers + * remove values with {@link take}, {@link takeN}, {@link takeBetween}, or + * {@link takeAll} + * - Bounded queues use an overflow strategy: {@link bounded} suspends + * producers, {@link dropping} rejects new values, and {@link sliding} drops + * old values + * - Queues can be completed with {@link end}, failed with {@link fail} or + * {@link failCause}, interrupted with {@link interrupt}, and shut down with + * {@link shutdown} + * - Operations are expressed as `Effect` values so waiting producers and + * consumers compose with interruption, scheduling, and structured + * concurrency + * + * **Common tasks** + * + * - Create queues: {@link make}, {@link bounded}, {@link dropping}, + * {@link sliding}, {@link unbounded} + * - Restrict capabilities: {@link asEnqueue}, {@link asDequeue} + * - Produce values: {@link offer}, {@link offerAll} + * - Consume values: {@link take}, {@link takeN}, {@link takeBetween}, + * {@link takeAll}, {@link poll}, {@link peek} + * - Drain or reset buffered values: {@link collect}, {@link clear} + * - Signal lifecycle: {@link end}, {@link fail}, {@link failCause}, + * {@link interrupt}, {@link shutdown} + * - Inspect state: {@link size}, {@link isFull} + * + * **Gotchas** + * + * - `take` waits when the queue is empty; use {@link poll} when absence should + * be represented as `Option.None` + * - `dropping` and `sliding` queues can lose values by design; use + * {@link bounded} when every offered value must be preserved + * - Completion and failure are observed by consumers through the queue's error + * channel, so include `Cause.Done` in the error type when using {@link end} + * - The `Unsafe` variants are synchronous, low-level operations; prefer the + * effectful APIs in application code + * + * **See also** + * + * - {@link Enqueue} for write-only queue handles + * - {@link Dequeue} for read-only queue handles + * - {@link Pull} for stream-style completion errors + * * @since 3.8.0 */ export * as Queue from "./Queue.ts" /** - * The Random module provides a service for generating random numbers in Effect - * programs. It offers a testable and composable way to work with randomness, - * supporting integers, floating-point numbers, and range-based generation. + * The `Random` module provides a service for generating pseudo-random numbers + * in Effect programs. It offers a testable and composable way to work with + * randomness, supporting integers, floating-point numbers, and range-based + * generation. + * + * The default `Random` service is not cryptographically secure. Do not use it + * for secrets, tokens, UUIDs, session identifiers, or other security-sensitive + * values. For cryptographically secure random generation, replace the service + * with a cryptographically secure implementation such as the platform `Crypto` + * service. `Random.withSeed` also replaces the service, but predictable seeds + * remain deterministic and must not be treated as cryptographically secure. * * **Example** (Generating random values) * @@ -2991,11 +3995,45 @@ export * as Queue from "./Queue.ts" export * as Random from "./Random.ts" /** + * The `RcMap` module provides a scoped, reference-counted map for sharing + * resources by key. It is useful when many fibers may request the same + * resource, such as a connection, client, session, or cached handle, and the + * resource should be acquired once, reused while it has active references, and + * released automatically when it is no longer needed. + * + * Each key is resolved with a user-provided lookup effect on first access via + * {@link get}. Further accesses to the same key share the in-flight or acquired + * resource and increment its reference count for the caller's current + * `Scope`. When those scopes close, references are released; resources can be + * closed immediately, kept alive for an idle time-to-live, invalidated + * explicitly, or bounded by a maximum capacity. + * + * `RcMap` is designed for Effect resource lifecycles rather than general + * mutable caching. The map itself is scoped, lookups require a `Scope`, and + * complex keys should provide `Equal` / `Hash` behavior when they need + * value-based lookup semantics. + * * @since 3.5.0 */ export * as RcMap from "./RcMap.ts" /** + * The `RcRef` module provides reference-counted access to a shared resource + * whose lifecycle is managed by `Scope`. An `RcRef` lazily acquires its + * resource the first time it is requested, shares that resource across active + * users, and releases it when the final scope holding a reference closes. + * + * Use `RcRef` when several scoped operations should reuse the same expensive + * or stateful resource, such as a connection, client, cache, or worker, without + * making each operation acquire and release its own copy. `make` defines how + * the resource is acquired, `get` borrows the current resource for the active + * scope, and `invalidate` forces a future `get` to acquire a fresh resource. + * + * The resource is tied to scopes rather than ordinary object reachability: + * every `get` must run with a `Scope`, and the reference count is decremented + * when that scope closes. If `idleTimeToLive` is configured, a resource whose + * reference count reaches zero can remain cached briefly before release. + * * @since 3.5.0 */ export * as RcRef from "./RcRef.ts" @@ -3229,11 +4267,86 @@ export * as RegExp from "./RegExp.ts" export * as Request from "./Request.ts" /** + * The `RequestResolver` module provides the data-loading side of + * `Effect.request`. A `Request` describes what a fiber needs, while a + * `RequestResolver` describes how to collect, batch, execute, cache, trace, + * and complete those requests. + * + * **Mental model** + * + * - A resolver receives one or more `Request.Entry` values and must complete + * each entry with either a success or failure + * - Concurrent requests made with the same resolver can be gathered into a + * batch before the resolver is run + * - Batch keys split pending requests into independent groups, which is useful + * when different backends, tenants, or query shapes must be resolved + * separately + * - Delays and `batchN` tune how long requests are collected and how large + * each batch may become + * - Resolvers can be wrapped with tracing, in-memory caching, cache services, + * and persistence without changing the request type + * + * **Common tasks** + * + * - Create a resolver from batch logic: {@link make} + * - Create grouped batch logic: {@link makeGrouped} or {@link grouped} + * - Create a resolver from pure logic: {@link fromFunction} or + * {@link fromFunctionBatched} + * - Create a resolver from effectful logic: {@link fromEffect} or + * {@link fromEffectTagged} + * - Control batching: {@link setDelay}, {@link setDelayEffect}, + * {@link batchN} + * - Add operational behavior: {@link around}, {@link race}, {@link withSpan} + * - Reuse results: {@link withCache}, {@link asCache}, {@link persisted} + * + * **Gotchas** + * + * - Every entry passed to a resolver must be completed; leaving an entry + * incomplete causes the waiting request to fail + * - Batched result collections must line up with the input entries in order + * and length when using the batched helper constructors + * - Grouping controls which requests share a resolver run; choose stable keys + * for requests that can safely be handled together + * - Caching and persistence depend on request identity and the request's + * equality semantics, so model request values deliberately when cached + * * @since 2.0.0 */ export * as RequestResolver from "./RequestResolver.ts" /** + * The `Resource` module provides refreshable, scoped values. A + * `Resource` stores the latest successful or failed acquisition result and + * can be read with {@link get}, refreshed manually with {@link refresh}, or + * refreshed automatically with {@link auto}. + * + * **Mental model** + * + * - A `Resource` wraps an acquisition `Effect` whose result is kept in a + * `ScopedRef` + * - Each refresh re-runs acquisition and replaces the stored `Exit` + * - Replacing the stored value releases resources associated with the previous + * scoped value + * - Reading a resource returns the current acquired value or fails with the + * current acquisition error + * + * **Common tasks** + * + * - Create a manually refreshed resource with {@link manual} + * - Create a schedule-driven resource with {@link auto} + * - Read the current value with {@link get} + * - Force a reload with {@link refresh} + * - Check whether an unknown value is a resource with {@link isResource} + * + * **Gotchas** + * + * - Creating a resource requires a `Scope`; when the scope closes, scoped + * values held by the resource are released + * - Failed acquisitions are stored too, so subsequent {@link get} calls fail + * until a refresh succeeds + * - Automatic refreshes run in the resource scope and stop when that scope is + * closed + * * @since 2.0.0 */ export * as Resource from "./Resource.ts" @@ -3375,6 +4488,25 @@ export * as Runtime from "./Runtime.ts" export * as Schedule from "./Schedule.ts" /** + * The `Scheduler` module defines the runtime scheduling services used by + * Effect fibers. A scheduler decides how runnable tasks are enqueued, when they + * are dispatched, and whether a fiber should yield after consuming its + * operation budget. + * + * **Common tasks** + * + * - Use {@link Scheduler} to provide a custom runtime scheduler + * - Use {@link MixedScheduler} for the default priority-aware scheduler + * - Use {@link MaxOpsBeforeYield} to tune fairness for CPU-bound fibers + * - Use {@link PreventSchedulerYield} only when a runtime should bypass yield checks + * + * **Gotchas** + * + * - Scheduler priorities affect the order of queued runtime tasks, not the + * semantic result of an `Effect` + * - Disabling scheduler yields can improve throughput for controlled workloads, + * but it can also let long-running fibers monopolize the JavaScript thread + * * @since 2.0.0 */ export * as Scheduler from "./Scheduler.ts" @@ -3714,6 +4846,25 @@ export * as SchemaGetter from "./SchemaGetter.ts" export * as SchemaIssue from "./SchemaIssue.ts" /** + * The `SchemaParser` module turns schemas into reusable runtime operations for + * constructing, validating, decoding, and encoding values. It is the execution + * layer behind a schema's AST: parsers walk the schema structure, apply + * transformations, honor parse options, run checks, and report failures as + * `SchemaIssue.Issue` values. + * + * Use this module when you need a parser with a specific result shape: + * `Effect` for effectful parsing and service requirements, `Promise` for + * JavaScript interop, `Exit` or `Result` when failures should stay in data, + * `Option` for yes/no validation, and synchronous helpers when throwing is the + * desired boundary. + * + * Decoding reads from the encoded/input side of a schema into its decoded + * `Type`, while encoding runs the schema in the opposite direction. The + * `make*` helpers construct decoded values and apply constructor defaults before + * validation. Parse options supplied when a parser is created are merged with + * options supplied at call time, and schema-level parse annotations can further + * refine behavior. + * * @since 4.0.0 */ export * as SchemaParser from "./SchemaParser.ts" @@ -3897,6 +5048,22 @@ export * as SchemaRepresentation from "./SchemaRepresentation.ts" export * as SchemaTransformation from "./SchemaTransformation.ts" /** + * The `SchemaUtils` module contains focused helpers for schema patterns that + * are useful but too specialized for the core `Schema` API surface. + * + * Use this module when you need to describe a native class with a schema while + * keeping a plain struct as its encoded representation. This is especially + * useful for classes such as `Data.Error` subclasses that should decode from + * structured data, encode back to that data, and still preserve class identity + * for instance checks and schema optics. + * + * **Gotchas** + * + * - The constructor is called with the decoded struct fields as a single + * argument, so the class constructor must accept that shape. + * - Encoding uses the instance itself as the encoded shape, so the instance + * should expose properties compatible with the provided struct schema. + * * @since 4.0.0 */ export * as SchemaUtils from "./SchemaUtils.ts" @@ -3919,31 +5086,179 @@ export * as SchemaUtils from "./SchemaUtils.ts" export * as Scope from "./Scope.ts" /** + * The `ScopedCache` module provides a cache for values that acquire scoped + * resources during lookup. Each cached entry owns a `Scope`, so resources + * created while computing a value stay alive for as long as that entry remains + * cached and are released when the entry is removed. + * + * A `ScopedCache` is itself created inside a scope. Calls to {@link get} run the + * lookup effect on cache misses, share the same in-flight lookup among + * concurrent callers for the same key, and store the resulting exit according + * to a time-to-live policy. Entries can be inserted manually with {@link set}, + * refreshed with {@link refresh}, inspected without triggering lookup with + * {@link getOption}, and removed with {@link invalidate} or + * {@link invalidateAll}. Capacity limits evict the oldest entries. + * + * **Lifecycle notes** + * + * - Entry scopes are closed when entries expire, are invalidated, are evicted, + * are replaced, or when the cache's owning scope closes + * - Successful and failed lookup exits are both cached according to the + * configured TTL + * - Expired entries may remain counted by {@link size} until a cache operation + * observes and removes them + * - Once the owning scope closes, the cache is closed and lookup-style + * operations interrupt instead of acquiring new values + * * @since 4.0.0 */ export * as ScopedCache from "./ScopedCache.ts" /** + * The `ScopedRef` module provides a mutable reference for values that are tied + * to scoped resources. Each value stored in a `ScopedRef` is acquired within its + * own `Scope`, and replacing the value safely releases the resources associated + * with the previous value. + * + * Use `ScopedRef` when an application needs to keep a current resource-backed + * value, such as a live client, connection, subscription, or cached handle, and + * later swap it for a newly acquired value without leaking the old resources. + * Reads are simple, while updates are synchronized and resource-safe. + * + * **Gotchas** + * + * - A `ScopedRef` must itself be created and used within a `Scope`; when that + * scope closes, the currently stored value is finalized. + * - Use {@link fromAcquire} or {@link set} for resourceful values so acquisition + * and finalization are tracked correctly. + * - Use {@link make} only for values that do not acquire resources. + * - Updating a `ScopedRef` waits for the replacement acquisition and old + * finalization to complete before returning. + * * @since 2.0.0 */ export * as ScopedRef from "./ScopedRef.ts" /** + * The `Semaphore` module provides a counting semaphore for coordinating + * concurrent access to shared or limited resources. A semaphore tracks a fixed + * number of permits: effects acquire permits before entering a critical section + * and release them when they leave. + * + * Use semaphores to bound parallel work, protect rate-limited services, or + * serialize access to resources that cannot safely handle unlimited + * concurrency. Prefer {@link withPermit} and {@link withPermits} when possible, + * because they release permits automatically when the protected effect exits. + * Use {@link take} and {@link release} for lower-level protocols that need + * manual control. + * + * **Gotchas** + * + * - Pending acquisitions wait until enough permits are available. + * - {@link withPermitsIfAvailable} does not wait; it returns `Option.none` when + * the requested permits cannot be acquired immediately. + * - Manual `take` / `release` usage must keep permit counts balanced. + * * @since 2.0.0 */ export * as Semaphore from "./Semaphore.ts" /** + * The `Sink` module provides composable consumers for `Stream` values. A + * `Sink` pulls input elements of type `In`, may require + * services `R`, may fail with `E`, and eventually produces a result `A` plus + * any leftover input `L` that was read but not consumed. + * + * **Mental model** + * + * - A sink is the terminal consumer used by `Stream.run` + * - Sinks can consume zero, one, many, or all input elements before finishing + * - Leftovers allow one sink to stop early without losing already-pulled input + * - Sink composition preserves typed errors and service requirements + * - Most sinks are built from `Channel` internally, but users compose them with + * the higher-level APIs in this module + * + * **Common tasks** + * + * - Create simple sinks: {@link succeed}, {@link fail}, {@link fromEffect} + * - Fold input: {@link fold}, {@link foldEffect}, {@link foldLeft} + * - Collect values: {@link collectAll}, {@link collectAllN}, {@link collectAllWhile} + * - Count or drain input: {@link count}, {@link drain} + * - Transform results: {@link map}, {@link mapEffect}, {@link as} + * - Combine sinks: {@link zip}, {@link zipWith}, {@link race} + * - Filter and refine input: {@link filterInput}, {@link filterInputEffect} + * + * **Gotchas** + * + * - A sink can finish before the stream is exhausted; check leftover-aware + * combinators when composing parsers or protocol decoders + * - `In` is contravariant, so a sink that accepts broader input can be used + * where narrower input is expected + * - Resource and service requirements are tracked in the `R` type parameter + * * @since 2.0.0 */ export * as Sink from "./Sink.ts" /** + * The `Stdio` module defines the service interface used by Effect programs to + * interact with process standard I/O. It models command-line arguments, + * standard output, standard error, and standard input as Effects, Sinks, and + * Streams so programs can depend on console I/O through `Context` instead of + * directly coupling to a specific runtime. + * + * Use this module when building command-line programs, tests, or platform + * integrations that need to read bytes from stdin, write text or bytes to + * stdout/stderr, or provide deterministic replacements for those capabilities. + * The `layerTest` helper is useful for tests because it supplies inert defaults + * and lets individual fields be overridden. + * + * Standard I/O operations are platform capabilities and may fail with + * `PlatformError`; handle those failures in the Effect error channel rather than + * assuming writes or reads are infallible. + * * @since 4.0.0 */ export * as Stdio from "./Stdio.ts" /** + * The `Stream` module provides a typed, composable way to describe effectful + * sequences of values. A `Stream` can emit zero or more `A` values, + * fail with an `E`, and require services from `R` while preserving + * backpressure and resource safety. + * + * **Mental model** + * + * - A stream is a lazy description; it runs only when consumed with a `run*` function + * - Streams are pull-based and emit chunks internally for efficient throughput + * - `A` is the element type, `E` is the failure type, and `R` is the required context + * - Stream composition mirrors `Effect`: use `map`, `flatMap`, error handling, and `pipe` + * - Resource scopes, interruption, and finalizers are tracked by the Effect runtime + * - Interop functions connect streams to queues, pub/subs, web streams, async iterables, and channels + * + * **Common tasks** + * + * - Create streams: {@link make}, {@link fromIterable}, {@link fromEffect}, {@link fromQueue} + * - Transform values: {@link map}, {@link mapEffect}, {@link flatMap}, {@link filter} + * - Combine streams: {@link concat}, {@link merge}, {@link zip}, {@link race} + * - Control demand and timing: {@link take}, {@link drop}, {@link debounce}, {@link throttle} + * - Manage errors: {@link catchCause}, {@link catchIf}, {@link mapError}, {@link retry} + * - Manage resources and services: {@link scoped}, {@link ensuring}, {@link provide} + * - Consume streams: {@link runCollect}, {@link runForEach}, {@link runFold}, {@link runDrain} + * + * **Gotchas** + * + * - A stream is not a collection; constructors and operators build a description until it is run + * - Re-running a stream re-executes its effects unless it is explicitly shared or backed by external state + * - Operators such as {@link merge}, {@link race}, and {@link broadcast} introduce concurrency and interruption semantics + * - Prefer bounded constructors and sinks for large or infinite streams instead of collecting everything into memory + * + * **See also** + * + * - {@link Effect.Effect} for single-result effectful programs + * - {@link Sink.Sink} for consuming and folding streams + * - {@link Channel.Channel} for the lower-level primitive underlying streams + * * @since 2.0.0 */ export * as Stream from "./Stream.ts" @@ -4034,31 +5349,147 @@ export * as String from "./String.ts" export * as Struct from "./Struct.ts" /** + * The `SubscriptionRef` module provides a mutable reference that can be read + * and updated like a `Ref`, while also exposing a stream of its current value + * and every subsequent change. It is useful when one part of an application + * owns evolving state and many fibers need to subscribe to consistent updates, + * such as configuration, coordination state, cached snapshots, or UI models. + * + * Updates are serialized with an internal semaphore and each update is + * published to subscribers. The {@link changes} stream replays the latest value + * first, then emits future updates, so new subscribers can start from the + * current state without performing a separate read. Prefer the effectful + * getters and update operations for concurrent code; the unsafe helpers bypass + * synchronization and should only be used when the caller already controls + * access. + * * @since 2.0.0 */ export * as SubscriptionRef from "./SubscriptionRef.ts" /** + * The `Symbol` module provides a small runtime guard for working with + * JavaScript `symbol` values. Use {@link isSymbol} when validating unknown + * input, narrowing union types, or building predicates that need to recognize + * primitive symbols such as those created by `Symbol()` or `Symbol.for`. + * + * The guard checks for the primitive `symbol` type; boxed objects created with + * `Object(Symbol())` are objects and do not satisfy this predicate. + * * @since 2.0.0 */ export * as Symbol from "./Symbol.ts" /** + * The `SynchronizedRef` module provides mutable references whose updates are + * serialized, including updates that run effects before deciding the next + * value. A `SynchronizedRef` behaves like a `Ref` for reading and basic + * updates, but uses an internal semaphore so concurrent modifications observe a + * consistent current value and apply one at a time. + * + * **When to use** + * + * - Coordinating shared state that may be updated by many fibers + * - Running effectful state transitions that must not overlap + * - Computing both a return value and a new stored value atomically + * - Applying partial updates with `Option`, where `None` leaves the value + * unchanged + * + * **Gotchas** + * + * - Effectful update functions run while the semaphore is held, so long-running + * effects delay other updates to the same ref + * - Failed effectful updates do not replace the stored value + * - `getUnsafe` and `makeUnsafe` bypass the `Effect` API and should be reserved + * for low-level or carefully controlled code + * * @since 2.0.0 */ export * as SynchronizedRef from "./SynchronizedRef.ts" /** + * The `Take` module provides the representation used by stream-like producers + * to describe a single pull result. A `Take` is either a + * non-empty batch of emitted values, a failed `Exit`, or a successful `Exit` + * carrying the stream's completion value. + * + * `Take` is useful at boundaries where pull results need to be stored, + * transferred, or interpreted later while preserving the distinction between + * emitted elements, failures, and normal completion. Use {@link toPull} to turn + * a `Take` back into a `Pull`: value batches become successful pulls, failure + * exits are propagated, and successful exits signal completion with `Done`. + * + * **Gotchas** + * + * - A value batch is always represented by a `NonEmptyReadonlyArray`; empty + * batches are not valid `Take` values. + * - Successful `Exit` values do not emit elements. They represent pull + * completion and carry the `Done` value. + * * @since 2.0.0 */ export * as Take from "./Take.ts" /** + * The `Terminal` module defines the service interface used by platform + * integrations to model command-line input and output. It gives programs a + * uniform way to query terminal dimensions, read lines, stream low-level key + * events, and write text without depending directly on Node, the browser, or a + * test-specific console implementation. + * + * Use this module when building interactive command-line tools, prompts, or + * platform abstractions that need terminal capabilities as an Effect service. + * Implementations are supplied through context, so application code can depend + * on `Terminal` while tests and runtimes provide the concrete behavior. + * + * `readLine` can fail with {@link QuitError} when the user requests to quit, + * commonly via `Ctrl+C`. For lower-level interaction, `readInput` returns a + * scoped stream of {@link UserInput} values containing parsed key metadata and + * any raw character input. + * * @since 4.0.0 */ export * as Terminal from "./Terminal.ts" /** + * The `Tracer` module defines the low-level tracing model used by Effect to + * describe and propagate spans. A span records the lifetime of an operation, + * including its name, parent, attributes, links, annotations, sampling decision, + * kind, and completion status. + * + * **Mental model** + * + * - `Tracer` is the backend interface responsible for creating spans + * - `Span` values represent Effect-managed operations with mutable lifecycle + * hooks for ending spans and adding attributes, events, or links + * - `ExternalSpan` represents trace context imported from another tracing + * system so Effect spans can be parented by or linked to external work + * - `ParentSpan`, `Tracer`, and related context references control propagation, + * sampling, and trace-level filtering through the Effect context + * + * **Common tasks** + * + * - Implement a custom tracing backend with {@link make} + * - Provide or inspect parent span context with {@link ParentSpan} + * - Convert external trace identifiers into Effect span values with + * {@link externalSpan} + * - Configure span metadata with {@link SpanOptions}, {@link SpanKind}, and + * {@link SpanLink} + * - Disable propagation or adjust trace filtering with + * {@link DisablePropagation}, {@link CurrentTraceLevel}, and + * {@link MinimumTraceLevel} + * + * **Gotchas** + * + * - This module exposes the tracing data model and backend hooks; most + * application code should create spans through higher-level Effect APIs such + * as `Effect.withSpan` + * - `ExternalSpan` only carries identity and metadata from another system; it + * does not have lifecycle methods like `Span` + * - Propagation and sampling are context-dependent, so parent selection can be + * affected by disabled propagation, root span options, and trace-level + * thresholds + * * @since 2.0.0 */ export * as Tracer from "./Tracer.ts" @@ -4177,11 +5608,56 @@ export * as TxChunk from "./TxChunk.ts" export * as TxDeferred from "./TxDeferred.ts" /** + * The `TxHashMap` module provides a transactional hash map for storing and + * updating key-value pairs inside Effect transactions. It is useful when + * multiple fibers need to coordinate shared map state and each read-modify-write + * sequence must be committed atomically. + * + * A `TxHashMap` has the familiar shape of a `HashMap`, but every + * operation returns an `Effect` and participates in transaction semantics + * through `TxRef`. Use it for concurrent registries, caches, counters, indexes, + * and other mutable maps whose updates should compose safely with other + * transactional references. + * + * **Common tasks** + * + * - Create maps with {@link empty}, {@link fromIterable}, or {@link make} + * - Read entries with {@link get}, {@link has}, {@link keys}, {@link values}, and {@link entries} + * - Update entries with {@link set}, {@link modify}, {@link modifyAt}, and {@link remove} + * - Inspect aggregate state with {@link size}, {@link isEmpty}, and {@link reduce} + * + * **Gotchas** + * + * - Operations are effectful; run them in `Effect.gen` and wrap multi-step + * transactions with `Effect.tx` when the whole sequence must commit together. + * - Reads that may be absent return `Option`, so handle both `Some` and `None` + * instead of assuming a key exists. + * * @since 2.0.0 */ export * as TxHashMap from "./TxHashMap.ts" /** + * The `TxHashSet` module provides a transactional hash set for storing unique + * values inside Effect transactions. A `TxHashSet` wraps a `HashSet` in a + * transactional reference, so reads and writes can be composed with other + * transactional operations and committed atomically. + * + * **Common tasks** + * + * - Create transactional sets with {@link empty}, {@link make}, or {@link fromIterable} + * - Mutate an existing set with {@link add}, {@link remove}, and {@link clear} + * - Query membership and size with {@link has}, {@link size}, and {@link isEmpty} + * - Derive new sets with {@link map}, {@link filter}, {@link union}, {@link intersection}, and {@link difference} + * - Fold or collect values with {@link reduce}, {@link toArray}, and {@link toHashSet} + * + * **Gotchas** + * + * - Mutation operations update the same transactional set; transform operations + * return a new `TxHashSet` + * - Operations are `Effect` values and must be yielded, piped, or run to take effect + * - Use `Effect.tx` when several operations must observe and commit one atomic transaction + * * @since 2.0.0 */ export * as TxHashSet from "./TxHashSet.ts" @@ -4241,6 +5717,24 @@ export * as TxReentrantLock from "./TxReentrantLock.ts" export * as TxRef from "./TxRef.ts" /** + * The `TxSemaphore` module provides a transactional semaphore for coordinating + * access to limited resources from within Effect transactions. A semaphore + * tracks a fixed number of permits, and transactional operations can acquire, + * release, or inspect those permits atomically with other transactional state. + * + * Use `TxSemaphore` when permit accounting needs to compose with `TxRef` and + * other transactional updates, such as guarding resource pools, rate-limited + * sections, or workflows that must reserve capacity consistently before + * committing related state changes. + * + * **Gotchas** + * + * - Permit operations are intended for transactional workflows and are wrapped + * with `Effect.tx`. + * - The semaphore capacity is fixed at construction time; releasing more + * permits than the original capacity fails. + * - Creating a semaphore with a negative number of permits defects. + * * @since 4.0.0 */ export * as TxSemaphore from "./TxSemaphore.ts" @@ -4341,6 +5835,19 @@ export * as Types from "./Types.ts" export * as UndefinedOr from "./UndefinedOr.ts" /** + * The `Unify` module contains the type-level protocol Effect uses to normalize + * unions of data types that opt in to unification. It is primarily a library + * authoring tool: data types expose hidden symbol properties describing how + * their variants should be widened, and {@link Unify} turns those protocol + * entries into the user-facing union type that TypeScript should infer. + * + * Most application code does not need to interact with these symbols directly. + * The main runtime helper, {@link unify}, is an identity function that preserves + * values and functions at runtime while applying {@link Unify} to the relevant + * static type. This is useful when authoring APIs that return branded or + * protocol-enabled values and need inference to collapse to the public Effect + * data type rather than exposing implementation details. + * * @since 2.0.0 */ export * as Unify from "./Unify.ts" diff --git a/packages/effect/src/testing/index.ts b/packages/effect/src/testing/index.ts index 459557cbab..4d0c2f6e61 100644 --- a/packages/effect/src/testing/index.ts +++ b/packages/effect/src/testing/index.ts @@ -18,11 +18,45 @@ export * as FastCheck from "./FastCheck.ts" /** + * The `TestClock` module provides a controllable implementation of the Effect + * `Clock` service for tests. Instead of waiting for real time to pass, effects + * that use `Effect.sleep`, timeouts, schedules, retries, and other time-based + * operators can be driven deterministically by advancing the test clock. + * + * **Common use cases** + * + * - Testing sleeps, delays, timeouts, debouncing, retries, and schedules without + * slowing the test suite down + * - Advancing time with {@link adjust} or jumping to an exact timestamp with + * {@link setTime} + * - Running a specific effect against the live clock with {@link withLive} + * while the rest of the test remains under test-clock control + * + * **Testing gotchas** + * + * - Effects that sleep semantically block until the clock is advanced far + * enough, so tests usually fork the time-dependent effect before calling + * {@link adjust} or {@link setTime} + * - Scheduled sleeps are resumed in clock-time order as the test clock moves + * forward + * - If a test uses time but never advances the `TestClock`, the module starts a + * delayed warning to help identify a hanging test + * * @since 2.0.0 */ export * as TestClock from "./TestClock.ts" /** + * The `TestConsole` module provides a test implementation of the `Console` + * service that records console calls instead of writing them to the host + * environment. It is useful when testing workflows that use `Console.log` or + * `Console.error` and need to assert on the produced output. + * + * Use {@link layer} to provide the test console to an effect, then inspect + * captured output with {@link logLines} and {@link errorLines}. Because console + * operations are service-based effects, programs under test must be run with + * this layer for their output to be captured. + * * @since 4.0.0 */ export * as TestConsole from "./TestConsole.ts" diff --git a/packages/effect/src/unstable/ai/index.ts b/packages/effect/src/unstable/ai/index.ts index 6b6ef5a2f3..2c3aa91513 100644 --- a/packages/effect/src/unstable/ai/index.ts +++ b/packages/effect/src/unstable/ai/index.ts @@ -282,11 +282,62 @@ export * as IdGenerator from "./IdGenerator.ts" export * as LanguageModel from "./LanguageModel.ts" /** + * The `McpSchema` module defines Effect Schema and RPC models for the Model + * Context Protocol (MCP). It provides the shared protocol vocabulary used by + * MCP clients and servers, including JSON-RPC request identifiers, metadata, + * capabilities, errors, resources, prompts, tools, logging, sampling, + * completions, roots, and elicitation. + * + * **Common tasks** + * + * - Describe MCP payloads with schemas such as {@link Resource}, {@link Tool}, + * {@link Prompt}, and {@link ContentBlock} + * - Build typed protocol handlers with request RPCs such as {@link Initialize}, + * {@link ListResources}, {@link ReadResource}, {@link ListTools}, and + * {@link CallTool} + * - Work with protocol notifications such as + * {@link ProgressNotification}, {@link ResourceUpdatedNotification}, and + * {@link LoggingMessageNotification} + * - Use {@link ClientRpcs} and server RPC groups to derive encoded request, + * notification, success, and failure message types + * + * **Gotchas** + * + * - MCP distinguishes absent fields from present fields whose value is + * `undefined`; use {@link optional} and {@link optionalWithDefault} for + * protocol fields so encoding omits undefined values consistently. + * - Capability objects are extensible. Known fields are modeled here, but + * `experimental` and `extensions` allow implementations to advertise + * additional protocol features. + * * @since 4.0.0 */ export * as McpSchema from "./McpSchema.ts" /** + * The `McpServer` module provides Effect services and layers for building + * Model Context Protocol servers. It keeps track of registered tools, + * resources, resource templates, prompts, completions, and server + * notifications, then exposes them through the MCP request handlers. + * + * **Common tasks** + * + * - Start a server over stdio with {@link layerStdio} + * - Register HTTP routes for an existing `HttpRouter` with {@link layerHttp} + * - Expose Effect AI toolkits as MCP tools with {@link registerToolkit} + * - Register resources, resource templates, and prompts with {@link resource} + * and {@link prompt} + * - Ask the connected MCP client for structured input with {@link elicit} + * + * **Gotchas** + * + * - Registration helpers require an `McpServer` service, usually provided by + * one of this module's layers. + * - HTTP clients must complete MCP initialization before other requests; the + * server tracks initialized sessions with the `Mcp-Session-Id` header. + * - Resource template parameters are decoded with the schemas embedded in the + * template literal. + * * @since 4.0.0 */ export * as McpServer from "./McpServer.ts" @@ -420,6 +471,18 @@ export * as Prompt from "./Prompt.ts" export * as Response from "./Response.ts" /** + * The `ResponseIdTracker` module provides a small service for reusing provider + * response IDs across incremental language model calls. It records which prompt + * message objects were sent for a provider response, then prepares a later + * prompt by returning the recognized `previousResponseId` together with only + * the new messages that should be sent. + * + * Use this when integrating providers that support continuing a conversation + * from a prior response ID instead of resending the entire prompt. The tracker + * is intentionally identity-based and mutable: it only recognizes the same + * message objects that were previously marked, and it clears its state when a + * prompt can no longer be matched safely. + * * @since 4.0.0 */ export * as ResponseIdTracker from "./ResponseIdTracker.ts" @@ -540,10 +603,9 @@ export * as Tool from "./Tool.ts" * **Example** (Creating and implementing toolkits) * * ```ts - * import { Effect, Schema } from "effect" + * import { Effect, Schema, Stream } from "effect" * import { Tool, Toolkit } from "effect/unstable/ai" * - * // Create individual tools * const GetCurrentTime = Tool.make("GetCurrentTime", { * description: "Get the current timestamp", * success: Schema.Number @@ -558,17 +620,29 @@ export * as Tool from "./Tool.ts" * }) * }) * - * // Create a toolkit with multiple tools * const MyToolkit = Toolkit.make(GetCurrentTime, GetWeather) * * const MyToolkitLayer = MyToolkit.toLayer({ - * GetCurrentTime: () => Effect.succeed(Date.now()), + * GetCurrentTime: () => Effect.succeed(1_704_067_200_000), * GetWeather: ({ location }) => * Effect.succeed({ * temperature: 72, - * condition: "sunny" + * condition: `sunny in ${location}` * }) * }) + * + * const program = Effect.gen(function*() { + * const toolkit = yield* MyToolkit + * const stream = yield* toolkit.handle("GetWeather", { + * location: "San Francisco" + * }) + * const results = yield* Stream.runCollect(stream) + * + * return Array.from(results, ({ result }) => result) + * }).pipe(Effect.provide(MyToolkitLayer)) + * + * console.log(Effect.runSync(program)) + * // [{ temperature: 72, condition: "sunny in San Francisco" }] * ``` * * @since 4.0.0 diff --git a/packages/effect/src/unstable/cli/Flag.ts b/packages/effect/src/unstable/cli/Flag.ts index 73e0df970b..586be076c2 100644 --- a/packages/effect/src/unstable/cli/Flag.ts +++ b/packages/effect/src/unstable/cli/Flag.ts @@ -487,6 +487,30 @@ export const withMetavar: { (self: Flag, metavar: string): Flag } = dual(2, (self: Flag, metavar: string) => Param.withMetavar(self, metavar)) +/** + * Hides a flag from generated help output and shell completions while keeping + * it fully parseable on the command line. + * + * Useful for experimental or internal flags that should be accepted but not + * advertised — for example, `--experimental-foo`, debug toggles, or escape + * hatches that are not yet committed to the public CLI surface. + * + * **Example** (Hiding a flag from help) + * + * ```ts + * import { Flag } from "effect/unstable/cli" + * + * // Flag still parses --experimental-foo, but it does not appear in --help. + * const experimental = Flag.boolean("experimental-foo").pipe( + * Flag.withHidden + * ) + * ``` + * + * @category metadata + * @since 4.0.0 + */ +export const withHidden = (self: Flag): Flag => Param.withHidden(self) + /** * Makes a flag optional, returning an Option type that can be None if not provided. * diff --git a/packages/effect/src/unstable/cli/Param.ts b/packages/effect/src/unstable/cli/Param.ts index b9833e51a2..eb92f81e84 100644 --- a/packages/effect/src/unstable/cli/Param.ts +++ b/packages/effect/src/unstable/cli/Param.ts @@ -205,6 +205,7 @@ export interface Single extends Param { readonly aliases: ReadonlyArray readonly primitiveType: Primitive.Primitive readonly typeName?: string | undefined + readonly hidden: boolean } /** @@ -346,6 +347,7 @@ export const makeSingle = (params: { readonly typeName?: string | undefined readonly description?: Option.Option | undefined readonly aliases?: ReadonlyArray | undefined + readonly hidden?: boolean | undefined }): Single => { const parse: Parse = (args) => params.kind === argumentKind @@ -356,6 +358,7 @@ export const makeSingle = (params: { ...params, description: params.description ?? Option.none(), aliases: params.aliases ?? [], + hidden: params.hidden ?? false, parse }) } @@ -1001,6 +1004,35 @@ export const withDescription: { })) }) +/** + * Hides a parameter from generated help output and completions while keeping + * it parseable on the command line. + * + * Useful for experimental, internal, or deprecated flags that should be + * accepted but not advertised. + * + * **Example** (Hiding a flag from help) + * + * ```ts + * import { Param } from "effect/unstable/cli" + * + * // @internal - this module is not exported publicly + * + * const experimental = Param.boolean(Param.flagKind, "experimental-foo").pipe( + * Param.withHidden + * ) + * ``` + * + * @category metadata + * @since 4.0.0 + */ +export const withHidden = (self: Param): Param => + transformSingle(self, (single: Single) => + makeSingle({ + ...single, + hidden: true + })) + /** * Transforms the parsed value of an option using a mapping function. * diff --git a/packages/effect/src/unstable/cli/index.ts b/packages/effect/src/unstable/cli/index.ts index d81b5187f0..a8b4c0a59b 100644 --- a/packages/effect/src/unstable/cli/index.ts +++ b/packages/effect/src/unstable/cli/index.ts @@ -5,21 +5,116 @@ // @barrel: Auto-generated exports. Do not edit manually. /** + * The `Argument` module defines typed positional command-line arguments for + * Effect CLI applications. Arguments consume ordered values after a command name + * and its flags, then parse them into the types your command handler expects. + * + * Use this module for required inputs such as file paths, directories, numbers, + * dates, choices, secrets, and structured configuration files. Arguments can be + * made optional, variadic, validated with Schema, transformed with pure or + * effectful functions, and supplied from defaults, config, or prompts when + * missing. + * + * **Gotchas** + * + * - Positional arguments are order-sensitive; compose them in the same order the + * user should type them. + * - `variadic` arguments can consume more than one value, so place them where + * the remaining positional input belongs. + * - Use {@link withDescription} and {@link withMetavar} to keep generated help + * text clear, especially when the parser name differs from the value users + * should provide. + * - Boolean positional arguments are intentionally not provided. Prefer boolean + * flags, or use {@link choice} for explicit `"true"` / `"false"` values. + * * @since 4.0.0 */ export * as Argument from "./Argument.ts" /** + * The `CliError` module defines the structured error model used by the + * unstable CLI parser and runner. It distinguishes command-line parse failures, + * CLI definition problems, explicit help requests, and user handler failures so + * applications can report errors consistently while still pattern matching on + * the exact cause. + * + * **Common tasks** + * + * - Detect CLI errors at runtime with {@link isCliError} + * - Represent parse failures such as unknown flags, missing required inputs, or + * invalid argument values + * - Attach parse or validation errors to {@link ShowHelp} when the runner should + * render help text together with the failure + * - Preserve command handler failures with {@link UserError} + * + * **Gotchas** + * + * - {@link ShowHelp} is a control-flow error, not a parse failure; it exits with + * code `0` for explicit help and `1` when it carries errors + * - Duplicate option names between parent and child commands are rejected + * because the parent command claims the flag before the child can see it + * - Suggestion-bearing errors keep suggestions separate from the primary cause + * so help renderers can decide how much guidance to display + * * @since 4.0.0 */ export * as CliError from "./CliError.ts" /** + * The `CliOutput` module provides the formatting service used by Effect CLI + * applications to turn parsed CLI metadata and failures into terminal text. + * It renders help documents, parser errors, grouped errors, and version output, + * while allowing applications and tests to replace the formatter through a + * `Context` service or `Layer`. + * + * **Common tasks** + * + * - Render generated {@link HelpDoc} values for `--help` output + * - Display parser failures and validation errors from CLI programs + * - Customize output for plain text, colored terminals, tests, or alternate + * formats + * - Format version strings consistently with the rest of CLI output + * + * **Gotchas** + * + * - Color output is auto-detected from `process.stdout.isTTY` and `NO_COLOR`, + * but can be forced with {@link defaultFormatter} + * - Help tables measure visible width after stripping ANSI escape codes, so + * colored output stays aligned with plain output + * * @since 4.0.0 */ export * as CliOutput from "./CliOutput.ts" /** + * The `Command` module provides the core building block for defining and + * running Effect-based command-line applications. A `Command` combines a name, + * typed flags and positional arguments, optional subcommands, metadata for help + * output, and an effectful handler. + * + * **Common tasks** + * + * - Create commands with {@link make} + * - Add handlers with {@link withHandler} + * - Build nested command trees with {@link withSubcommands} + * - Share parent flags with subcommands using {@link withSharedFlags} + * - Add command-scoped global flags with {@link withGlobalFlags} + * - Attach help metadata with {@link withDescription}, {@link withShortDescription}, + * {@link withAlias}, and {@link withExamples} + * - Provide handler dependencies with {@link provide}, {@link provideSync}, + * {@link provideEffect}, and {@link provideEffectDiscard} + * - Execute commands with {@link run} or test them with {@link runWith} + * + * **Gotchas** + * + * - `withSharedFlags` accepts only flags, not positional arguments, and the + * parsed values are available to descendants by yielding the parent command. + * - Shared flags may be written before or after the selected subcommand name. + * - Duplicate flags across command scopes are rejected so parsing and help + * output remain unambiguous. + * - `runWith` is the preferred entry point for tests because it accepts an + * explicit argument array instead of reading from the `Stdio` service. + * * @since 4.0.0 */ export * as Command from "./Command.ts" @@ -32,30 +127,110 @@ export * as Command from "./Command.ts" export * as Completions from "./Completions.ts" /** + * The `Flag` module provides typed command-line options for Effect CLI + * applications. A `Flag` describes how to read one named option from the + * parsed command line, validate it, and produce a value of type `A`. + * + * Use flags for inputs that are naturally named options, such as ports, + * verbosity switches, configuration files, output directories, enum-like + * choices, secrets, and repeated values. Constructors such as {@link string}, + * {@link boolean}, {@link integer}, {@link file}, and {@link fileSchema} + * define the accepted input shape, while combinators add aliases, defaults, + * optionality, fallback config or prompts, validation, and typed mapping. + * + * Flag names are rendered as long options, for example `Flag.integer("port")` + * parses `--port 8080`. Boolean flags also support the disabled form shown by + * this module's boolean documentation, and repeated flags are modeled with the + * repetition combinators instead of by manually inspecting raw arguments. Help + * text is generated from flag metadata, so prefer {@link withDescription} and + * {@link withMetavar} when a flag's value, format, or file-system expectation + * would otherwise be ambiguous. + * * @since 4.0.0 */ export * as Flag from "./Flag.ts" /** + * The `GlobalFlag` module defines flags that are available to every command in + * an Effect CLI application. Global flags are useful for cross-cutting command + * line behavior such as printing help, showing the application version, + * generating shell completions, or configuring shared handler settings like the + * minimum log level. + * + * **Common tasks** + * + * - Create an action flag with {@link action} for side effects that should run + * before the selected command, such as `--help` or `--version` + * - Create a setting flag with {@link setting} for values that should be made + * available to command handlers through the Effect context + * - Reuse the built-in {@link Help}, {@link Version}, {@link Completions}, and + * {@link LogLevel} flags when constructing command runners + * + * **Gotchas** + * + * - Action flags are intended to perform their effect and exit instead of + * continuing into the command handler + * - Setting flags allocate a distinct context service for each call to + * {@link setting}, so reuse exported settings when handlers need to read the + * same parsed global value + * * @since 4.0.0 */ export * as GlobalFlag from "./GlobalFlag.ts" /** + * The `HelpDoc` module defines the structured documentation model used by the + * unstable CLI package to describe command help. A `HelpDoc` value captures the + * user-facing parts of a command, including its description, usage string, + * positional arguments, flags, global flags, subcommands, and examples. + * + * **Common tasks** + * + * - Build help data from command definitions before rendering it + * - Pass command documentation to `CliOutput.Formatter` implementations + * - Represent custom help output formats without changing command parsing + * - Group subcommands and distinguish local flags from global flags + * + * **Gotchas** + * + * - `HelpDoc` is format-agnostic; layout, ANSI styling, and table alignment are + * handled by the output formatter + * - Optional argument and flag descriptions use `Option.Option`, while + * optional sections are omitted when they have no entries + * - Long names, aliases, and descriptions may require formatter-specific width + * handling when rendering terminal help + * * @since 4.0.0 */ export * as HelpDoc from "./HelpDoc.ts" /** - * @internal + * The `Param` module defines the shared parser tree used by the unstable CLI + * `Argument` and `Flag` modules. A `Param` describes how to consume + * either positional arguments or named flags from parsed command-line input and + * return a typed value. * - * Param is the polymorphic implementation shared by Argument.ts and Flag.ts. - * The `Kind` type parameter ("argument" | "flag") enables type-safe separation - * while sharing parsing logic and combinators. + * **Common tasks** * - * Users should import from `Argument` and `Flag` modules, not this module directly. - * This module is not exported from the public API. + * - Build primitive CLI inputs such as strings, booleans, numbers, choices, + * paths, files, and redacted values + * - Attach help metadata with aliases and descriptions + * - Transform parsed values with pure or effectful validation + * - Model missing inputs with `Option`, defaults, config fallbacks, or prompts + * - Accept repeated inputs with variadic, bounded, and non-empty parameters * + * **Gotchas** + * + * - The `Kind` type parameter (`"argument"` or `"flag"`) keeps positional + * arguments and flags separate while allowing the implementation and + * combinators to be shared. + * - Combinators preserve the parameter kind, so an argument parameter cannot be + * accidentally composed into a flag parameter or the reverse. + * - Parsers return both the remaining positional arguments and the parsed + * value; this is important for argument ordering and variadic parameters. + * - Some parsers require CLI services such as filesystem, path, terminal, or + * child-process support through the parsing environment. + * @internal * @since 4.0.0 */ export * as Param from "./Param.ts" @@ -76,6 +251,30 @@ export * as Param from "./Param.ts" export * as Primitive from "./Primitive.ts" /** + * The `Prompt` module provides composable, effectful building blocks for + * interactive command-line questions. A `Prompt` describes terminal UI that + * renders frames, reads keyboard input, validates responses, and eventually + * produces a value of type `A`. + * + * **Common tasks** + * + * - Ask for text, password, hidden, list, confirm, toggle, number, or date input + * - Let users choose from select, autocomplete, multi-select, and file prompts + * - Combine prompts with {@link all}, {@link map}, and {@link flatMap} + * - Build specialized prompts with {@link custom} + * - Run a prompt against the current terminal with {@link run} + * + * **Gotchas** + * + * - Prompts require terminal services and may fail with `Terminal.QuitError` + * when input ends or the prompt is quit + * - Rendering is frame-based: custom prompts must return ANSI output from + * `render` and matching ANSI clearing output from `clear` + * - Choices and file lists are paged by `maxPerPage`, so keyboard navigation + * and filtering should account for hidden off-page entries + * - `password` and `hidden` return `Redacted` values; unwrap them only at the + * boundary where the secret is needed + * * @since 4.0.0 */ export * as Prompt from "./Prompt.ts" diff --git a/packages/effect/src/unstable/cli/internal/command.ts b/packages/effect/src/unstable/cli/internal/command.ts index 2b6c6f7fcb..a7de705684 100644 --- a/packages/effect/src/unstable/cli/internal/command.ts +++ b/packages/effect/src/unstable/cli/internal/command.ts @@ -157,6 +157,9 @@ export const makeCommand = ()` and + * `outputParam()`; they are not inferred from the Tedious data type. Input + * values must be keyed by the parameter names in the definition, output + * parameters are collected separately from returned rows, and `withRows` only + * records the expected TypeScript row type, so row names and transforms still + * follow the configured `MssqlClient` result handling. + * * @since 4.0.0 */ export * as Procedure from "./Procedure.ts" diff --git a/packages/sql/mysql2/CHANGELOG.md b/packages/sql/mysql2/CHANGELOG.md index 4771fbad6d..cd88615093 100644 --- a/packages/sql/mysql2/CHANGELOG.md +++ b/packages/sql/mysql2/CHANGELOG.md @@ -1,5 +1,12 @@ # @effect/sql-mysql2 +## 4.0.0-beta.68 + +### Patch Changes + +- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: + - effect@4.0.0-beta.68 + ## 4.0.0-beta.67 ### Patch Changes diff --git a/packages/sql/mysql2/package.json b/packages/sql/mysql2/package.json index 3f17eca6e1..c3b442ff75 100644 --- a/packages/sql/mysql2/package.json +++ b/packages/sql/mysql2/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-mysql2", - "version": "4.0.0-beta.67", + "version": "4.0.0-beta.68", "type": "module", "license": "MIT", "description": "A MySQL toolkit for Effect", diff --git a/packages/sql/mysql2/src/index.ts b/packages/sql/mysql2/src/index.ts index a49cb4eca3..4a5155d8f2 100644 --- a/packages/sql/mysql2/src/index.ts +++ b/packages/sql/mysql2/src/index.ts @@ -5,11 +5,46 @@ // @barrel: Auto-generated exports. Do not edit manually. /** + * MySQL client implementation for Effect SQL, backed by the `mysql2` driver. + * + * This module exposes constructors and layers for providing both the MySQL-specific + * `MysqlClient` service and the generic `SqlClient` service. It is intended for server + * applications, background workers, migrations, and tests that need Effect SQL query + * compilation, scoped resource management, streaming queries, and consistent `SqlError` + * classification for MySQL driver failures. + * + * Each client owns a scoped mysql2 pool, validates connectivity with `SELECT 1` during + * acquisition, and closes the pool when the surrounding scope is released. You can configure + * the pool from a connection URI or discrete connection fields; when `url` is supplied it + * takes precedence over the host, port, database, username, and password fields. Regular + * queries run through the shared pool, while transactions acquire a dedicated pooled + * connection for their lifetime, so long-running transactions and streams can occupy pool + * capacity. Size `maxConnections`, `connectionTTL`, and any mysql2 `poolConfig` with that in + * mind. + * * @since 4.0.0 */ export * as MysqlClient from "./MysqlClient.ts" /** + * Utilities for applying Effect SQL migrations to MySQL databases through the + * mysql2-backed `SqlClient`. + * + * This module re-exports the shared `Migrator` loaders and error types, then + * provides `run` and `layer` helpers for applying ordered migrations using the + * currently configured MySQL `SqlClient`. It is commonly used during application + * startup, in integration tests that provision a temporary schema, or in layer + * graphs where dependent services should not start until the database schema is + * current. + * + * Applied migrations are stored in `effect_sql_migrations` by default and use + * the shared `_` loader convention. Only migrations with ids greater + * than the latest recorded id are run. MySQL DDL can cause implicit commits, and + * this adapter relies on migration table constraints to detect concurrent + * runners, so coordinate startup runners and write migrations to tolerate + * MySQL's transactional semantics. Schema dump support is not enabled in this + * adapter, so `schemaDirectory` does not emit a MySQL dump. + * * @since 4.0.0 */ export * as MysqlMigrator from "./MysqlMigrator.ts" diff --git a/packages/sql/pg/CHANGELOG.md b/packages/sql/pg/CHANGELOG.md index 0a2bda5cfc..85c503585c 100644 --- a/packages/sql/pg/CHANGELOG.md +++ b/packages/sql/pg/CHANGELOG.md @@ -1,5 +1,12 @@ # @effect/sql-pg +## 4.0.0-beta.68 + +### Patch Changes + +- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: + - effect@4.0.0-beta.68 + ## 4.0.0-beta.67 ### Patch Changes diff --git a/packages/sql/pg/package.json b/packages/sql/pg/package.json index 222fc59dc5..9ddf2983fd 100644 --- a/packages/sql/pg/package.json +++ b/packages/sql/pg/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-pg", - "version": "4.0.0-beta.67", + "version": "4.0.0-beta.68", "type": "module", "license": "MIT", "description": "A PostgreSQL toolkit for Effect", diff --git a/packages/sql/pg/src/index.ts b/packages/sql/pg/src/index.ts index 5a556221d3..f46dda4ba8 100644 --- a/packages/sql/pg/src/index.ts +++ b/packages/sql/pg/src/index.ts @@ -5,11 +5,48 @@ // @barrel: Auto-generated exports. Do not edit manually. /** + * PostgreSQL client implementation for Effect SQL, backed by `pg`. + * + * This module exposes constructors for creating a scoped `PgClient` from a + * managed `pg` pool, a single managed `pg` client, or lower-level connection + * acquirers. The resulting service can be provided as both `PgClient` and the + * generic `SqlClient`, and is intended for application database access, + * migrations, transactional workflows, row streaming, JSON parameters, and + * PostgreSQL LISTEN/NOTIFY integration. + * + * Pool-backed clients acquire connections per operation and reserve dedicated + * connections for transactions and cursor streams. Clients built from one + * `pg.Client` serialize shared access; enable `acquireForStream` when streams + * or listeners need their own client instead of sharing the query connection. + * LISTEN uses a scoped long-lived client and automatically issues `UNLISTEN` + * when the stream scope closes, so listeners should be scoped for as long as + * notifications are needed. + * * @since 4.0.0 */ export * as PgClient from "./PgClient.ts" /** + * Utilities for applying Effect SQL migrations to PostgreSQL databases. + * + * This module re-exports the shared `Migrator` loaders and error types, then + * provides `run` and `layer` helpers for applying ordered migrations through + * the current PostgreSQL `SqlClient` and `PgClient`. It is typically used at + * application startup, during deployment, in integration tests that provision a + * temporary PostgreSQL database, or in layer graphs that must prepare the + * schema before dependent services are acquired. + * + * Migrations are recorded in `effect_sql_migrations` by default and are loaded + * using the shared `_` file or record-key convention. Only migrations + * with an id greater than the latest recorded id are applied, so concurrent + * application instances should coordinate startup against the same database and + * avoid racing to install the same changes. When `schemaDirectory` is enabled, + * this adapter shells out to `pg_dump` using the active `PgClient` + * configuration, so `pg_dump` must be available on `PATH` and the layer must + * provide child process, filesystem, and path services. The generated dumps + * intentionally strip comments, session settings, ownership, and privilege + * statements to keep schema snapshots portable across PostgreSQL environments. + * * @since 4.0.0 */ export * as PgMigrator from "./PgMigrator.ts" diff --git a/packages/sql/pglite/CHANGELOG.md b/packages/sql/pglite/CHANGELOG.md index 62d82e2d37..9fd2d1a224 100644 --- a/packages/sql/pglite/CHANGELOG.md +++ b/packages/sql/pglite/CHANGELOG.md @@ -1,5 +1,12 @@ # @effect/sql-pglite +## 4.0.0-beta.68 + +### Patch Changes + +- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: + - effect@4.0.0-beta.68 + ## 4.0.0-beta.67 ### Patch Changes diff --git a/packages/sql/pglite/package.json b/packages/sql/pglite/package.json index fae9b16f66..53f2a7e697 100644 --- a/packages/sql/pglite/package.json +++ b/packages/sql/pglite/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-pglite", - "version": "4.0.0-beta.67", + "version": "4.0.0-beta.68", "type": "module", "license": "MIT", "description": "A PGlite toolkit for Effect", diff --git a/packages/sql/pglite/src/index.ts b/packages/sql/pglite/src/index.ts index aae0bf02cc..0bb08f27ee 100644 --- a/packages/sql/pglite/src/index.ts +++ b/packages/sql/pglite/src/index.ts @@ -5,11 +5,50 @@ // @barrel: Auto-generated exports. Do not edit manually. /** + * Embedded PostgreSQL client implementation for Effect SQL, backed by + * `@electric-sql/pglite`. + * + * This module exposes constructors and layers for providing a `PgliteClient` + * as both the PGlite-specific service and the generic `SqlClient`. It can + * create a scoped `PGlite` instance from constructor options or wrap a + * caller-owned `liveClient`, making it useful for local-first browser storage, + * web worker databases, tests, demos, migrations, and development tools that + * want PostgreSQL syntax without connecting to a separate PostgreSQL server. + * + * The client uses the PostgreSQL statement compiler and adds PGlite-specific + * access to the underlying instance, JSON fragments, LISTEN/NOTIFY streams, + * data directory dumps, and array type refresh. Because PGlite is embedded in + * the current JavaScript runtime, operations share the supplied instance and + * are serialized by this client; a `liveClient` remains caller-owned and is not + * closed by the layer. In browsers or workers, persistence, durability, + * extension availability, and lifecycle all follow the selected PGlite + * `dataDir`/runtime rather than a hosted PostgreSQL process. + * * @since 4.0.0 */ export * as PgliteClient from "./PgliteClient.ts" /** + * Utilities for applying Effect SQL migrations to embedded PGlite databases. + * + * This module re-exports the shared `Migrator` loaders and error types, then + * provides `run` and `layer` helpers for applying ordered migrations through the + * current PGlite-backed `SqlClient`. It is typically used to bootstrap schemas + * for local-first applications, browser or Node.js integration tests, examples, + * and layer graphs that need an embedded PostgreSQL-compatible database to be + * ready before dependent services start. + * + * Migrations are recorded in `effect_sql_migrations` by default and are loaded + * using the shared `_` file or record-key convention. PGlite uses + * PostgreSQL semantics inside the embedded database, so migrations run in a + * transaction and the shared migrator uses PostgreSQL table locking to avoid + * concurrent runners. Coordinate every process or layer using the same + * `liveClient` or `dataDir`, and remember that in-memory PGlite clients start + * with no recorded migrations. This adapter does not currently write schema + * dumps for `schemaDirectory`; use PGlite data-directory persistence or + * `PgliteClient.dumpDataDir` when a portable embedded database snapshot is + * needed. + * * @since 4.0.0 */ export * as PgliteMigrator from "./PgliteMigrator.ts" diff --git a/packages/sql/sqlite-bun/CHANGELOG.md b/packages/sql/sqlite-bun/CHANGELOG.md index 43359255ff..fcc3470028 100644 --- a/packages/sql/sqlite-bun/CHANGELOG.md +++ b/packages/sql/sqlite-bun/CHANGELOG.md @@ -1,5 +1,12 @@ # @effect/sql-sqlite-bun +## 4.0.0-beta.68 + +### Patch Changes + +- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: + - effect@4.0.0-beta.68 + ## 4.0.0-beta.67 ### Patch Changes diff --git a/packages/sql/sqlite-bun/package.json b/packages/sql/sqlite-bun/package.json index 910bce932a..0d7b183afc 100644 --- a/packages/sql/sqlite-bun/package.json +++ b/packages/sql/sqlite-bun/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-sqlite-bun", - "version": "4.0.0-beta.67", + "version": "4.0.0-beta.68", "type": "module", "license": "MIT", "description": "A SQLite toolkit for Effect", diff --git a/packages/sql/sqlite-bun/src/index.ts b/packages/sql/sqlite-bun/src/index.ts index 986ce8399a..bfc6dd4e65 100644 --- a/packages/sql/sqlite-bun/src/index.ts +++ b/packages/sql/sqlite-bun/src/index.ts @@ -5,11 +5,48 @@ // @barrel: Auto-generated exports. Do not edit manually. /** + * Bun SQLite client implementation for Effect SQL, backed by `bun:sqlite`. + * + * This module provides constructors and layers for using a Bun-managed SQLite database as both the + * SQLite-specific `SqliteClient` service and the generic `SqlClient` service. It is intended for + * file-backed or in-memory databases in Bun applications, local development tools, migrations, + * integration tests, and embedded persistence use cases that need Effect SQL query compilation plus + * SQLite-specific helpers such as database export and native extension loading. + * + * Each client owns one scoped `bun:sqlite` `Database` handle and serializes access through it, which + * is important because Bun executes SQLite statements synchronously. WAL mode is enabled by default, + * so set `disableWAL` when opening read-only databases or when the database file or directory cannot + * be updated with SQLite's WAL side files. A transaction holds the serialized connection permit for + * the transaction scope, so concurrent fibers using the same client wait until it completes, while + * separate database handles or processes can still contend for SQLite write locks. Safe integer + * handling follows the `SqlClient` fiber-local setting, `executeStream` is not implemented, and + * SQLite does not support `updateValues`. + * * @since 4.0.0 */ export * as SqliteClient from "./SqliteClient.ts" /** + * Utilities for applying Effect SQL migrations to Bun SQLite databases. + * + * This module re-exports the shared `Migrator` loaders and error types, then + * provides `run` and `layer` helpers for applying ordered migrations through + * the current Bun-backed SQLite `SqlClient`. It is typically used at + * application startup, in deployment or setup scripts that prepare a local + * SQLite file, in integration tests with temporary database files, or in layer + * graphs that must install the schema before dependent services are acquired. + * + * Migrations are recorded in `effect_sql_migrations` by default and are loaded + * using the shared `_` file or record-key convention. Only migrations + * with an id greater than the latest recorded id are applied, so every client + * involved in startup should point at the same SQLite filename and use a + * writable Bun SQLite configuration. The Bun client enables WAL by default and + * serializes access through a single `bun:sqlite` database handle, but separate + * handles or processes can still contend for SQLite write locks. Bun's SQLite + * driver runs statements synchronously, so large migration sets can block the + * invoking runtime thread, and this adapter does not currently write SQLite + * schema dumps for `schemaDirectory`. + * * @since 4.0.0 */ export * as SqliteMigrator from "./SqliteMigrator.ts" diff --git a/packages/sql/sqlite-do/CHANGELOG.md b/packages/sql/sqlite-do/CHANGELOG.md index 5de4a891ca..3a399320c6 100644 --- a/packages/sql/sqlite-do/CHANGELOG.md +++ b/packages/sql/sqlite-do/CHANGELOG.md @@ -1,5 +1,12 @@ # @effect/sql-sqlite-do +## 4.0.0-beta.68 + +### Patch Changes + +- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: + - effect@4.0.0-beta.68 + ## 4.0.0-beta.67 ### Patch Changes diff --git a/packages/sql/sqlite-do/package.json b/packages/sql/sqlite-do/package.json index 1bde11d60e..df55906d4b 100644 --- a/packages/sql/sqlite-do/package.json +++ b/packages/sql/sqlite-do/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-sqlite-do", - "version": "4.0.0-beta.67", + "version": "4.0.0-beta.68", "type": "module", "license": "MIT", "description": "A SQLite toolkit for Effect", diff --git a/packages/sql/sqlite-do/src/index.ts b/packages/sql/sqlite-do/src/index.ts index 986ce8399a..7af58bf180 100644 --- a/packages/sql/sqlite-do/src/index.ts +++ b/packages/sql/sqlite-do/src/index.ts @@ -5,11 +5,50 @@ // @barrel: Auto-generated exports. Do not edit manually. /** + * Provides an Effect SQL client for Cloudflare Durable Object SQLite storage. + * + * This module adapts a Durable Object `SqlStorage` handle into both the + * Durable Object-specific `SqliteClient` service and the generic Effect + * `SqlClient` service. Use it from inside a Durable Object to run local + * per-object queries, repositories, migrations, transactional read/write + * workflows, and tests that exercise Cloudflare's SQLite-backed storage API. + * + * Durable Object SQLite storage is scoped to one object id, so each object + * instance has its own database and callers should pass the same `SqlStorage` + * handle that the object uses for normal reads and writes. This adapter + * serializes Effect SQL access through one connection; a transaction holds that + * permit for the lifetime of its scope, so keep transactions short, avoid + * suspending them across unrelated work, and use them when multi-statement + * writes must commit atomically. `SqlStorage.exec` returns `ArrayBuffer` values + * for SQLite blobs, which this client normalizes to `Uint8Array`, and SQLite + * does not support `updateValues`. + * * @since 4.0.0 */ export * as SqliteClient from "./SqliteClient.ts" /** + * Utilities for applying Effect SQL migrations to Cloudflare Durable Object SQLite storage. + * + * This module re-exports the shared `Migrator` loaders and error types, then + * provides `run` and `layer` helpers that execute ordered migrations through the + * current Durable Object `SqlStorage`-backed `SqlClient`. Use it when a Durable + * Object needs to create or upgrade its local schema during construction, before + * repositories or request handlers use the object storage, or in tests that + * exercise Durable Object persistence. + * + * Migrations are recorded in `effect_sql_migrations` by default and are loaded + * using the shared `_` file or record-key convention. The underlying + * storage is scoped to a Durable Object id, so running migrations for one object + * does not update any other object instance; run the migrator against the same + * `SqlStorage` handle that the object uses for normal queries. These SQL + * migrations are separate from Cloudflare's Durable Object class migrations, and + * the Durable Object must already be configured with SQLite storage before this + * module can apply schema changes. Repeated startup runs are expected and are + * guarded by the migrations table, but request handling should wait until the + * migration layer has finished. This adapter does not currently write SQLite + * schema dumps for `schemaDirectory`. + * * @since 4.0.0 */ export * as SqliteMigrator from "./SqliteMigrator.ts" diff --git a/packages/sql/sqlite-node/CHANGELOG.md b/packages/sql/sqlite-node/CHANGELOG.md index 6b6ce6611d..4f3a996a2a 100644 --- a/packages/sql/sqlite-node/CHANGELOG.md +++ b/packages/sql/sqlite-node/CHANGELOG.md @@ -1,5 +1,12 @@ # @effect/sql-sqlite-node +## 4.0.0-beta.68 + +### Patch Changes + +- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: + - effect@4.0.0-beta.68 + ## 4.0.0-beta.67 ### Patch Changes diff --git a/packages/sql/sqlite-node/package.json b/packages/sql/sqlite-node/package.json index 2fd70644b9..ce5bd4c92b 100644 --- a/packages/sql/sqlite-node/package.json +++ b/packages/sql/sqlite-node/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-sqlite-node", - "version": "4.0.0-beta.67", + "version": "4.0.0-beta.68", "type": "module", "license": "MIT", "description": "A SQLite toolkit for Effect", diff --git a/packages/sql/sqlite-node/src/index.ts b/packages/sql/sqlite-node/src/index.ts index 986ce8399a..7823968b61 100644 --- a/packages/sql/sqlite-node/src/index.ts +++ b/packages/sql/sqlite-node/src/index.ts @@ -5,11 +5,40 @@ // @barrel: Auto-generated exports. Do not edit manually. /** + * Node.js SQLite client implementation for Effect SQL, backed by `better-sqlite3`. + * + * This module exposes constructors and layers for providing both the SQLite-specific `SqliteClient` + * service and the generic `SqlClient` service. It is intended for file-backed or in-memory SQLite + * databases in Node applications, local development tools, tests, migrations, and embedded + * persistence use cases that need Effect SQL query compilation plus SQLite-specific operations such + * as exporting a database, creating backups, or loading native SQLite extensions. + * + * Each client owns one scoped `better-sqlite3` connection and serializes access through it. WAL mode + * is enabled by default, so set `disableWAL` when opening read-only databases or when the database + * location cannot change journal mode. Prepared statements are cached by SQL text, safe integer + * handling follows the `SqlClient` fiber-local setting, `executeStream` is not implemented, and + * SQLite does not support `updateValues`. + * * @since 4.0.0 */ export * as SqliteClient from "./SqliteClient.ts" /** + * Utilities for applying Effect SQL migrations to Node.js SQLite databases. + * + * This module re-exports the shared `Migrator` loaders and error types, then + * provides `run` and `layer` helpers for applying ordered migrations through the + * current SQLite `SqlClient`. It is typically used at application startup, in + * tests that create temporary database files, or in layer graphs that must + * ensure a file-backed SQLite schema exists before dependent services start. + * + * Migrations are recorded in `effect_sql_migrations` by default and are loaded + * using the shared `_` file or record-key convention. Only migrations + * with an id greater than the latest recorded id are applied, so every client + * involved in startup should point at the same SQLite filename. Concurrent + * writers can surface SQLite lock timeout errors, and this adapter does not + * currently write SQLite schema dumps for `schemaDirectory`. + * * @since 4.0.0 */ export * as SqliteMigrator from "./SqliteMigrator.ts" diff --git a/packages/sql/sqlite-react-native/CHANGELOG.md b/packages/sql/sqlite-react-native/CHANGELOG.md index 533e9a6414..55320c6e79 100644 --- a/packages/sql/sqlite-react-native/CHANGELOG.md +++ b/packages/sql/sqlite-react-native/CHANGELOG.md @@ -1,5 +1,12 @@ # @effect/sql-sqlite-react-native +## 4.0.0-beta.68 + +### Patch Changes + +- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: + - effect@4.0.0-beta.68 + ## 4.0.0-beta.67 ### Patch Changes diff --git a/packages/sql/sqlite-react-native/package.json b/packages/sql/sqlite-react-native/package.json index ae35a254b8..0227087241 100644 --- a/packages/sql/sqlite-react-native/package.json +++ b/packages/sql/sqlite-react-native/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-sqlite-react-native", - "version": "4.0.0-beta.67", + "version": "4.0.0-beta.68", "type": "module", "license": "MIT", "description": "A SQLite toolkit for Effect", diff --git a/packages/sql/sqlite-react-native/src/index.ts b/packages/sql/sqlite-react-native/src/index.ts index 986ce8399a..c53001a394 100644 --- a/packages/sql/sqlite-react-native/src/index.ts +++ b/packages/sql/sqlite-react-native/src/index.ts @@ -5,11 +5,44 @@ // @barrel: Auto-generated exports. Do not edit manually. /** + * Provides a React Native SQLite `SqlClient` backed by `@op-engineering/op-sqlite`. + * + * Use this module to open an on-device SQLite database, expose it as both the + * React Native-specific `SqliteClient` and the generic Effect `SqlClient`, and + * run application queries, migrations, and transactional reads or writes from + * Effect services and layers. + * + * The client uses one serialized connection. Regular queries and transactions + * share that handle, and a transaction holds it for the lifetime of its scope, + * so keep mobile transactions short and wrap multi-statement writes in a + * transaction to avoid partial updates. By default statements use the driver's + * synchronous API, which can block the JavaScript thread; `withAsyncQuery` + * switches a fiber to the asynchronous driver API when UI responsiveness is more + * important than sync execution. + * * @since 4.0.0 */ export * as SqliteClient from "./SqliteClient.ts" /** + * Utilities for applying Effect SQL migrations to React Native SQLite databases. + * + * This module re-exports the shared `Migrator` loaders and error types, then + * provides `run` and `layer` helpers that execute ordered migrations through the + * current React Native SQLite `SqlClient`. Use it when a mobile app needs to + * bring its on-device database schema up to date during startup, before opening + * repositories or sync services, or in integration tests that create app-local + * database files. + * + * React Native SQLite databases are scoped by the client configuration, so the + * migrator should be run with the same `filename`, `location`, and encryption + * key as the rest of the application. Migrations run through the package's + * single serialized connection; by default statements use the synchronous + * driver API and can block the JS thread, so long migration sets may want to run + * under `SqliteClient.withAsyncQuery`. Mobile upgrades can be interrupted by app + * suspension or process death, so keep migrations transaction-aware and avoid + * assuming a fresh database on every launch. + * * @since 4.0.0 */ export * as SqliteMigrator from "./SqliteMigrator.ts" diff --git a/packages/sql/sqlite-wasm/CHANGELOG.md b/packages/sql/sqlite-wasm/CHANGELOG.md index 6e7d01d86f..7f95e54099 100644 --- a/packages/sql/sqlite-wasm/CHANGELOG.md +++ b/packages/sql/sqlite-wasm/CHANGELOG.md @@ -1,5 +1,12 @@ # @effect/sql-sqlite-wasm +## 4.0.0-beta.68 + +### Patch Changes + +- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: + - effect@4.0.0-beta.68 + ## 4.0.0-beta.67 ### Patch Changes diff --git a/packages/sql/sqlite-wasm/package.json b/packages/sql/sqlite-wasm/package.json index 441a4baa70..6ae9986356 100644 --- a/packages/sql/sqlite-wasm/package.json +++ b/packages/sql/sqlite-wasm/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-sqlite-wasm", - "version": "4.0.0-beta.67", + "version": "4.0.0-beta.68", "type": "module", "license": "MIT", "description": "A SQLite toolkit for Effect", diff --git a/packages/tools/openapi-generator/CHANGELOG.md b/packages/tools/openapi-generator/CHANGELOG.md index 658a92bc23..1b8a7e4a66 100644 --- a/packages/tools/openapi-generator/CHANGELOG.md +++ b/packages/tools/openapi-generator/CHANGELOG.md @@ -1,5 +1,13 @@ # @effect/openapi-generator +## 4.0.0-beta.68 + +### Patch Changes + +- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: + - effect@4.0.0-beta.68 + - @effect/platform-node@4.0.0-beta.68 + ## 4.0.0-beta.67 ### Patch Changes diff --git a/packages/tools/openapi-generator/package.json b/packages/tools/openapi-generator/package.json index 3aab83926a..ea0d8b0c98 100644 --- a/packages/tools/openapi-generator/package.json +++ b/packages/tools/openapi-generator/package.json @@ -1,7 +1,7 @@ { "name": "@effect/openapi-generator", "type": "module", - "version": "4.0.0-beta.67", + "version": "4.0.0-beta.68", "license": "MIT", "description": "Generate Effect Schema types, HTTP clients, and HttpApi modules from OpenAPI specifications", "homepage": "https://effect.website", diff --git a/packages/vitest/CHANGELOG.md b/packages/vitest/CHANGELOG.md index 8f058e7fd0..cbe9b552a4 100644 --- a/packages/vitest/CHANGELOG.md +++ b/packages/vitest/CHANGELOG.md @@ -1,5 +1,12 @@ # @effect/vitest +## 4.0.0-beta.68 + +### Patch Changes + +- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: + - effect@4.0.0-beta.68 + ## 4.0.0-beta.67 ### Patch Changes diff --git a/packages/vitest/package.json b/packages/vitest/package.json index 2601d58635..2d191fd725 100644 --- a/packages/vitest/package.json +++ b/packages/vitest/package.json @@ -1,6 +1,6 @@ { "name": "@effect/vitest", - "version": "4.0.0-beta.67", + "version": "4.0.0-beta.68", "type": "module", "license": "MIT", "description": "A set of helpers for testing Effects with vitest", diff --git a/scripts/analyze-jsdoc.mjs b/scripts/analyze-jsdoc.mjs deleted file mode 100644 index fafbfc02bf..0000000000 --- a/scripts/analyze-jsdoc.mjs +++ /dev/null @@ -1,539 +0,0 @@ -#!/usr/bin/env node - -import * as Fs from "node:fs" -import * as Path from "node:path" -import * as Process from "node:process" - -// Parse command line arguments -const args = Process.argv.slice(2) -const fileFilter = args.find((arg) => arg.startsWith("--file="))?.replace("--file=", "") - -/** - * Analyzes TypeScript files for missing JSDoc examples and category tags - */ -class JSDocAnalyzer { - constructor() { - this.results = { - totalFiles: 0, - totalExports: 0, - missingExamples: 0, - missingCategories: 0, - fileDetails: [], - missingItems: [] - } - } - - /** - * Get all TypeScript files in the effect/src directory (including schema subdirectory) - */ - getEffectFiles() { - const effectSrcDir = Path.join(Process.cwd(), "packages/effect/src") - const files = Fs.readdirSync(effectSrcDir) - const allFiles = [] - - // Add root level files - files - .filter((file) => file.endsWith(".ts")) - .filter((file) => !file.endsWith(".test.ts")) - .filter((file) => { - // Only include files, not directories - const fullPath = Path.join(effectSrcDir, file) - return Fs.statSync(fullPath).isFile() - }) - .forEach((file) => allFiles.push(Path.join(effectSrcDir, file))) - - // Add schema subdirectory files - const schemaDir = Path.join(effectSrcDir, "schema") - if (Fs.existsSync(schemaDir)) { - const schemaFiles = Fs.readdirSync(schemaDir) - schemaFiles - .filter((file) => file.endsWith(".ts")) - .filter((file) => !file.endsWith(".test.ts")) - .filter((file) => { - const fullPath = Path.join(schemaDir, file) - return Fs.statSync(fullPath).isFile() - }) - .forEach((file) => allFiles.push(Path.join(schemaDir, file))) - } - - // Add config subdirectory files - const configDir = Path.join(effectSrcDir, "config") - if (Fs.existsSync(configDir)) { - const configFiles = Fs.readdirSync(configDir) - configFiles - .filter((file) => file.endsWith(".ts")) - .filter((file) => !file.endsWith(".test.ts")) - .filter((file) => { - const fullPath = Path.join(configDir, file) - return Fs.statSync(fullPath).isFile() - }) - .forEach((file) => allFiles.push(Path.join(configDir, file))) - } - - return allFiles - } - - /** - * Extract exported members from a TypeScript file using more comprehensive parsing - */ - extractExports(content, filename) { - const exports = [] - const lines = content.split("\n") - const processedFunctions = new Set() // Track functions to avoid counting overloads - - for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim() - - // Skip comments and empty lines - if (line.startsWith("//") || line.startsWith("*") || !line) continue - - // More comprehensive export patterns including multi-line declarations - // Note: Using [\w$]+ to include $ character in export names (e.g., Array$, Object$) - const exportPatterns = [ - /^export\s+const\s+([\w$]+)[\s:=]/, - /^export\s+function\s+([\w$]+)\s*[(<]/, - /^export\s+type\s+([\w$]+)[\s=<]/, - /^export\s+interface\s+([\w$]+)[\s<{]/, - /^export\s+class\s+([\w$]+)[\s<{]/, - /^export\s+enum\s+([\w$]+)[\s{]/, - /^export\s+namespace\s+([\w$]+)[\s{]/, - /^export\s+declare\s+const\s+([\w$]+)[\s:]/, - /^export\s+declare\s+function\s+([\w$]+)\s*[(<]/, - /^export\s+declare\s+type\s+([\w$]+)[\s=<]/, - /^export\s+declare\s+interface\s+([\w$]+)[\s<{]/, - /^export\s+declare\s+class\s+([\w$]+)[\s<{]/, - /^export\s+declare\s+enum\s+([\w$]+)[\s{]/, - /^export\s+declare\s+namespace\s+([\w$]+)[\s{]/, - // Handle object destructuring exports - /^export\s+\{\s*([\w$]+)/ - ] - - for (const pattern of exportPatterns) { - const match = line.match(pattern) - if (match) { - const exportName = match[1] - - // Skip re-exports and internal exports - if (line.includes("from ") || exportName.startsWith("_")) { - continue - } - - // Skip certain common re-export patterns - if (line.includes("export {") && line.includes("}")) { - continue - } - - // For function overloads, only count the first declaration (with JSDoc) - if (line.includes("function ")) { - if (processedFunctions.has(exportName)) { - continue // Skip this overload - } - processedFunctions.add(exportName) - } - - // Find associated JSDoc block - const jsdoc = this.findJSDocBlock(lines, i) - - // Skip internal exports - they don't need categories or examples - if (jsdoc.isInternal) { - break - } - - const exportType = this.getExportType(line) - - const effectSrcDir = Path.join(Process.cwd(), "packages/effect/src") - exports.push({ - name: exportName, - line: i + 1, - type: exportType, - hasExample: jsdoc.hasExample, - hasCategory: jsdoc.hasCategory, - jsdocStart: jsdoc.start, - filename: Path.relative(effectSrcDir, filename), - exportLine: line - }) - break - } - } - } - - return exports - } - - /** - * Find JSDoc block preceding an export - improved to handle gaps and better detection - */ - findJSDocBlock(lines, exportLineIndex) { - let hasExample = false - let hasCategory = false - let isInternal = false - let jsdocStartLine = -1 - let jsdocEndLine = -1 - let emptyLinesCount = 0 - - // Look backwards for JSDoc block, allowing for empty lines - for (let i = exportLineIndex - 1; i >= 0; i--) { - const line = lines[i].trim() - - // Empty line - count them but continue searching - if (!line) { - emptyLinesCount++ - // Allow up to 3 empty lines between JSDoc and export - if (emptyLinesCount > 3 && jsdocEndLine === -1) { - break - } - continue - } - - // Reset empty line count when we find content - if (line) { - emptyLinesCount = 0 - } - - // End of JSDoc block - if (line === "*/") { - jsdocEndLine = i - continue - } - - // Start of JSDoc block - if (line.startsWith("/**")) { - jsdocStartLine = i - - // Single line JSDoc /** comment */ - if (line.endsWith("*/")) { - if (line.includes("@example")) hasExample = true - if (line.includes("@category")) hasCategory = true - if (line.includes("@internal")) isInternal = true - break - } - - // Multi-line JSDoc block - scan the entire block - // Note: jsdocEndLine is found first (going backwards), then jsdocStartLine - if (jsdocEndLine !== -1) { - for (let j = jsdocStartLine; j <= jsdocEndLine; j++) { - const blockLine = lines[j].trim() - if (blockLine.includes("@example")) { - hasExample = true - } - if (blockLine.includes("@category")) { - hasCategory = true - } - if (blockLine.includes("@internal")) { - isInternal = true - } - } - } - break - } - - // Hit another export/declaration - stop searching if we haven't found JSDoc yet - if ( - line && (line.startsWith("export ") || - line.startsWith("import ") || - line.startsWith("const ") || - line.startsWith("function ") || - line.startsWith("class ") || - line.startsWith("interface ") || - line.startsWith("type ") || - line.startsWith("enum ")) - ) { - break - } - } - - return { hasExample, hasCategory, isInternal, start: jsdocStartLine } - } - - /** - * Determine the type of export with better detection - */ - getExportType(line) { - if (line.includes("const ")) return "const" - if (line.includes("function ")) return "function" - if (line.includes("type ")) return "type" - if (line.includes("interface ")) return "interface" - if (line.includes("class ")) return "class" - if (line.includes("enum ")) return "enum" - if (line.includes("namespace ")) return "namespace" - if (line.includes("declare ")) return "declare" - return "unknown" - } - - /** - * Analyze a single file - */ - analyzeFile(filepath) { - const content = Fs.readFileSync(filepath, "utf8") - const effectSrcDir = Path.join(Process.cwd(), "packages/effect/src") - const filename = Path.relative(effectSrcDir, filepath) - const exports = this.extractExports(content, filepath) - - const fileStats = { - filename, - totalExports: exports.length, - missingExamples: exports.filter((e) => !e.hasExample).length, - missingCategories: exports.filter((e) => !e.hasCategory).length, - exports: exports.map((e) => ({ - name: e.name, - type: e.type, - line: e.line, - hasExample: e.hasExample, - hasCategory: e.hasCategory - })) - } - - // Track missing items for detailed reporting - exports.forEach((exp) => { - if (!exp.hasExample || !exp.hasCategory) { - this.results.missingItems.push({ - file: filename, - name: exp.name, - type: exp.type, - line: exp.line, - missingExample: !exp.hasExample, - missingCategory: !exp.hasCategory - }) - } - }) - - return fileStats - } - - /** - * Run analysis on all Effect source files or a specific file - */ - analyze(targetFile = null) { - const files = this.getEffectFiles() - - if (targetFile) { - const targetPath = Path.join(Process.cwd(), "packages/effect/src", targetFile) - - if (!files.includes(targetPath)) { - Process.stdout.write(`Error: File '${targetFile}' not found.\n`) - Process.stdout.write(`Use relative paths from packages/effect/src/:\n`) - Process.stdout.write(` - For root files: Effect.ts, Array.ts, etc.\n`) - Process.stdout.write(` - For schema files: schema/Schema.ts, schema/AST.ts, etc.\n`) - Process.stdout.write(` - For config files: config/Config.ts, config/ConfigError.ts, etc.\n\n`) - Process.stdout.write(`Available files:\n`) - const effectSrcDir = Path.join(Process.cwd(), "packages/effect/src") - files.forEach((f) => { - const relativePath = Path.relative(effectSrcDir, f) - Process.stdout.write(` ${relativePath}\n`) - }) - return - } - - // Analyze only the target file - const fileStats = this.analyzeFile(targetPath) - this.generateFileReport(fileStats) - return - } - - Process.stdout.write( - `Analyzing ${files.length} TypeScript files in packages/effect/src/ (including schema and config subdirectories)...\n\n` - ) - - this.results.totalFiles = files.length - - for (const filepath of files) { - const fileStats = this.analyzeFile(filepath) - this.results.fileDetails.push(fileStats) - - this.results.totalExports += fileStats.totalExports - this.results.missingExamples += fileStats.missingExamples - this.results.missingCategories += fileStats.missingCategories - } - - this.generateReport() - } - - /** - * Generate report for a single file - */ - generateFileReport(fileStats) { - const { exports, filename, missingCategories, missingExamples, totalExports } = fileStats - - Process.stdout.write("=".repeat(60) + "\n") - Process.stdout.write(` ${filename.toUpperCase()} DOCUMENTATION REPORT\n`) - Process.stdout.write("=".repeat(60) + "\n\n") - - // Summary - Process.stdout.write("šŸ“Š SUMMARY\n") - Process.stdout.write("-".repeat(20) + "\n") - Process.stdout.write(`Total exports: ${totalExports}\n`) - Process.stdout.write( - `Missing examples: ${missingExamples} (${((missingExamples / totalExports) * 100).toFixed(1)}%)\n` - ) - Process.stdout.write( - `Missing categories: ${missingCategories} (${((missingCategories / totalExports) * 100).toFixed(1)}%)\n\n` - ) - - // Missing examples - if (missingExamples > 0) { - Process.stdout.write("šŸ“ MISSING EXAMPLES\n") - Process.stdout.write("-".repeat(30) + "\n") - const missingExampleItems = exports.filter((e) => !e.hasExample) - missingExampleItems.forEach((item, index) => { - Process.stdout.write(`${index + 1}. ${item.name} (${item.type}) - Line ${item.line}\n`) - }) - Process.stdout.write("\n") - } - - // Missing categories - if (missingCategories > 0) { - Process.stdout.write("šŸ·ļø MISSING CATEGORIES\n") - Process.stdout.write("-".repeat(30) + "\n") - const missingCategoryItems = exports.filter((e) => !e.hasCategory) - missingCategoryItems.forEach((item, index) => { - Process.stdout.write(`${index + 1}. ${item.name} (${item.type}) - Line ${item.line}\n`) - }) - Process.stdout.write("\n") - } - - // Breakdown by type - Process.stdout.write("šŸ“‹ BREAKDOWN BY TYPE\n") - Process.stdout.write("-".repeat(25) + "\n") - const typeStats = {} - exports.forEach((exp) => { - if (!typeStats[exp.type]) { - typeStats[exp.type] = { total: 0, missingExample: 0, missingCategory: 0 } - } - typeStats[exp.type].total++ - if (!exp.hasExample) typeStats[exp.type].missingExample++ - if (!exp.hasCategory) typeStats[exp.type].missingCategory++ - }) - - Object.entries(typeStats).forEach(([type, stats]) => { - Process.stdout.write( - `${type}: ${stats.total} total, ${stats.missingExample} missing examples, ${stats.missingCategory} missing categories\n` - ) - }) - - Process.stdout.write("\n" + "=".repeat(60) + "\n") - Process.stdout.write(`Analysis complete for ${filename}!\n`) - Process.stdout.write("=".repeat(60) + "\n") - } - - /** - * Generate comprehensive analysis report - */ - generateReport() { - const { fileDetails, missingCategories, missingExamples, missingItems, totalExports, totalFiles } = this.results - - Process.stdout.write("=".repeat(60) + "\n") - Process.stdout.write(" EFFECT JSDOC ANALYSIS REPORT\n") - Process.stdout.write("=".repeat(60) + "\n") - Process.stdout.write("\n") - - // Summary Statistics - Process.stdout.write("šŸ“Š SUMMARY STATISTICS\n") - Process.stdout.write("-".repeat(30) + "\n") - Process.stdout.write(`Total files analyzed: ${totalFiles}\n`) - Process.stdout.write(`Total exported members: ${totalExports}\n`) - Process.stdout.write( - `Missing @example: ${missingExamples} (${((missingExamples / totalExports) * 100).toFixed(1)}%)\n` - ) - Process.stdout.write( - `Missing @category: ${missingCategories} (${((missingCategories / totalExports) * 100).toFixed(1)}%)\n` - ) - Process.stdout.write("\n") - - // Top files needing attention (sorted by total missing items) - Process.stdout.write("šŸŽÆ TOP FILES NEEDING ATTENTION\n") - Process.stdout.write("-".repeat(40) + "\n") - const sortedFiles = fileDetails - .filter((f) => f.missingExamples > 0 || f.missingCategories > 0) - .sort((a, b) => (b.missingExamples + b.missingCategories) - (a.missingExamples + a.missingCategories)) - .slice(0, 15) - - sortedFiles.forEach((file, index) => { - Process.stdout.write(`${index + 1}. ${file.filename}\n`) - Process.stdout.write( - ` šŸ“ ${file.missingExamples} missing examples, šŸ·ļø ${file.missingCategories} missing categories\n` - ) - Process.stdout.write(` šŸ“¦ ${file.totalExports} total exports\n`) - }) - - Process.stdout.write("\n") - - // Files with perfect documentation - const perfectFiles = fileDetails.filter((f) => f.missingExamples === 0 && f.missingCategories === 0) - if (perfectFiles.length > 0) { - Process.stdout.write("āœ… PERFECTLY DOCUMENTED FILES\n") - Process.stdout.write("-".repeat(35) + "\n") - perfectFiles.forEach((file) => { - Process.stdout.write(` ${file.filename} (${file.totalExports} exports)\n`) - }) - Process.stdout.write("\n") - } - - // Show a sample of missing items for the top file - if (sortedFiles.length > 0) { - const topFile = sortedFiles[0] - const topFileMissingItems = missingItems.filter((item) => item.file === topFile.filename).slice(0, 10) - if (topFileMissingItems.length > 0) { - Process.stdout.write(`šŸ” SAMPLE MISSING ITEMS FROM ${topFile.filename}\n`) - Process.stdout.write("-".repeat(35) + "\n") - topFileMissingItems.forEach((item) => { - const missing = [] - if (item.missingExample) missing.push("example") - if (item.missingCategory) missing.push("category") - Process.stdout.write(` ${item.name} (${item.type}, line ${item.line}): missing ${missing.join(", ")}\n`) - }) - Process.stdout.write("\n") - } - } - - // Detailed breakdown by type - Process.stdout.write("šŸ“‹ BREAKDOWN BY EXPORT TYPE\n") - Process.stdout.write("-".repeat(35) + "\n") - const typeStats = {} - missingItems.forEach((item) => { - if (!typeStats[item.type]) { - typeStats[item.type] = { total: 0, missingExample: 0, missingCategory: 0 } - } - typeStats[item.type].total++ - if (item.missingExample) typeStats[item.type].missingExample++ - if (item.missingCategory) typeStats[item.type].missingCategory++ - }) - - Object.entries(typeStats).forEach(([type, stats]) => { - Process.stdout.write( - `${type}: ${stats.missingExample} missing examples, ${stats.missingCategory} missing categories\n` - ) - }) - - Process.stdout.write("\n") - - // Progress tracking - const documentedExamples = totalExports - missingExamples - const documentedCategories = totalExports - missingCategories - Process.stdout.write("šŸ“ˆ DOCUMENTATION PROGRESS\n") - Process.stdout.write("-".repeat(30) + "\n") - Process.stdout.write( - `Examples: ${documentedExamples}/${totalExports} (${ - ((documentedExamples / totalExports) * 100).toFixed(1) - }% complete)\n` - ) - Process.stdout.write( - `Categories: ${documentedCategories}/${totalExports} (${ - ((documentedCategories / totalExports) * 100).toFixed(1) - }% complete)\n` - ) - Process.stdout.write("\n") - - Process.stdout.write("=".repeat(60) + "\n") - Process.stdout.write(`Analysis complete! ${missingExamples + missingCategories} items need attention.\n`) - Process.stdout.write("=".repeat(60) + "\n") - - // Save detailed results to JSON for further analysis - const outputFile = "jsdoc-analysis-results.json" - Fs.writeFileSync(outputFile, JSON.stringify(this.results, null, 2)) - Process.stdout.write(`\nšŸ“„ Detailed results saved to: ${outputFile}\n`) - } -} - -// Run the analysis -const analyzer = new JSDocAnalyzer() -analyzer.analyze(fileFilter) diff --git a/scripts/audit-jsdoc-categories.mjs b/scripts/audit-jsdoc-categories.mjs deleted file mode 100644 index 6f66eaf35f..0000000000 --- a/scripts/audit-jsdoc-categories.mjs +++ /dev/null @@ -1,479 +0,0 @@ -#!/usr/bin/env node - -// Collects files that need JSDoc category cleanup into a per-file queue. -// This script deliberately does not judge category quality; category taxonomy -// redesign is handled by reviewers/agents working directly in each source file. - -import * as Fs from "node:fs" -import * as Path from "node:path" -import * as Process from "node:process" -import ts from "typescript" - -const cwd = Process.cwd() -const args = Process.argv.slice(2) -const outputDirectory = getArg("--output-dir") ?? "reports/jsdoc-categories" - -const files = listSourceFiles(Path.join(cwd, "packages")) -const entries = [] -let topLevelRelevantTotal = 0 -let topLevelMissingCategoryTotal = 0 -let topLevelCategoryWithoutSinceTotal = 0 -let topLevelNamespaceCategoryTotal = 0 -let nestedJSDocTotal = 0 -let nestedCategoryTotal = 0 - -for (const file of files) { - const source = Fs.readFileSync(file, "utf8") - const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true) - const summary = summarizeFile(sourceFile) - - topLevelRelevantTotal += summary.topLevelRelevant - topLevelMissingCategoryTotal += summary.topLevelMissingCategory - topLevelCategoryWithoutSinceTotal += summary.topLevelCategoryWithoutSince - topLevelNamespaceCategoryTotal += summary.topLevelNamespaceCategory - nestedJSDocTotal += summary.nestedJSDoc - nestedCategoryTotal += summary.nestedCategory - - if ( - summary.topLevelRelevant === 0 && - summary.topLevelNamespaceCategory === 0 && - summary.nestedCategory === 0 - ) { - continue - } - - entries.push({ - file: normalizePath(Path.relative(cwd, file)), - packageName: getPackageName(file), - moduleName: getModuleName(file), - ...summary - }) -} - -entries.sort((a, b) => - a.packageName.localeCompare(b.packageName) || - a.moduleName.localeCompare(b.moduleName) || - a.file.localeCompare(b.file) -) - -writeQueue(entries, { - files: files.length, - topLevelRelevant: topLevelRelevantTotal, - topLevelMissingCategory: topLevelMissingCategoryTotal, - topLevelCategoryWithoutSince: topLevelCategoryWithoutSinceTotal, - topLevelNamespaceCategory: topLevelNamespaceCategoryTotal, - nestedJSDoc: nestedJSDocTotal, - nestedCategory: nestedCategoryTotal -}) - -console.log(`Scanned ${files.length} source file(s).`) -console.log(`Queued ${entries.length} file(s) for JSDoc category cleanup.`) -console.log(`Found ${topLevelRelevantTotal} public non-namespace exported JSDoc block(s) with @since or @category.`) -console.log(`Found ${topLevelMissingCategoryTotal} public non-namespace exported @since block(s) missing @category.`) -console.log(`Found ${topLevelNamespaceCategoryTotal} namespace @category tag(s) to remove.`) -console.log(`Found ${nestedCategoryTotal} nested @category tag(s) to remove.`) -console.log(`Wrote ${Path.join(outputDirectory, "queue.md")}.`) - -function getArg(name) { - const direct = args.find((arg) => arg.startsWith(`${name}=`)) - if (direct !== undefined) { - return direct.slice(name.length + 1) - } - const index = args.indexOf(name) - return index === -1 ? undefined : args[index + 1] -} - -function listSourceFiles(root) { - const out = [] - - function visit(directory) { - for (const entry of Fs.readdirSync(directory, { withFileTypes: true })) { - const fullPath = Path.join(directory, entry.name) - if (entry.isDirectory()) { - if ( - entry.name === "node_modules" || - entry.name === "dist" || - entry.name === "docs" || - entry.name === "test" || - entry.name === "typetest" - ) { - continue - } - visit(fullPath) - continue - } - - if (!entry.isFile() || !entry.name.endsWith(".ts")) { - continue - } - if ( - entry.name === "index.ts" || - entry.name.endsWith(".d.ts") || - entry.name.endsWith(".test.ts") || - entry.name.endsWith(".tst.ts") || - entry.name.endsWith("Generated.ts") - ) { - continue - } - if (!normalizePath(fullPath).includes("/src/")) { - continue - } - out.push(fullPath) - } - } - - visit(root) - return out.sort((a, b) => normalizePath(a).localeCompare(normalizePath(b))) -} - -function summarizeFile(sourceFile) { - const categories = new Map() - const nestedCategories = new Map() - let topLevelRelevant = 0 - let topLevelMissingCategory = 0 - let topLevelCategoryWithoutSince = 0 - let topLevelNamespaceCategory = 0 - let nestedJSDoc = 0 - let nestedCategory = 0 - - for (const statement of sourceFile.statements) { - if (!isExportedTopLevelDeclaration(statement)) { - continue - } - - let topLevelInternal = false - const jsdoc = getLeadingJSDoc(statement) - if (jsdoc !== undefined) { - const parsed = parseJSDoc(sourceFile, jsdoc) - topLevelInternal = hasTag(parsed, "internal") - if (!topLevelInternal && ts.isModuleDeclaration(statement) && hasTag(parsed, "category")) { - topLevelNamespaceCategory += tagCount(parsed, "category") - } - if (!topLevelInternal && !ts.isModuleDeclaration(statement)) { - const summary = summarizePublicJSDoc(parsed) - topLevelRelevant += summary.relevant - topLevelMissingCategory += summary.missingCategory - topLevelCategoryWithoutSince += summary.categoryWithoutSince - addCategories(categories, summary.categories) - } - } - - if (topLevelInternal) { - continue - } - - if (ts.isModuleDeclaration(statement)) { - const namespace = summarizeNamespaceMembers(sourceFile, statement) - topLevelRelevant += namespace.relevant - topLevelMissingCategory += namespace.missingCategory - topLevelCategoryWithoutSince += namespace.categoryWithoutSince - topLevelNamespaceCategory += namespace.namespaceCategory - nestedJSDoc += namespace.nestedJSDoc - nestedCategory += namespace.nestedCategory - addCategories(categories, namespace.categories) - addCategories(nestedCategories, namespace.nestedCategories) - } else { - const nested = summarizeNestedJSDocs(sourceFile, statement) - nestedJSDoc += nested.count - nestedCategory += nested.category - addCategories(nestedCategories, nested.categories) - } - } - - return { - topLevelRelevant, - topLevelMissingCategory, - topLevelCategoryWithoutSince, - topLevelNamespaceCategory, - nestedJSDoc, - nestedCategory, - categories: Array.from(categories, ([name, count]) => ({ name, count })) - .sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)), - nestedCategories: Array.from(nestedCategories, ([name, count]) => ({ name, count })) - .sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)) - } -} - -function isExportedTopLevelDeclaration(node) { - return ( - ts.isClassDeclaration(node) || - ts.isEnumDeclaration(node) || - ts.isFunctionDeclaration(node) || - ts.isInterfaceDeclaration(node) || - ts.isModuleDeclaration(node) || - ts.isTypeAliasDeclaration(node) || - ts.isVariableStatement(node) - ) && hasModifier(node, ts.SyntaxKind.ExportKeyword) -} - -function summarizeNamespaceMembers(sourceFile, namespaceNode) { - const categories = new Map() - const nestedCategories = new Map() - let relevant = 0 - let missingCategory = 0 - let categoryWithoutSince = 0 - let namespaceCategory = 0 - let nestedJSDoc = 0 - let nestedCategory = 0 - - for (const statement of getNamespaceStatements(namespaceNode)) { - if (!isExportedTopLevelDeclaration(statement)) { - continue - } - - let internal = false - const jsdoc = getLeadingJSDoc(statement) - if (jsdoc !== undefined) { - const parsed = parseJSDoc(sourceFile, jsdoc) - internal = hasTag(parsed, "internal") - if (!internal && ts.isModuleDeclaration(statement) && hasTag(parsed, "category")) { - namespaceCategory += tagCount(parsed, "category") - } - if (!internal && !ts.isModuleDeclaration(statement)) { - const summary = summarizePublicJSDoc(parsed) - relevant += summary.relevant - missingCategory += summary.missingCategory - categoryWithoutSince += summary.categoryWithoutSince - addCategories(categories, summary.categories) - } - } - - if (internal) { - continue - } - - const nested = ts.isModuleDeclaration(statement) - ? summarizeNamespaceMembers(sourceFile, statement) - : summarizeNestedJSDocs(sourceFile, statement) - - if (ts.isModuleDeclaration(statement)) { - relevant += nested.relevant - missingCategory += nested.missingCategory - categoryWithoutSince += nested.categoryWithoutSince - namespaceCategory += nested.namespaceCategory - nestedJSDoc += nested.nestedJSDoc - nestedCategory += nested.nestedCategory - addCategories(categories, nested.categories) - addCategories(nestedCategories, nested.nestedCategories) - } else { - nestedJSDoc += nested.count - nestedCategory += nested.category - addCategories(nestedCategories, nested.categories) - } - } - - return { - relevant, - missingCategory, - categoryWithoutSince, - namespaceCategory, - nestedJSDoc, - nestedCategory, - categories: Array.from(categories, ([name, count]) => ({ name, count })), - nestedCategories: Array.from(nestedCategories, ([name, count]) => ({ name, count })) - } -} - -function summarizePublicJSDoc(parsed) { - const categoryValues = tagValues(parsed, "category") - const relevant = hasTag(parsed, "since") || categoryValues.length > 0 ? 1 : 0 - return { - relevant, - missingCategory: relevant === 1 && categoryValues.length === 0 ? 1 : 0, - categoryWithoutSince: categoryValues.length > 0 && !hasTag(parsed, "since") ? 1 : 0, - categories: categoryValues.map((name) => ({ name, count: 1 })) - } -} - -function getNamespaceStatements(namespaceNode) { - return namespaceNode.body?.statements ?? [] -} - -function hasModifier(node, kind) { - return node.modifiers?.some((modifier) => modifier.kind === kind) === true -} - -function addCategories(target, categories) { - for (const category of categories) { - target.set(category.name, (target.get(category.name) ?? 0) + category.count) - } -} - -function summarizeNestedJSDocs(sourceFile, root) { - let count = 0 - let category = 0 - const categories = new Map() - - function visit(node) { - if (node !== root) { - const jsdoc = getLeadingJSDoc(node) - if (jsdoc !== undefined) { - const parsed = parseJSDoc(sourceFile, jsdoc) - if (!hasTag(parsed, "internal")) { - count++ - const categoryValues = tagValues(parsed, "category") - category += categoryValues.length - for (const value of categoryValues) { - categories.set(value, (categories.get(value) ?? 0) + 1) - } - } - } - } - - if (ts.isBlock(node)) { - return - } - - ts.forEachChild(node, visit) - } - - ts.forEachChild(root, visit) - return { - count, - category, - categories: Array.from(categories, ([name, count]) => ({ name, count })) - } -} - -function getLeadingJSDoc(node) { - const jsdocs = node.jsDoc - if (jsdocs === undefined || jsdocs.length === 0) { - return undefined - } - return jsdocs[jsdocs.length - 1] -} - -function parseJSDoc(sourceFile, jsdoc) { - const raw = sourceFile.text.slice(jsdoc.pos, jsdoc.end) - const lines = raw - .replace(/^\/\*\*/, "") - .replace(/\*\/$/, "") - .split(/\r\n|\r|\n/) - .map((line) => line.replace(/^\s*\* ?/, "").trimEnd()) - - const tags = [] - let inFence = false - - for (const line of lines) { - const trimmed = line.trim() - if (trimmed.startsWith("```")) { - inFence = !inFence - continue - } - if (inFence) { - continue - } - - const tag = trimmed.match(/^@([A-Za-z][\w-]*)(?:\s+(.*))?$/) - if (tag !== null) { - tags.push({ - name: tag[1], - value: tag[2]?.trim() ?? "" - }) - } - } - - return { tags } -} - -function hasTag(jsdoc, name) { - return jsdoc.tags.some((tag) => tag.name === name) -} - -function tagCount(jsdoc, name) { - return jsdoc.tags.filter((tag) => tag.name === name).length -} - -function tagValues(jsdoc, name) { - return jsdoc.tags - .filter((tag) => tag.name === name) - .map((tag) => tag.value.trim()) - .filter((value) => value !== "") -} - -function writeQueue(entries, summary) { - const output = Path.join(cwd, outputDirectory) - Fs.mkdirSync(output, { recursive: true }) - Fs.writeFileSync(Path.join(output, "queue.md"), renderQueue(entries, summary)) -} - -function renderQueue(entries, summary) { - const lines = [ - "# JSDoc Category Cleanup Queue", - "", - "Generated by `node scripts/audit-jsdoc-categories.mjs`.", - "", - "This file is a work queue, not a semantic audit result. The script only partitions files that contain public non-namespace exported JSDoc blocks with `@since` or `@category`, namespace JSDoc blocks with `@category`, or ordinary nested JSDoc blocks that already contain `@category`.", - "", - "## Swarm Instructions", - "", - "- Use one worker per file.", - "- Keep at most 6 workers active at a time.", - "- Workers may edit only `@category` lines in their assigned source file.", - "- For public non-namespace exported JSDocs, including direct namespace exports, redesign the file-local category taxonomy for generated docs navigation.", - "- For public non-namespace exported JSDocs with `@since` but no `@category`, add a category from the file-local taxonomy.", - "- For namespace JSDocs, remove `@category`.", - "- For nested JSDocs inside top-level exported declarations, remove `@category`.", - "- Do not edit descriptions, examples, `@since`, runtime code, types, imports, exports, or generated `index.ts` files.", - "- Only the coordinator should update this queue.", - "- Mark a file as done by changing `[ ]` to `[x]`; leave a short note when a category decision was ambiguous.", - "", - "## Summary", - "", - `- Source files scanned: ${summary.files}`, - `- Files queued: ${entries.length}`, - `- Public non-namespace exported JSDoc blocks with @since or @category: ${summary.topLevelRelevant}`, - `- Public non-namespace exported @since blocks missing @category: ${summary.topLevelMissingCategory}`, - `- Public non-namespace exported @category blocks missing @since: ${summary.topLevelCategoryWithoutSince}`, - `- Namespace @category tags to remove: ${summary.topLevelNamespaceCategory}`, - `- Nested JSDoc blocks inside top-level exports: ${summary.nestedJSDoc}`, - `- Nested @category tags to remove: ${summary.nestedCategory}`, - "", - "## Files", - "" - ] - - for (const entry of entries) { - lines.push( - `- [ ] \`${entry.file}\``, - ` - Package: \`${entry.packageName}\``, - ` - Module: \`${entry.moduleName}\``, - ` - Public non-namespace relevant JSDocs: ${entry.topLevelRelevant}`, - ` - Missing public non-namespace @category: ${entry.topLevelMissingCategory}`, - ` - Public non-namespace @category without @since: ${entry.topLevelCategoryWithoutSince}`, - ` - Namespace @category tags to remove: ${entry.topLevelNamespaceCategory}`, - ` - Nested JSDoc blocks: ${entry.nestedJSDoc}`, - ` - Nested @category tags to remove: ${entry.nestedCategory}`, - ` - Current top-level categories: ${renderCategories(entry.categories)}`, - ` - Current nested categories: ${renderCategories(entry.nestedCategories)}` - ) - } - - lines.push("") - return lines.join("\n") -} - -function renderCategories(categories) { - if (categories.length === 0) { - return "(none)" - } - return categories.map((category) => `\`${category.name}\` (${category.count})`).join(", ") -} - -function getPackageName(file) { - const relative = normalizePath(Path.relative(cwd, file)) - const parts = relative.split("/") - const srcIndex = parts.indexOf("src") - return parts.slice(0, srcIndex).join("/") -} - -function getModuleName(file) { - const relative = normalizePath(Path.relative(cwd, file)) - const parts = relative.split("/") - const srcIndex = parts.indexOf("src") - return parts.slice(srcIndex + 1).join("/").replace(/\.ts$/, "") -} - -function normalizePath(file) { - return file.split(Path.sep).join("/") -} diff --git a/scripts/audit-jsdoc-descriptions.mjs b/scripts/audit-jsdoc-descriptions.mjs deleted file mode 100644 index 03a4ca6d37..0000000000 --- a/scripts/audit-jsdoc-descriptions.mjs +++ /dev/null @@ -1,412 +0,0 @@ -#!/usr/bin/env node - -// Collects exported, non-internal JSDoc descriptions into review chunks. -// This script deliberately does not judge quality; human/agent reviewers read -// the chunks and write only the descriptions they flag into the backlog. - -import * as Fs from "node:fs" -import * as Path from "node:path" -import * as Process from "node:process" -import ts from "typescript" - -const cwd = Process.cwd() -const args = Process.argv.slice(2) -const outputDirectory = getArg("--output-dir") ?? "reports/jsdoc-descriptions" -const chunkSize = Number(getArg("--chunk-size") ?? 120) - -const ignoredBasenames = new Set(["Generated.ts", "index.ts"]) -const declarationKinds = new Set([ - ts.SyntaxKind.ClassDeclaration, - ts.SyntaxKind.EnumDeclaration, - ts.SyntaxKind.FunctionDeclaration, - ts.SyntaxKind.InterfaceDeclaration, - ts.SyntaxKind.ModuleDeclaration, - ts.SyntaxKind.TypeAliasDeclaration, - ts.SyntaxKind.VariableStatement -]) - -const files = listSourceFiles(Path.join(cwd, "packages")) -const entries = [] -let skippedInternal = 0 - -for (const file of files) { - const source = Fs.readFileSync(file, "utf8") - const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true) - - visitSourceFile(sourceFile, (node, exportPath) => { - const jsdoc = getLeadingJSDoc(node) - if (jsdoc === undefined) { - return - } - - const parsed = parseJSDoc(sourceFile, jsdoc) - if (parsed.tags.includes("internal")) { - skippedInternal++ - return - } - - const declarations = getExportedDeclarations(sourceFile, node) - for (const declaration of declarations) { - const position = sourceFile.getLineAndCharacterOfPosition(declaration.position) - entries.push({ - packageName: getPackageName(file), - moduleName: getModuleName(file), - file: normalizePath(Path.relative(cwd, file)), - line: position.line + 1, - apiName: [...exportPath, declaration.name].join("."), - apiKind: declaration.kind, - description: parsed.description, - tags: parsed.tags - }) - } - }) -} - -entries.sort((a, b) => - a.packageName.localeCompare(b.packageName) || - a.moduleName.localeCompare(b.moduleName) || - a.line - b.line || - a.apiName.localeCompare(b.apiName) -) - -writeInventory(entries, { - files: files.length, - skippedInternal -}) - -console.log(`Collected ${entries.length} exported non-internal JSDoc declaration(s) from ${files.length} file(s).`) -console.log(`Skipped ${skippedInternal} @internal JSDoc declaration(s).`) -console.log(`Wrote inventory and review chunks to ${outputDirectory}.`) - -function getArg(name) { - const direct = args.find((arg) => arg.startsWith(`${name}=`)) - if (direct !== undefined) { - return direct.slice(name.length + 1) - } - const index = args.indexOf(name) - return index === -1 ? undefined : args[index + 1] -} - -function listSourceFiles(root) { - const out = [] - - function visit(directory) { - for (const entry of Fs.readdirSync(directory, { withFileTypes: true })) { - const fullPath = Path.join(directory, entry.name) - if (entry.isDirectory()) { - if ( - entry.name === "node_modules" || - entry.name === "dist" || - entry.name === "docs" || - entry.name === "test" || - entry.name === "typetest" - ) { - continue - } - visit(fullPath) - continue - } - - if (!entry.isFile() || !entry.name.endsWith(".ts")) { - continue - } - if ( - entry.name.endsWith(".d.ts") || - entry.name.endsWith(".test.ts") || - entry.name.endsWith(".tst.ts") || - ignoredBasenames.has(entry.name) - ) { - continue - } - if (!normalizePath(fullPath).includes("/src/")) { - continue - } - out.push(fullPath) - } - } - - visit(root) - return out.sort((a, b) => normalizePath(a).localeCompare(normalizePath(b))) -} - -function visitSourceFile(sourceFile, onExport) { - function visit(node, exportPath, exportedNamespaceDepth) { - if (!declarationKinds.has(node.kind)) { - ts.forEachChild(node, (child) => visit(child, exportPath, exportedNamespaceDepth)) - return - } - - const isExported = hasModifier(node, ts.SyntaxKind.ExportKeyword) || exportedNamespaceDepth > 0 - - if (isExported) { - onExport(node, exportPath) - } - - if (ts.isModuleDeclaration(node) && isExported && node.name !== undefined) { - const namespaceName = node.name.getText(sourceFile).replaceAll("\"", "") - if (node.body !== undefined) { - ts.forEachChild(node.body, (child) => visit(child, [...exportPath, namespaceName], exportedNamespaceDepth + 1)) - } - return - } - - ts.forEachChild(node, (child) => visit(child, exportPath, exportedNamespaceDepth)) - } - - ts.forEachChild(sourceFile, (node) => visit(node, [], 0)) -} - -function hasModifier(node, kind) { - return node.modifiers?.some((modifier) => modifier.kind === kind) === true -} - -function getLeadingJSDoc(node) { - const jsdocs = node.jsDoc - if (jsdocs === undefined || jsdocs.length === 0) { - return undefined - } - return jsdocs[jsdocs.length - 1] -} - -function getExportedDeclarations(sourceFile, node) { - if (ts.isVariableStatement(node)) { - return node.declarationList.declarations - .filter((declaration) => ts.isIdentifier(declaration.name)) - .map((declaration) => ({ - name: declaration.name.text, - kind: "value", - position: declaration.name.getStart(sourceFile) - })) - } - - if ( - (ts.isFunctionDeclaration(node) || - ts.isClassDeclaration(node) || - ts.isInterfaceDeclaration(node) || - ts.isTypeAliasDeclaration(node) || - ts.isEnumDeclaration(node) || - ts.isModuleDeclaration(node)) && - node.name !== undefined - ) { - return [{ - name: node.name.getText(sourceFile), - kind: getApiKind(node), - position: node.name.getStart(sourceFile) - }] - } - - return [] -} - -function getApiKind(node) { - if (ts.isFunctionDeclaration(node)) { - return "value" - } - if (ts.isClassDeclaration(node)) { - return "class" - } - if (ts.isInterfaceDeclaration(node)) { - return "interface" - } - if (ts.isTypeAliasDeclaration(node)) { - return "type" - } - if (ts.isEnumDeclaration(node)) { - return "enum" - } - if (ts.isModuleDeclaration(node)) { - return "namespace" - } - return "value" -} - -function parseJSDoc(sourceFile, jsdoc) { - const raw = sourceFile.text.slice(jsdoc.pos, jsdoc.end) - const lines = raw - .replace(/^\/\*\*/, "") - .replace(/\*\/$/, "") - .split(/\r\n|\r|\n/) - .map((line) => line.replace(/^\s*\* ?/, "").trimEnd()) - - const tags = [] - let inFence = false - let inExample = false - const descriptionLines = [] - - for (const line of lines) { - const trimmed = line.trim() - if (trimmed.startsWith("```")) { - inFence = !inFence - continue - } - if (inFence) { - continue - } - - const tag = trimmed.match(/^@([A-Za-z][\w-]*)/) - if (tag !== null) { - tags.push(tag[1]) - if (tag[1] === "example") { - inExample = true - } - continue - } - - if (/^\*\*Examples?\*\*/i.test(trimmed) || /^#{2,}\s+Examples?/i.test(trimmed)) { - inExample = true - continue - } - - if (inExample) { - continue - } - - if (trimmed !== "") { - descriptionLines.push(trimmed) - } - } - - return { - tags: Array.from(new Set(tags)), - description: normalizeDescription(descriptionLines.join("\n")) - } -} - -function normalizeDescription(description) { - return description - .replaceAll(/\{@link\s+([^}\s]+)(?:\s+[^}]*)?\}/g, "$1") - .replaceAll(/[ \t]+\n/g, "\n") - .replaceAll(/\n{3,}/g, "\n\n") - .trim() -} - -function writeInventory(entries, summary) { - const output = Path.join(cwd, outputDirectory) - const chunksDirectory = Path.join(output, "chunks") - const findingsDirectory = Path.join(output, "findings") - - Fs.mkdirSync(output, { recursive: true }) - Fs.rmSync(chunksDirectory, { recursive: true, force: true }) - Fs.mkdirSync(chunksDirectory, { recursive: true }) - Fs.mkdirSync(findingsDirectory, { recursive: true }) - - Fs.writeFileSync(Path.join(output, "inventory.json"), `${JSON.stringify({ - generatedBy: "node scripts/audit-jsdoc-descriptions.mjs", - scannedFiles: summary.files, - skippedInternal: summary.skippedInternal, - entries - }, null, 2)}\n`) - - const chunks = [] - for (let start = 0; start < entries.length; start += chunkSize) { - const chunkEntries = entries.slice(start, start + chunkSize) - const chunkNumber = String(chunks.length + 1).padStart(3, "0") - const chunkPath = Path.join(chunksDirectory, `chunk-${chunkNumber}.md`) - const chunk = { - number: chunks.length + 1, - path: normalizePath(Path.relative(cwd, chunkPath)), - start: start + 1, - end: start + chunkEntries.length, - count: chunkEntries.length - } - chunks.push(chunk) - Fs.writeFileSync(chunkPath, renderChunk(chunk, chunkEntries)) - } - - const backlogPath = Path.join(output, "review-needed.md") - if (!Fs.existsSync(backlogPath)) { - Fs.writeFileSync(backlogPath, renderBacklogSkeleton(entries, chunks, summary)) - } -} - -function renderBacklogSkeleton(entries, chunks, summary) { - return [ - "# JSDoc Description Review Needed", - "", - "Generated by `node scripts/audit-jsdoc-descriptions.mjs`.", - "", - "This backlog is intentionally empty until reviewers judge the inventory chunks.", - "The script only detects exported declarations with leading JSDoc blocks that are not tagged `@internal`; it does not decide whether a description is good.", - "", - "## Summary", - "", - `- Source files scanned: ${summary.files}`, - `- Exported non-internal JSDoc declarations collected: ${entries.length}`, - `- @internal JSDoc declarations skipped: ${summary.skippedInternal}`, - `- Review chunks: ${chunks.length}`, - "- Review-needed entries remaining: 0", - "", - "## Review Chunks", - "", - ...chunks.map((chunk) => `- [${Path.basename(chunk.path, ".md")}](${chunk.path}) (${chunk.count} entries, ${chunk.start}-${chunk.end})`), - "", - "## Entries", - "", - "No reviewed findings have been merged yet.", - "" - ].join("\n") -} - -function renderChunk(chunk, entries) { - const lines = [ - `# JSDoc Description Inventory ${String(chunk.number).padStart(3, "0")}`, - "", - "This file is reviewer input, not a backlog. Judge each description manually.", - "Only copy flagged descriptions into `reports/jsdoc-descriptions/review-needed.md` or a worker findings file.", - "", - "For each flagged entry, record: severity, issue labels, reason, current description, and suggested replacement.", - "", - `Entries: ${chunk.start}-${chunk.end}`, - "" - ] - - let currentPackage = "" - let currentModule = "" - - for (const entry of entries) { - if (entry.packageName !== currentPackage) { - currentPackage = entry.packageName - currentModule = "" - lines.push("", `## ${entry.packageName}`) - } - if (entry.moduleName !== currentModule) { - currentModule = entry.moduleName - lines.push("", `### ${entry.moduleName}`) - } - - lines.push( - "", - `#### ${entry.apiName}`, - "", - `- File: \`${entry.file}:${entry.line}\``, - `- API kind: \`${entry.apiKind}\``, - `- Tags: ${entry.tags.length === 0 ? "(none)" : entry.tags.map((tag) => `\`${tag}\``).join(", ")}`, - "- Current description:", - "", - "```md", - entry.description === "" ? "(missing)" : entry.description, - "```" - ) - } - - lines.push("") - return lines.join("\n") -} - -function getPackageName(file) { - const relative = normalizePath(Path.relative(cwd, file)) - const parts = relative.split("/") - const srcIndex = parts.indexOf("src") - return parts.slice(0, srcIndex).join("/") -} - -function getModuleName(file) { - const relative = normalizePath(Path.relative(cwd, file)) - const parts = relative.split("/") - const srcIndex = parts.indexOf("src") - return parts.slice(srcIndex + 1).join("/").replace(/\.ts$/, "") -} - -function normalizePath(file) { - return file.split(Path.sep).join("/") -} diff --git a/scripts/codemod-ts-fence.mjs b/scripts/codemod-ts-fence.mjs deleted file mode 100644 index f802b3581b..0000000000 --- a/scripts/codemod-ts-fence.mjs +++ /dev/null @@ -1,19 +0,0 @@ -// @ts-check -import * as Glob from "glob" -import * as Jscodeshift from "jscodeshift/src/Runner.js" -import * as Path from "node:path" - -// Look up files in all workspace packages including those nested in -// sub-packages (e.g. `packages/ai/openapi`). -const pattern = "packages/{*,*/*}/src/**/*.ts" - -const paths = Glob.globSync(pattern, { - ignore: ["**/internal/**"] -}).map((path) => Path.resolve(path)) - -const transformer = Path.resolve("scripts/codemods/ts-fence.ts") - -Jscodeshift.run(transformer, paths, { - babel: true, - parser: "ts" -}) diff --git a/scripts/codemods/ts-fence.test.ts b/scripts/codemods/ts-fence.test.ts deleted file mode 100644 index 595bd62c2b..0000000000 --- a/scripts/codemods/ts-fence.test.ts +++ /dev/null @@ -1,206 +0,0 @@ -// You can run this test suite with the following command: -// npx vitest scripts/codemods/ts-fence.test.ts --config scripts/codemods/vitest.config.ts -import type * as cs from "jscodeshift" -import * as TestUtils from "jscodeshift/src/testUtils" -import transformer from "./ts-fence.ts" - -const expectTransformation_ = (transformer: cs.Transform) => -( - description: string, - input: string, - output: string -) => { - TestUtils.defineInlineTest( - { default: transformer, parser: "ts" }, - {}, - input, - output, - description - ) -} - -const expectTransformation = expectTransformation_(transformer) - -expectTransformation( - "should ignore line comments", - `// description -const v = 1`, - `// description -const v = 1` -) - -expectTransformation( - "should ignore block comments that don't contain an @example tag", - ` -/** - * description - */ -const v = 1`, - ` -/** - * description - */ -const v = 1` -) - -expectTransformation( - "should wrap the given code in a ts fence (without following tags)", - ` -/** - * a - * - * b - * - * @example - * const x = 1 - * - */ -const v = 1`, - ` -/** - * a - * - * b - * - * @example - * \`\`\`ts - * const x = 1 - * - * \`\`\` - */ -const v = 1` -) - -expectTransformation( - "should wrap the given code in a ts fence (with following tags)", - ` -/** - * a - * - * b - * - * @example - * const x = 1 - * - * @since 1.0.0 - * @category collecting & elements - */ -const v = 1`, - ` -/** - * a - * - * b - * - * @example - * \`\`\`ts - * const x = 1 - * \`\`\` - * - * @since 1.0.0 - * @category collecting & elements - */ -const v = 1` -) - -expectTransformation( - "should skip wrapping if the code is already in a ts fence", - ` -/** - * a - * - * b - * - * @example - * \`\`\`ts - * const x = 1 - * \`\`\` - */ -const v = 1`, - ` -/** - * a - * - * b - * - * @example - * \`\`\`ts - * const x = 1 - * \`\`\` - */ -const v = 1` -) - -expectTransformation( - "should skip wrapping indented examples that are already in a ts fence", - ` -export declare namespace ReadonlyArray { - /** - * Flattens a nested array type. - * - * @example - * \`\`\`ts - * import type { Array } from "effect" - * - * type Nested = ReadonlyArray> - * type Flattened = Array.ReadonlyArray.Flatten - * // Flattened is Array - * \`\`\` - * - * @category types - * @since 2.0.0 - */ - export type Flatten>> = Array -}`, - ` -export declare namespace ReadonlyArray { - /** - * Flattens a nested array type. - * - * @example - * \`\`\`ts - * import type { Array } from "effect" - * - * type Nested = ReadonlyArray> - * type Flattened = Array.ReadonlyArray.Flatten - * // Flattened is Array - * \`\`\` - * - * @category types - * @since 2.0.0 - */ - export type Flatten>> = Array -}` -) - -expectTransformation( - "should wrap indented examples using the current doc prefix", - ` -export declare namespace ReadonlyArray { - /** - * Infers the element type of an iterable. - * - * @example - * type A = string - * - * @category types - * @since 2.0.0 - */ - export type Infer> = S extends Iterable ? A : never -}`, - ` -export declare namespace ReadonlyArray { - /** - * Infers the element type of an iterable. - * - * @example - * \`\`\`ts - * type A = string - * \`\`\` - * - * @category types - * @since 2.0.0 - */ - export type Infer> = S extends Iterable ? A : never -}` -) diff --git a/scripts/codemods/ts-fence.ts b/scripts/codemods/ts-fence.ts deleted file mode 100644 index 1739c023ab..0000000000 --- a/scripts/codemods/ts-fence.ts +++ /dev/null @@ -1,140 +0,0 @@ -import type * as cs from "jscodeshift" - -export default function transformer(file: cs.FileInfo, api: cs.API) { - const j = api.jscodeshift - const root = j(file.source) - - root.find(j.Comment as any).forEach((path) => { - if (path.value.type === "CommentBlock") { - const value = (path.value) as any - const comment = value.value - const wrapped = wrapExamplesWithFence(comment) - if (wrapped !== comment) { - value.value = wrapped - } - } - }) - - return root.toSource() -} - -function wrapExamplesWithFence(jsdocComment: string) { - const lines = normalizeJSDocLines(jsdocComment.split("\n")) - const output: Array = [] - let changed = false - - for (let i = 0; i < lines.length; i++) { - const line = lines[i] - output.push(line) - - if (!isExampleLine(line)) { - continue - } - - const nextContentLine = findNextContentLine(lines, i + 1) - if (nextContentLine !== -1 && isTsFenceLine(lines[nextContentLine])) { - continue - } - - changed = true - - const prefix = getJSDocLine(line)?.prefix ?? " * " - const nextTagLine = findNextTagLine(lines, i + 1) - const endLine = nextTagLine === -1 ? findClosingPaddingLine(lines) : nextTagLine - let exampleEnd = endLine === -1 ? lines.length : endLine - - if (nextTagLine !== -1) { - while (exampleEnd > i + 1 && isBlankJSDocLine(lines[exampleEnd - 1])) { - exampleEnd-- - } - } - - output.push(`${prefix}\`\`\`ts`) - for (let j = i + 1; j < exampleEnd; j++) { - output.push(lines[j]) - } - output.push(`${prefix}\`\`\``) - - if (endLine !== -1) { - for (let j = exampleEnd; j < endLine; j++) { - output.push(lines[j]) - } - i = endLine - 1 - } else { - i = lines.length - 1 - } - } - - return changed ? output.join("\n") : jsdocComment -} - -function normalizeJSDocLines(lines: Array) { - return lines.map((line, index) => { - const jsdocLine = getJSDocLine(line) - if (jsdocLine === null) { - return isClosingPaddingLine(line) ? " " : line - } - if (index === 0 && jsdocLine.text === "") { - return "*" - } - return jsdocLine.text === "" ? " *" : ` * ${jsdocLine.text}` - }) -} - -function getJSDocLine(line: string) { - const match = line.match(/^(\s*\*\s?)(.*)$/) - if (!match) { - return null - } - return { prefix: match[1], text: match[2] } -} - -function isExampleLine(line: string) { - const jsdocLine = getJSDocLine(line) - return jsdocLine !== null && /^@example\b/.test(jsdocLine.text.trim()) -} - -function isTsFenceLine(line: string) { - const jsdocLine = getJSDocLine(line) - return jsdocLine !== null && /^```(?:ts|typescript)?\s*$/.test(jsdocLine.text.trim()) -} - -function isTagLine(line: string) { - const jsdocLine = getJSDocLine(line) - return jsdocLine !== null && /^@\w/.test(jsdocLine.text.trim()) -} - -function isBlankJSDocLine(line: string) { - const jsdocLine = getJSDocLine(line) - return jsdocLine !== null && jsdocLine.text.trim() === "" -} - -function isClosingPaddingLine(line: string) { - return getJSDocLine(line) === null && line.trim() === "" -} - -function findNextContentLine(lines: Array, start: number) { - for (let i = start; i < lines.length; i++) { - if (!isBlankJSDocLine(lines[i])) { - return i - } - } - return -1 -} - -function findNextTagLine(lines: Array, start: number) { - for (let i = start; i < lines.length; i++) { - if (isTagLine(lines[i])) { - return i - } - } - return -1 -} - -function findClosingPaddingLine(lines: Array) { - const lastLineIndex = lines.length - 1 - if (lastLineIndex >= 0 && isClosingPaddingLine(lines[lastLineIndex])) { - return lastLineIndex - } - return -1 -} diff --git a/scripts/codemods/vitest.config.ts b/scripts/codemods/vitest.config.ts deleted file mode 100644 index 0a565d350a..0000000000 --- a/scripts/codemods/vitest.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default { - test: { - globals: true - } -} diff --git a/scripts/jsdocs/code2jsdoc-example.html b/scripts/jsdocs/code2jsdoc-example.html deleted file mode 100644 index b592aef991..0000000000 --- a/scripts/jsdocs/code2jsdoc-example.html +++ /dev/null @@ -1,69 +0,0 @@ - - - - - code 2 jsdoc @example - - - -
-

code 2 jsdoc @example

-

- Given an example pasted in the left textarea, will generate a result - ready to be pasted into a JSDoc comment. -

-
- - -
-
- - -