Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Alternative Effect.Service suggestion #3778

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 87 additions & 36 deletions packages/effect/src/Effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import type * as HashSet from "./HashSet.js"
import type { TypeLambda } from "./HKT.js"
import * as _console from "./internal/console.js"
import { TagProto } from "./internal/context.js"
import { TagProto, TagTypeId } from "./internal/context.js"
import * as effect from "./internal/core-effect.js"
import * as core from "./internal/core.js"
import * as defaultServices from "./internal/defaultServices.js"
Expand Down Expand Up @@ -57,7 +57,7 @@
import type * as Scope from "./Scope.js"
import type * as Supervisor from "./Supervisor.js"
import type * as Tracer from "./Tracer.js"
import type { Concurrency, Contravariant, Covariant, NoExcessProperties, NoInfer, NotFunction } from "./Types.js"

Check failure on line 60 in packages/effect/src/Effect.ts

View workflow job for this annotation

GitHub Actions / Snapshot

'Contravariant' is declared but never used.

Check failure on line 60 in packages/effect/src/Effect.ts

View workflow job for this annotation

GitHub Actions / Lint

'Contravariant' is defined but never used. Allowed unused vars must match /^_/u

Check failure on line 60 in packages/effect/src/Effect.ts

View workflow job for this annotation

GitHub Actions / Types

'Contravariant' is declared but never used.

Check failure on line 60 in packages/effect/src/Effect.ts

View workflow job for this annotation

GitHub Actions / Types

'Contravariant' is declared but never used.
import type * as Unify from "./Unify.js"
import type { YieldWrap } from "./Utils.js"

Expand Down Expand Up @@ -6330,6 +6330,10 @@
return makeTagProxy(TagClass as any)
}

type SupportDependencies = Record<
string,
Layer.Layer.Any | Layer.Layer<never> | Context.Tag<any, any> & { Default: Layer.Layer.Any | Layer.Layer<never> }
>
/**
* @since 3.9.0
* @category context
Expand All @@ -6338,86 +6342,96 @@
export const Service: <Self>() => {
<
const Key extends string,
Dependencies extends SupportDependencies,
const Make extends
| {
readonly scoped: Effect<Service.AllowedType<Key, Make>, any, any>
readonly dependencies?: ReadonlyArray<Layer.Layer.Any>
readonly scoped: (
deps: Service.MakeDepsM<{ dependencies: Dependencies }>
) => Effect<Service.AllowedType<Key, Make>, any, any>
readonly accessors?: boolean
/** @deprecated */
readonly ಠ_ಠ: never
}
| {
readonly effect: Effect<Service.AllowedType<Key, Make>, any, any>
readonly dependencies?: ReadonlyArray<Layer.Layer.Any>
readonly effect: (
deps: Service.MakeDepsM<{ dependencies: Dependencies }>
) => Effect<Service.AllowedType<Key, Make>, any, any>
readonly accessors?: boolean
/** @deprecated */
readonly ಠ_ಠ: never
}
| {
readonly sync: LazyArg<Service.AllowedType<Key, Make>>
readonly dependencies?: ReadonlyArray<Layer.Layer.Any>
readonly sync: (deps: Service.MakeDepsM<{ dependencies: Dependencies }>) => Service.AllowedType<Key, Make>
readonly accessors?: boolean
/** @deprecated */
readonly ಠ_ಠ: never
}
| {
readonly succeed: Service.AllowedType<Key, Make>
readonly dependencies?: ReadonlyArray<Layer.Layer.Any>
readonly accessors?: boolean
/** @deprecated */
readonly ಠ_ಠ: never
}
>(
key: Key,
dependencies: Dependencies,
make: Make
): Service.Class<Self, Key, Make>
): Service.Class<Self, Key, Make & { dependencies: Dependencies }>
<
const Key extends string,
Dependencies extends SupportDependencies,
const Make extends NoExcessProperties<{
readonly scoped: Effect<Service.AllowedType<Key, Make>, any, any>
readonly dependencies?: ReadonlyArray<Layer.Layer.Any>
readonly scoped: (
deps: Service.MakeDepsM<{ dependencies: Dependencies }>
) => Effect<Service.AllowedType<Key, Make>, any, any>
readonly accessors?: boolean
}, Make>
>(
key: Key,
dependencies: Dependencies,
make: Make
): Service.Class<Self, Key, Make>
): Service.Class<Self, Key, Make & { dependencies: Dependencies }>
<
const Key extends string,
Dependencies extends SupportDependencies,
const Make extends NoExcessProperties<{
readonly effect: Effect<Service.AllowedType<Key, Make>, any, any>
readonly dependencies?: ReadonlyArray<Layer.Layer.Any>
readonly effect: (
deps: Service.MakeDepsM<{ dependencies: Dependencies }>
) => Effect<Service.AllowedType<Key, Make>, any, any>
readonly accessors?: boolean
}, Make>
>(
key: Key,
dependencies: Dependencies,
make: Make
): Service.Class<Self, Key, Make>
): Service.Class<Self, Key, Make & { dependencies: Dependencies }>
<
const Key extends string,
Dependencies extends SupportDependencies,
const Make extends NoExcessProperties<{
readonly sync: LazyArg<Service.AllowedType<Key, Make>>
readonly dependencies?: ReadonlyArray<Layer.Layer.Any>
readonly sync: (deps: Service.MakeDepsM<{ dependencies: Dependencies }>) => Service.AllowedType<Key, Make>
readonly accessors?: boolean
}, Make>
>(
key: Key,
dependencies: Dependencies,
make: Make
): Service.Class<Self, Key, Make>
): Service.Class<Self, Key, Make & { dependencies: Dependencies }>
<
const Key extends string,
Dependencies extends SupportDependencies,
const Make extends NoExcessProperties<{
readonly succeed: Service.AllowedType<Key, Make>
readonly dependencies?: ReadonlyArray<Layer.Layer.Any>
readonly accessors?: boolean
}, Make>
>(
key: Key,
dependencies: Dependencies,
make: Make
): Service.Class<Self, Key, Make>
): Service.Class<Self, Key, Make & { dependencies: Dependencies }>
} = function() {
return function() {
const [id, maker] = arguments
const [id, dependencies, maker] = arguments
const proxy = "accessors" in maker ? maker["accessors"] : false
const limit = Error.stackTraceLimit
Error.stackTraceLimit = 2
Expand Down Expand Up @@ -6465,25 +6479,37 @@
}
})

const hasDeps = "dependencies" in maker && maker.dependencies.length > 0
const hasDeps = Object.keys(dependencies).length > 0
const layerName = hasDeps ? "DefaultWithoutDependencies" : "Default"
let layerCache: Layer.Layer.Any | undefined
const deps = hasDeps
? Object.keys(dependencies as Record<string, Layer.Layer.Any | Context.Tag<any, any>>).reduce((acc, cur) => {
if (TagTypeId in dependencies[cur]) acc[cur[0].toLowerCase() + cur.slice(1)] = dependencies[cur]
return acc
}, {} as Record<string, any>)
: {}
if ("effect" in maker) {
Object.defineProperty(TagClass, layerName, {
get(this: any) {
return layerCache ??= layer.fromEffect(TagClass, map(maker.effect, (_) => new this(_)))
return layerCache ??= layer.fromEffect(
TagClass,
all(deps).pipe(flatMap(maker.effect), map((_) => new this(_)))
)
}
})
} else if ("scoped" in maker) {
Object.defineProperty(TagClass, layerName, {
get(this: any) {
return layerCache ??= layer.scoped(TagClass, map(maker.scoped, (_) => new this(_)))
return layerCache ??= layer.scoped(TagClass, all(deps).pipe(flatMap(maker.scoped), map((_) => new this(_))))
}
})
} else if ("sync" in maker) {
Object.defineProperty(TagClass, layerName, {
get(this: any) {
return layerCache ??= layer.sync(TagClass, () => new this(maker.sync()))
return layerCache ??= layer.fromEffect(
TagClass,
all(deps).pipe(map(maker.sync), map((_) => new this(_)))
)
}
})
} else {
Expand All @@ -6500,7 +6526,7 @@
get(this: any) {
return layerWithDepsCache ??= layer.provide(
this.DefaultWithoutDependencies,
maker.dependencies
Object.values(dependencies).map((_) => "Default" in _ ? _["Default"] : _) as any
)
}
})
Expand Down Expand Up @@ -6582,37 +6608,62 @@
/**
* @since 3.9.0
*/
export type MakeService<Make> = Make extends { readonly effect: Effect<infer _A, infer _E, infer _R> } ? _A
: Make extends { readonly scoped: Effect<infer _A, infer _E, infer _R> } ? _A
: Make extends { readonly sync: LazyArg<infer A> } ? A
export type MakeService<Make> = Make extends
{ readonly effect: (deps: any) => Effect<infer _A, infer _E, infer _R> } ? _A
: Make extends { readonly scoped: (deps: any) => Effect<infer _A, infer _E, infer _R> } ? _A
: Make extends { readonly sync: (deps: any) => infer A } ? A
: Make extends { readonly succeed: infer A } ? A
: never

/**
* @since 3.9.0
*/
export type MakeError<Make> = Make extends { readonly effect: Effect<infer _A, infer _E, infer _R> } ? _E
: Make extends { readonly scoped: Effect<infer _A, infer _E, infer _R> } ? _E
export type MakeError<Make> = Make extends { readonly effect: (deps: any) => Effect<infer _A, infer _E, infer _R> } ?
_E
: Make extends { readonly scoped: (deps: any) => Effect<infer _A, infer _E, infer _R> } ? _E
: never

/**
* @since 3.9.0
*/
export type MakeContext<Make> = Make extends { readonly effect: Effect<infer _A, infer _E, infer _R> } ? _R
: Make extends { readonly scoped: Effect<infer _A, infer _E, infer _R> } ? Exclude<_R, Scope.Scope>
export type MakeContext<Make> = Make extends
{ readonly effect: (deps: any) => Effect<infer _A, infer _E, infer _R> } ? _R
: Make extends { readonly scoped: (deps: any) => Effect<infer _A, infer _E, infer _R> } ? Exclude<_R, Scope.Scope>
: never

type Values<T> = T[keyof T]

/**
* @since 3.9.0
*/
export type MakeDeps<Make> = Make extends { readonly dependencies: ReadonlyArray<Layer.Layer.Any> }
? Make["dependencies"][number]
export type MakeDeps<Make> = Make extends
{ readonly dependencies: Record<string, Layer.Layer.Any | { Default: Layer.Layer.Any }> } ? Values<
{
[K in keyof Make["dependencies"]]: Make["dependencies"][K] extends { Default: Layer.Layer.Any }
? Make["dependencies"][K]["Default"]
: Make["dependencies"][K] extends Layer.Layer.Any ? Make["dependencies"][K]
: never
}
>
: never

/**
* @since 3.9.0
*/
export type MakeDepsOut<Make> = Contravariant.Type<MakeDeps<Make>[Layer.LayerTypeId]["_ROut"]>
export type MakeDepsOut<Make> = MakeDeps<Make>[Layer.LayerTypeId]["_ROut"]

export type LowerFirst<S extends PropertyKey> = S extends `${infer First}${infer Rest}` ? `${Lowercase<First>}${Rest}`
: S
export type ToLowerFirst<T extends Record<string, any>> = {
[key in keyof T as LowerFirst<key>]: T[key]
}
export type MakeDepsM<Make> = Make extends { readonly dependencies: Record<string, any> } ? ToLowerFirst<
{
[K in keyof Make["dependencies"] as Make["dependencies"][K] extends Context.Tag<any, any> ? K : never]:
Context.Tag.Service<Make["dependencies"][K]>
}
>
: never

/**
* @since 3.9.0
Expand Down
57 changes: 25 additions & 32 deletions packages/effect/test/Effect/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,51 +5,44 @@ 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>()("Prefix", {
class Prefix extends Effect.Service<Prefix>()("Prefix", {}, {
sync: () => ({
prefix: "PRE"
})
}) {}

class Postfix extends Effect.Service<Postfix>()("Postfix", {
class Postfix extends Effect.Service<Postfix>()("Postfix", {}, {
sync: () => ({
postfix: "POST"
})
}) {}

const messages: Array<string> = []

class Logger extends Effect.Service<Logger>()("Logger", {
class Logger extends Effect.Service<Logger>()("Logger", { Postfix, Prefix }, {
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]
sync: ({ postfix, prefix }) => ({
info: (message: string) =>
Effect.sync(() => {
messages.push(`[${prefix.prefix}][${message}][${postfix.postfix}]`)
})
})
}) {
static Test = Layer.succeed(this, new Logger({ info: () => Effect.void }))
}

class Scoped extends Effect.Service<Scoped>()("Scoped", {
class Scoped extends Effect.Service<Scoped>()("Scoped", { Prefix, Postfix }, {
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]
scoped: ({ postfix, prefix }) =>
Effect.gen(function*() {
yield* Scope.Scope
return {
info: (message: string) =>
Effect.sync(() => {
messages.push(`[${prefix.prefix}][${message}][${postfix.postfix}]`)
})
}
})
}) {}

describe("Effect.Service", () => {
Expand Down Expand Up @@ -80,7 +73,7 @@ describe("Effect.Service", () => {
))

it.effect("inherits prototype", () => {
class Time extends Effect.Service<Time>()("Time", {
class Time extends Effect.Service<Time>()("Time", {}, {
sync: () => ({}),
accessors: true
}) {
Expand All @@ -102,7 +95,7 @@ describe("Effect.Service", () => {
return this.#now ||= new Date()
}
}
class Time extends Effect.Service<Time>()("Time", {
class Time extends Effect.Service<Time>()("Time", {}, {
sync: () => new DateTest(),
accessors: true
}) {
Expand All @@ -128,8 +121,8 @@ describe("Effect.Service", () => {
}
}

class Time extends Effect.Service<Time>()("Time", {
effect: Effect.sync(() => new TimeLive()),
class Time extends Effect.Service<Time>()("Time", {}, {
effect: () => Effect.sync(() => new TimeLive()),
accessors: true
}) {}

Expand All @@ -144,7 +137,7 @@ describe("Effect.Service", () => {

it.effect("js primitive", () =>
Effect.gen(function*() {
class MapThing extends Effect.Service<MapThing>()("MapThing", {
class MapThing extends Effect.Service<MapThing>()("MapThing", {}, {
sync: () => new Map<string, number>(),
accessors: true
}) {}
Expand Down
Loading