diff --git a/.changeset/chilly-berries-talk.md b/.changeset/chilly-berries-talk.md new file mode 100644 index 0000000000..3cd38cd2ce --- /dev/null +++ b/.changeset/chilly-berries-talk.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +include Error.cause stack in log output diff --git a/.changeset/clever-parents-leave.md b/.changeset/clever-parents-leave.md new file mode 100644 index 0000000000..5dd8872705 --- /dev/null +++ b/.changeset/clever-parents-leave.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +add renderErrorCause option to Cause.pretty diff --git a/.changeset/clever-walls-smoke.md b/.changeset/clever-walls-smoke.md new file mode 100644 index 0000000000..5920c21aed --- /dev/null +++ b/.changeset/clever-walls-smoke.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-d1": minor +--- + +Add new cloudflare @effect/sql-d1 package diff --git a/.changeset/fast-dodos-impress.md b/.changeset/fast-dodos-impress.md new file mode 100644 index 0000000000..1085f69794 --- /dev/null +++ b/.changeset/fast-dodos-impress.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +set stackTraceLimit to 1 in PrettyError to address performance issues diff --git a/.changeset/five-olives-applaud.md b/.changeset/five-olives-applaud.md new file mode 100644 index 0000000000..ac148f1349 --- /dev/null +++ b/.changeset/five-olives-applaud.md @@ -0,0 +1,25 @@ +--- +"effect": minor +--- + +add RcRef module + +An `RcRef` wraps a reference counted resource that can be acquired and released multiple times. + +The resource is lazily acquired on the first call to `get` and released when the last reference is released. + +```ts +import { Effect, RcRef } from "effect"; + +Effect.gen(function* () { + const ref = yield* RcRef.make({ + acquire: Effect.acquireRelease(Effect.succeed("foo"), () => + Effect.log("release foo"), + ), + }); + + // will only acquire the resource once, and release it + // when the scope is closed + yield* RcRef.get(ref).pipe(Effect.andThen(RcRef.get(ref)), Effect.scoped); +}); +``` diff --git a/.changeset/hungry-drinks-pretend.md b/.changeset/hungry-drinks-pretend.md new file mode 100644 index 0000000000..4f825de44a --- /dev/null +++ b/.changeset/hungry-drinks-pretend.md @@ -0,0 +1,25 @@ +--- +"effect": minor +--- + +allowing customizing Stream pubsub strategy + +```ts +import { Schedule, Stream } from "effect"; + +// toPubSub +Stream.fromSchedule(Schedule.spaced(1000)).pipe( + Stream.toPubSub({ + capacity: 16, // or "unbounded" + strategy: "dropping", // or "sliding" / "suspend" + }), +); + +// also for the broadcast apis +Stream.fromSchedule(Schedule.spaced(1000)).pipe( + Stream.broadcastDynamic({ + capacity: 16, + strategy: "dropping", + }), +); +``` diff --git a/.changeset/little-taxis-drop.md b/.changeset/little-taxis-drop.md new file mode 100644 index 0000000000..fbddf8f058 --- /dev/null +++ b/.changeset/little-taxis-drop.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +add Duration.isZero, for checking if a Duration is zero diff --git a/.changeset/lucky-poets-guess.md b/.changeset/lucky-poets-guess.md new file mode 100644 index 0000000000..3a4ed83bb9 --- /dev/null +++ b/.changeset/lucky-poets-guess.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +Add `Success` type util for `Config`. diff --git a/.changeset/nervous-cougars-taste.md b/.changeset/nervous-cougars-taste.md new file mode 100644 index 0000000000..4e43454a50 --- /dev/null +++ b/.changeset/nervous-cougars-taste.md @@ -0,0 +1,15 @@ +--- +"effect": minor +--- + +add Logger.prettyLogger and Logger.pretty + +`Logger.pretty` is a new logger that leverages the features of the `console` APIs to provide a more visually appealing output. + +To try it out, provide it to your program: + +```ts +import { Effect, Logger } from "effect" + +Effect.log("Hello, World!").pipe(Effect.provide(Logger.pretty)) +``` diff --git a/.changeset/ninety-starfishes-flow.md b/.changeset/ninety-starfishes-flow.md new file mode 100644 index 0000000000..7443ff37db --- /dev/null +++ b/.changeset/ninety-starfishes-flow.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +add .groupCollapsed to UnsafeConsole diff --git a/.changeset/rich-insects-jog.md b/.changeset/rich-insects-jog.md new file mode 100644 index 0000000000..3a413ccf78 --- /dev/null +++ b/.changeset/rich-insects-jog.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +export Random.make taking hashable values as seed diff --git a/.changeset/seven-ghosts-move.md b/.changeset/seven-ghosts-move.md new file mode 100644 index 0000000000..7a1651efed --- /dev/null +++ b/.changeset/seven-ghosts-move.md @@ -0,0 +1,18 @@ +--- +"effect": minor +--- + +add `replay` option to PubSub constructors + +This option adds a replay buffer in front of the given PubSub. The buffer will +replay the last `n` messages to any new subscriber. + +```ts +Effect.gen(function*() { + const messages = [1, 2, 3, 4, 5] + const pubsub = yield* PubSub.bounded({ capacity: 16, replay: 3 }) + yield* PubSub.publishAll(pubsub, messages) + const sub = yield* PubSub.subscribe(pubsub) + assert.deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeAll(sub)), [3, 4, 5]) +})) +``` diff --git a/.changeset/spicy-hats-travel.md b/.changeset/spicy-hats-travel.md new file mode 100644 index 0000000000..071fef7dd4 --- /dev/null +++ b/.changeset/spicy-hats-travel.md @@ -0,0 +1,31 @@ +--- +"effect": minor +--- + +add RcMap module + +An `RcMap` can contain multiple reference counted resources that can be indexed +by a key. The resources are lazily acquired on the first call to `get` and +released when the last reference is released. + +Complex keys can extend `Equal` and `Hash` to allow lookups by value. + +```ts +import { Effect, RcMap } from "effect"; + +Effect.gen(function* () { + const map = yield* RcMap.make({ + lookup: (key: string) => + Effect.acquireRelease(Effect.succeed(`acquired ${key}`), () => + Effect.log(`releasing ${key}`), + ), + }); + + // Get "foo" from the map twice, which will only acquire it once + // It will then be released once the scope closes. + yield* RcMap.get(map, "foo").pipe( + Effect.andThen(RcMap.get(map, "foo")), + Effect.scoped, + ); +}); +``` diff --git a/.changeset/spotty-beds-arrive.md b/.changeset/spotty-beds-arrive.md new file mode 100644 index 0000000000..ca552242d7 --- /dev/null +++ b/.changeset/spotty-beds-arrive.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +ensure "cause" is rendered in Data.Error output diff --git a/.changeset/stream-channel-run-env-fix.md b/.changeset/stream-channel-run-env-fix.md new file mode 100644 index 0000000000..d4995b0a0c --- /dev/null +++ b/.changeset/stream-channel-run-env-fix.md @@ -0,0 +1,9 @@ +--- +"effect": minor +--- + +Ensure `Scope` is excluded from `R` in the `Channel` / `Stream` `run*` functions. + +This fix ensures that `Scope` is now properly excluded from the resulting effect environment. +The affected functions include `run`, `runCollect`, `runCount`, `runDrain` and other non-scoped `run*` in both `Stream` and `Channel` modules. +This fix brings the type declaration in line with the runtime implementation. diff --git a/.changeset/stream-merge-left-right.md b/.changeset/stream-merge-left-right.md new file mode 100644 index 0000000000..7f04d2aef9 --- /dev/null +++ b/.changeset/stream-merge-left-right.md @@ -0,0 +1,7 @@ +--- +"effect": minor +--- + +refactor(Stream/mergeLeft): rename `self`/`that` argument names to `left`/`right` for clarity + +refactor(Stream/mergeRight): rename `self`/`that` argument names to `left`/`right` for clarity diff --git a/.changeset/stream-race-all.md b/.changeset/stream-race-all.md new file mode 100644 index 0000000000..ae50dfabfe --- /dev/null +++ b/.changeset/stream-race-all.md @@ -0,0 +1,24 @@ +--- +"effect": minor +--- + +feat(Stream): implement "raceAll" operator, which returns a stream that mirrors the first source stream to emit an item. + +```ts +import { Stream, Schedule, Console, Effect } from "effect"; + +const stream = Stream.raceAll( + Stream.fromSchedule(Schedule.spaced("1 millis")), + Stream.fromSchedule(Schedule.spaced("2 millis")), + Stream.fromSchedule(Schedule.spaced("4 millis")), +).pipe(Stream.take(6), Stream.tap(Console.log)); + +Effect.runPromise(Stream.runDrain(stream)); +// Output only from the first stream, the rest streams are interrupted +// 0 +// 1 +// 2 +// 3 +// 4 +// 5 +``` diff --git a/.changeset/stream-tuple.md b/.changeset/stream-tuple.md new file mode 100644 index 0000000000..c03042a4fa --- /dev/null +++ b/.changeset/stream-tuple.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +refactor(Stream): use new built-in `Types.TupleOf` instead of `Stream.DynamicTuple` and deprecate it diff --git a/.changeset/swift-dodos-double.md b/.changeset/swift-dodos-double.md new file mode 100644 index 0000000000..dd6396493b --- /dev/null +++ b/.changeset/swift-dodos-double.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +support ErrorOptions in YieldableError constructor diff --git a/.changeset/tame-zoos-vanish.md b/.changeset/tame-zoos-vanish.md new file mode 100644 index 0000000000..03f076062e --- /dev/null +++ b/.changeset/tame-zoos-vanish.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix types of UnsafeConsole.group diff --git a/.changeset/ten-swans-rest.md b/.changeset/ten-swans-rest.md new file mode 100644 index 0000000000..f6c39fd38c --- /dev/null +++ b/.changeset/ten-swans-rest.md @@ -0,0 +1,19 @@ +--- +"effect": minor +--- + +allow customizing the output buffer for the Stream.async\* apis + +```ts +import { Stream } from "effect"; + +Stream.async( + (emit) => { + // ... + }, + { + bufferSize: 16, + strategy: "dropping", // you can also use "sliding" or "suspend" + }, +); +``` diff --git a/packages/effect/dtslint/Config.ts b/packages/effect/dtslint/Config.ts index f18654e53a..820eb62da5 100644 --- a/packages/effect/dtslint/Config.ts +++ b/packages/effect/dtslint/Config.ts @@ -1,5 +1,5 @@ import * as Config from "effect/Config" -import { pipe } from "effect/Function" +import { hole, pipe } from "effect/Function" declare const string: Config.Config declare const number: Config.Config @@ -45,3 +45,18 @@ Config.all(numberRecord) // $ExpectType Config<{ [x: string]: number; }> pipe(numberRecord, Config.all) + +// ------------------------------------------------------------------------------------- +// Success +// ------------------------------------------------------------------------------------- + +// $ExpectType string +hole>() + +// $ExpectType number +hole>() + +const object = Config.all({ a: string, b: number }) + +// $ExpectType { a: string; b: number; } +hole>() diff --git a/packages/effect/src/Cause.ts b/packages/effect/src/Cause.ts index 3368592163..7c7ce6c9a2 100644 --- a/packages/effect/src/Cause.ts +++ b/packages/effect/src/Cause.ts @@ -111,6 +111,18 @@ export const InvalidPubSubCapacityExceptionTypeId: unique symbol = core.InvalidP */ export type InvalidPubSubCapacityExceptionTypeId = typeof InvalidPubSubCapacityExceptionTypeId +/** + * @since 3.5.0 + * @category symbols + */ +export const ExceededCapacityExceptionTypeId: unique symbol = core.ExceededCapacityExceptionTypeId + +/** + * @since 3.5.0 + * @category symbols + */ +export type ExceededCapacityExceptionTypeId = typeof ExceededCapacityExceptionTypeId + /** * @since 2.0.0 * @category symbols @@ -264,6 +276,18 @@ export interface InvalidPubSubCapacityException extends YieldableError { readonly [InvalidPubSubCapacityExceptionTypeId]: InvalidPubSubCapacityExceptionTypeId } +/** + * Represents a checked exception which occurs when a resources capacity has + * been exceeded. + * + * @since 3.5.0 + * @category models + */ +export interface ExceededCapacityException extends YieldableError { + readonly _tag: "ExceededCapacityException" + readonly [ExceededCapacityExceptionTypeId]: ExceededCapacityExceptionTypeId +} + /** * Represents a checked exception which occurs when a computation doesn't * finish on schedule. @@ -907,13 +931,35 @@ export const UnknownException: new(error: unknown, message?: string | undefined) */ export const isUnknownException: (u: unknown) => u is UnknownException = core.isUnknownException +/** + * Represents a checked exception which occurs when a resources capacity has + * been exceeded. + * + * @since 3.5.0 + * @category errors + */ +export const ExceededCapacityException: new(message?: string | undefined) => ExceededCapacityException = + core.ExceededCapacityException + +/** + * Returns `true` if the specified value is an `ExceededCapacityException`, `false` + * otherwise. + * + * @since 3.5.0 + * @category refinements + */ +export const isExceededCapacityException: (u: unknown) => u is ExceededCapacityException = + core.isExceededCapacityException + /** * Returns the specified `Cause` as a pretty-printed string. * * @since 2.0.0 * @category rendering */ -export const pretty: (cause: Cause) => string = internal.pretty +export const pretty: (cause: Cause, options?: { + readonly renderErrorCause?: boolean | undefined +}) => string = internal.pretty /** * @since 3.2.0 diff --git a/packages/effect/src/Channel.ts b/packages/effect/src/Channel.ts index d8e369eafc..723d004101 100644 --- a/packages/effect/src/Channel.ts +++ b/packages/effect/src/Channel.ts @@ -1916,7 +1916,7 @@ export const repeated: ( */ export const run: ( self: Channel -) => Effect.Effect = channel.run +) => Effect.Effect> = channel.run /** * Run the channel until it finishes with a done value or fails with an error @@ -1929,7 +1929,7 @@ export const run: ( */ export const runCollect: ( self: Channel -) => Effect.Effect<[Chunk.Chunk, OutDone], OutErr, Env> = channel.runCollect +) => Effect.Effect<[Chunk.Chunk, OutDone], OutErr, Exclude> = channel.runCollect /** * Runs a channel until the end is received. @@ -1939,7 +1939,7 @@ export const runCollect: ( */ export const runDrain: ( self: Channel -) => Effect.Effect = channel.runDrain +) => Effect.Effect> = channel.runDrain /** * Use a scoped effect to emit an output element. diff --git a/packages/effect/src/Config.ts b/packages/effect/src/Config.ts index afb9557569..294157446c 100644 --- a/packages/effect/src/Config.ts +++ b/packages/effect/src/Config.ts @@ -51,6 +51,12 @@ export declare namespace Config { } } + /** + * @since 2.5.0 + * @category models + */ + export type Success> = [T] extends [Config] ? _A : never + /** * @since 2.0.0 * @category models diff --git a/packages/effect/src/Console.ts b/packages/effect/src/Console.ts index a0fa27a7aa..40b6d516a6 100644 --- a/packages/effect/src/Console.ts +++ b/packages/effect/src/Console.ts @@ -63,10 +63,8 @@ export interface UnsafeConsole { dir(item: any, options?: any): void dirxml(...args: ReadonlyArray): void error(...args: ReadonlyArray): void - group(options?: { - readonly label?: string | undefined - readonly collapsed?: boolean | undefined - }): void + group(label?: string | undefined): void + groupCollapsed(label?: string | undefined): void groupEnd(): void info(...args: ReadonlyArray): void log(...args: ReadonlyArray): void diff --git a/packages/effect/src/Data.ts b/packages/effect/src/Data.ts index dacabf0590..d793a1c21d 100644 --- a/packages/effect/src/Data.ts +++ b/packages/effect/src/Data.ts @@ -532,7 +532,7 @@ export const Error: new = {}>( ) => Cause.YieldableError & Readonly = (function() { return class Base extends core.YieldableError { constructor(args: any) { - super() + super(args?.message, { cause: args?.cause }) if (args) { Object.assign(this, args) } diff --git a/packages/effect/src/Duration.ts b/packages/effect/src/Duration.ts index 2a64a491f7..0203f384c9 100644 --- a/packages/effect/src/Duration.ts +++ b/packages/effect/src/Duration.ts @@ -200,6 +200,24 @@ export const isDuration = (u: unknown): u is Duration => hasProperty(u, TypeId) */ export const isFinite = (self: Duration): boolean => self.value._tag !== "Infinity" +/** + * @since 3.5.0 + * @category guards + */ +export const isZero = (self: Duration): boolean => { + switch (self.value._tag) { + case "Millis": { + return self.value.millis === 0 + } + case "Nanos": { + return self.value.nanos === bigint0 + } + case "Infinity": { + return false + } + } +} + /** * @since 2.0.0 * @category constructors diff --git a/packages/effect/src/Logger.ts b/packages/effect/src/Logger.ts index 3ccacd7b6b..8690529e22 100644 --- a/packages/effect/src/Logger.ts +++ b/packages/effect/src/Logger.ts @@ -362,6 +362,19 @@ export const logfmtLogger: Logger = internal.logfmtLogger */ export const stringLogger: Logger = internal.stringLogger +/** + * @since 3.5.0 + * @category constructors + */ +export const prettyLogger: ( + options?: { + readonly colors?: "auto" | boolean | undefined + readonly stderr?: boolean | undefined + readonly formatDate?: ((date: Date) => string) | undefined + readonly mode?: "browser" | "tty" | "auto" | undefined + } +) => Logger = internal.prettyLogger + /** * @since 2.0.0 * @category constructors @@ -397,6 +410,12 @@ export const json: Layer.Layer = replace(fiberRuntime.defaultLogger, fibe */ export const logFmt: Layer.Layer = replace(fiberRuntime.defaultLogger, fiberRuntime.logFmtLogger) +/** + * @since 3.5.0 + * @category constructors + */ +export const pretty: Layer.Layer = replace(fiberRuntime.defaultLogger, fiberRuntime.prettyLogger) + /** * @since 2.0.0 * @category constructors diff --git a/packages/effect/src/PubSub.ts b/packages/effect/src/PubSub.ts index 5f586f7968..581e80b06c 100644 --- a/packages/effect/src/PubSub.ts +++ b/packages/effect/src/PubSub.ts @@ -46,7 +46,9 @@ export interface PubSub extends Queue.Enqueue, Pipeable { * @since 2.0.0 * @category constructors */ -export const bounded: (requestedCapacity: number) => Effect.Effect> = internal.bounded +export const bounded: ( + capacity: number | { readonly capacity: number; readonly replay?: number | undefined } +) => Effect.Effect> = internal.bounded /** * Creates a bounded `PubSub` with the dropping strategy. The `PubSub` will drop new @@ -57,7 +59,9 @@ export const bounded: (requestedCapacity: number) => Effect.Effect> * @since 2.0.0 * @category constructors */ -export const dropping: (requestedCapacity: number) => Effect.Effect> = internal.dropping +export const dropping: ( + capacity: number | { readonly capacity: number; readonly replay?: number | undefined } +) => Effect.Effect> = internal.dropping /** * Creates a bounded `PubSub` with the sliding strategy. The `PubSub` will add new @@ -68,7 +72,9 @@ export const dropping: (requestedCapacity: number) => Effect.Effect * @since 2.0.0 * @category constructors */ -export const sliding: (requestedCapacity: number) => Effect.Effect> = internal.sliding +export const sliding: ( + capacity: number | { readonly capacity: number; readonly replay?: number | undefined } +) => Effect.Effect> = internal.sliding /** * Creates an unbounded `PubSub`. @@ -76,7 +82,8 @@ export const sliding: (requestedCapacity: number) => Effect.Effect> * @since 2.0.0 * @category constructors */ -export const unbounded: () => Effect.Effect> = internal.unbounded +export const unbounded: (options?: { readonly replay?: number | undefined }) => Effect.Effect> = + internal.unbounded /** * Returns the number of elements the queue can hold. diff --git a/packages/effect/src/Random.ts b/packages/effect/src/Random.ts index 48af9ac255..b312160149 100644 --- a/packages/effect/src/Random.ts +++ b/packages/effect/src/Random.ts @@ -118,3 +118,12 @@ export const randomWith: (f: (random: Random) => Effect.Effect * @category context */ export const Random: Context.Tag = internal.randomTag + +/** + * Constructs the `Random` service, seeding the pseudo-random number generator + * with an hash of the specified seed. + * + * @since 3.5.0 + * @category constructors + */ +export const make: (seed: A) => Random = internal.make diff --git a/packages/effect/src/RcMap.ts b/packages/effect/src/RcMap.ts new file mode 100644 index 0000000000..54f0170da7 --- /dev/null +++ b/packages/effect/src/RcMap.ts @@ -0,0 +1,103 @@ +/** + * @since 3.5.0 + */ +import type * as Cause from "./Cause.js" +import type * as Duration from "./Duration.js" +import type * as Effect from "./Effect.js" +import * as internal from "./internal/rcMap.js" +import { type Pipeable } from "./Pipeable.js" +import type * as Scope from "./Scope.js" +import type * as Types from "./Types.js" + +/** + * @since 3.5.0 + * @category type ids + */ +export const TypeId: unique symbol = internal.TypeId + +/** + * @since 3.5.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 3.5.0 + * @category models + */ +export interface RcMap extends Pipeable { + readonly [TypeId]: RcMap.Variance +} + +/** + * @since 3.5.0 + * @category models + */ +export declare namespace RcMap { + /** + * @since 3.5.0 + * @category models + */ + export interface Variance { + readonly _K: Types.Contravariant + readonly _A: Types.Covariant + readonly _E: Types.Covariant + } +} + +/** + * An `RcMap` can contain multiple reference counted resources that can be indexed + * by a key. The resources are lazily acquired on the first call to `get` and + * released when the last reference is released. + * + * Complex keys can extend `Equal` and `Hash` to allow lookups by value. + * + * @since 3.5.0 + * @category models + * @param capacity The maximum number of resources that can be held in the map. + * @param idleTimeToLive When the reference count reaches zero, the resource will be released after this duration. + * @example + * import { Effect, RcMap } from "effect" + * + * Effect.gen(function*() { + * const map = yield* RcMap.make({ + * lookup: (key: string) => + * Effect.acquireRelease( + * Effect.succeed(`acquired ${key}`), + * () => Effect.log(`releasing ${key}`) + * ) + * }) + * + * // Get "foo" from the map twice, which will only acquire it once. + * // It will then be released once the scope closes. + * yield* RcMap.get(map, "foo").pipe( + * Effect.andThen(RcMap.get(map, "foo")), + * Effect.scoped + * ) + * }) + */ +export const make: { + ( + options: { + readonly lookup: (key: K) => Effect.Effect + readonly idleTimeToLive?: Duration.DurationInput | undefined + readonly capacity?: undefined + } + ): Effect.Effect, never, Scope.Scope | R> + ( + options: { + readonly lookup: (key: K) => Effect.Effect + readonly idleTimeToLive?: Duration.DurationInput | undefined + readonly capacity: number + } + ): Effect.Effect, never, Scope.Scope | R> +} = internal.make + +/** + * @since 3.5.0 + * @category combinators + */ +export const get: { + (key: K): (self: RcMap) => Effect.Effect + (self: RcMap, key: K): Effect.Effect +} = internal.get diff --git a/packages/effect/src/RcRef.ts b/packages/effect/src/RcRef.ts new file mode 100644 index 0000000000..bbdf780a5e --- /dev/null +++ b/packages/effect/src/RcRef.ts @@ -0,0 +1,91 @@ +/** + * @since 3.5.0 + */ +import type * as Duration from "./Duration.js" +import type * as Effect from "./Effect.js" +import * as internal from "./internal/rcRef.js" +import { type Pipeable } from "./Pipeable.js" +import type * as Scope from "./Scope.js" +import type * as Types from "./Types.js" + +/** + * @since 3.5.0 + * @category type ids + */ +export const TypeId: unique symbol = internal.TypeId + +/** + * @since 3.5.0 + * @category type ids + */ +export type TypeId = typeof TypeId + +/** + * @since 3.5.0 + * @category models + */ +export interface RcRef extends Pipeable { + readonly [TypeId]: RcRef.Variance +} + +/** + * @since 3.5.0 + * @category models + */ +export declare namespace RcRef { + /** + * @since 3.5.0 + * @category models + */ + export interface Variance { + readonly _A: Types.Covariant + readonly _E: Types.Covariant + } +} + +/** + * Create an `RcRef` from an acquire `Effect`. + * + * An RcRef wraps a reference counted resource that can be acquired and released + * multiple times. + * + * The resource is lazily acquired on the first call to `get` and released when + * the last reference is released. + * + * @since 3.5.0 + * @category constructors + * @example + * import { Effect, RcRef } from "effect" + * + * Effect.gen(function*() { + * const ref = yield* RcRef.make({ + * acquire: Effect.acquireRelease( + * Effect.succeed("foo"), + * () => Effect.log("release foo") + * ) + * }) + * + * // will only acquire the resource once, and release it + * // when the scope is closed + * yield* RcRef.get(ref).pipe( + * Effect.andThen(RcRef.get(ref)), + * Effect.scoped + * ) + * }) + */ +export const make: ( + options: { + readonly acquire: Effect.Effect + /** + * When the reference count reaches zero, the resource will be released + * after this duration. + */ + readonly idleTimeToLive?: Duration.DurationInput | undefined + } +) => Effect.Effect, never, R | Scope.Scope> = internal.make + +/** + * @since 3.5.0 + * @category combinators + */ +export const get: (self: RcRef) => Effect.Effect = internal.get diff --git a/packages/effect/src/Stream.ts b/packages/effect/src/Stream.ts index de85861b94..ecb35c6d47 100644 --- a/packages/effect/src/Stream.ts +++ b/packages/effect/src/Stream.ts @@ -30,7 +30,7 @@ import type * as Emit from "./StreamEmit.js" import type * as HaltStrategy from "./StreamHaltStrategy.js" import type * as Take from "./Take.js" import type * as Tracer from "./Tracer.js" -import type { Covariant, NoInfer } from "./Types.js" +import type { Covariant, NoInfer, TupleOf } from "./Types.js" import type * as Unify from "./Unify.js" /** @@ -146,6 +146,7 @@ export declare namespace Stream { /** * @since 2.0.0 * @category models + * @deprecated use Types.TupleOf instead */ export type DynamicTuple = N extends N ? number extends N ? Array : DynamicTupleOf : never @@ -153,6 +154,7 @@ export declare namespace Stream { /** * @since 2.0.0 * @category models + * @deprecated use Types.TupleOf instead */ export type DynamicTupleOf> = R["length"] extends N ? R : DynamicTupleOf @@ -310,7 +312,10 @@ export const as: { const _async: ( register: (emit: Emit.Emit) => Effect.Effect | void, - outputBuffer?: number + bufferSize?: number | "unbounded" | { + readonly bufferSize?: number | undefined + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } | undefined ) => Stream = internal._async export { @@ -362,7 +367,10 @@ export { */ export const asyncEffect: ( register: (emit: Emit.Emit) => Effect.Effect, - outputBuffer?: number + bufferSize?: number | "unbounded" | { + readonly bufferSize?: number | undefined + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } | undefined ) => Stream = internal.asyncEffect /** @@ -376,7 +384,10 @@ export const asyncEffect: ( */ export const asyncScoped: ( register: (emit: Emit.Emit) => Effect.Effect, - outputBuffer?: number + bufferSize?: number | "unbounded" | { + readonly bufferSize?: number | undefined + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } | undefined ) => Stream> = internal.asyncScoped /** @@ -484,12 +495,12 @@ export const broadcast: { maximumLag: number ): ( self: Stream - ) => Effect.Effect, N>, never, Scope.Scope | R> + ) => Effect.Effect>, never, Scope.Scope | R> ( self: Stream, n: N, maximumLag: number - ): Effect.Effect, N>, never, Scope.Scope | R> + ): Effect.Effect>, never, Scope.Scope | R> } = internal.broadcast /** @@ -521,12 +532,12 @@ export const broadcastedQueues: { maximumLag: number ): ( self: Stream - ) => Effect.Effect>, N>, never, R | Scope.Scope> + ) => Effect.Effect>>, never, R | Scope.Scope> ( self: Stream, n: N, maximumLag: number - ): Effect.Effect>, N>, never, Scope.Scope | R> + ): Effect.Effect>>, never, Scope.Scope | R> } = internal.broadcastedQueues /** @@ -1139,7 +1150,7 @@ export const distributedWith: { } ): ( self: Stream - ) => Effect.Effect>>, N>, never, Scope.Scope | R> + ) => Effect.Effect>>>, never, Scope.Scope | R> ( self: Stream, options: { @@ -1147,7 +1158,7 @@ export const distributedWith: { readonly maximumLag: number readonly decide: (a: A) => Effect.Effect> } - ): Effect.Effect>>, N>, never, Scope.Scope | R> + ): Effect.Effect>>>, never, Scope.Scope | R> } = internal.distributedWith /** @@ -2843,8 +2854,8 @@ export const mergeEither: { * @category utils */ export const mergeLeft: { - (that: Stream): (self: Stream) => Stream - (self: Stream, that: Stream): Stream + (right: Stream): (left: Stream) => Stream + (left: Stream, right: Stream): Stream } = internal.mergeLeft /** @@ -2855,8 +2866,8 @@ export const mergeLeft: { * @category utils */ export const mergeRight: { - (that: Stream): (self: Stream) => Stream - (self: Stream, that: Stream): Stream + (right: Stream): (left: Stream) => Stream + (left: Stream, right: Stream): Stream } = internal.mergeRight /** @@ -3357,6 +3368,40 @@ export const provideSomeLayer: { ): Stream> } = internal.provideSomeLayer +/** + * Returns a stream that mirrors the first upstream to emit an item. + * As soon as one of the upstream emits a first value, all the others are interrupted. + * The resulting stream will forward all items from the "winning" source stream. + * Any upstream failures will cause the returned stream to fail. + * + * @example + * import { Stream, Schedule, Console, Effect } from "effect" + * + * const stream = Stream.raceAll( + * Stream.fromSchedule(Schedule.spaced('1 millis')), + * Stream.fromSchedule(Schedule.spaced('2 millis')), + * Stream.fromSchedule(Schedule.spaced('4 millis')), + * ).pipe(Stream.take(6), Stream.tap(Console.log)) + * + * Effect.runPromise(Stream.runDrain(stream)) + * // Output each millisecond from the first stream, the rest streams are interrupted + * // 0 + * // 1 + * // 2 + * // 3 + * // 4 + * // 5 + * @since 3.5.0 + * @category racing + */ +export const raceAll: >>( + ...streams: S +) => Stream< + Stream.Success, + Stream.Error, + Stream.Context +> = internal.raceAll + /** * Constructs a stream from a range of integers, including both endpoints. * @@ -3638,7 +3683,7 @@ export const run: { ( self: Stream, sink: Sink.Sink - ): Effect.Effect + ): Effect.Effect> } = internal.run /** @@ -3647,7 +3692,8 @@ export const run: { * @since 2.0.0 * @category destructors */ -export const runCollect: (self: Stream) => Effect.Effect, E, R> = internal.runCollect +export const runCollect: (self: Stream) => Effect.Effect, E, Exclude> = + internal.runCollect /** * Runs the stream and emits the number of elements processed @@ -3655,7 +3701,8 @@ export const runCollect: (self: Stream) => Effect.Effect(self: Stream) => Effect.Effect = internal.runCount +export const runCount: (self: Stream) => Effect.Effect> = + internal.runCount /** * Runs the stream only for its effects. The emitted elements are discarded. @@ -3663,7 +3710,8 @@ export const runCount: (self: Stream) => Effect.Effect(self: Stream) => Effect.Effect = internal.runDrain +export const runDrain: (self: Stream) => Effect.Effect> = + internal.runDrain /** * Executes a pure fold over the stream of values - reduces all elements in @@ -3673,8 +3721,8 @@ export const runDrain: (self: Stream) => Effect.Effect(s: S, f: (s: S, a: A) => S): (self: Stream) => Effect.Effect - (self: Stream, s: S, f: (s: S, a: A) => S): Effect.Effect + (s: S, f: (s: S, a: A) => S): (self: Stream) => Effect.Effect> + (self: Stream, s: S, f: (s: S, a: A) => S): Effect.Effect> } = internal.runFold /** @@ -3687,12 +3735,12 @@ export const runFoldEffect: { ( s: S, f: (s: S, a: A) => Effect.Effect - ): (self: Stream) => Effect.Effect + ): (self: Stream) => Effect.Effect> ( self: Stream, s: S, f: (s: S, a: A) => Effect.Effect - ): Effect.Effect + ): Effect.Effect> } = internal.runFoldEffect /** @@ -3734,8 +3782,17 @@ export const runFoldScopedEffect: { * @category destructors */ export const runFoldWhile: { - (s: S, cont: Predicate, f: (s: S, a: A) => S): (self: Stream) => Effect.Effect - (self: Stream, s: S, cont: Predicate, f: (s: S, a: A) => S): Effect.Effect + ( + s: S, + cont: Predicate, + f: (s: S, a: A) => S + ): (self: Stream) => Effect.Effect> + ( + self: Stream, + s: S, + cont: Predicate, + f: (s: S, a: A) => S + ): Effect.Effect> } = internal.runFoldWhile /** @@ -3750,13 +3807,13 @@ export const runFoldWhileEffect: { s: S, cont: Predicate, f: (s: S, a: A) => Effect.Effect - ): (self: Stream) => Effect.Effect + ): (self: Stream) => Effect.Effect> ( self: Stream, s: S, cont: Predicate, f: (s: S, a: A) => Effect.Effect - ): Effect.Effect + ): Effect.Effect> } = internal.runFoldWhileEffect /** diff --git a/packages/effect/src/index.ts b/packages/effect/src/index.ts index 2eb6e1273a..eb5ce51c2d 100644 --- a/packages/effect/src/index.ts +++ b/packages/effect/src/index.ts @@ -605,6 +605,16 @@ export * as Random from "./Random.js" */ export * as RateLimiter from "./RateLimiter.js" +/** + * @since 3.5.0 + */ +export * as RcMap from "./RcMap.js" + +/** + * @since 3.5.0 + */ +export * as RcRef from "./RcRef.js" + /** * @since 2.0.0 */ diff --git a/packages/effect/src/internal/cause.ts b/packages/effect/src/internal/cause.ts index 27b328667f..680a19e8c4 100644 --- a/packages/effect/src/internal/cause.ts +++ b/packages/effect/src/internal/cause.ts @@ -970,25 +970,46 @@ export const reduceWithContext = dual< // ----------------------------------------------------------------------------- /** @internal */ -export const pretty = (cause: Cause.Cause): string => { +export const pretty = (cause: Cause.Cause, options?: { + readonly renderErrorCause?: boolean | undefined +}): string => { if (isInterruptedOnly(cause)) { return "All fibers interrupted without errors." } - return prettyErrors(cause).map((e) => e.stack).join("\n") + return prettyErrors(cause).map(function(e) { + if (options?.renderErrorCause !== true || e.cause === undefined) { + return e.stack + } + return `${e.stack} {\n${renderErrorCause(e.cause as PrettyError, " ")}\n}` + }).join("\n") +} + +const renderErrorCause = (cause: PrettyError, prefix: string) => { + const lines = cause.stack!.split("\n") + let stack = `${prefix}[cause]: ${lines[0]}` + for (let i = 1, len = lines.length; i < len; i++) { + stack += `\n${prefix}${lines[i]}` + } + return stack } class PrettyError extends globalThis.Error implements Cause.PrettyError { span: undefined | Span = undefined constructor(originalError: unknown) { + const originalErrorIsObject = typeof originalError === "object" && originalError !== null const prevLimit = Error.stackTraceLimit - Error.stackTraceLimit = 0 - super(prettyErrorMessage(originalError)) + Error.stackTraceLimit = 1 + super(prettyErrorMessage(originalError), { + cause: originalErrorIsObject && "cause" in originalError && typeof originalError.cause !== "undefined" + ? new PrettyError(originalError.cause) + : undefined + }) if (this.message === "") { this.message = "An error has occurred" } Error.stackTraceLimit = prevLimit this.name = originalError instanceof Error ? originalError.name : "Error" - if (typeof originalError === "object" && originalError !== null) { + if (originalErrorIsObject) { if (spanSymbol in originalError) { this.span = originalError[spanSymbol] as Span } diff --git a/packages/effect/src/internal/channel.ts b/packages/effect/src/internal/channel.ts index f5039f4c2a..ecaf38072c 100644 --- a/packages/effect/src/internal/channel.ts +++ b/packages/effect/src/internal/channel.ts @@ -2052,17 +2052,18 @@ export const repeated = ( /** @internal */ export const run = ( self: Channel.Channel -): Effect.Effect => Effect.scoped(executor.runScoped(self)) +): Effect.Effect> => Effect.scoped(executor.runScoped(self)) /** @internal */ export const runCollect = ( self: Channel.Channel -): Effect.Effect<[Chunk.Chunk, OutDone], OutErr, Env> => executor.run(core.collectElements(self)) +): Effect.Effect<[Chunk.Chunk, OutDone], OutErr, Exclude> => + executor.run(core.collectElements(self)) /** @internal */ export const runDrain = ( self: Channel.Channel -): Effect.Effect => executor.run(drain(self)) +): Effect.Effect> => executor.run(drain(self)) /** @internal */ export const scoped = ( diff --git a/packages/effect/src/internal/channel/channelExecutor.ts b/packages/effect/src/internal/channel/channelExecutor.ts index 64b5bfb83c..fa1cc76dd7 100644 --- a/packages/effect/src/internal/channel/channelExecutor.ts +++ b/packages/effect/src/internal/channel/channelExecutor.ts @@ -1095,7 +1095,7 @@ export const readUpstream = ( /** @internal */ export const run = ( self: Channel.Channel -): Effect.Effect => pipe(runScoped(self), Effect.scoped) +): Effect.Effect> => pipe(runScoped(self), Effect.scoped) /** @internal */ export const runScoped = ( diff --git a/packages/effect/src/internal/core-effect.ts b/packages/effect/src/internal/core-effect.ts index d63958e7e1..577191cd72 100644 --- a/packages/effect/src/internal/core-effect.ts +++ b/packages/effect/src/internal/core-effect.ts @@ -911,11 +911,6 @@ export const logWithLevel = (level?: LogLevel.LogLevel) => i-- } } - if (message.length === 0) { - message = "" as any - } else if (message.length === 1) { - message = message[0] - } if (cause === undefined) { cause = internalCause.empty } diff --git a/packages/effect/src/internal/core.ts b/packages/effect/src/internal/core.ts index f40a8c089d..2385a82524 100644 --- a/packages/effect/src/internal/core.ts +++ b/packages/effect/src/internal/core.ts @@ -2174,23 +2174,19 @@ export const causeSquashWith = dual< // ----------------------------------------------------------------------------- /** @internal */ -export const YieldableError: new(message?: string) => Cause.YieldableError = (function() { +export const YieldableError: new(message?: string, options?: ErrorOptions) => Cause.YieldableError = (function() { class YieldableError extends globalThis.Error { commit() { return fail(this) } - toString() { - return this.message ? `${this.name}: ${this.message}` : this.name - } toJSON() { return { ...this } } - [NodeInspectSymbol](): string { - const stack = this.stack - if (stack) { - return `${this.toString()}\n${stack.split("\n").slice(1).join("\n")}` + [NodeInspectSymbol]() { + if (this.toString !== globalThis.Error.prototype.toString) { + return this.stack ? `${this.toString()}\n${this.stack.split("\n").slice(1).join("\n")}` : this.toString() } - return this.toString() + return this } } Object.assign(YieldableError.prototype, StructuralCommitPrototype) @@ -2274,6 +2270,20 @@ export const InvalidPubSubCapacityException = makeException({ + [ExceededCapacityExceptionTypeId]: ExceededCapacityExceptionTypeId +}, "ExceededCapacityException") + +/** @internal */ +export const isExceededCapacityException = (u: unknown): u is Cause.ExceededCapacityException => + hasProperty(u, ExceededCapacityExceptionTypeId) + /** @internal */ export const isInvalidCapacityError = (u: unknown): u is Cause.InvalidPubSubCapacityException => hasProperty(u, InvalidPubSubCapacityExceptionTypeId) diff --git a/packages/effect/src/internal/defaultServices.ts b/packages/effect/src/internal/defaultServices.ts index bcc2ccda98..f8c6ade906 100644 --- a/packages/effect/src/internal/defaultServices.ts +++ b/packages/effect/src/internal/defaultServices.ts @@ -22,7 +22,7 @@ export const liveServices: Context.Context = pi Context.empty(), Context.add(clock.clockTag, clock.make()), Context.add(console_.consoleTag, console_.defaultConsole), - Context.add(random.randomTag, random.make((Math.random() * 4294967296) >>> 0)), + Context.add(random.randomTag, random.make(Math.random())), Context.add(configProvider.configProviderTag, configProvider.fromEnv()), Context.add(tracer.tracerTag, tracer.nativeTracer) ) diff --git a/packages/effect/src/internal/fiberRuntime.ts b/packages/effect/src/internal/fiberRuntime.ts index 096fb9c54c..ace835a650 100644 --- a/packages/effect/src/internal/fiberRuntime.ts +++ b/packages/effect/src/internal/fiberRuntime.ts @@ -1421,6 +1421,12 @@ export const logFmtLogger: Logger = globalValue( () => loggerWithConsoleLog(internalLogger.logfmtLogger) ) +/** @internal */ +export const prettyLogger: Logger = globalValue( + Symbol.for("effect/Logger/prettyLogger"), + () => internalLogger.prettyLogger() +) + /** @internal */ export const structuredLogger: Logger = globalValue( Symbol.for("effect/Logger/structuredLogger"), diff --git a/packages/effect/src/internal/logger.ts b/packages/effect/src/internal/logger.ts index 21205c04d9..3404359712 100644 --- a/packages/effect/src/internal/logger.ts +++ b/packages/effect/src/internal/logger.ts @@ -1,3 +1,6 @@ +import * as Arr from "../Array.js" +import * as Context from "../Context.js" +import * as FiberRefs from "../FiberRefs.js" import type { LazyArg } from "../Function.js" import { constVoid, dual, pipe } from "../Function.js" import * as HashMap from "../HashMap.js" @@ -9,6 +12,8 @@ import * as LogSpan from "../LogSpan.js" import * as Option from "../Option.js" import { pipeArguments } from "../Pipeable.js" import * as Cause from "./cause.js" +import * as defaultServices from "./defaultServices.js" +import { consoleTag } from "./defaultServices/console.js" import * as _fiberId from "./fiberId.js" /** @internal */ @@ -157,7 +162,7 @@ export const zipRight = dual< >(2, (self, that) => map(zip(self, that), (tuple) => tuple[1])) /** @internal */ -export const stringLogger: Logger.Logger = makeLogger( +export const stringLogger: Logger.Logger = makeLogger( ({ annotations, cause, date, fiberId, logLevel, message, spans }) => { const nowMillis = date.getTime() @@ -169,16 +174,9 @@ export const stringLogger: Logger.Logger = makeLogger 0) { - output = output + " message=" - output = appendQuoted(stringMessage, output) - } - } - } else { - const stringMessage = Inspectable.toStringUnknown(message) + const messageArr = Arr.ensure(message) + for (let i = 0; i < messageArr.length; i++) { + const stringMessage = Inspectable.toStringUnknown(messageArr[i]) if (stringMessage.length > 0) { output = output + " message=" output = appendQuoted(stringMessage, output) @@ -187,7 +185,7 @@ export const stringLogger: Logger.Logger = makeLogger( let output = outputArray.join(" ") - if (Array.isArray(message)) { - for (let i = 0; i < message.length; i++) { - const stringMessage = Inspectable.toStringUnknown(message[i], 0) - if (stringMessage.length > 0) { - output = output + " message=" - output = appendQuotedLogfmt(stringMessage, output) - } - } - } else { - const stringMessage = Inspectable.toStringUnknown(message, 0) + const messageArr = Arr.ensure(message) + for (let i = 0; i < messageArr.length; i++) { + const stringMessage = Inspectable.toStringUnknown(messageArr[i], 0) if (stringMessage.length > 0) { output = output + " message=" output = appendQuotedLogfmt(stringMessage, output) @@ -264,7 +255,7 @@ export const logfmtLogger = makeLogger( if (cause != null && cause._tag !== "Empty") { output = output + " cause=" - output = appendQuotedLogfmt(Cause.pretty(cause), output) + output = appendQuotedLogfmt(Cause.pretty(cause, { renderErrorCause: true }), output) } if (List.isCons(spans)) { @@ -281,7 +272,7 @@ export const logfmtLogger = makeLogger( } } - if (pipe(annotations, HashMap.size) > 0) { + if (HashMap.size(annotations) > 0) { output = output + " " let first = true @@ -328,11 +319,12 @@ export const structuredLogger = makeLogger (self: LogSpan.LogSpan): string => export const isLogger = (u: unknown): u is Logger.Logger => { return typeof u === "object" && u != null && LoggerTypeId in u } + +const processStdoutIsTTY = typeof process === "object" && "stdout" in process && process.stdout.isTTY === true +const hasWindow = typeof window === "object" + +const withColor = (text: string, ...colors: ReadonlyArray) => { + let out = "" + for (let i = 0; i < colors.length; i++) { + out += `\x1b[${colors[i]}m` + } + return out + text + "\x1b[0m" +} +const withColorNoop = (text: string, ..._colors: ReadonlyArray) => text +const colors = { + bold: "1", + red: "31", + green: "32", + yellow: "33", + blue: "34", + cyan: "36", + white: "37", + gray: "90", + black: "30", + bgBrightRed: "101" +} as const + +const logLevelColors: Record> = { + None: [], + All: [], + Trace: [colors.gray], + Debug: [colors.blue], + Info: [colors.green], + Warning: [colors.yellow], + Error: [colors.red], + Fatal: [colors.bgBrightRed, colors.black] +} + +const defaultDateFormat = (date: Date): string => + `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}:${ + date.getSeconds().toString().padStart(2, "0") + }.${date.getMilliseconds().toString().padStart(3, "0")}` + +/** @internal */ +export const prettyLogger = (options?: { + readonly colors?: "auto" | boolean | undefined + readonly stderr?: boolean | undefined + readonly formatDate?: ((date: Date) => string) | undefined + readonly mode?: "browser" | "tty" | "auto" | undefined +}) => { + const mode_ = options?.mode ?? "auto" + const mode = mode_ === "auto" ? (hasWindow ? "browser" : "tty") : mode_ + const isBrowser = mode === "browser" + const showColors = typeof options?.colors === "boolean" ? options.colors : processStdoutIsTTY || isBrowser + const color = showColors ? withColor : withColorNoop + const formatDate = options?.formatDate ?? defaultDateFormat + + return makeLogger( + ({ annotations, cause, context, date, fiberId, logLevel, message: message_, spans }) => { + const services = FiberRefs.getOrDefault(context, defaultServices.currentServices) + const console = Context.get(services, consoleTag).unsafe + const log = options?.stderr === true ? console.error : console.log + + const message = Arr.ensure(message_) + + let firstLine = color(`[${formatDate(date)}]`, colors.white) + + ` ${color(logLevel.label, ...logLevelColors[logLevel._tag])}` + + ` (${_fiberId.threadName(fiberId)})` + + if (List.isCons(spans)) { + const now = date.getTime() + const render = renderLogSpanLogfmt(now) + for (const span of spans) { + firstLine += " " + render(span) + } + } + + firstLine += ":" + let messageIndex = 0 + if (message.length > 0) { + const firstMaybeString = structuredMessage(message[0]) + if (typeof firstMaybeString === "string") { + firstLine += " " + color(firstMaybeString, colors.bold, colors.cyan) + messageIndex++ + } + } + + if (isBrowser) { + console.groupCollapsed(firstLine) + } else { + log(firstLine) + console.group() + } + if (!Cause.isEmpty(cause)) { + if (isBrowser) { + console.error(Cause.pretty(cause, { renderErrorCause: true })) + } else { + log(Cause.pretty(cause, { renderErrorCause: true })) + } + } + + if (messageIndex < message.length) { + for (; messageIndex < message.length; messageIndex++) { + log(message[messageIndex]) + } + } + + if (HashMap.size(annotations) > 0) { + for (const [key, value] of annotations) { + log(color(`${key}:`, colors.bold, colors.white), value) + } + } + console.groupEnd() + } + ) +} diff --git a/packages/effect/src/internal/pubsub.ts b/packages/effect/src/internal/pubsub.ts index 9d39dc0404..46275a878b 100644 --- a/packages/effect/src/internal/pubsub.ts +++ b/packages/effect/src/internal/pubsub.ts @@ -28,6 +28,7 @@ export interface AtomicPubSub { publishAll(elements: Iterable): Chunk.Chunk slide(): void subscribe(): Subscription + replayWindow(): ReplayWindow } /** @internal */ @@ -73,32 +74,49 @@ const removeSubscribers = ( } /** @internal */ -export const bounded = (requestedCapacity: number): Effect.Effect> => - pipe( - core.sync(() => makeBoundedPubSub(requestedCapacity)), - core.flatMap((atomicPubSub) => makePubSub(atomicPubSub, new BackPressureStrategy())) - ) +export const bounded = ( + capacity: number | { + readonly capacity: number + readonly replay?: number | undefined + } +): Effect.Effect> => + core.suspend(() => { + const pubsub = makeBoundedPubSub(capacity) + return makePubSub(pubsub, new BackPressureStrategy()) + }) /** @internal */ -export const dropping = (requestedCapacity: number): Effect.Effect> => - pipe( - core.sync(() => makeBoundedPubSub(requestedCapacity)), - core.flatMap((atomicPubSub) => makePubSub(atomicPubSub, new DroppingStrategy())) - ) +export const dropping = ( + capacity: number | { + readonly capacity: number + readonly replay?: number | undefined + } +): Effect.Effect> => + core.suspend(() => { + const pubsub = makeBoundedPubSub(capacity) + return makePubSub(pubsub, new DroppingStrategy()) + }) /** @internal */ -export const sliding = (requestedCapacity: number): Effect.Effect> => - pipe( - core.sync(() => makeBoundedPubSub(requestedCapacity)), - core.flatMap((atomicPubSub) => makePubSub(atomicPubSub, new SlidingStrategy())) - ) +export const sliding = ( + capacity: number | { + readonly capacity: number + readonly replay?: number | undefined + } +): Effect.Effect> => + core.suspend(() => { + const pubsub = makeBoundedPubSub(capacity) + return makePubSub(pubsub, new SlidingStrategy()) + }) /** @internal */ -export const unbounded = (): Effect.Effect> => - pipe( - core.sync(() => makeUnboundedPubSub()), - core.flatMap((atomicPubSub) => makePubSub(atomicPubSub, new DroppingStrategy())) - ) +export const unbounded = (options?: { + readonly replay?: number | undefined +}): Effect.Effect> => + core.suspend(() => { + const pubsub = makeUnboundedPubSub(options) + return makePubSub(pubsub, new DroppingStrategy()) + }) /** @internal */ export const capacity = (self: PubSub.PubSub): number => self.capacity() @@ -138,21 +156,28 @@ export const subscribe = (self: PubSub.PubSub): Effect.Effect(requestedCapacity: number): AtomicPubSub => { - ensureCapacity(requestedCapacity) - if (requestedCapacity === 1) { - return new BoundedPubSubSingle() - } else if (nextPow2(requestedCapacity) === requestedCapacity) { - return new BoundedPubSubPow2(requestedCapacity) +const makeBoundedPubSub = ( + capacity: number | { + readonly capacity: number + readonly replay?: number | undefined + } +): AtomicPubSub => { + const options = typeof capacity === "number" ? { capacity } : capacity + ensureCapacity(options.capacity) + const replayBuffer = options.replay && options.replay > 0 ? new ReplayBuffer(Math.ceil(options.replay)) : undefined + if (options.capacity === 1) { + return new BoundedPubSubSingle(replayBuffer) + } else if (nextPow2(options.capacity) === options.capacity) { + return new BoundedPubSubPow2(options.capacity, replayBuffer) } else { - return new BoundedPubSubArb(requestedCapacity) + return new BoundedPubSubArb(options.capacity, replayBuffer) } } /** @internal */ -const makeUnboundedPubSub = (): AtomicPubSub => { - return new UnboundedPubSub() -} +const makeUnboundedPubSub = (options?: { + readonly replay?: number | undefined +}): AtomicPubSub => new UnboundedPubSub(options?.replay ? new ReplayBuffer(options.replay) : undefined) /** @internal */ const makeSubscription = ( @@ -180,17 +205,17 @@ export const unsafeMakeSubscription = ( shutdownHook: Deferred.Deferred, shutdownFlag: MutableRef.MutableRef, strategy: PubSubStrategy -): Queue.Dequeue => { - return new SubscriptionImpl( +): Queue.Dequeue => + new SubscriptionImpl( pubsub, subscribers, subscription, pollers, shutdownHook, shutdownFlag, - strategy + strategy, + pubsub.replayWindow() ) -} /** @internal */ class BoundedPubSubArb implements AtomicPubSub { @@ -200,12 +225,13 @@ class BoundedPubSubArb implements AtomicPubSub { subscriberCount = 0 subscribersIndex = 0 - readonly capacity: number + constructor(readonly capacity: number, readonly replayBuffer: ReplayBuffer | undefined) { + this.array = Array.from({ length: capacity }) + this.subscribers = Array.from({ length: capacity }) + } - constructor(requestedCapacity: number) { - this.array = Array.from({ length: requestedCapacity }) - this.subscribers = Array.from({ length: requestedCapacity }) - this.capacity = requestedCapacity + replayWindow(): ReplayWindow { + return this.replayBuffer ? new ReplayWindowImpl(this.replayBuffer) : emptyReplayWindow } isEmpty(): boolean { @@ -230,11 +256,17 @@ class BoundedPubSubArb implements AtomicPubSub { this.subscribers[index] = this.subscriberCount this.publisherIndex += 1 } + if (this.replayBuffer) { + this.replayBuffer.offer(value) + } return true } publishAll(elements: Iterable): Chunk.Chunk { if (this.subscriberCount === 0) { + if (this.replayBuffer) { + this.replayBuffer.offerAll(elements) + } return Chunk.empty() } const chunk = Chunk.fromIterable(elements) @@ -253,6 +285,9 @@ class BoundedPubSubArb implements AtomicPubSub { this.array[index] = a this.subscribers[index] = this.subscriberCount this.publisherIndex += 1 + if (this.replayBuffer) { + this.replayBuffer.offer(a) + } } return Chunk.drop(chunk, iteratorIndex) } @@ -264,6 +299,9 @@ class BoundedPubSubArb implements AtomicPubSub { this.subscribers[index] = 0 this.subscribersIndex += 1 } + if (this.replayBuffer) { + this.replayBuffer.slide() + } } subscribe(): Subscription { @@ -368,13 +406,14 @@ class BoundedPubSubPow2 implements AtomicPubSub { subscriberCount = 0 subscribersIndex = 0 - readonly capacity: number + constructor(readonly capacity: number, readonly replayBuffer: ReplayBuffer | undefined) { + this.array = Array.from({ length: capacity }) + this.mask = capacity - 1 + this.subscribers = Array.from({ length: capacity }) + } - constructor(requestedCapacity: number) { - this.array = Array.from({ length: requestedCapacity }) - this.mask = requestedCapacity - 1 - this.subscribers = Array.from({ length: requestedCapacity }) - this.capacity = requestedCapacity + replayWindow(): ReplayWindow { + return this.replayBuffer ? new ReplayWindowImpl(this.replayBuffer) : emptyReplayWindow } isEmpty(): boolean { @@ -399,11 +438,17 @@ class BoundedPubSubPow2 implements AtomicPubSub { this.subscribers[index] = this.subscriberCount this.publisherIndex += 1 } + if (this.replayBuffer) { + this.replayBuffer.offer(value) + } return true } publishAll(elements: Iterable): Chunk.Chunk { if (this.subscriberCount === 0) { + if (this.replayBuffer) { + this.replayBuffer.offerAll(elements) + } return Chunk.empty() } const chunk = Chunk.fromIterable(elements) @@ -422,6 +467,9 @@ class BoundedPubSubPow2 implements AtomicPubSub { this.array[index] = elem this.subscribers[index] = this.subscriberCount this.publisherIndex += 1 + if (this.replayBuffer) { + this.replayBuffer.offer(elem) + } } return Chunk.drop(chunk, iteratorIndex) } @@ -433,6 +481,9 @@ class BoundedPubSubPow2 implements AtomicPubSub { this.subscribers[index] = 0 this.subscribersIndex += 1 } + if (this.replayBuffer) { + this.replayBuffer.slide() + } } subscribe(): Subscription { @@ -536,6 +587,11 @@ class BoundedPubSubSingle implements AtomicPubSub { value: A = AbsentValue as unknown as A readonly capacity = 1 + constructor(readonly replayBuffer: ReplayBuffer | undefined) {} + + replayWindow(): ReplayWindow { + return this.replayBuffer ? new ReplayWindowImpl(this.replayBuffer) : emptyReplayWindow + } pipe() { return pipeArguments(this, arguments) @@ -562,11 +618,17 @@ class BoundedPubSubSingle implements AtomicPubSub { this.subscribers = this.subscriberCount this.publisherIndex += 1 } + if (this.replayBuffer) { + this.replayBuffer.offer(value) + } return true } publishAll(elements: Iterable): Chunk.Chunk { if (this.subscriberCount === 0) { + if (this.replayBuffer) { + this.replayBuffer.offerAll(elements) + } return Chunk.empty() } const chunk = Chunk.fromIterable(elements) @@ -585,6 +647,9 @@ class BoundedPubSubSingle implements AtomicPubSub { this.subscribers = 0 this.value = AbsentValue as unknown as A } + if (this.replayBuffer) { + this.replayBuffer.slide() + } } subscribe(): Subscription { @@ -673,6 +738,11 @@ class UnboundedPubSub implements AtomicPubSub { subscribersIndex = 0 readonly capacity = Number.MAX_SAFE_INTEGER + constructor(readonly replayBuffer: ReplayBuffer | undefined) {} + + replayWindow(): ReplayWindow { + return this.replayBuffer ? new ReplayWindowImpl(this.replayBuffer) : emptyReplayWindow + } isEmpty(): boolean { return this.publisherHead === this.publisherTail @@ -697,6 +767,9 @@ class UnboundedPubSub implements AtomicPubSub { this.publisherTail = this.publisherTail.next this.publisherIndex += 1 } + if (this.replayBuffer) { + this.replayBuffer.offer(value) + } return true } @@ -705,6 +778,8 @@ class UnboundedPubSub implements AtomicPubSub { for (const a of elements) { this.publish(a) } + } else if (this.replayBuffer) { + this.replayBuffer.offerAll(elements) } return Chunk.empty() } @@ -715,6 +790,9 @@ class UnboundedPubSub implements AtomicPubSub { this.publisherHead.value = AbsentValue this.subscribersIndex += 1 } + if (this.replayBuffer) { + this.replayBuffer.slide() + } } subscribe(): Subscription { @@ -841,9 +919,9 @@ class SubscriptionImpl implements Queue.Dequeue { readonly pollers: MutableQueue.MutableQueue>, readonly shutdownHook: Deferred.Deferred, readonly shutdownFlag: MutableRef.MutableRef, - readonly strategy: PubSubStrategy - ) { - } + readonly strategy: PubSubStrategy, + readonly replayWindow: ReplayWindow + ) {} pipe() { return pipeArguments(this, arguments) @@ -861,7 +939,7 @@ class SubscriptionImpl implements Queue.Dequeue { return core.suspend(() => MutableRef.get(this.shutdownFlag) ? core.interrupt - : core.succeed(this.subscription.size()) + : core.succeed(this.subscription.size() + this.replayWindow.remaining) ) } @@ -869,11 +947,15 @@ class SubscriptionImpl implements Queue.Dequeue { if (MutableRef.get(this.shutdownFlag)) { return Option.none() } - return Option.some(this.subscription.size()) + return Option.some(this.subscription.size() + this.replayWindow.remaining) } get isFull(): Effect.Effect { - return core.map(this.size, (size) => size === this.capacity()) + return core.suspend(() => + MutableRef.get(this.shutdownFlag) + ? core.interrupt + : core.succeed(this.subscription.size() === this.capacity()) + ) } get isEmpty(): Effect.Effect { @@ -915,6 +997,10 @@ class SubscriptionImpl implements Queue.Dequeue { if (MutableRef.get(this.shutdownFlag)) { return core.interrupt } + if (this.replayWindow.remaining > 0) { + const message = this.replayWindow.take()! + return core.succeed(message) + } const message = MutableQueue.isEmpty(this.pollers) ? this.subscription.poll(MutableQueue.EmptyMutableQueue) : MutableQueue.EmptyMutableQueue @@ -950,6 +1036,9 @@ class SubscriptionImpl implements Queue.Dequeue { ? unsafePollAllSubscription(this.subscription) : Chunk.empty() this.strategy.unsafeOnPubSubEmptySpace(this.pubsub, this.subscribers) + if (this.replayWindow.remaining > 0) { + return core.succeed(Chunk.appendAll(this.replayWindow.takeAll(), as)) + } return core.succeed(as) }) } @@ -959,11 +1048,19 @@ class SubscriptionImpl implements Queue.Dequeue { if (MutableRef.get(this.shutdownFlag)) { return core.interrupt } + let replay: Chunk.Chunk | undefined = undefined + if (this.replayWindow.remaining >= max) { + const as = this.replayWindow.takeN(max) + return core.succeed(as) + } else if (this.replayWindow.remaining > 0) { + replay = this.replayWindow.takeAll() + max = max - replay.length + } const as = MutableQueue.isEmpty(this.pollers) ? unsafePollN(this.subscription, max) : Chunk.empty() this.strategy.unsafeOnPubSubEmptySpace(this.pubsub, this.subscribers) - return core.succeed(as) + return replay ? core.succeed(Chunk.appendAll(replay, as)) : core.succeed(as) }) } @@ -1019,8 +1116,7 @@ class PubSubImpl implements PubSub.PubSub { readonly shutdownHook: Deferred.Deferred, readonly shutdownFlag: MutableRef.MutableRef, readonly strategy: PubSubStrategy - ) { - } + ) {} capacity(): number { return this.pubsub.capacity @@ -1075,7 +1171,7 @@ class PubSubImpl implements PubSub.PubSub { return core.interrupt } - if ((this.pubsub as AtomicPubSub).publish(value)) { + if (this.pubsub.publish(value)) { this.strategy.unsafeCompleteSubscribers(this.pubsub, this.subscribers) return core.succeed(true) } @@ -1179,9 +1275,7 @@ export const unsafeMakePubSub = ( shutdownHook: Deferred.Deferred, shutdownFlag: MutableRef.MutableRef, strategy: PubSubStrategy -): PubSub.PubSub => { - return new PubSubImpl(pubsub, subscribers, scope, shutdownHook, shutdownFlag, strategy) -} +): PubSub.PubSub => new PubSubImpl(pubsub, subscribers, scope, shutdownHook, shutdownFlag, strategy) /** @internal */ const ensureCapacity = (capacity: number): void => { @@ -1562,3 +1656,100 @@ const unsafeStrategyCompleteSubscribers = ( } } } + +interface ReplayNode { + value: A | AbsentValue + next: ReplayNode | null +} + +class ReplayBuffer { + constructor(readonly capacity: number) {} + + head: ReplayNode = { value: AbsentValue, next: null } + tail: ReplayNode = this.head + size = 0 + index = 0 + + slide() { + this.index++ + } + offer(a: A): void { + this.tail.value = a + this.tail.next = { + value: AbsentValue, + next: null + } + this.tail = this.tail.next + if (this.size === this.capacity) { + this.head = this.head.next! + } else { + this.size += 1 + } + } + offerAll(as: Iterable): void { + for (const a of as) { + this.offer(a) + } + } +} + +interface ReplayWindow { + take(): A | undefined + takeN(n: number): Chunk.Chunk + takeAll(): Chunk.Chunk + readonly remaining: number +} + +class ReplayWindowImpl implements ReplayWindow { + head: ReplayNode + index: number + remaining: number + constructor(readonly buffer: ReplayBuffer) { + this.index = buffer.index + this.remaining = buffer.size + this.head = buffer.head + } + fastForward() { + while (this.index < this.buffer.index) { + this.head = this.head.next! + this.index++ + } + } + take(): A | undefined { + if (this.remaining === 0) { + return undefined + } else if (this.index < this.buffer.index) { + this.fastForward() + } + this.remaining-- + const value = this.head.value + this.head = this.head.next! + return value as A + } + takeN(n: number): Chunk.Chunk { + if (this.remaining === 0) { + return Chunk.empty() + } else if (this.index < this.buffer.index) { + this.fastForward() + } + const len = Math.min(n, this.remaining) + const items = new Array(len) + for (let i = 0; i < len; i++) { + const value = this.head.value as A + this.head = this.head.next! + items[i] = value + } + this.remaining -= len + return Chunk.unsafeFromArray(items) + } + takeAll(): Chunk.Chunk { + return this.takeN(this.remaining) + } +} + +const emptyReplayWindow: ReplayWindow = { + remaining: 0, + take: () => undefined, + takeN: () => Chunk.empty(), + takeAll: () => Chunk.empty() +} diff --git a/packages/effect/src/internal/random.ts b/packages/effect/src/internal/random.ts index 609f2d0417..bb831d7ae1 100644 --- a/packages/effect/src/internal/random.ts +++ b/packages/effect/src/internal/random.ts @@ -2,6 +2,7 @@ import * as Chunk from "../Chunk.js" import * as Context from "../Context.js" import type * as Effect from "../Effect.js" import { pipe } from "../Function.js" +import * as Hash from "../Hash.js" import type * as Random from "../Random.js" import * as PCGRandom from "../Utils.js" import * as core from "./core.js" @@ -85,4 +86,4 @@ const swap = (buffer: Array, index1: number, index2: number): Array => return buffer } -export const make = (seed: number): Random.Random => new RandomImpl(seed) +export const make = (seed: A): Random.Random => new RandomImpl(Hash.hash(seed)) diff --git a/packages/effect/src/internal/rcMap.ts b/packages/effect/src/internal/rcMap.ts new file mode 100644 index 0000000000..566d8e9476 --- /dev/null +++ b/packages/effect/src/internal/rcMap.ts @@ -0,0 +1,213 @@ +import type * as Cause from "../Cause.js" +import * as Context from "../Context.js" +import type * as Deferred from "../Deferred.js" +import * as Duration from "../Duration.js" +import type { Effect } from "../Effect.js" +import type { RuntimeFiber } from "../Fiber.js" +import { dual, identity } from "../Function.js" +import * as MutableHashMap from "../MutableHashMap.js" +import { pipeArguments } from "../Pipeable.js" +import type * as RcMap from "../RcMap.js" +import type * as Scope from "../Scope.js" +import * as coreEffect from "./core-effect.js" +import * as core from "./core.js" +import * as circular from "./effect/circular.js" +import * as fiberRuntime from "./fiberRuntime.js" + +/** @internal */ +export const TypeId: RcMap.TypeId = Symbol.for("effect/RcMap") as RcMap.TypeId + +type State = State.Open | State.Closed + +declare namespace State { + interface Open { + readonly _tag: "Open" + readonly map: MutableHashMap.MutableHashMap> + } + + interface Closed { + readonly _tag: "Closed" + } + + interface Entry { + readonly deferred: Deferred.Deferred + readonly scope: Scope.CloseableScope + fiber: RuntimeFiber | undefined + refCount: number + } +} + +const variance: RcMap.RcMap.Variance = { + _K: identity, + _A: identity, + _E: identity +} + +class RcMapImpl implements RcMap.RcMap { + readonly [TypeId]: RcMap.RcMap.Variance + + state: State = { + _tag: "Open", + map: MutableHashMap.empty() + } + readonly semaphore = circular.unsafeMakeSemaphore(1) + + constructor( + readonly lookup: (key: K) => Effect, + readonly context: Context.Context, + readonly scope: Scope.Scope, + readonly idleTimeToLive: Duration.Duration | undefined, + readonly capacity: number + ) { + this[TypeId] = variance + } + + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export const make: { + (options: { + readonly lookup: (key: K) => Effect + readonly idleTimeToLive?: Duration.DurationInput | undefined + readonly capacity?: undefined + }): Effect, never, Scope.Scope | R> + (options: { + readonly lookup: (key: K) => Effect + readonly idleTimeToLive?: Duration.DurationInput | undefined + readonly capacity: number + }): Effect, never, Scope.Scope | R> +} = (options: { + readonly lookup: (key: K) => Effect + readonly idleTimeToLive?: Duration.DurationInput | undefined + readonly capacity?: number | undefined +}) => + core.withFiberRuntime, never, R | Scope.Scope>((fiber) => { + const context = fiber.getFiberRef(core.currentContext) as Context.Context + const scope = Context.get(context, fiberRuntime.scopeTag) + const self = new RcMapImpl( + options.lookup as any, + context, + scope, + options.idleTimeToLive ? Duration.decode(options.idleTimeToLive) : undefined, + Math.max(options.capacity ?? Number.POSITIVE_INFINITY, 0) + ) + return core.as( + scope.addFinalizer(() => + core.suspend(() => { + if (self.state._tag === "Closed") { + return core.void + } + const map = self.state.map + self.state = { _tag: "Closed" } + return core.forEachSequentialDiscard( + map, + ([, entry]) => core.scopeClose(entry.scope, core.exitVoid) + ).pipe( + core.tap(() => { + MutableHashMap.clear(map) + }), + self.semaphore.withPermits(1) + ) + }) + ), + self + ) + }) + +/** @internal */ +export const get: { + (key: K): (self: RcMap.RcMap) => Effect + (self: RcMap.RcMap, key: K): Effect +} = dual( + 2, + (self_: RcMap.RcMap, key: K): Effect => { + const self = self_ as RcMapImpl + return core.uninterruptibleMask((restore) => + core.suspend(() => { + if (self.state._tag === "Closed") { + return core.interrupt + } + const state = self.state + const o = MutableHashMap.get(state.map, key) + if (o._tag === "Some") { + const entry = o.value + entry.refCount++ + return entry.fiber + ? core.as(core.interruptFiber(entry.fiber), entry) + : core.succeed(entry) + } else if (Number.isFinite(self.capacity) && MutableHashMap.size(self.state.map) >= self.capacity) { + return core.fail( + new core.ExceededCapacityException(`RcMap attempted to exceed capacity of ${self.capacity}`) + ) as Effect + } + const acquire = self.lookup(key) + return fiberRuntime.scopeMake().pipe( + coreEffect.bindTo("scope"), + coreEffect.bind("deferred", () => core.deferredMake()), + core.tap(({ deferred, scope }) => + restore(core.fiberRefLocally( + acquire as Effect, + core.currentContext, + Context.add(self.context, fiberRuntime.scopeTag, scope) + )).pipe( + core.exit, + core.flatMap((exit) => core.deferredDone(deferred, exit)), + circular.forkIn(scope) + ) + ), + core.map(({ deferred, scope }) => { + const entry: State.Entry = { + deferred, + scope, + fiber: undefined, + refCount: 1 + } + MutableHashMap.set(state.map, key, entry) + return entry + }) + ) + }).pipe( + self.semaphore.withPermits(1), + coreEffect.bindTo("entry"), + coreEffect.bind("scope", () => fiberRuntime.scopeTag), + core.tap(({ entry, scope }) => + scope.addFinalizer(() => + core.suspend(() => { + entry.refCount-- + if (entry.refCount > 0) { + return core.void + } else if (self.idleTimeToLive === undefined) { + if (self.state._tag === "Open") { + MutableHashMap.remove(self.state.map, key) + } + return core.scopeClose(entry.scope, core.exitVoid) + } + return coreEffect.sleep(self.idleTimeToLive).pipe( + core.interruptible, + core.zipRight(core.suspend(() => { + if (self.state._tag === "Open" && entry.refCount === 0) { + MutableHashMap.remove(self.state.map, key) + return core.scopeClose(entry.scope, core.exitVoid) + } + return core.void + })), + fiberRuntime.ensuring(core.sync(() => { + entry.fiber = undefined + })), + circular.forkIn(self.scope), + core.tap((fiber) => { + entry.fiber = fiber + }), + self.semaphore.withPermits(1) + ) + }) + ) + ), + core.flatMap(({ entry }) => restore(core.deferredAwait(entry.deferred))) + ) + ) + } +) diff --git a/packages/effect/src/internal/rcRef.ts b/packages/effect/src/internal/rcRef.ts new file mode 100644 index 0000000000..8fe9841397 --- /dev/null +++ b/packages/effect/src/internal/rcRef.ts @@ -0,0 +1,172 @@ +import * as Context from "../Context.js" +import * as Duration from "../Duration.js" +import type { Effect } from "../Effect.js" +import type { RuntimeFiber } from "../Fiber.js" +import { identity } from "../Function.js" +import { pipeArguments } from "../Pipeable.js" +import type * as RcRef from "../RcRef.js" +import type * as Scope from "../Scope.js" +import * as coreEffect from "./core-effect.js" +import * as core from "./core.js" +import * as circular from "./effect/circular.js" +import * as fiberRuntime from "./fiberRuntime.js" + +/** @internal */ +export const TypeId: RcRef.TypeId = Symbol.for("effect/RcRef") as RcRef.TypeId + +type State = State.Empty | State.Acquired | State.Closed + +declare namespace State { + interface Empty { + readonly _tag: "Empty" + } + + interface Acquired { + readonly _tag: "Acquired" + readonly value: A + readonly scope: Scope.CloseableScope + fiber: RuntimeFiber | undefined + refCount: number + } + + interface Closed { + readonly _tag: "Closed" + } +} + +const stateEmpty: State = { _tag: "Empty" } +const stateClosed: State = { _tag: "Closed" } + +const variance: RcRef.RcRef.Variance = { + _A: identity, + _E: identity +} + +class RcRefImpl implements RcRef.RcRef { + readonly [TypeId]: RcRef.RcRef.Variance + + state: State = stateEmpty + readonly semaphore = circular.unsafeMakeSemaphore(1) + + constructor( + readonly acquire: Effect, + readonly context: Context.Context, + readonly scope: Scope.Scope, + readonly idleTimeToLive: Duration.Duration | undefined + ) { + this[TypeId] = variance + } + + pipe() { + return pipeArguments(this, arguments) + } +} + +/** @internal */ +export const make = (options: { + readonly acquire: Effect + readonly idleTimeToLive?: Duration.DurationInput | undefined +}) => + core.withFiberRuntime, never, R | Scope.Scope>((fiber) => { + const context = fiber.getFiberRef(core.currentContext) as Context.Context + const scope = Context.get(context, fiberRuntime.scopeTag) + const ref = new RcRefImpl( + options.acquire as Effect, + context, + scope, + options.idleTimeToLive ? Duration.decode(options.idleTimeToLive) : undefined + ) + return core.as( + scope.addFinalizer(() => + ref.semaphore.withPermits(1)(core.suspend(() => { + const close = ref.state._tag === "Acquired" + ? core.scopeClose(ref.state.scope, core.exitVoid) + : core.void + ref.state = stateClosed + return close + })) + ), + ref + ) + }) + +/** @internal */ +export const get = ( + self_: RcRef.RcRef +): Effect => { + const self = self_ as RcRefImpl + return core.uninterruptibleMask((restore) => + core.suspend(() => { + switch (self.state._tag) { + case "Closed": { + return core.interrupt + } + case "Acquired": { + self.state.refCount++ + return self.state.fiber + ? core.as(core.interruptFiber(self.state.fiber), self.state) + : core.succeed(self.state) + } + case "Empty": { + return fiberRuntime.scopeMake().pipe( + coreEffect.bindTo("scope"), + coreEffect.bind("value", ({ scope }) => + restore(core.fiberRefLocally( + self.acquire as Effect, + core.currentContext, + Context.add(self.context, fiberRuntime.scopeTag, scope) + ))), + core.map(({ scope, value }) => { + const state: State.Acquired = { + _tag: "Acquired", + value, + scope, + fiber: undefined, + refCount: 1 + } + self.state = state + return state + }) + ) + } + } + }) + ).pipe( + self.semaphore.withPermits(1), + coreEffect.bindTo("state"), + coreEffect.bind("scope", () => fiberRuntime.scopeTag), + core.tap(({ scope, state }) => + scope.addFinalizer(() => + core.suspend(() => { + state.refCount-- + if (state.refCount > 0) { + return core.void + } + if (self.idleTimeToLive === undefined) { + self.state = stateEmpty + return core.scopeClose(state.scope, core.exitVoid) + } + return coreEffect.sleep(self.idleTimeToLive).pipe( + core.interruptible, + core.zipRight(core.suspend(() => { + if (self.state._tag === "Acquired" && self.state.refCount === 0) { + self.state = stateEmpty + return core.scopeClose(state.scope, core.exitVoid) + } + return core.void + })), + fiberRuntime.ensuring(core.sync(() => { + state.fiber = undefined + })), + circular.forkIn(self.scope), + core.tap((fiber) => { + state.fiber = fiber + }), + self.semaphore.withPermits(1) + ) + }) + ) + ), + core.map(({ state }) => state.value) + ) +} diff --git a/packages/effect/src/internal/stream.ts b/packages/effect/src/internal/stream.ts index 9fd8aa29cf..0fc9179f49 100644 --- a/packages/effect/src/internal/stream.ts +++ b/packages/effect/src/internal/stream.ts @@ -31,7 +31,7 @@ import * as HaltStrategy from "../StreamHaltStrategy.js" import type * as Take from "../Take.js" import type * as Tracer from "../Tracer.js" import * as Tuple from "../Tuple.js" -import type { NoInfer } from "../Types.js" +import type { NoInfer, TupleOf } from "../Types.js" import * as channel from "./channel.js" import * as channelExecutor from "./channel/channelExecutor.js" import * as MergeStrategy from "./channel/mergeStrategy.js" @@ -461,15 +461,39 @@ export const as = dual< (self: Stream.Stream, value: B) => Stream.Stream >(2, (self: Stream.Stream, value: B): Stream.Stream => map(self, () => value)) +const queueFromBufferOptions = ( + bufferSize?: number | "unbounded" | { + readonly bufferSize?: number | undefined + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } | undefined +): Effect.Effect>> => { + if (bufferSize === "unbounded") { + return Queue.unbounded() + } else if (typeof bufferSize === "number" || bufferSize === undefined) { + return Queue.bounded(bufferSize ?? 16) + } + switch (bufferSize.strategy) { + case "dropping": + return Queue.dropping(bufferSize.bufferSize ?? 16) + case "sliding": + return Queue.sliding(bufferSize.bufferSize ?? 16) + default: + return Queue.bounded(bufferSize.bufferSize ?? 16) + } +} + /** @internal */ export const _async = ( register: ( emit: Emit.Emit ) => Effect.Effect | void, - outputBuffer = 16 + bufferSize?: number | "unbounded" | { + readonly bufferSize?: number | undefined + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } | undefined ): Stream.Stream => Effect.acquireRelease( - Queue.bounded>(outputBuffer), + queueFromBufferOptions(bufferSize), (queue) => Queue.shutdown(queue) ).pipe( Effect.flatMap((output) => @@ -518,11 +542,14 @@ export const _async = ( /** @internal */ export const asyncEffect = ( register: (emit: Emit.Emit) => Effect.Effect, - outputBuffer = 16 + bufferSize?: number | "unbounded" | { + readonly bufferSize?: number | undefined + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } | undefined ): Stream.Stream => pipe( Effect.acquireRelease( - Queue.bounded>(outputBuffer), + queueFromBufferOptions(bufferSize), (queue) => Queue.shutdown(queue) ), Effect.flatMap((output) => @@ -573,11 +600,14 @@ export const asyncEffect = ( /** @internal */ export const asyncScoped = ( register: (emit: Emit.Emit) => Effect.Effect, - outputBuffer = 16 + bufferSize?: number | "unbounded" | { + readonly bufferSize?: number | undefined + readonly strategy?: "dropping" | "sliding" | "suspend" | undefined + } | undefined ): Stream.Stream> => pipe( Effect.acquireRelease( - Queue.bounded>(outputBuffer), + queueFromBufferOptions(bufferSize), (queue) => Queue.shutdown(queue) ), Effect.flatMap((output) => @@ -682,72 +712,128 @@ export const branchAfter = dual< export const broadcast = dual< ( n: N, - maximumLag: number + maximumLag: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } ) => ( self: Stream.Stream - ) => Effect.Effect, N>, never, Scope.Scope | R>, + ) => Effect.Effect>, never, Scope.Scope | R>, ( self: Stream.Stream, n: N, - maximumLag: number - ) => Effect.Effect, N>, never, Scope.Scope | R> + maximumLag: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } + ) => Effect.Effect>, never, Scope.Scope | R> >(3, ( self: Stream.Stream, n: N, - maximumLag: number -): Effect.Effect, N>, never, Scope.Scope | R> => + maximumLag: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } +): Effect.Effect>, never, Scope.Scope | R> => pipe( self, broadcastedQueues(n, maximumLag), Effect.map((tuple) => - tuple.map((queue) => flattenTake(fromQueue(queue, { shutdown: true }))) as Stream.Stream.DynamicTuple< - Stream.Stream, - N - > + tuple.map((queue) => flattenTake(fromQueue(queue, { shutdown: true }))) as TupleOf> ) )) /** @internal */ export const broadcastDynamic = dual< ( - maximumLag: number + maximumLag: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } ) => (self: Stream.Stream) => Effect.Effect, never, Scope.Scope | R>, ( self: Stream.Stream, - maximumLag: number + maximumLag: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } ) => Effect.Effect, never, Scope.Scope | R> >(2, ( self: Stream.Stream, - maximumLag: number + maximumLag: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } ): Effect.Effect, never, Scope.Scope | R> => - pipe( - self, - broadcastedQueuesDynamic(maximumLag), - Effect.map((effect) => flattenTake(flatMap(scoped(effect), fromQueue))) - )) + Effect.map(toPubSub(self, maximumLag), (pubsub) => flattenTake(fromPubSub(pubsub)))) /** @internal */ export const broadcastedQueues = dual< ( n: N, - maximumLag: number + maximumLag: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } ) => ( self: Stream.Stream - ) => Effect.Effect>, N>, never, Scope.Scope | R>, + ) => Effect.Effect>>, never, Scope.Scope | R>, ( self: Stream.Stream, n: N, - maximumLag: number - ) => Effect.Effect>, N>, never, Scope.Scope | R> + maximumLag: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } + ) => Effect.Effect>>, never, Scope.Scope | R> >(3, ( self: Stream.Stream, n: N, - maximumLag: number -): Effect.Effect>, N>, never, Scope.Scope | R> => - Effect.flatMap(PubSub.bounded>(maximumLag), (pubsub) => + maximumLag: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } +): Effect.Effect>>, never, Scope.Scope | R> => + Effect.flatMap(pubsubFromOptions(maximumLag), (pubsub) => pipe( Effect.all(Array.from({ length: n }, () => PubSub.subscribe(pubsub))) as Effect.Effect< - Stream.Stream.DynamicTuple>, N>, + TupleOf>>, never, R >, @@ -757,17 +843,38 @@ export const broadcastedQueues = dual< /** @internal */ export const broadcastedQueuesDynamic = dual< ( - maximumLag: number + maximumLag: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } ) => ( self: Stream.Stream ) => Effect.Effect>, never, Scope.Scope>, never, Scope.Scope | R>, ( self: Stream.Stream, - maximumLag: number + maximumLag: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } ) => Effect.Effect>, never, Scope.Scope>, never, Scope.Scope | R> >(2, ( self: Stream.Stream, - maximumLag: number + maximumLag: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } ): Effect.Effect>, never, Scope.Scope>, never, Scope.Scope | R> => Effect.map(toPubSub(self, maximumLag), PubSub.subscribe)) @@ -1764,7 +1871,7 @@ export const distributedWith = dual< ) => ( self: Stream.Stream ) => Effect.Effect< - Stream.Stream.DynamicTuple>>, N>, + TupleOf>>>, never, Scope.Scope | R >, @@ -1776,7 +1883,7 @@ export const distributedWith = dual< readonly decide: (a: A) => Effect.Effect> } ) => Effect.Effect< - Stream.Stream.DynamicTuple>>, N>, + TupleOf>>>, never, Scope.Scope | R > @@ -1790,7 +1897,7 @@ export const distributedWith = dual< readonly decide: (a: A) => Effect.Effect> } ): Effect.Effect< - Stream.Stream.DynamicTuple>>, N>, + TupleOf>>>, never, Scope.Scope | R > => @@ -1829,7 +1936,7 @@ export const distributedWith = dual< Deferred.succeed(deferred, (a: A) => Effect.map(options.decide(a), (f) => (key: number) => pipe(f(mappings.get(key)!)))), Effect.as( - Array.from(queues) as Stream.Stream.DynamicTuple>>, N> + Array.from(queues) as TupleOf>>> ) ) }) @@ -3921,36 +4028,36 @@ export const mergeEither = dual< /** @internal */ export const mergeLeft = dual< - ( - that: Stream.Stream - ) => (self: Stream.Stream) => Stream.Stream, - ( - self: Stream.Stream, - that: Stream.Stream - ) => Stream.Stream + ( + right: Stream.Stream + ) => (left: Stream.Stream) => Stream.Stream, + ( + left: Stream.Stream, + right: Stream.Stream + ) => Stream.Stream >( 2, - ( - self: Stream.Stream, - that: Stream.Stream - ): Stream.Stream => pipe(self, merge(drain(that))) + ( + left: Stream.Stream, + right: Stream.Stream + ): Stream.Stream => pipe(left, merge(drain(right))) ) /** @internal */ export const mergeRight = dual< - ( - that: Stream.Stream - ) => (self: Stream.Stream) => Stream.Stream, - ( - self: Stream.Stream, - that: Stream.Stream - ) => Stream.Stream + ( + right: Stream.Stream + ) => (left: Stream.Stream) => Stream.Stream, + ( + left: Stream.Stream, + right: Stream.Stream + ) => Stream.Stream >( 2, - ( - self: Stream.Stream, - that: Stream.Stream - ): Stream.Stream => pipe(drain(self), merge(that)) + ( + left: Stream.Stream, + right: Stream.Stream + ): Stream.Stream => pipe(drain(left), merge(right)) ) /** @internal */ @@ -4686,6 +4793,40 @@ export const range = (min: number, max: number, chunkSize = DefaultChunkSize): S return new StreamImpl(go(min, max, chunkSize)) }) +export const raceAll = >>( + ...streams: S +): Stream.Stream< + Stream.Stream.Success, + Stream.Stream.Error, + Stream.Stream.Context +> => + Deferred.make().pipe( + Effect.map((halt) => { + let winner: number | null = null + return mergeAll( + streams.map((stream, index) => + stream.pipe( + takeWhile(() => { + if (winner === null) { + winner = index + Deferred.unsafeDone(halt, Exit.void) + return true + } + return winner === index + }), + interruptWhen( + Deferred.await(halt).pipe( + Effect.flatMap(() => winner === index ? Effect.never : Effect.void) + ) + ) + ) + ), + { concurrency: streams.length } + ) + }), + unwrap + ) + /** @internal */ export const rechunk = dual< (n: number) => (self: Stream.Stream) => Stream.Stream, @@ -5097,37 +5238,44 @@ export const retry = dual< export const run = dual< ( sink: Sink.Sink - ) => (self: Stream.Stream) => Effect.Effect, + ) => (self: Stream.Stream) => Effect.Effect>, ( self: Stream.Stream, sink: Sink.Sink - ) => Effect.Effect + ) => Effect.Effect> >(2, ( self: Stream.Stream, sink: Sink.Sink -): Effect.Effect => +): Effect.Effect> => pipe(toChannel(self), channel.pipeToOrFail(_sink.toChannel(sink)), channel.runDrain)) /** @internal */ -export const runCollect = (self: Stream.Stream): Effect.Effect, E, R> => - pipe(self, run(_sink.collectAll())) +export const runCollect = ( + self: Stream.Stream +): Effect.Effect, E, Exclude> => pipe(self, run(_sink.collectAll())) /** @internal */ -export const runCount = (self: Stream.Stream): Effect.Effect => +export const runCount = (self: Stream.Stream): Effect.Effect> => pipe(self, run(_sink.count)) /** @internal */ -export const runDrain = (self: Stream.Stream): Effect.Effect => +export const runDrain = (self: Stream.Stream): Effect.Effect> => pipe(self, run(_sink.drain)) /** @internal */ export const runFold = dual< - (s: S, f: (s: S, a: A) => S) => (self: Stream.Stream) => Effect.Effect, - (self: Stream.Stream, s: S, f: (s: S, a: A) => S) => Effect.Effect + ( + s: S, + f: (s: S, a: A) => S + ) => (self: Stream.Stream) => Effect.Effect>, + (self: Stream.Stream, s: S, f: (s: S, a: A) => S) => Effect.Effect> >( 3, - (self: Stream.Stream, s: S, f: (s: S, a: A) => S): Effect.Effect => - pipe(self, runFoldWhileScoped(s, constTrue, f), Effect.scoped) + ( + self: Stream.Stream, + s: S, + f: (s: S, a: A) => S + ): Effect.Effect> => pipe(self, runFoldWhileScoped(s, constTrue, f), Effect.scoped) ) /** @internal */ @@ -5135,17 +5283,18 @@ export const runFoldEffect = dual< ( s: S, f: (s: S, a: A) => Effect.Effect - ) => (self: Stream.Stream) => Effect.Effect, + ) => (self: Stream.Stream) => Effect.Effect>, ( self: Stream.Stream, s: S, f: (s: S, a: A) => Effect.Effect - ) => Effect.Effect + ) => Effect.Effect> >(3, ( self: Stream.Stream, s: S, f: (s: S, a: A) => Effect.Effect -): Effect.Effect => pipe(self, runFoldWhileScopedEffect(s, constTrue, f), Effect.scoped)) +): Effect.Effect> => + pipe(self, runFoldWhileScopedEffect(s, constTrue, f), Effect.scoped)) /** @internal */ export const runFoldScoped = dual< @@ -5180,14 +5329,19 @@ export const runFoldWhile = dual< s: S, cont: Predicate, f: (s: S, a: A) => S - ) => (self: Stream.Stream) => Effect.Effect, - (self: Stream.Stream, s: S, cont: Predicate, f: (s: S, a: A) => S) => Effect.Effect + ) => (self: Stream.Stream) => Effect.Effect>, + ( + self: Stream.Stream, + s: S, + cont: Predicate, + f: (s: S, a: A) => S + ) => Effect.Effect> >(4, ( self: Stream.Stream, s: S, cont: Predicate, f: (s: S, a: A) => S -): Effect.Effect => pipe(self, runFoldWhileScoped(s, cont, f), Effect.scoped)) +): Effect.Effect> => pipe(self, runFoldWhileScoped(s, cont, f), Effect.scoped)) /** @internal */ export const runFoldWhileEffect = dual< @@ -5195,19 +5349,20 @@ export const runFoldWhileEffect = dual< s: S, cont: Predicate, f: (s: S, a: A) => Effect.Effect - ) => (self: Stream.Stream) => Effect.Effect, + ) => (self: Stream.Stream) => Effect.Effect>, ( self: Stream.Stream, s: S, cont: Predicate, f: (s: S, a: A) => Effect.Effect - ) => Effect.Effect + ) => Effect.Effect> >(4, ( self: Stream.Stream, s: S, cont: Predicate, f: (s: S, a: A) => Effect.Effect -): Effect.Effect => pipe(self, runFoldWhileScopedEffect(s, cont, f), Effect.scoped)) +): Effect.Effect> => + pipe(self, runFoldWhileScopedEffect(s, cont, f), Effect.scoped)) /** @internal */ export const runFoldWhileScoped = dual< @@ -5253,29 +5408,29 @@ export const runFoldWhileScopedEffect = dual< export const runForEach = dual< ( f: (a: A) => Effect.Effect - ) => (self: Stream.Stream) => Effect.Effect, + ) => (self: Stream.Stream) => Effect.Effect>, ( self: Stream.Stream, f: (a: A) => Effect.Effect - ) => Effect.Effect + ) => Effect.Effect> >(2, ( self: Stream.Stream, f: (a: A) => Effect.Effect -): Effect.Effect => pipe(self, run(_sink.forEach(f)))) +): Effect.Effect> => pipe(self, run(_sink.forEach(f)))) /** @internal */ export const runForEachChunk = dual< ( f: (a: Chunk.Chunk) => Effect.Effect - ) => (self: Stream.Stream) => Effect.Effect, + ) => (self: Stream.Stream) => Effect.Effect>, ( self: Stream.Stream, f: (a: Chunk.Chunk) => Effect.Effect - ) => Effect.Effect + ) => Effect.Effect> >(2, ( self: Stream.Stream, f: (a: Chunk.Chunk) => Effect.Effect -): Effect.Effect => pipe(self, run(_sink.forEachChunk(f)))) +): Effect.Effect> => pipe(self, run(_sink.forEachChunk(f)))) /** @internal */ export const runForEachChunkScoped = dual< @@ -5309,15 +5464,15 @@ export const runForEachScoped = dual< export const runForEachWhile = dual< ( f: (a: A) => Effect.Effect - ) => (self: Stream.Stream) => Effect.Effect, + ) => (self: Stream.Stream) => Effect.Effect>, ( self: Stream.Stream, f: (a: A) => Effect.Effect - ) => Effect.Effect + ) => Effect.Effect> >(2, ( self: Stream.Stream, f: (a: A) => Effect.Effect -): Effect.Effect => pipe(self, run(_sink.forEachWhile(f)))) +): Effect.Effect> => pipe(self, run(_sink.forEachWhile(f)))) /** @internal */ export const runForEachWhileScoped = dual< @@ -5334,17 +5489,25 @@ export const runForEachWhileScoped = dual< ): Effect.Effect => pipe(self, runScoped(_sink.forEachWhile(f)))) /** @internal */ -export const runHead = (self: Stream.Stream): Effect.Effect, E, R> => - pipe(self, run(_sink.head())) +export const runHead = ( + self: Stream.Stream +): Effect.Effect, E, Exclude> => pipe(self, run(_sink.head())) /** @internal */ export const runIntoPubSub = dual< - (pubsub: PubSub.PubSub>) => (self: Stream.Stream) => Effect.Effect, - (self: Stream.Stream, pubsub: PubSub.PubSub>) => Effect.Effect + ( + pubsub: PubSub.PubSub> + ) => (self: Stream.Stream) => Effect.Effect>, + ( + self: Stream.Stream, + pubsub: PubSub.PubSub> + ) => Effect.Effect> >( 2, - (self: Stream.Stream, pubsub: PubSub.PubSub>): Effect.Effect => - pipe(self, runIntoQueue(pubsub)) + ( + self: Stream.Stream, + pubsub: PubSub.PubSub> + ): Effect.Effect> => pipe(self, runIntoQueue(pubsub)) ) /** @internal */ @@ -5363,12 +5526,19 @@ export const runIntoPubSubScoped = dual< /** @internal */ export const runIntoQueue = dual< - (queue: Queue.Enqueue>) => (self: Stream.Stream) => Effect.Effect, - (self: Stream.Stream, queue: Queue.Enqueue>) => Effect.Effect + ( + queue: Queue.Enqueue> + ) => (self: Stream.Stream) => Effect.Effect>, + ( + self: Stream.Stream, + queue: Queue.Enqueue> + ) => Effect.Effect> >( 2, - (self: Stream.Stream, queue: Queue.Enqueue>): Effect.Effect => - pipe(self, runIntoQueueScoped(queue), Effect.scoped) + ( + self: Stream.Stream, + queue: Queue.Enqueue> + ): Effect.Effect> => pipe(self, runIntoQueueScoped(queue), Effect.scoped) ) /** @internal */ @@ -5431,8 +5601,9 @@ export const runIntoQueueScoped = dual< }) /** @internal */ -export const runLast = (self: Stream.Stream): Effect.Effect, E, R> => - pipe(self, run(_sink.last())) +export const runLast = ( + self: Stream.Stream +): Effect.Effect, E, Exclude> => pipe(self, run(_sink.last())) /** @internal */ export const runScoped = dual< @@ -5455,7 +5626,7 @@ export const runScoped = dual< )) /** @internal */ -export const runSum = (self: Stream.Stream): Effect.Effect => +export const runSum = (self: Stream.Stream): Effect.Effect> => pipe(self, run(_sink.sum)) /** @internal */ @@ -6435,24 +6606,67 @@ export const timeoutTo = dual< } ) +const pubsubFromOptions = ( + options: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } +): Effect.Effect>> => { + if (typeof options === "number") { + return PubSub.bounded(options) + } else if (options.capacity === "unbounded") { + return PubSub.unbounded({ replay: options.replay }) + } + switch (options.strategy) { + case "dropping": + return PubSub.dropping(options) + case "sliding": + return PubSub.sliding(options) + default: + return PubSub.bounded(options) + } +} + /** @internal */ export const toPubSub = dual< ( - capacity: number + capacity: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } ) => (self: Stream.Stream) => Effect.Effect>, never, Scope.Scope | R>, ( self: Stream.Stream, - capacity: number + capacity: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } ) => Effect.Effect>, never, Scope.Scope | R> >(2, ( self: Stream.Stream, - capacity: number + capacity: number | { + readonly capacity: "unbounded" + readonly replay?: number | undefined + } | { + readonly capacity: number + readonly strategy?: "sliding" | "dropping" | "suspend" | undefined + readonly replay?: number | undefined + } ): Effect.Effect>, never, Scope.Scope | R> => pipe( - Effect.acquireRelease( - PubSub.bounded>(capacity), - (pubsub) => PubSub.shutdown(pubsub) - ), + Effect.acquireRelease(pubsubFromOptions(capacity), (pubsub) => PubSub.shutdown(pubsub)), Effect.tap((pubsub) => pipe(self, runIntoPubSubScoped(pubsub), Effect.forkScoped)) )) diff --git a/packages/effect/test/Effect/cause-rendering.test.ts b/packages/effect/test/Effect/cause-rendering.test.ts index 058fa163c3..21567cfa57 100644 --- a/packages/effect/test/Effect/cause-rendering.test.ts +++ b/packages/effect/test/Effect/cause-rendering.test.ts @@ -154,4 +154,14 @@ error message at`)) })) + + it.effect("pretty includes error.cause with renderErrorCause: true", () => + Effect.gen(function*() { + const cause = yield* Effect.fail(new Error("parent", { cause: new Error("child") })).pipe( + Effect.sandbox, + Effect.flip + ) + const pretty = Cause.pretty(cause, { renderErrorCause: true }) + assert.include(pretty, "[cause]: Error: child") + })) }) diff --git a/packages/effect/test/Effect/error.test.ts b/packages/effect/test/Effect/error.test.ts index 0890480b4a..42fbebe1be 100644 --- a/packages/effect/test/Effect/error.test.ts +++ b/packages/effect/test/Effect/error.test.ts @@ -83,6 +83,13 @@ describe("Effect", () => { expect(inspect(new MessageError()).startsWith("fail\n")).toBe(true) assert.deepStrictEqual(new MessageError().toJSON(), { _tag: "MessageError" }) }) + + it.it("cause", () => { + class MessageError extends Data.TaggedError("MessageError")<{ + cause: unknown + }> {} + expect(inspect(new MessageError({ cause: new Error("boom") }))).includes("[cause]: Error: boom") + }) } it.it("toJSON", () => { diff --git a/packages/effect/test/Effect/scope-ref.test.ts b/packages/effect/test/Effect/scope-ref.test.ts index 4e677566c1..8bc6d44b53 100644 --- a/packages/effect/test/Effect/scope-ref.test.ts +++ b/packages/effect/test/Effect/scope-ref.test.ts @@ -46,8 +46,8 @@ describe("Effect", () => { ) assert.deepStrictEqual(messages, [ - "1 | acquire | A > INNER > OUTER > EXTERN", - "1 | release | R > INNER > OUTER > EXTERN" + ["1 | acquire | A > INNER > OUTER > EXTERN"], + ["1 | release | R > INNER > OUTER > EXTERN"] ]) })) }) diff --git a/packages/effect/test/PubSub.test.ts b/packages/effect/test/PubSub.test.ts index 1fffa11254..7c07915cea 100644 --- a/packages/effect/test/PubSub.test.ts +++ b/packages/effect/test/PubSub.test.ts @@ -1,4 +1,4 @@ -import { Option } from "effect" +import { Chunk, Option } from "effect" import * as Array from "effect/Array" import * as Deferred from "effect/Deferred" import * as Effect from "effect/Effect" @@ -661,4 +661,82 @@ describe("PubSub", () => { yield* PubSub.publishAll(pubsub, [1, 2]) assert.deepStrictEqual(pubsub.unsafeSize(), Option.some(0)) })) + + describe("replay", () => { + it.scoped("unbounded", () => + Effect.gen(function*() { + const messages = [1, 2, 3, 4, 5] + const pubsub = yield* PubSub.unbounded({ replay: 3 }) + yield* PubSub.publishAll(pubsub, messages) + const sub = yield* PubSub.subscribe(pubsub) + assert.deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeAll(sub)), [3, 4, 5]) + })) + + it.effect("unbounded takeUpTo", () => { + const messages = [1, 2, 3, 4, 5] + return PubSub.unbounded({ replay: 3 }).pipe( + Effect.flatMap((pubsub) => + Effect.scoped( + Effect.gen(function*() { + yield* PubSub.publishAll(pubsub, messages) + + const dequeue1 = yield* PubSub.subscribe(pubsub) + yield* PubSub.publish(pubsub, 6) + const dequeue2 = yield* PubSub.subscribe(pubsub) + + assert.strictEqual(yield* Queue.size(dequeue1), 4) + assert.strictEqual(yield* Queue.size(dequeue2), 3) + assert.deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeUpTo(dequeue1, 2)), [3, 4]) + assert.deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeUpTo(dequeue1, 2)), [5, 6]) + assert.deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeUpTo(dequeue2, 3)), [4, 5, 6]) + }) + ) + ) + ) + }) + + it.scoped("dropping", () => + Effect.gen(function*() { + const messages = [1, 2, 3, 4, 5] + const pubsub = yield* PubSub.dropping({ capacity: 2, replay: 3 }) + + yield* PubSub.publishAll(pubsub, messages) + const sub = yield* PubSub.subscribe(pubsub) + assert.deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeAll(sub)), [3, 4, 5]) + yield* PubSub.publishAll(pubsub, [6, 7]) + assert.deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeAll(sub)), [6, 7]) + + const sub2 = yield* PubSub.subscribe(pubsub) + assert.deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeAll(sub2)), [5, 6, 7]) + + yield* PubSub.publishAll(pubsub, [8, 9, 10, 11]) + assert.deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeAll(sub)), [8, 9]) + assert.deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeAll(sub2)), [8, 9]) + + const sub3 = yield* PubSub.subscribe(pubsub) + assert.deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeAll(sub3)), [7, 8, 9]) + })) + + it.scoped("sliding", () => + Effect.gen(function*() { + const messages = [1, 2, 3, 4, 5] + const pubsub = yield* PubSub.sliding({ capacity: 4, replay: 3 }) + + yield* PubSub.publishAll(pubsub, messages) + const sub = yield* PubSub.subscribe(pubsub) + assert.deepStrictEqual(yield* Queue.take(sub), 3) + yield* PubSub.publishAll(pubsub, [6, 7, 8, 9, 10]) + assert.deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeAll(sub)), [5, 6, 7, 8, 9, 10]) + + const sub2 = yield* PubSub.subscribe(pubsub) + assert.deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeAll(sub2)), [8, 9, 10]) + + yield* PubSub.publishAll(pubsub, [11, 12, 13, 14, 15, 16]) + assert.deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeAll(sub)), [13, 14, 15, 16]) + assert.deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeAll(sub2)), [13, 14, 15, 16]) + + const sub3 = yield* PubSub.subscribe(pubsub) + assert.deepStrictEqual(Chunk.toReadonlyArray(yield* Queue.takeAll(sub3)), [14, 15, 16]) + })) + }) }) diff --git a/packages/effect/test/Random.test.ts b/packages/effect/test/Random.test.ts index b4fad2a5bc..5bcf690a09 100644 --- a/packages/effect/test/Random.test.ts +++ b/packages/effect/test/Random.test.ts @@ -1,4 +1,4 @@ -import { Array, Chunk, Effect, Random } from "effect" +import { Array, Chunk, Data, Effect, Random } from "effect" import * as it from "effect/test/utils/extend" import { assert, describe } from "vitest" @@ -10,4 +10,19 @@ describe("Random", () => { assert.isTrue(Chunk.every(end, (n) => n !== undefined)) assert.deepStrictEqual(start.sort(), Array.fromIterable(end).sort()) }).pipe(Effect.repeatN(100))) + + it.effect("make", () => + Effect.gen(function*() { + const random0 = Random.make("foo") + const random1 = Random.make("foo") + const random2 = Random.make(Data.struct({ foo: "bar" })) + const random3 = Random.make(Data.struct({ foo: "bar" })) + const n0 = yield* random0.next + const n1 = yield* random1.next + const n2 = yield* random2.next + const n3 = yield* random3.next + assert.strictEqual(n0, n1) + assert.strictEqual(n2, n3) + assert.notStrictEqual(n0, n2) + })) }) diff --git a/packages/effect/test/RcMap.test.ts b/packages/effect/test/RcMap.test.ts new file mode 100644 index 0000000000..7e353879f7 --- /dev/null +++ b/packages/effect/test/RcMap.test.ts @@ -0,0 +1,127 @@ +import { Cause, Data, Effect, Exit, RcMap, Scope, TestClock } from "effect" +import { assert, describe, it } from "effect/test/utils/extend" + +describe("RcMap", () => { + it.effect("deallocation", () => + Effect.gen(function*() { + const acquired: Array = [] + const released: Array = [] + const mapScope = yield* Scope.make() + const map = yield* RcMap.make({ + lookup: (key: string) => + Effect.acquireRelease( + Effect.sync(() => { + acquired.push(key) + return key + }), + () => Effect.sync(() => released.push(key)) + ) + }).pipe( + Scope.extend(mapScope) + ) + + assert.deepStrictEqual(acquired, []) + assert.strictEqual(yield* Effect.scoped(RcMap.get(map, "foo")), "foo") + assert.deepStrictEqual(acquired, ["foo"]) + assert.deepStrictEqual(released, ["foo"]) + + const scopeA = yield* Scope.make() + const scopeB = yield* Scope.make() + yield* RcMap.get(map, "bar").pipe(Scope.extend(scopeA)) + yield* Effect.scoped(RcMap.get(map, "bar")) + yield* RcMap.get(map, "baz").pipe(Scope.extend(scopeB)) + yield* Effect.scoped(RcMap.get(map, "baz")) + assert.deepStrictEqual(acquired, ["foo", "bar", "baz"]) + assert.deepStrictEqual(released, ["foo"]) + yield* Scope.close(scopeB, Exit.void) + assert.deepStrictEqual(acquired, ["foo", "bar", "baz"]) + assert.deepStrictEqual(released, ["foo", "baz"]) + yield* Scope.close(scopeA, Exit.void) + assert.deepStrictEqual(acquired, ["foo", "bar", "baz"]) + assert.deepStrictEqual(released, ["foo", "baz", "bar"]) + + const scopeC = yield* Scope.make() + yield* RcMap.get(map, "qux").pipe(Scope.extend(scopeC)) + assert.deepStrictEqual(acquired, ["foo", "bar", "baz", "qux"]) + assert.deepStrictEqual(released, ["foo", "baz", "bar"]) + + yield* Scope.close(mapScope, Exit.void) + assert.deepStrictEqual(acquired, ["foo", "bar", "baz", "qux"]) + assert.deepStrictEqual(released, ["foo", "baz", "bar", "qux"]) + + const exit = yield* RcMap.get(map, "boom").pipe(Effect.scoped, Effect.exit) + assert.isTrue(Exit.isInterrupted(exit)) + })) + + it.scoped("idleTimeToLive", () => + Effect.gen(function*() { + const acquired: Array = [] + const released: Array = [] + const map = yield* RcMap.make({ + lookup: (key: string) => + Effect.acquireRelease( + Effect.sync(() => { + acquired.push(key) + return key + }), + () => Effect.sync(() => released.push(key)) + ), + idleTimeToLive: 1000 + }) + + assert.deepStrictEqual(acquired, []) + assert.strictEqual(yield* Effect.scoped(RcMap.get(map, "foo")), "foo") + assert.deepStrictEqual(acquired, ["foo"]) + assert.deepStrictEqual(released, []) + + yield* TestClock.adjust(1000) + assert.deepStrictEqual(released, ["foo"]) + + assert.strictEqual(yield* Effect.scoped(RcMap.get(map, "bar")), "bar") + assert.deepStrictEqual(acquired, ["foo", "bar"]) + assert.deepStrictEqual(released, ["foo"]) + + yield* TestClock.adjust(500) + assert.strictEqual(yield* Effect.scoped(RcMap.get(map, "bar")), "bar") + assert.deepStrictEqual(acquired, ["foo", "bar"]) + assert.deepStrictEqual(released, ["foo"]) + + yield* TestClock.adjust(1000) + assert.deepStrictEqual(released, ["foo", "bar"]) + })) + + it.scoped("capacity", () => + Effect.gen(function*() { + const map = yield* RcMap.make({ + lookup: (key: string) => Effect.succeed(key), + capacity: 2, + idleTimeToLive: 1000 + }) + + assert.strictEqual(yield* Effect.scoped(RcMap.get(map, "foo")), "foo") + assert.strictEqual(yield* Effect.scoped(RcMap.get(map, "foo")), "foo") + assert.strictEqual(yield* Effect.scoped(RcMap.get(map, "bar")), "bar") + + const exit = yield* RcMap.get(map, "baz").pipe(Effect.scoped, Effect.exit) + assert.deepStrictEqual( + exit, + Exit.fail(new Cause.ExceededCapacityException(`RcMap attempted to exceed capacity of 2`)) + ) + + yield* TestClock.adjust(1000) + assert.strictEqual(yield* Effect.scoped(RcMap.get(map, "baz")), "baz") + })) + + it.scoped("complex key", () => + Effect.gen(function*() { + class Key extends Data.Class<{ readonly id: number }> {} + const map = yield* RcMap.make({ + lookup: (key: Key) => Effect.succeed(key.id), + capacity: 1 + }) + + assert.strictEqual(yield* RcMap.get(map, new Key({ id: 1 })), 1) + // no failure means a hit + assert.strictEqual(yield* RcMap.get(map, new Key({ id: 1 })), 1) + })) +}) diff --git a/packages/effect/test/RcRef.test.ts b/packages/effect/test/RcRef.test.ts new file mode 100644 index 0000000000..c3c439cf2b --- /dev/null +++ b/packages/effect/test/RcRef.test.ts @@ -0,0 +1,94 @@ +import { Effect, Exit, RcRef, Scope, TestClock } from "effect" +import { assert, describe, it } from "effect/test/utils/extend" + +describe("RcRef", () => { + it.effect("deallocation", () => + Effect.gen(function*() { + let acquired = 0 + let released = 0 + const refScope = yield* Scope.make() + const ref = yield* RcRef.make({ + acquire: Effect.acquireRelease( + Effect.sync(() => { + acquired++ + return "foo" + }), + () => + Effect.sync(() => { + released++ + }) + ) + }).pipe( + Scope.extend(refScope) + ) + + assert.strictEqual(acquired, 0) + assert.strictEqual(yield* Effect.scoped(RcRef.get(ref)), "foo") + assert.strictEqual(acquired, 1) + assert.strictEqual(released, 1) + + const scopeA = yield* Scope.make() + const scopeB = yield* Scope.make() + yield* RcRef.get(ref).pipe(Scope.extend(scopeA)) + yield* RcRef.get(ref).pipe(Scope.extend(scopeB)) + assert.strictEqual(acquired, 2) + assert.strictEqual(released, 1) + yield* Scope.close(scopeB, Exit.void) + assert.strictEqual(acquired, 2) + assert.strictEqual(released, 1) + yield* Scope.close(scopeA, Exit.void) + assert.strictEqual(acquired, 2) + assert.strictEqual(released, 2) + + const scopeC = yield* Scope.make() + yield* RcRef.get(ref).pipe(Scope.extend(scopeC)) + assert.strictEqual(acquired, 3) + assert.strictEqual(released, 2) + + yield* Scope.close(refScope, Exit.void) + assert.strictEqual(acquired, 3) + assert.strictEqual(released, 3) + + const exit = yield* RcRef.get(ref).pipe(Effect.scoped, Effect.exit) + assert.isTrue(Exit.isInterrupted(exit)) + })) + + it.scoped("idleTimeToLive", () => + Effect.gen(function*() { + let acquired = 0 + let released = 0 + const ref = yield* RcRef.make({ + acquire: Effect.acquireRelease( + Effect.sync(() => { + acquired++ + return "foo" + }), + () => + Effect.sync(() => { + released++ + }) + ), + idleTimeToLive: 1000 + }) + + assert.strictEqual(acquired, 0) + assert.strictEqual(yield* Effect.scoped(RcRef.get(ref)), "foo") + assert.strictEqual(acquired, 1) + assert.strictEqual(released, 0) + + yield* TestClock.adjust(1000) + assert.strictEqual(released, 1) + + assert.strictEqual(yield* Effect.scoped(RcRef.get(ref)), "foo") + assert.strictEqual(acquired, 2) + assert.strictEqual(released, 1) + + yield* TestClock.adjust(500) + assert.strictEqual(yield* Effect.scoped(RcRef.get(ref)), "foo") + assert.strictEqual(acquired, 2) + assert.strictEqual(released, 1) + + yield* TestClock.adjust(1000) + assert.strictEqual(released, 2) + })) +}) diff --git a/packages/effect/test/Stream/racing.test.ts b/packages/effect/test/Stream/racing.test.ts new file mode 100644 index 0000000000..dbd7b58a42 --- /dev/null +++ b/packages/effect/test/Stream/racing.test.ts @@ -0,0 +1,67 @@ +import * as Chunk from "effect/Chunk" +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +import * as Schedule from "effect/Schedule" +import * as Stream from "effect/Stream" +import * as it from "effect/test/utils/extend" +import * as TestClock from "effect/TestClock" +import { assert, describe } from "vitest" + +describe("Stream", () => { + it.effect("raceAll sync", () => + Effect.gen(function*($) { + const result = yield* $( + Stream.raceAll( + Stream.make(0, 1, 2, 3), + Stream.make(4, 5, 6, 7), + Stream.make(7, 8, 9, 10) + ), + Stream.runCollect, + Effect.map(Chunk.toReadonlyArray) + ) + assert.deepStrictEqual(result, [0, 1, 2, 3]) + })) + + it.effect("raceAll async", () => + Effect.gen(function*($) { + const fiber = yield* $( + Stream.raceAll( + Stream.fromSchedule(Schedule.spaced("1 second")), + Stream.fromSchedule(Schedule.spaced("2 second")) + ), + Stream.take(5), + Stream.runCollect, + Effect.map(Chunk.toReadonlyArray), + Effect.fork + ) + yield* TestClock.adjust("5 second") + const result = yield* Fiber.join(fiber) + assert.deepStrictEqual(result, [0, 1, 2, 3, 4]) + })) + + it.effect("raceAll combined async + sync", () => + Effect.gen(function*($) { + const result = yield* $( + Stream.raceAll( + Stream.fromSchedule(Schedule.spaced("1 second")), + Stream.make(0, 1, 2, 3) + ), + Stream.runCollect, + Effect.map(Chunk.toReadonlyArray) + ) + assert.deepStrictEqual(result, [0, 1, 2, 3]) + })) + + it.effect("raceAll combined sync + async", () => + Effect.gen(function*($) { + const result = yield* $( + Stream.raceAll( + Stream.make(0, 1, 2, 3), + Stream.fromSchedule(Schedule.spaced("1 second")) + ), + Stream.runCollect, + Effect.map(Chunk.toReadonlyArray) + ) + assert.deepStrictEqual(result, [0, 1, 2, 3]) + })) +}) diff --git a/packages/platform/test/HttpClient.test.ts b/packages/platform/test/HttpClient.test.ts index 77a9bab4e6..8dc0f03a9f 100644 --- a/packages/platform/test/HttpClient.test.ts +++ b/packages/platform/test/HttpClient.test.ts @@ -150,7 +150,7 @@ describe("HttpClient", () => { Effect.scoped ) - expect(logs).toEqual(["hello", "world"]) + expect(logs).toEqual([["hello"], ["world"]]) }).pipe(Effect.provide(HttpClient.layer), Effect.runPromise)) it("ClientRequest parses URL instances", () => { diff --git a/packages/sql-d1/CHANGELOG.md b/packages/sql-d1/CHANGELOG.md new file mode 100644 index 0000000000..b0ddc6f603 --- /dev/null +++ b/packages/sql-d1/CHANGELOG.md @@ -0,0 +1 @@ +# @effect/sql-d1 diff --git a/packages/sql-d1/LICENSE b/packages/sql-d1/LICENSE new file mode 100644 index 0000000000..7f6fe480f7 --- /dev/null +++ b/packages/sql-d1/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023-present The Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/sql-d1/README.md b/packages/sql-d1/README.md new file mode 100644 index 0000000000..8d9d6bb397 --- /dev/null +++ b/packages/sql-d1/README.md @@ -0,0 +1,5 @@ +# Effect SQL - Cloudflare D1 + +An @effect/sql implementation using cloudflare D1. + +See here for more information: https://github.com/Effect-TS/effect/tree/main/packages/sql diff --git a/packages/sql-d1/docgen.json b/packages/sql-d1/docgen.json new file mode 100644 index 0000000000..f980a32f52 --- /dev/null +++ b/packages/sql-d1/docgen.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../node_modules/@effect/docgen/schema.json", + "exclude": [ + "src/internal/**/*.ts" + ] +} diff --git a/packages/sql-d1/package.json b/packages/sql-d1/package.json new file mode 100644 index 0000000000..3044aeb3c3 --- /dev/null +++ b/packages/sql-d1/package.json @@ -0,0 +1,60 @@ +{ + "name": "@effect/sql-d1", + "version": "0.0.1", + "type": "module", + "license": "MIT", + "description": "A Cloudflare D1 integration for Effect", + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/sql-d1" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect/issues" + }, + "tags": [ + "typescript", + "sql", + "database", + "cloudflare", + "D1" + ], + "keywords": [ + "typescript", + "sql", + "database", + "cloudflare", + "D1" + ], + "publishConfig": { + "access": "public", + "directory": "dist", + "provenance": true + }, + "scripts": { + "codegen": "build-utils prepare-v2", + "build": "pnpm build-esm && pnpm build-cjs && pnpm build-annotate && build-utils pack-v2", + "build-esm": "tsc -b tsconfig.build.json", + "build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", + "build-annotate": "babel build --plugins annotate-pure-calls --out-dir build --source-maps", + "check": "tsc -b tsconfig.json", + "test": "vitest", + "coverage": "vitest --coverage" + }, + "devDependencies": { + "@effect/platform": "workspace:^", + "@effect/sql": "workspace:^", + "effect": "workspace:^", + "miniflare": "^3.20240610.1" + }, + "peerDependencies": { + "@effect/platform": "workspace:^", + "@effect/sql": "workspace:^", + "effect": "workspace:^" + }, + "dependencies": { + "@cloudflare/workers-types": "^4.20240620.0", + "@opentelemetry/semantic-conventions": "^1.24.1" + } +} diff --git a/packages/sql-d1/src/D1Client.ts b/packages/sql-d1/src/D1Client.ts new file mode 100644 index 0000000000..2505d2df22 --- /dev/null +++ b/packages/sql-d1/src/D1Client.ts @@ -0,0 +1,194 @@ +/** + * @since 1.0.0 + */ +import type { D1Database, D1PreparedStatement } from "@cloudflare/workers-types" +import * as Client from "@effect/sql/SqlClient" +import type { Connection } from "@effect/sql/SqlConnection" +import { SqlError } from "@effect/sql/SqlError" +import * as Statement from "@effect/sql/Statement" +import * as Otel from "@opentelemetry/semantic-conventions" +import * as Cache from "effect/Cache" +import * as Config from "effect/Config" +import type { ConfigError } from "effect/ConfigError" +import * as Context from "effect/Context" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import { identity } from "effect/Function" +import * as Layer from "effect/Layer" +import type * as Scope from "effect/Scope" + +/** + * @category type ids + * @since 1.0.0 + */ +export const TypeId: unique symbol = Symbol.for("@effect/sql-d1/D1Client") + +/** + * @category type ids + * @since 1.0.0 + */ +export type TypeId = typeof TypeId + +/** + * @category models + * @since 1.0.0 + */ +export interface D1Client extends Client.SqlClient { + readonly [TypeId]: TypeId + readonly config: D1ClientConfig + + /** Not supported in d1 */ + readonly updateValues: never +} + +/** + * @category tags + * @since 1.0.0 + */ +export const D1Client = Context.GenericTag("@effect/sql-d1/D1Client") + +/** + * @category models + * @since 1.0.0 + */ +export interface D1ClientConfig { + readonly db: D1Database + readonly prepareCacheSize?: number | undefined + readonly prepareCacheTTL?: Duration.DurationInput | undefined + readonly spanAttributes?: Record | undefined + + readonly transformResultNames?: ((str: string) => string) | undefined + readonly transformQueryNames?: ((str: string) => string) | undefined +} + +/** + * @category constructor + * @since 1.0.0 + */ +export const make = ( + options: D1ClientConfig +): Effect.Effect => + Effect.gen(function*() { + const compiler = Statement.makeCompilerSqlite(options.transformQueryNames) + const transformRows = Statement.defaultTransforms( + options.transformResultNames! + ).array + + const makeConnection = Effect.gen(function*() { + const db = options.db + + const prepareCache = yield* Cache.make({ + capacity: options.prepareCacheSize ?? 200, + timeToLive: options.prepareCacheTTL ?? Duration.minutes(10), + lookup: (sql: string) => + Effect.try({ + try: () => db.prepare(sql), + catch: (error) => new SqlError({ error }) + }) + }) + + const runStatement = ( + statement: D1PreparedStatement, + params: ReadonlyArray = [] + ) => + Effect.tryPromise({ + try: async () => { + const response = await statement.bind(...params).all() + if (response.error) { + throw response.error + } + return response.results || [] + }, + catch: (error) => new SqlError({ error }) + }) + + const run = ( + sql: string, + params: ReadonlyArray = [] + ) => Effect.flatMap(prepareCache.get(sql), (s) => runStatement(s, params)) + + const runRaw = ( + sql: string, + params: ReadonlyArray = [] + ) => Effect.map(runStatement(db.prepare(sql), params), transformRows) + + const runTransform = options.transformResultNames + ? (sql: string, params?: ReadonlyArray) => Effect.map(run(sql, params), transformRows) + : run + + const runValues = ( + sql: string, + params: ReadonlyArray + ) => + Effect.flatMap( + prepareCache.get(sql), + (statement) => + Effect.tryPromise({ + try: () => { + return statement.bind(...params).raw() as Promise< + ReadonlyArray< + ReadonlyArray + > + > + }, + catch: (error) => new SqlError({ error }) + }) + ) + + return identity({ + execute(sql, params) { + return runTransform(sql, params) + }, + executeValues(sql, params) { + return runValues(sql, params) + }, + executeWithoutTransform(sql, params) { + return run(sql, params) + }, + executeRaw(sql, params) { + return runRaw(sql, params) + }, + executeStream(_sql, _params) { + return Effect.dieMessage("executeStream not implemented") + } + }) + }) + + const connection = yield* makeConnection + const acquirer = Effect.succeed(connection) + const transactionAcquirer = Effect.dieMessage("transactions are not supported in D1") + + return Object.assign( + Client.make({ + acquirer, + compiler, + transactionAcquirer, + spanAttributes: [ + ...(options.spanAttributes ? Object.entries(options.spanAttributes) : []), + [Otel.SEMATTRS_DB_SYSTEM, Otel.DBSYSTEMVALUES_SQLITE] + ] + }) as D1Client, + { + [TypeId]: TypeId as TypeId, + config: options + } + ) + }) + +/** + * @category layers + * @since 1.0.0 + */ +export const layer = ( + config: Config.Config.Wrap +): Layer.Layer => + Layer.scopedContext( + Config.unwrap(config).pipe( + Effect.flatMap(make), + Effect.map((client) => + Context.make(D1Client, client).pipe( + Context.add(Client.SqlClient, client) + ) + ) + ) + ) diff --git a/packages/sql-d1/src/index.ts b/packages/sql-d1/src/index.ts new file mode 100644 index 0000000000..e7e1b4a838 --- /dev/null +++ b/packages/sql-d1/src/index.ts @@ -0,0 +1,4 @@ +/** + * @since 1.0.0 + */ +export * as D1Client from "./D1Client.js" diff --git a/packages/sql-d1/test/Client.test.ts b/packages/sql-d1/test/Client.test.ts new file mode 100644 index 0000000000..0f051842e3 --- /dev/null +++ b/packages/sql-d1/test/Client.test.ts @@ -0,0 +1,43 @@ +import { D1Client } from "@effect/sql-d1" +import { assert, describe, it } from "@effect/vitest" +import { Cause, Effect } from "effect" +import { D1Miniflare } from "./utils.js" + +describe("Client", () => { + it.scoped("should handle queries without transactions", () => + Effect.gen(function*() { + const sql = yield* D1Client.D1Client + yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)` + yield* sql`INSERT INTO test (name) VALUES ('hello')` + let rows = yield* sql`SELECT * FROM test` + assert.deepStrictEqual(rows, [{ id: 1, name: "hello" }]) + yield* sql`INSERT INTO test (name) VALUES ('world')` + rows = yield* sql`SELECT * FROM test` + assert.deepStrictEqual(rows, [ + { id: 1, name: "hello" }, + { id: 2, name: "world" } + ]) + }).pipe(Effect.provide(D1Miniflare.ClientLive))) + + it.scoped("should handle queries with params without transactions", () => + Effect.gen(function*() { + const sql = yield* D1Client.D1Client + yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)` + yield* sql`INSERT INTO test ${sql.insert({ name: "hello" })}` + const rows = yield* sql`SELECT * FROM test WHERE name = ${"hello"}` + assert.deepStrictEqual(rows, [{ id: 1, name: "hello" }]) + }).pipe(Effect.provide(D1Miniflare.ClientLive))) + + it.scoped("should defect on transactions", () => + Effect.gen(function*() { + const sql = yield* D1Client.D1Client + yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)` + const res = yield* sql`INSERT INTO test ${sql.insert({ name: "hello" })}`.pipe( + sql.withTransaction, + Effect.catchAllDefect((defect) => Effect.succeed(defect)) + ) + const rows = yield* sql`SELECT * FROM test` + assert.deepStrictEqual(rows, []) + assert.equal(Cause.isRuntimeException(res), true) + }).pipe(Effect.provide(D1Miniflare.ClientLive))) +}) diff --git a/packages/sql-d1/test/Resolver.test.ts b/packages/sql-d1/test/Resolver.test.ts new file mode 100644 index 0000000000..96e0a73473 --- /dev/null +++ b/packages/sql-d1/test/Resolver.test.ts @@ -0,0 +1,169 @@ +import * as Schema from "@effect/schema/Schema" +import { SqlError, SqlResolver } from "@effect/sql" +import { D1Client } from "@effect/sql-d1" +import { assert, describe, it } from "@effect/vitest" +import { Array, Effect, Option } from "effect" +import { D1Miniflare } from "./utils.js" + +const seededClient = Effect.gen(function*(_) { + const sql = yield* D1Client.D1Client + yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)` + yield* Effect.forEach(Array.range(1, 100), (id) => sql`INSERT INTO test ${sql.insert({ id, name: `name${id}` })}`) + return sql +}) + +describe("Resolver", () => { + describe("ordered", () => { + it.scoped("insert", () => + Effect.gen(function*(_) { + const batches: Array> = [] + const sql = yield* _(seededClient) + const Insert = yield* _(SqlResolver.ordered("Insert", { + Request: Schema.String, + Result: Schema.Struct({ id: Schema.Number, name: Schema.String }), + execute: (names) => { + batches.push(names) + return sql`INSERT INTO test ${sql.insert(names.map((name) => ({ name })))} RETURNING *` + } + })) + assert.deepStrictEqual( + yield* _(Effect.all({ + one: Insert.execute("one"), + two: Insert.execute("two") + }, { batching: true })), + { + one: { id: 101, name: "one" }, + two: { id: 102, name: "two" } + } + ) + assert.deepStrictEqual(batches, [["one", "two"]]) + }).pipe(Effect.provide(D1Miniflare.ClientLive))) + + it.scoped("result length mismatch", () => + Effect.gen(function*(_) { + const batches: Array> = [] + const sql = yield* _(seededClient) + const Select = yield* _(SqlResolver.ordered("Select", { + Request: Schema.Number, + Result: Schema.Struct({ id: Schema.Number, name: Schema.String }), + execute: (ids) => { + batches.push(ids) + return sql`SELECT * FROM test WHERE id IN ${sql.in(ids)}` + } + })) + const error = yield* _( + Effect.all([ + Select.execute(1), + Select.execute(2), + Select.execute(3), + Select.execute(101) + ], { batching: true }), + Effect.flip + ) + assert(error instanceof SqlError.ResultLengthMismatch) + assert.strictEqual(error.actual, 3) + assert.strictEqual(error.expected, 4) + assert.deepStrictEqual(batches, [[1, 2, 3, 101]]) + }).pipe(Effect.provide(D1Miniflare.ClientLive))) + }) + + describe("grouped", () => { + it.scoped("find by name", () => + Effect.gen(function*(_) { + const sql = yield* _(seededClient) + const FindByName = yield* _(SqlResolver.grouped("FindByName", { + Request: Schema.String, + RequestGroupKey: (name) => name, + Result: Schema.Struct({ id: Schema.Number, name: Schema.String }), + ResultGroupKey: (result) => result.name, + execute: (names) => sql`SELECT * FROM test WHERE name IN ${sql.in(names)}` + })) + yield* _(sql`INSERT INTO test ${sql.insert({ name: "name1" })}`) + assert.deepStrictEqual( + yield* _(Effect.all({ + one: FindByName.execute("name1"), + two: FindByName.execute("name2"), + three: FindByName.execute("name0") + }, { batching: true })), + { + one: [{ id: 1, name: "name1" }, { id: 101, name: "name1" }], + two: [{ id: 2, name: "name2" }], + three: [] + } + ) + }).pipe(Effect.provide(D1Miniflare.ClientLive))) + + it.scoped("using raw rows", () => + Effect.gen(function*(_) { + const sql = yield* _(seededClient) + const FindByName = yield* _(SqlResolver.grouped("FindByName", { + Request: Schema.String, + RequestGroupKey: (name) => name, + Result: Schema.Struct({ id: Schema.Number, name: Schema.String }), + ResultGroupKey: (_, result: any) => result.name, + execute: (names) => sql`SELECT * FROM test WHERE name IN ${sql.in(names)}` + })) + yield* _(sql`INSERT INTO test ${sql.insert({ name: "name1" })}`) + assert.deepStrictEqual( + yield* _(Effect.all({ + one: FindByName.execute("name1"), + two: FindByName.execute("name2"), + three: FindByName.execute("name0") + }, { batching: true })), + { + one: [{ id: 1, name: "name1" }, { id: 101, name: "name1" }], + two: [{ id: 2, name: "name2" }], + three: [] + } + ) + }).pipe(Effect.provide(D1Miniflare.ClientLive))) + }) + + describe("findById", () => { + it.scoped("find by id", () => + Effect.gen(function*(_) { + const sql = yield* _(seededClient) + const FindById = yield* _(SqlResolver.findById("FindById", { + Id: Schema.Number, + Result: Schema.Struct({ id: Schema.Number, name: Schema.String }), + ResultId: (result) => result.id, + execute: (ids) => sql`SELECT * FROM test WHERE id IN ${sql.in(ids)}` + })) + assert.deepStrictEqual( + yield* _(Effect.all({ + one: FindById.execute(1), + two: FindById.execute(2), + three: FindById.execute(101) + }, { batching: true })), + { + one: Option.some({ id: 1, name: "name1" }), + two: Option.some({ id: 2, name: "name2" }), + three: Option.none() + } + ) + }).pipe(Effect.provide(D1Miniflare.ClientLive))) + + it.scoped("using raw rows", () => + Effect.gen(function*(_) { + const sql = yield* _(seededClient) + const FindById = yield* _(SqlResolver.findById("FindById", { + Id: Schema.Number, + Result: Schema.Struct({ id: Schema.Number, name: Schema.String }), + ResultId: (_, result: any) => result.id, + execute: (ids) => sql`SELECT * FROM test WHERE id IN ${sql.in(ids)}` + })) + assert.deepStrictEqual( + yield* _(Effect.all({ + one: FindById.execute(1), + two: FindById.execute(2), + three: FindById.execute(101) + }, { batching: true })), + { + one: Option.some({ id: 1, name: "name1" }), + two: Option.some({ id: 2, name: "name2" }), + three: Option.none() + } + ) + }).pipe(Effect.provide(D1Miniflare.ClientLive))) + }) +}) diff --git a/packages/sql-d1/test/utils.ts b/packages/sql-d1/test/utils.ts new file mode 100644 index 0000000000..36dc43eaaf --- /dev/null +++ b/packages/sql-d1/test/utils.ts @@ -0,0 +1,41 @@ +import type { D1Database } from "@cloudflare/workers-types" +import { D1Client } from "@effect/sql-d1" +import { Config, Context, Data, Effect, Layer } from "effect" +import { Miniflare } from "miniflare" + +export class MiniflareError extends Data.TaggedError("MiniflareError")<{ + cause: unknown +}> {} + +export class D1Miniflare extends Context.Tag("test/D1Miniflare")< + D1Miniflare, + Miniflare +>() { + static Live = Layer.scoped( + this, + Effect.acquireRelease( + Effect.try({ + try: () => + new Miniflare({ + modules: true, + d1Databases: { + DB: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + }, + script: "" + }), + catch: (cause) => new MiniflareError({ cause }) + }), + (miniflare) => Effect.promise(() => miniflare.dispose()) + ) + ) + + static ClientLive = Layer.unwrapEffect( + Effect.gen(function*() { + const miniflare = yield* D1Miniflare + const db: D1Database = yield* Effect.tryPromise(() => miniflare.getD1Database("DB")) + return D1Client.layer({ + db: Config.succeed(db) + }) + }) + ).pipe(Layer.provide(this.Live)) +} diff --git a/packages/sql-d1/tsconfig.build.json b/packages/sql-d1/tsconfig.build.json new file mode 100644 index 0000000000..8a5c6b4d6e --- /dev/null +++ b/packages/sql-d1/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.src.json", + "references": [ + { "path": "../effect/tsconfig.build.json" }, + { "path": "../sql/tsconfig.build.json" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", + "outDir": "build/esm", + "declarationDir": "build/dts", + "stripInternal": true + } +} diff --git a/packages/sql-d1/tsconfig.examples.json b/packages/sql-d1/tsconfig.examples.json new file mode 100644 index 0000000000..119ff192eb --- /dev/null +++ b/packages/sql-d1/tsconfig.examples.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["examples"], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "../effect" }, + { "path": "../experimental" }, + { "path": "../sql" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/examples.tsbuildinfo", + "rootDir": "examples", + "noEmit": true + } +} diff --git a/packages/sql-d1/tsconfig.json b/packages/sql-d1/tsconfig.json new file mode 100644 index 0000000000..3edbf6b8a5 --- /dev/null +++ b/packages/sql-d1/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "tsconfig.test.json" }, + { "path": "tsconfig.examples.json" } + ] +} diff --git a/packages/sql-d1/tsconfig.src.json b/packages/sql-d1/tsconfig.src.json new file mode 100644 index 0000000000..424a843861 --- /dev/null +++ b/packages/sql-d1/tsconfig.src.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"], + "references": [{ "path": "../effect" }, { "path": "../sql" }], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", + "rootDir": "src", + "outDir": "build/src" + } +} diff --git a/packages/sql-d1/tsconfig.test.json b/packages/sql-d1/tsconfig.test.json new file mode 100644 index 0000000000..9d0a40abed --- /dev/null +++ b/packages/sql-d1/tsconfig.test.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["test"], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "../effect" }, + { "path": "../sql" }, + { "path": "../vitest" } + ], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", + "rootDir": "test", + "noEmit": true + } +} diff --git a/packages/sql-d1/vitest.config.ts b/packages/sql-d1/vitest.config.ts new file mode 100644 index 0000000000..0411095f25 --- /dev/null +++ b/packages/sql-d1/vitest.config.ts @@ -0,0 +1,6 @@ +import { mergeConfig, type UserConfigExport } from "vitest/config" +import shared from "../../vitest.shared.js" + +const config: UserConfigExport = {} + +export default mergeConfig(shared, config) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e14f256a3..d9442011c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -576,6 +576,29 @@ importers: version: link:../effect/dist publishDirectory: dist + packages/sql-d1: + dependencies: + '@cloudflare/workers-types': + specifier: ^4.20240620.0 + version: 4.20240620.0 + '@opentelemetry/semantic-conventions': + specifier: ^1.24.1 + version: 1.25.1 + devDependencies: + '@effect/platform': + specifier: workspace:^ + version: link:../platform/dist + '@effect/sql': + specifier: workspace:^ + version: link:../sql/dist + effect: + specifier: workspace:^ + version: link:../effect/dist + miniflare: + specifier: ^3.20240610.1 + version: 3.20240610.1 + publishDirectory: dist + packages/sql-drizzle: devDependencies: '@effect/sql': @@ -589,7 +612,7 @@ importers: version: 10.10.0 drizzle-orm: specifier: ^0.31.2 - version: 0.31.2(@op-engineering/op-sqlite@6.0.7(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(react@18.3.1))(react@18.3.1))(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.10)(better-sqlite3@11.0.0)(bun-types@1.1.16)(mysql2@3.10.1)(postgres@3.4.4)(react@18.3.1) + version: 0.31.2(@cloudflare/workers-types@4.20240620.0)(@op-engineering/op-sqlite@6.0.7(react@18.3.1))(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.10)(better-sqlite3@11.0.0)(bun-types@1.1.16)(mysql2@3.10.1)(postgres@3.4.4)(react@18.3.1) effect: specifier: workspace:^ version: link:../effect/dist @@ -1675,6 +1698,43 @@ packages: '@changesets/write@0.3.1': resolution: {integrity: sha512-SyGtMXzH3qFqlHKcvFY2eX+6b0NGiFcNav8AFsYwy5l8hejOeoeTDemu5Yjmke2V5jpzY+pBvM0vCCQ3gdZpfw==} + '@cloudflare/workerd-darwin-64@1.20240610.1': + resolution: {integrity: sha512-YanZ1iXgMGaUWlleB5cswSE6qbzyjQ8O7ENWZcPAcZZ6BfuL7q3CWi0t9iM1cv2qx92rRztsRTyjcfq099++XQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20240610.1': + resolution: {integrity: sha512-bRe/y/LKjIgp3L2EHjc+CvoCzfHhf4aFTtOBkv2zW+VToNJ4KlXridndf7LvR9urfsFRRo9r4TXCssuKaU+ypQ==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20240610.1': + resolution: {integrity: sha512-2zDcadR7+Gs9SjcMXmwsMji2Xs+yASGNA2cEHDuFc4NMUup+eL1mkzxc/QzvFjyBck98e92rBjMZt2dVscpGKg==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20240610.1': + resolution: {integrity: sha512-7y41rPi5xmIYJN8CY+t3RHnjLL0xx/WYmaTd/j552k1qSr02eTE2o/TGyWZmGUC+lWnwdPQJla0mXbvdqgRdQg==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20240610.1': + resolution: {integrity: sha512-B0LyT3DB6rXHWNptnntYHPaoJIy0rXnGfeDBM3nEVV8JIsQrx8MEFn2F2jYioH1FkUVavsaqKO/zUosY3tZXVA==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cloudflare/workers-types@4.20240620.0': + resolution: {integrity: sha512-CQD8YS6evRob7LChvIX3gE3zYo0KVgaLDOu1SwNP1BVIS2Sa0b+FC8S1e1hhrNN8/E4chYlVN+FDAgA4KRDUEQ==} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + '@dependents/detective-less@4.1.0': resolution: {integrity: sha512-KrkT6qO5NxqNfy68sBl6CTSoJ4SNDIS5iQArkibhlbGU4LaDukZ3q2HIkh8aUKDio6o4itU4xDR7t82Y2eP1Bg==} engines: {node: '>=14'} @@ -1883,6 +1943,10 @@ packages: resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@fastify/busboy@2.1.1': + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + '@hapi/hoek@9.3.0': resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} @@ -1975,6 +2039,9 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@js-joda/core@5.6.3': resolution: {integrity: sha512-T1rRxzdqkEXcou0ZprN1q9yDRlvzCPLqmlNt5IIsGBzoEVgLCCYrKEwc84+TvsXuAc95VAZwtWD2zVsKPY4bcA==} @@ -2872,6 +2939,9 @@ packages: resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} engines: {node: '>= 0.4'} + as-table@1.0.55: + resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} + asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} @@ -3117,6 +3187,9 @@ packages: caniuse-lite@1.0.30001636: resolution: {integrity: sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==} + capnp-ts@0.7.0: + resolution: {integrity: sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g==} + chai@4.4.1: resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==} engines: {node: '>=4'} @@ -3271,6 +3344,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + core-js-compat@3.37.1: resolution: {integrity: sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==} @@ -3305,6 +3382,9 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} + data-uri-to-buffer@2.0.2: + resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} + dataloader@1.4.0: resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} @@ -3824,6 +3904,10 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} + exit-hook@2.2.1: + resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} + engines: {node: '>=6'} + expand-range@1.8.2: resolution: {integrity: sha512-AFASGfIlnIbkKPQwX1yHaDjFvh/1gyKJODme52V6IORh69uEYgZp0o9C+qsIGNVEiuuhQU0CSSl++Rlegg1qvA==} engines: {node: '>=0.10.0'} @@ -4039,6 +4123,9 @@ packages: resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==} engines: {node: '>=8'} + get-source@2.0.12: + resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} + get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -4068,6 +4155,9 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + glob@10.4.2: resolution: {integrity: sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==} engines: {node: '>=16 || 14 >=14.18'} @@ -4969,6 +5059,11 @@ packages: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} + miniflare@3.20240610.1: + resolution: {integrity: sha512-ZkfSpBmX3nJW00yYhvF2kGvjb6f77TOimRR6+2GQvsArbwo6e0iYqLGM9aB/cnJzgFjLMvOv1qj4756iynSxJQ==} + engines: {node: '>=16.13'} + hasBin: true + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -5067,6 +5162,10 @@ packages: multipasta@0.2.2: resolution: {integrity: sha512-KKGdmXIJUmt9BV45LsbUdMnju8eCNSyF9KpbyqK2E3wQXjpPQOg52/Hc+nsmBacmEkNxLVT5h1y3ZgEXB4prXg==} + mustache@4.2.0: + resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} + hasBin: true + mysql2@3.10.1: resolution: {integrity: sha512-6zo1T3GILsXMCex3YEu7hCz2OXLUarxFsxvFcUHWMpkPtmZLeTTWgRdc1gWyNJiYt6AxITmIf9bZDRy/jAfWew==} engines: {node: '>= 8.0'} @@ -5475,6 +5574,9 @@ packages: resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==} engines: {node: '>=10'} + printable-characters@1.0.42: + resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} + private@0.1.8: resolution: {integrity: sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==} engines: {node: '>= 0.6'} @@ -5981,6 +6083,9 @@ packages: resolution: {integrity: sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==} engines: {node: '>=6'} + stacktracey@2.1.8: + resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==} + standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} @@ -6351,6 +6456,10 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici@5.28.4: + resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} + engines: {node: '>=14.0'} + undici@6.19.2: resolution: {integrity: sha512-JfjKqIauur3Q6biAtHJ564e3bWa8VvT+7cSiOJHFbX4Erv6CLGDpg8z+Fmg/1OI/47RA+GI2QZaF48SSaLvyBA==} engines: {node: '>=18.17'} @@ -6537,6 +6646,11 @@ packages: engines: {node: '>=8'} hasBin: true + workerd@1.20240610.1: + resolution: {integrity: sha512-Rtut5GrsODQMh6YU43b9WZ980Wd05Ov1/ds88pT/SoetmXFBvkBzdRfiHiATv+azmGX8KveE0i/Eqzk/yI01ug==} + engines: {node: '>=16'} + hasBin: true + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -6643,6 +6757,9 @@ packages: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} + youch@3.3.3: + resolution: {integrity: sha512-qSFXUk3UZBLfggAW3dJKg0BMblG5biqSF8M34E06o5CSsZtH92u9Hqmj2RzGiHDi64fhe83+4tENFP2DB6t6ZA==} + zip-stream@4.1.1: resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} engines: {node: '>= 10'} @@ -7974,6 +8091,27 @@ snapshots: human-id: 1.0.2 prettier: 2.8.8 + '@cloudflare/workerd-darwin-64@1.20240610.1': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20240610.1': + optional: true + + '@cloudflare/workerd-linux-64@1.20240610.1': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20240610.1': + optional: true + + '@cloudflare/workerd-windows-64@1.20240610.1': + optional: true + + '@cloudflare/workers-types@4.20240620.0': {} + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + '@dependents/detective-less@4.1.0': dependencies: gonzales-pe: 4.3.0 @@ -8123,6 +8261,8 @@ snapshots: '@eslint/js@8.57.0': {} + '@fastify/busboy@2.1.1': {} + '@hapi/hoek@9.3.0': {} '@hapi/topo@5.1.0': @@ -8236,6 +8376,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + '@js-joda/core@5.6.3': {} '@lmdb/lmdb-darwin-arm64@3.0.12': @@ -9363,6 +9508,10 @@ snapshots: is-array-buffer: 3.0.4 is-shared-array-buffer: 1.0.3 + as-table@1.0.55: + dependencies: + printable-characters: 1.0.42 + asap@2.0.6: {} asn1@0.2.6: @@ -9689,6 +9838,13 @@ snapshots: caniuse-lite@1.0.30001636: {} + capnp-ts@0.7.0: + dependencies: + debug: 4.3.5 + tslib: 2.6.3 + transitivePeerDependencies: + - supports-color + chai@4.4.1: dependencies: assertion-error: 1.1.0 @@ -9865,6 +10021,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie@0.5.0: {} + core-js-compat@3.37.1: dependencies: browserslist: 4.23.1 @@ -9905,6 +10063,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + data-uri-to-buffer@2.0.2: {} + dataloader@1.4.0: {} dayjs@1.11.11: {} @@ -10074,8 +10234,9 @@ snapshots: dotenv@8.6.0: {} - drizzle-orm@0.31.2(@op-engineering/op-sqlite@6.0.7(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(react@18.3.1))(react@18.3.1))(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.10)(better-sqlite3@11.0.0)(bun-types@1.1.16)(mysql2@3.10.1)(postgres@3.4.4)(react@18.3.1): + drizzle-orm@0.31.2(@cloudflare/workers-types@4.20240620.0)(@op-engineering/op-sqlite@6.0.7(react@18.3.1))(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.10)(better-sqlite3@11.0.0)(bun-types@1.1.16)(mysql2@3.10.1)(postgres@3.4.4)(react@18.3.1): optionalDependencies: + '@cloudflare/workers-types': 4.20240620.0 '@op-engineering/op-sqlite': 6.0.7(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(react@18.3.1))(react@18.3.1) '@opentelemetry/api': 1.9.0 '@types/better-sqlite3': 7.6.10 @@ -10465,6 +10626,8 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 + exit-hook@2.2.1: {} + expand-range@1.8.2: dependencies: fill-range: 2.2.4 @@ -10706,6 +10869,11 @@ snapshots: get-port@5.1.1: {} + get-source@2.0.12: + dependencies: + data-uri-to-buffer: 2.0.2 + source-map: 0.6.1 + get-stream@6.0.1: {} get-stream@8.0.1: {} @@ -10734,6 +10902,8 @@ snapshots: dependencies: is-glob: 4.0.3 + glob-to-regexp@0.4.1: {} + glob@10.4.2: dependencies: foreground-child: 3.2.1 @@ -11798,6 +11968,25 @@ snapshots: mimic-response@3.1.0: {} + miniflare@3.20240610.1: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + acorn: 8.12.0 + acorn-walk: 8.3.2 + capnp-ts: 0.7.0 + exit-hook: 2.2.1 + glob-to-regexp: 0.4.1 + stoppable: 1.1.0 + undici: 5.28.4 + workerd: 1.20240610.1 + ws: 8.17.1 + youch: 3.3.3 + zod: 3.23.8 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -11891,6 +12080,8 @@ snapshots: multipasta@0.2.2: {} + mustache@4.2.0: {} + mysql2@3.10.1: dependencies: denque: 2.1.0 @@ -12293,6 +12484,8 @@ snapshots: dependencies: parse-ms: 2.1.0 + printable-characters@1.0.42: {} + private@0.1.8: {} process-nextick-args@2.0.1: {} @@ -12880,6 +13073,11 @@ snapshots: dependencies: type-fest: 0.7.1 + stacktracey@2.1.8: + dependencies: + as-table: 1.0.55 + get-source: 2.0.12 + standard-as-callback@2.1.0: {} statuses@1.5.0: {} @@ -13289,6 +13487,10 @@ snapshots: undici-types@5.26.5: {} + undici@5.28.4: + dependencies: + '@fastify/busboy': 2.1.1 + undici@6.19.2: {} unicode-canonical-property-names-ecmascript@2.0.0: {} @@ -13467,6 +13669,14 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + workerd@1.20240610.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20240610.1 + '@cloudflare/workerd-darwin-arm64': 1.20240610.1 + '@cloudflare/workerd-linux-64': 1.20240610.1 + '@cloudflare/workerd-linux-arm64': 1.20240610.1 + '@cloudflare/workerd-windows-64': 1.20240610.1 + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -13555,6 +13765,12 @@ snapshots: yocto-queue@1.0.0: {} + youch@3.3.3: + dependencies: + cookie: 0.5.0 + mustache: 4.2.0 + stacktracey: 2.1.8 + zip-stream@4.1.1: dependencies: archiver-utils: 3.0.4 diff --git a/tsconfig.base.json b/tsconfig.base.json index e2a6c58ba2..d996f5c453 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -48,13 +48,17 @@ "@effect/cluster/test/*": ["./packages/cluster/test/*.js"], "@effect/cluster-browser": ["./packages/cluster-browser/src/index.js"], "@effect/cluster-browser/*": ["./packages/cluster-browser/src/*.js"], - "@effect/cluster-browser/test/*": ["./packages/cluster-browser/test/*.js"], + "@effect/cluster-browser/test/*": [ + "./packages/cluster-browser/test/*.js" + ], "@effect/cluster-node": ["./packages/cluster-node/src/index.js"], "@effect/cluster-node/*": ["./packages/cluster-node/src/*.js"], "@effect/cluster-node/test/*": ["./packages/cluster-node/test/*.js"], "@effect/cluster-workflow": ["./packages/cluster-workflow/src/index.js"], "@effect/cluster-workflow/*": ["./packages/cluster-workflow/src/*.js"], - "@effect/cluster-workflow/test/*": ["./packages/cluster-workflow/test/*.js"], + "@effect/cluster-workflow/test/*": [ + "./packages/cluster-workflow/test/*.js" + ], "@effect/experimental": ["./packages/experimental/src/index.js"], "@effect/experimental/*": ["./packages/experimental/src/*.js"], "@effect/experimental/test/*": ["./packages/experimental/test/*.js"], @@ -102,6 +106,9 @@ "@effect/sql": ["./packages/sql/src/index.js"], "@effect/sql/*": ["./packages/sql/src/*.js"], "@effect/sql/test/*": ["./packages/sql/test/*.js"], + "@effect/sql-d1": ["./packages/sql-d1/src/index.js"], + "@effect/sql-d1/*": ["./packages/sql-d1/src/*.js"], + "@effect/sql-d1/test/*": ["./packages/sql-d1/test/*.js"], "@effect/sql-drizzle": ["./packages/sql-drizzle/src/index.js"], "@effect/sql-drizzle/*": ["./packages/sql-drizzle/src/*.js"], "@effect/sql-drizzle/test/*": ["./packages/sql-drizzle/test/*.js"], diff --git a/tsconfig.build.json b/tsconfig.build.json index ef5e83aa77..e3868b2877 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -21,6 +21,7 @@ { "path": "packages/rpc-http/tsconfig.build.json" }, { "path": "packages/schema/tsconfig.build.json" }, { "path": "packages/sql/tsconfig.build.json" }, + { "path": "packages/sql-d1/tsconfig.build.json" }, { "path": "packages/sql-drizzle/tsconfig.build.json" }, { "path": "packages/sql-mysql2/tsconfig.build.json" }, { "path": "packages/sql-mssql/tsconfig.build.json" }, diff --git a/tsconfig.json b/tsconfig.json index 924272f72b..ca8fb9b4a8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,6 +22,7 @@ { "path": "packages/schema" }, { "path": "packages/schema" }, { "path": "packages/sql" }, + { "path": "packages/sql-d1" }, { "path": "packages/sql-drizzle" }, { "path": "packages/sql-mssql" }, { "path": "packages/sql-mysql2" }, diff --git a/vitest.shared.ts b/vitest.shared.ts index f059116057..82e5c4547e 100644 --- a/vitest.shared.ts +++ b/vitest.shared.ts @@ -47,6 +47,7 @@ const config: UserConfig = { ...alias("rpc-http"), ...alias("schema"), ...alias("sql"), + ...alias("sql-d1"), ...alias("sql-drizzle"), ...alias("sql-mssql"), ...alias("sql-mysql2"),