From d75140c7a664ceda43142d999f4ff8dcd36d6dda Mon Sep 17 00:00:00 2001 From: Michael Arnaldi Date: Sun, 6 Oct 2024 12:44:18 +0200 Subject: [PATCH] Implement Effect.Service and allow multiple layers to be provided in Effect.provide (#3690) Co-authored-by: Tim Co-authored-by: Patrick Roza --- .changeset/old-ways-accept.md | 5 + .changeset/twelve-dots-enjoy.md | 53 +++ packages/effect/src/Context.ts | 5 +- packages/effect/src/Effect.ts | 421 +++++++++++++++++--- packages/effect/src/Layer.ts | 46 ++- packages/effect/src/Types.ts | 41 ++ packages/effect/src/internal/context.ts | 1 - packages/effect/src/internal/layer.ts | 110 +++-- packages/effect/src/internal/stm/core.ts | 5 +- packages/effect/test/Effect/service.test.ts | 171 ++++++++ 10 files changed, 763 insertions(+), 95 deletions(-) create mode 100644 .changeset/old-ways-accept.md create mode 100644 .changeset/twelve-dots-enjoy.md create mode 100644 packages/effect/test/Effect/service.test.ts diff --git a/.changeset/old-ways-accept.md b/.changeset/old-ways-accept.md new file mode 100644 index 0000000000..a887f0a9a8 --- /dev/null +++ b/.changeset/old-ways-accept.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +Support providing an array of layers via Effect.provide and Layer.provide diff --git a/.changeset/twelve-dots-enjoy.md b/.changeset/twelve-dots-enjoy.md new file mode 100644 index 0000000000..e8af418923 --- /dev/null +++ b/.changeset/twelve-dots-enjoy.md @@ -0,0 +1,53 @@ +--- +"effect": minor +--- + +Implement Effect.Service as a Tag and Layer with Opaque Type. + +Namely the following is now possible: + +```ts +class Prefix extends Effect.Service()("Prefix", { + sync: () => ({ + prefix: "PRE" + }) +}) {} + +class Postfix extends Effect.Service()("Postfix", { + sync: () => ({ + postfix: "POST" + }) +}) {} + +const messages: Array = [] + +class Logger extends Effect.Service()("Logger", { + accessors: true, + effect: Effect.gen(function* () { + const { prefix } = yield* Prefix + const { postfix } = yield* Postfix + return { + info: (message: string) => + Effect.sync(() => { + messages.push(`[${prefix}][${message}][${postfix}]`) + }) + } + }), + dependencies: [Prefix.Default, Postfix.Default] +}) {} + +describe("Effect", () => { + it.effect("Service correctly wires dependencies", () => + Effect.gen(function* () { + const { _tag } = yield* Logger + expect(_tag).toEqual("Logger") + yield* Logger.info("Ok") + expect(messages).toEqual(["[PRE][Ok][POST]"]) + const { prefix } = yield* Prefix + expect(prefix).toEqual("PRE") + const { postfix } = yield* Postfix + expect(postfix).toEqual("POST") + }).pipe(Effect.provide([Logger.Default, Prefix.Default, Postfix.Default])) + ) +}) +``` diff --git a/packages/effect/src/Context.ts b/packages/effect/src/Context.ts index 9cf2440bdd..9cad9e3b9c 100644 --- a/packages/effect/src/Context.ts +++ b/packages/effect/src/Context.ts @@ -29,7 +29,6 @@ export type TagTypeId = typeof TagTypeId * @category models */ export interface Tag extends Pipeable, Inspectable { - readonly _tag: "Tag" readonly _op: "Tag" readonly Service: Value readonly Identifier: Id @@ -85,13 +84,13 @@ export declare namespace Tag { /** * @since 2.0.0 */ - export type Service | TagClassShape> = T extends Tag ? A + export type Service | TagClassShape> = T extends Tag ? T["Service"] : T extends TagClassShape ? A : never /** * @since 2.0.0 */ - export type Identifier | TagClassShape> = T extends Tag ? A + export type Identifier | TagClassShape> = T extends Tag ? T["Identifier"] : T extends TagClassShape ? T : never } diff --git a/packages/effect/src/Effect.ts b/packages/effect/src/Effect.ts index f9929c4312..e4b4f756c0 100644 --- a/packages/effect/src/Effect.ts +++ b/packages/effect/src/Effect.ts @@ -57,7 +57,7 @@ import * as Scheduler from "./Scheduler.js" import type * as Scope from "./Scope.js" import type * as Supervisor from "./Supervisor.js" import type * as Tracer from "./Tracer.js" -import type { Concurrency, Covariant, NoInfer, NotFunction } from "./Types.js" +import type { Concurrency, Contravariant, Covariant, NoExcessProperties, NoInfer, NotFunction } from "./Types.js" import type * as Unify from "./Unify.js" import type { YieldWrap } from "./Utils.js" @@ -3326,14 +3326,33 @@ export const mapInputContext: { * @category context */ export const provide: { + ]>( + layers: Layers + ): ( + self: Effect + ) => Effect< + A, + E | { [k in keyof Layers]: Layer.Layer.Error }[number], + | { [k in keyof Layers]: Layer.Layer.Context }[number] + | Exclude }[number]> + > ( layer: Layer.Layer - ): (self: Effect) => Effect> + ): (self: Effect) => Effect> (context: Context.Context): (self: Effect) => Effect> (runtime: Runtime.Runtime): (self: Effect) => Effect> ( managedRuntime: ManagedRuntime.ManagedRuntime ): (self: Effect) => Effect> + ]>( + self: Effect, + layers: Layers + ): Effect< + A, + E | { [k in keyof Layers]: Layer.Layer.Error }[number], + | { [k in keyof Layers]: Layer.Layer.Context }[number] + | Exclude }[number]> + > ( self: Effect, layer: Layer.Layer @@ -6216,12 +6235,13 @@ export declare namespace Tag { Service?: `property "Service" is forbidden` Identifier?: `property "Identifier" is forbidden` _op?: `property "_op" is forbidden` - _tag?: `property "_tag" is forbidden` of?: `property "of" is forbidden` context?: `property "context" is forbidden` key?: `property "key" is forbidden` stack?: `property "stack" is forbidden` name?: `property "name" is forbidden` + pipe?: `property "pipe" is forbidden` + use?: `property "use" is forbidden` } /** @@ -6229,29 +6249,62 @@ export declare namespace Tag { * @category models */ export type AllowedType = (Record & ProhibitedType) | string | number | symbol + + /** + * @since 3.9.0 + * @category models + */ + export type Proxy = { + [ + k in keyof Type as Type[k] extends ((...args: [...infer Args]) => infer Ret) ? + ((...args: Readonly) => Ret) extends Type[k] ? k : never + : k + ]: Type[k] extends (...args: [...infer Args]) => Effect ? + (...args: Readonly) => Effect + : Type[k] extends (...args: [...infer Args]) => infer A ? (...args: Readonly) => Effect + : Type[k] extends Effect ? Effect + : Effect + } +} + +const makeTagProxy = (TagClass: Context.Tag & Record) => { + const cache = new Map() + return new Proxy(TagClass, { + get(target: any, prop: any, receiver) { + if (prop in target) { + return Reflect.get(target, prop, receiver) + } + if (cache.has(prop)) { + return cache.get(prop) + } + const fn = (...args: Array) => + core.andThen(target, (s: any) => { + if (typeof s[prop] === "function") { + cache.set(prop, (...args: Array) => core.andThen(target, (s: any) => s[prop](...args))) + return s[prop](...args) + } + cache.set(prop, core.andThen(target, (s: any) => s[prop])) + return s[prop] + }) + const cn = core.andThen(target, (s: any) => s[prop]) + Object.assign(fn, cn) + Object.setPrototypeOf(fn, Object.getPrototypeOf(cn)) + cache.set(prop, fn) + return fn + } + }) } /** * @since 2.0.0 - * @category constructors + * @category context */ export const Tag: (id: Id) => < Self, Type extends Tag.AllowedType >() => & Context.TagClass - & (Type extends Record ? { - [ - k in keyof Type as Type[k] extends ((...args: [...infer Args]) => infer Ret) ? - ((...args: Readonly) => Ret) extends Type[k] ? k : never - : k - ]: Type[k] extends (...args: [...infer Args]) => Effect ? - (...args: Readonly) => Effect - : Type[k] extends (...args: [...infer Args]) => infer A ? (...args: Readonly) => Effect - : Type[k] extends Effect ? Effect - : Effect - } : - {}) + & (Type extends Record ? Tag.Proxy : {}) & { use: ( body: (_: Type) => X @@ -6264,44 +6317,316 @@ export const Tag: (id: Id) => < function TagClass() {} Object.setPrototypeOf(TagClass, TagProto) TagClass.key = id + Object.defineProperty(TagClass, "use", { + get() { + return (body: (_: any) => any) => core.andThen(this, body) + } + }) Object.defineProperty(TagClass, "stack", { get() { return creationError.stack } }) - const cache = new Map() - const done = new Proxy(TagClass, { - get(_target: any, prop: any, _receiver) { - if (prop === "use") { - // @ts-expect-error - return (body) => core.andThen(TagClass, body) - } - if (prop in TagClass) { - // @ts-expect-error - return TagClass[prop] - } - if (cache.has(prop)) { - return cache.get(prop) + return makeTagProxy(TagClass as any) + } + +/** + * @since 3.9.0 + * @category context + * @experimental might be up for breaking changes + */ +export const Service: () => { + < + const Key extends string, + const Make extends + | { + readonly scoped: Effect, any, any> + readonly dependencies?: ReadonlyArray + readonly accessors?: boolean + /** @deprecated */ + readonly ಠ_ಠ: never + } + | { + readonly effect: Effect, any, any> + readonly dependencies?: ReadonlyArray + readonly accessors?: boolean + /** @deprecated */ + readonly ಠ_ಠ: never + } + | { + readonly sync: LazyArg> + readonly dependencies?: ReadonlyArray + readonly accessors?: boolean + /** @deprecated */ + readonly ಠ_ಠ: never + } + | { + readonly succeed: Service.AllowedType + readonly dependencies?: ReadonlyArray + readonly accessors?: boolean + /** @deprecated */ + readonly ಠ_ಠ: never + } + >( + key: Key, + make: Make + ): Service.Class + < + const Key extends string, + const Make extends NoExcessProperties<{ + readonly scoped: Effect, any, any> + readonly dependencies?: ReadonlyArray + readonly accessors?: boolean + }, Make> + >( + key: Key, + make: Make + ): Service.Class + < + const Key extends string, + const Make extends NoExcessProperties<{ + readonly effect: Effect, any, any> + readonly dependencies?: ReadonlyArray + readonly accessors?: boolean + }, Make> + >( + key: Key, + make: Make + ): Service.Class + < + const Key extends string, + const Make extends NoExcessProperties<{ + readonly sync: LazyArg> + readonly dependencies?: ReadonlyArray + readonly accessors?: boolean + }, Make> + >( + key: Key, + make: Make + ): Service.Class + < + const Key extends string, + const Make extends NoExcessProperties<{ + readonly succeed: Service.AllowedType + readonly dependencies?: ReadonlyArray + readonly accessors?: boolean + }, Make> + >( + key: Key, + make: Make + ): Service.Class +} = function() { + return function() { + const [id, maker] = arguments + const proxy = "accessors" in maker ? maker["accessors"] : false + const limit = Error.stackTraceLimit + Error.stackTraceLimit = 2 + const creationError = new Error() + Error.stackTraceLimit = limit + + let patchState: "unchecked" | "plain" | "patched" = "unchecked" + const TagClass: any = function(this: any, service: any) { + if (patchState === "unchecked") { + const proto = Object.getPrototypeOf(service) + if (proto === Object.prototype || proto === null) { + patchState = "plain" + } else { + const selfProto = Object.getPrototypeOf(this) + Object.setPrototypeOf(selfProto, proto) + patchState = "patched" } - const fn = (...args: Array) => - // @ts-expect-error - core.andThen(TagClass, (s: any) => { - if (typeof s[prop] === "function") { - // @ts-expect-error - cache.set(prop, (...args: Array) => core.andThen(TagClass, (s: any) => s[prop](...args))) - return s[prop](...args) - } - // @ts-expect-error - cache.set(prop, core.andThen(TagClass, (s) => s[prop])) - return s[prop] - }) - // @ts-expect-error - const cn = core.andThen(TagClass, (s) => s[prop]) - Object.assign(fn, cn) - Object.setPrototypeOf(fn, Object.getPrototypeOf(cn)) - cache.set(prop, fn) - return fn + } + if (patchState === "plain") { + Object.assign(this, service) + } else if (patchState === "patched") { + Object.setPrototypeOf(service, Object.getPrototypeOf(this)) + return service + } + } + + TagClass.prototype._tag = id + Object.defineProperty(TagClass, "make", { + get() { + return (service: any) => new this(service) + } + }) + Object.defineProperty(TagClass, "use", { + get() { + return (body: any) => core.andThen(this, body) + } + }) + TagClass.key = id + + Object.assign(TagClass, TagProto) + + Object.defineProperty(TagClass, "stack", { + get() { + return creationError.stack } }) - return done + + const hasDeps = "dependencies" in maker && maker.dependencies.length > 0 + const layerName = hasDeps ? "DefaultWithoutDependencies" : "Default" + let layerCache: Layer.Layer.Any | undefined + if ("effect" in maker) { + Object.defineProperty(TagClass, layerName, { + get(this: any) { + return layerCache ??= layer.fromEffect(TagClass, map(maker.effect, (_) => new this(_))) + } + }) + } else if ("scoped" in maker) { + Object.defineProperty(TagClass, layerName, { + get(this: any) { + return layerCache ??= layer.scoped(TagClass, map(maker.scoped, (_) => new this(_))) + } + }) + } else if ("sync" in maker) { + Object.defineProperty(TagClass, layerName, { + get(this: any) { + return layerCache ??= layer.sync(TagClass, () => new this(maker.sync())) + } + }) + } else { + Object.defineProperty(TagClass, layerName, { + get(this: any) { + return layerCache ??= layer.succeed(TagClass, new this(maker.succeed)) + } + }) + } + + if (hasDeps) { + let layerWithDepsCache: Layer.Layer.Any | undefined + Object.defineProperty(TagClass, "Default", { + get(this: any) { + return layerWithDepsCache ??= layer.provide( + this.DefaultWithoutDependencies, + maker.dependencies + ) + } + }) + } + + return proxy === true ? makeTagProxy(TagClass) : TagClass } +} + +/** + * @since 3.9.0 + * @category context + */ +export declare namespace Service { + /** + * @since 3.9.0 + */ + export interface ProhibitedType { + Service?: `property "Service" is forbidden` + Identifier?: `property "Identifier" is forbidden` + Default?: `property "Default" is forbidden` + DefaultWithoutDependencies?: `property "DefaultWithoutDependencies" is forbidden` + _op_layer?: `property "_op_layer" is forbidden` + _op?: `property "_op" is forbidden` + of?: `property "of" is forbidden` + make?: `property "make" is forbidden` + context?: `property "context" is forbidden` + key?: `property "key" is forbidden` + stack?: `property "stack" is forbidden` + name?: `property "name" is forbidden` + pipe?: `property "pipe" is forbidden` + use?: `property "use" is forbidden` + _tag?: `property "_tag" is forbidden` + } + + /** + * @since 3.9.0 + */ + export type AllowedType = MakeAccessors extends true ? + & Record + & { + readonly [K in Extract, keyof ProhibitedType>]: K extends "_tag" ? Key + : ProhibitedType[K] + } + : Record & { readonly _tag?: Key } + + /** + * @since 3.9.0 + */ + export type Class< + Self, + Key extends string, + Make + > = + & { + new(_: MakeService): MakeService & { + readonly _tag: Key + } + readonly use: ( + body: (_: Self) => X + ) => X extends Effect ? Effect : Effect + readonly make: (_: MakeService) => Self + } + & Context.Tag + & (MakeAccessors extends true ? Tag.Proxy> : {}) + & (MakeDeps extends never ? { + readonly Default: Layer.Layer, MakeContext> + } : + { + readonly DefaultWithoutDependencies: Layer.Layer, MakeContext> + readonly Default: Layer.Layer< + Self, + MakeError | MakeDepsE, + | Exclude, MakeDepsOut> + | MakeDepsIn + > + }) + + /** + * @since 3.9.0 + */ + export type MakeService = Make extends { readonly effect: Effect } ? _A + : Make extends { readonly scoped: Effect } ? _A + : Make extends { readonly sync: LazyArg } ? A + : Make extends { readonly succeed: infer A } ? A + : never + + /** + * @since 3.9.0 + */ + export type MakeError = Make extends { readonly effect: Effect } ? _E + : Make extends { readonly scoped: Effect } ? _E + : never + + /** + * @since 3.9.0 + */ + export type MakeContext = Make extends { readonly effect: Effect } ? _R + : Make extends { readonly scoped: Effect } ? Exclude<_R, Scope.Scope> + : never + + /** + * @since 3.9.0 + */ + export type MakeDeps = Make extends { readonly dependencies: ReadonlyArray } + ? Make["dependencies"][number] + : never + + /** + * @since 3.9.0 + */ + export type MakeDepsOut = Contravariant.Type[Layer.LayerTypeId]["_ROut"]> + + /** + * @since 3.9.0 + */ + export type MakeDepsE = Covariant.Type[Layer.LayerTypeId]["_E"]> + + /** + * @since 3.9.0 + */ + export type MakeDepsIn = Covariant.Type[Layer.LayerTypeId]["_RIn"]> + + /** + * @since 3.9.0 + */ + export type MakeAccessors = Make extends { readonly accessors: true } ? true + : false +} diff --git a/packages/effect/src/Layer.ts b/packages/effect/src/Layer.ts index 0e6a1ebfe4..da20435258 100644 --- a/packages/effect/src/Layer.ts +++ b/packages/effect/src/Layer.ts @@ -76,23 +76,34 @@ export declare namespace Layer { readonly _RIn: Types.Covariant } } + /** + * @since 3.9.0 + * @category type-level + */ + export interface Any { + readonly [LayerTypeId]: { + readonly _ROut: any + readonly _E: any + readonly _RIn: any + } + } /** * @since 2.0.0 * @category type-level */ - export type Context> = [T] extends [Layer] ? _RIn + export type Context = [T] extends [Layer] ? _RIn : never /** * @since 2.0.0 * @category type-level */ - export type Error> = [T] extends [Layer] ? _E + export type Error = [T] extends [Layer] ? _E : never /** * @since 2.0.0 * @category type-level */ - export type Success> = [T] extends [Layer] ? _ROut + export type Success = [T] extends [Layer] ? _ROut : never } @@ -833,12 +844,31 @@ export const toRuntimeWithMemoMap: { */ export const provide: { ( - self: Layer - ): (that: Layer) => Layer> + that: Layer + ): (self: Layer) => Layer> + ]>( + that: Layers + ): ( + self: Layer + ) => Layer< + A, + E | { [k in keyof Layers]: Layer.Error }[number], + | { [k in keyof Layers]: Layer.Context }[number] + | Exclude }[number]> + > ( - that: Layer, - self: Layer - ): Layer> + self: Layer, + that: Layer + ): Layer> + ]>( + self: Layer, + that: Layers + ): Layer< + A, + E | { [k in keyof Layers]: Layer.Error }[number], + | { [k in keyof Layers]: Layer.Context }[number] + | Exclude }[number]> + > } = internal.provide /** diff --git a/packages/effect/src/Types.ts b/packages/effect/src/Types.ts index 3dfe8e71af..423c8ce414 100644 --- a/packages/effect/src/Types.ts +++ b/packages/effect/src/Types.ts @@ -254,6 +254,18 @@ export type NoInfer = [A][A extends any ? 0 : never] */ export type Invariant = (_: A) => A +/** + * @since 3.9.0 + * @category models + */ +export declare namespace Invariant { + /** + * @since 3.9.0 + * @category models + */ + export type Type = A extends Invariant ? U : never +} + /** * Covariant helper. * @@ -262,6 +274,18 @@ export type Invariant = (_: A) => A */ export type Covariant = (_: never) => A +/** + * @since 3.9.0 + * @category models + */ +export declare namespace Covariant { + /** + * @since 3.9.0 + * @category models + */ + export type Type = A extends Covariant ? U : never +} + /** * Contravariant helper. * @@ -270,6 +294,18 @@ export type Covariant = (_: never) => A */ export type Contravariant = (_: A) => void +/** + * @since 3.9.0 + * @category models + */ +export declare namespace Contravariant { + /** + * @since 3.9.0 + * @category models + */ + export type Type = A extends Contravariant ? U : never +} + /** * @since 2.0.0 */ @@ -279,3 +315,8 @@ export type MatchRecord = {} extends S ? onTrue : onFalse * @since 2.0.0 */ export type NotFunction = T extends Function ? never : T + +/** + * @since 3.9.0 + */ +export type NoExcessProperties = T & { readonly [K in Exclude]: never } diff --git a/packages/effect/src/internal/context.ts b/packages/effect/src/internal/context.ts index b3bb12a7f5..2e68bb5ac3 100644 --- a/packages/effect/src/internal/context.ts +++ b/packages/effect/src/internal/context.ts @@ -25,7 +25,6 @@ export const STMTypeId: STM.STMTypeId = Symbol.for( /** @internal */ export const TagProto: any = { ...EffectPrototype, - _tag: "Tag", _op: "Tag", [STMTypeId]: effectVariance, [TagTypeId]: { diff --git a/packages/effect/src/internal/layer.ts b/packages/effect/src/internal/layer.ts index 7cf47e70a6..cd04afc973 100644 --- a/packages/effect/src/internal/layer.ts +++ b/packages/effect/src/internal/layer.ts @@ -51,7 +51,7 @@ const layerVariance = { } /** @internal */ -const proto = { +export const proto = { [LayerTypeId]: layerVariance, pipe() { return pipeArguments(this, arguments) @@ -81,7 +81,7 @@ export type Primitive = /** @internal */ export type Op = Layer.Layer & Body & { - readonly _tag: Tag + readonly _op_layer: Tag } /** @internal */ @@ -167,7 +167,7 @@ export const isLayer = (u: unknown): u is Layer.Layer /** @internal */ export const isFresh = (self: Layer.Layer): boolean => { - return (self as Primitive)._tag === OpCodes.OP_FRESH + return (self as Primitive)._op_layer === OpCodes.OP_FRESH } // ----------------------------------------------------------------------------- @@ -358,7 +358,7 @@ const makeBuilder = ( inMemoMap = false ): Effect.Effect<(memoMap: Layer.MemoMap) => Effect.Effect, E, RIn>> => { const op = self as Primitive - switch (op._tag) { + switch (op._op_layer) { case "Locally": { return core.sync(() => (memoMap: Layer.MemoMap) => op.f(memoMap.getOrElseMemoize(op.self, scope))) } @@ -489,7 +489,7 @@ export const extendScope = ( self: Layer.Layer ): Layer.Layer => { const extendScope = Object.create(proto) - extendScope._tag = OpCodes.OP_EXTEND_SCOPE + extendScope._op_layer = OpCodes.OP_EXTEND_SCOPE extendScope.layer = self return extendScope } @@ -535,7 +535,7 @@ export const flatten = dual< /** @internal */ export const fresh = (self: Layer.Layer): Layer.Layer => { const fresh = Object.create(proto) - fresh._tag = OpCodes.OP_FRESH + fresh._op_layer = OpCodes.OP_FRESH fresh.layer = self return fresh } @@ -567,7 +567,7 @@ export function fromEffectContext( effect: Effect.Effect, E, R> ): Layer.Layer { const fromEffect = Object.create(proto) - fromEffect._tag = OpCodes.OP_FROM_EFFECT + fromEffect._op_layer = OpCodes.OP_FROM_EFFECT fromEffect.effect = effect return fromEffect } @@ -589,7 +589,7 @@ export const locallyEffect = dual< ) => Layer.Layer >(2, (self, f) => { const locally = Object.create(proto) - locally._tag = "Locally" + locally._op_layer = "Locally" locally.self = self locally.f = f return locally @@ -660,7 +660,7 @@ export const matchCause = dual< ) => Layer.Layer >(2, (self, { onFailure, onSuccess }) => { const fold = Object.create(proto) - fold._tag = OpCodes.OP_FOLD + fold._op_layer = OpCodes.OP_FOLD fold.layer = self fold.failureK = onFailure fold.successK = onSuccess @@ -868,7 +868,7 @@ export const scopedContext = ( effect: Effect.Effect, E, R> ): Layer.Layer> => { const scoped = Object.create(proto) - scoped._tag = OpCodes.OP_SCOPED + scoped._op_layer = OpCodes.OP_SCOPED scoped.effect = effect return scoped } @@ -922,7 +922,7 @@ export const suspend = ( evaluate: LazyArg> ): Layer.Layer => { const suspend = Object.create(proto) - suspend._tag = OpCodes.OP_SUSPEND + suspend._op_layer = OpCodes.OP_SUSPEND suspend.evaluate = evaluate return suspend } @@ -1026,29 +1026,52 @@ export const toRuntimeWithMemoMap = dual< /** @internal */ export const provide = dual< - ( - self: Layer.Layer - ) => ( - that: Layer.Layer - ) => Layer.Layer>, - ( - that: Layer.Layer, - self: Layer.Layer - ) => Layer.Layer> ->(2, ( - that: Layer.Layer, - self: Layer.Layer + { + ( + that: Layer.Layer + ): ( + self: Layer.Layer + ) => Layer.Layer> + ]>( + that: Layers + ): ( + self: Layer.Layer + ) => Layer.Layer< + A, + E | { [k in keyof Layers]: Layer.Layer.Error }[number], + | { [k in keyof Layers]: Layer.Layer.Context }[number] + | Exclude }[number]> + > + }, + { + ( + self: Layer.Layer, + that: Layer.Layer + ): Layer.Layer> + ]>( + self: Layer.Layer, + that: Layers + ): Layer.Layer< + A, + E | { [k in keyof Layers]: Layer.Layer.Error }[number], + | { [k in keyof Layers]: Layer.Layer.Context }[number] + | Exclude }[number]> + > + } +>(2, ( + self: Layer.Layer.Any, + that: Layer.Layer.Any | ReadonlyArray ) => suspend(() => { const provideTo = Object.create(proto) - provideTo._tag = OpCodes.OP_PROVIDE + provideTo._op_layer = OpCodes.OP_PROVIDE provideTo.first = Object.create(proto, { - _tag: { value: OpCodes.OP_PROVIDE_MERGE, enumerable: true }, - first: { value: context>(), enumerable: true }, - second: { value: self }, - zipK: { value: (a: Context.Context, b: Context.Context) => pipe(a, Context.merge(b)) } + _op_layer: { value: OpCodes.OP_PROVIDE_MERGE, enumerable: true }, + first: { value: context(), enumerable: true }, + second: { value: Array.isArray(that) ? mergeAll(...that as any) : that }, + zipK: { value: (a: Context.Context, b: Context.Context) => pipe(a, Context.merge(b)) } }) - provideTo.second = that + provideTo.second = self return provideTo })) @@ -1065,7 +1088,7 @@ export const provideMerge = dual< ) => Layer.Layer> >(2, (that: Layer.Layer, self: Layer.Layer) => { const zipWith = Object.create(proto) - zipWith._tag = OpCodes.OP_PROVIDE_MERGE + zipWith._op_layer = OpCodes.OP_PROVIDE_MERGE zipWith.first = self zipWith.second = provide(that, self) zipWith.zipK = (a: Context.Context, b: Context.Context): Context.Context => { @@ -1088,7 +1111,7 @@ export const zipWith = dual< >(3, (self, that, f) => suspend(() => { const zipWith = Object.create(proto) - zipWith._tag = OpCodes.OP_ZIP_WITH + zipWith._op_layer = OpCodes.OP_ZIP_WITH zipWith.first = self zipWith.second = that zipWith.zipK = f @@ -1293,6 +1316,16 @@ const provideSomeRuntime = dual< /** @internal */ export const effect_provide = dual< { + ]>( + layers: Layers + ): ( + self: Effect.Effect + ) => Effect.Effect< + A, + E | { [k in keyof Layers]: Layer.Layer.Error }[number], + | { [k in keyof Layers]: Layer.Layer.Context }[number] + | Exclude }[number]> + > ( layer: Layer.Layer ): (self: Effect.Effect) => Effect.Effect> @@ -1307,6 +1340,15 @@ export const effect_provide = dual< ): (self: Effect.Effect) => Effect.Effect> }, { + ]>( + self: Effect.Effect, + layers: Layers + ): Effect.Effect< + A, + E | { [k in keyof Layers]: Layer.Layer.Error }[number], + | { [k in keyof Layers]: Layer.Layer.Context }[number] + | Exclude }[number]> + > ( self: Effect.Effect, layer: Layer.Layer @@ -1333,8 +1375,12 @@ export const effect_provide = dual< | Context.Context | Runtime.Runtime | ManagedRuntime.ManagedRuntime + | Array ): Effect.Effect> => { - if (isLayer(source)) { + if (Array.isArray(source)) { + // @ts-expect-error + return provideSomeLayer(self, mergeAll(...source)) + } else if (isLayer(source)) { return provideSomeLayer(self, source as Layer.Layer) } else if (Context.isContext(source)) { return core.provideSomeContext(self, source) diff --git a/packages/effect/src/internal/stm/core.ts b/packages/effect/src/internal/stm/core.ts index 062e0fb189..7c0f558715 100644 --- a/packages/effect/src/internal/stm/core.ts +++ b/packages/effect/src/internal/stm/core.ts @@ -56,7 +56,7 @@ export type Primitive = /** @internal */ type Op = STM.STM & Body & { - readonly _tag: OP_COMMIT + readonly _op: OP_COMMIT readonly effect_instruction_i0: Tag } @@ -150,7 +150,6 @@ const stmVariance = { /** @internal */ class STMPrimitive implements STM.STM { - public _tag = OP_COMMIT public _op = OP_COMMIT public effect_instruction_i1: any = undefined public effect_instruction_i2: any = undefined; @@ -481,7 +480,7 @@ export class STMDriver { try { const current = curr if (current) { - switch (current._tag) { + switch (current._op) { case "Tag": { curr = effect((_, __, env) => Context.unsafeGet(env, current)) as Primitive break diff --git a/packages/effect/test/Effect/service.test.ts b/packages/effect/test/Effect/service.test.ts new file mode 100644 index 0000000000..f515c80925 --- /dev/null +++ b/packages/effect/test/Effect/service.test.ts @@ -0,0 +1,171 @@ +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Scope from "effect/Scope" +import { describe, expect, it } from "effect/test/utils/extend" + +class Prefix extends Effect.Service()("Prefix", { + sync: () => ({ + prefix: "PRE" + }) +}) {} + +class Postfix extends Effect.Service()("Postfix", { + sync: () => ({ + postfix: "POST" + }) +}) {} + +const messages: Array = [] + +class Logger extends Effect.Service()("Logger", { + accessors: true, + effect: Effect.gen(function*() { + const { prefix } = yield* Prefix + const { postfix } = yield* Postfix + return { + info: (message: string) => + Effect.sync(() => { + messages.push(`[${prefix}][${message}][${postfix}]`) + }) + } + }), + dependencies: [Prefix.Default, Postfix.Default] +}) { + static Test = Layer.succeed(this, new Logger({ info: () => Effect.void })) +} + +class Scoped extends Effect.Service()("Scoped", { + accessors: true, + scoped: Effect.gen(function*() { + const { prefix } = yield* Prefix + const { postfix } = yield* Postfix + yield* Scope.Scope + return { + info: (message: string) => + Effect.sync(() => { + messages.push(`[${prefix}][${message}][${postfix}]`) + }) + } + }), + dependencies: [Prefix.Default, Postfix.Default] +}) {} + +describe("Effect.Service", () => { + it("make is a function", () => { + expect(pipe({ prefix: "OK" }, Prefix.make)).toBeInstanceOf(Prefix) + }) + it("tags is a tag and default is a layer", () => { + expect(Layer.isLayer(Logger.Default)).toBe(true) + expect(Layer.isLayer(Logger.DefaultWithoutDependencies)).toBe(true) + expect(Context.isTag(Logger)).toBe(true) + }) + + it.effect("correctly wires dependencies", () => + Effect.gen(function*() { + yield* Logger.info("Ok") + expect(messages).toEqual(["[PRE][Ok][POST]"]) + const { prefix } = yield* Prefix + expect(prefix).toEqual("PRE") + const { postfix } = yield* Postfix + expect(postfix).toEqual("POST") + expect(yield* Prefix.use((_) => _._tag)).toBe("Prefix") + }).pipe( + Effect.provide([ + Logger.Default, + Prefix.Default, + Postfix.Default + ]) + )) + + it.effect("inherits prototype", () => { + class Time extends Effect.Service