1/**
2 * Creates refreshable references to a single layer-built service context.
3 *
4 * A `LayerRef<I, E>` builds one `Layer` into a cached `Context<I>`, exposes it
5 * back as a layer or scoped effect, and supports invalidation so later users can
6 * acquire a fresh context.
7 *
8 * @since 4.0.0
9 */
10import * as Context from "./Context.ts"
11import type * as Duration from "./Duration.ts"
12import * as Effect from "./Effect.ts"
13import { identity } from "./Function.ts"
14import { getStackTraceLimit, setStackTraceLimit } from "./internal/stackTraceLimit.ts"
15import * as Layer from "./Layer.ts"
16import * as RcRef from "./RcRef.ts"
17import type * as Schedule from "./Schedule.ts"
18import * as Scope from "./Scope.ts"
19import type { Mutable } from "./Types.ts"
20
21const TypeId = "~effect/LayerRef"
22
23/**
24 * A refreshable reference to a single layer-built service context.
25 *
26 * **When to use**
27 *
28 * Use when you want to share one scoped layer resource across many users while
29 * retaining the ability to invalidate it and rebuild it later.
30 *
31 * **Details**
32 *
33 * A `LayerRef` is the unkeyed counterpart to a layer cache: it lazily builds the
34 * layer on first use, reuses the resulting context while it is borrowed or kept
35 * idle, and can invalidate the cached context so the next use rebuilds it.
36 *
37 * @see {@link make} for constructing a `LayerRef` from a layer
38 * @see {@link Service} for defining a `LayerRef` as a service class
39 *
40 * @category models
41 * @since 4.0.0
42 */
43export interface LayerRef<in out I, in out E = never> {
44 readonly [TypeId]: typeof TypeId
45
46 /**
47 * The underlying reference-counted cache that stores the built context.
48 */
49 readonly rcRef: RcRef.RcRef<Context.Context<I>, E>
50
51 /**
52 * Layer that provides the currently cached context, acquiring it if needed.
53 */
54 readonly get: Layer.Layer<I, E>
55
56 /**
57 * Scoped effect that retrieves the currently cached context, acquiring it if needed.
58 */
59 readonly contextEffect: Effect.Effect<Context.Context<I>, E, Scope.Scope>
60
61 /**
62 * Invalidates the cached context so the next use rebuilds the layer.
63 */
64 readonly invalidate: Effect.Effect<void>
65
66 /**
67 * Invalidates the cached context so the next use rebuilds the layer, and
68 * reacquires it.
69 */
70 readonly refresh: Effect.Effect<void, E>
71}
72
73/**
74 * Creates a `LayerRef` from a `Layer`.
75 *
76 * **When to use**
77 *
78 * Use when you have one layer-built resource that should be shared, optionally
79 * kept alive while idle, and refreshed on demand.
80 *
81 * **Details**
82 *
83 * The layer is built lazily on first use unless `preload` is `true`.
84 * `idleTimeToLive` keeps the context cached after it stops being used, and
85 * `invalidationSchedule` can periodically invalidate it. When `preload` is
86 * `true`, scheduled invalidation also reacquires the context.
87 *
88 * **Gotchas**
89 *
90 * Invalidation does not revoke contexts already borrowed by active scopes; those
91 * contexts remain usable until their scopes close.
92 *
93 * **Example** (Sharing one layer-built service)
94 *
95 * ```ts import.meta.vitest
96 * import { Context, Effect, Layer, LayerRef } from "effect"
97 *
98 * class Database extends Context.Service<Database, {
99 * readonly query: Effect.Effect<string>
100 * }>()("Database") {}
101 *
102 * const databaseLayer = Layer.succeed(Database, {
103 * query: Effect.succeed("result")
104 * })
105 *
106 * const query = Effect.gen(function*() {
107 * const database = yield* Database
108 * return yield* database.query
109 * })
110 *
111 * const program = Effect.scoped(
112 * Effect.gen(function*() {
113 * const ref = yield* LayerRef.make(databaseLayer, {
114 * idleTimeToLive: "5 seconds"
115 * })
116 *
117 * const result = yield* Effect.provide(query, ref.get)
118 *
119 * yield* ref.invalidate
120 *
121 * return result
122 * })
123 * )
124 *
125 * await Effect.runPromise(program) // => "result"
126 * ```
127 *
128 * @see {@link Service} for defining a reusable service class around a `LayerRef`
129 *
130 * @category constructors
131 * @since 4.0.0
132 */
133export const make = Effect.fnUntraced(
134 function*<I, E, R, X, const Preload extends boolean = never, SE = never, SR = never>(
135 layer: Layer.Layer<I, E, R>,
136 options?: {
137 /**
138 * Duration to keep the resource alive after it is no longer used.
139 */
140 readonly idleTimeToLive?: Duration.Input | undefined
141 /**
142 * Whether to acquire the resource during creation.
143 */
144 readonly preload?: Preload | undefined
145 /**
146 * Schedule used to invalidate the cached resource. When `preload` is
147 * `true`, each scheduled invalidation also reacquires the resource.
148 */
149 readonly invalidationSchedule?: Schedule.Schedule<X, unknown, SE, SR> | undefined
150 } | undefined
151 ): Effect.fn.Return<
152 LayerRef<I, E>,
153 [Preload] extends [true] ? E : never,
154 Scope.Scope | R | SR
155 > {
156 const context = yield* Effect.context<never>()
157 const memoMap = Layer.CurrentMemoMap.forkOrCreate(context)
158
159 const rcRef = yield* RcRef.make({
160 acquire: Effect.contextWith((_: Context.Context<Scope.Scope>) =>
161 Layer.buildWithMemoMap(layer, memoMap, Context.get(_, Scope.Scope))
162 ),
163 idleTimeToLive: options?.idleTimeToLive
164 })
165
166 const refresh = RcRef.invalidate(rcRef).pipe(
167 Effect.andThen(Effect.scoped(RcRef.get(rcRef))),
168 Effect.asVoid
169 )
170
171 if (options?.preload) {
172 yield* refresh as Effect.Effect<void>
173 }
174
175 if (options?.invalidationSchedule) {
176 const onRefresh = options.preload ? refresh : RcRef.invalidate(rcRef)
177 yield* onRefresh.pipe(
178 Effect.ignoreCause,
179 Effect.schedule(options.invalidationSchedule),
180 Effect.forkScoped
181 )
182 }
183
184 return identity<LayerRef<I, any>>({
185 [TypeId]: TypeId,
186 rcRef,
187 get: Layer.effectContext(RcRef.get(rcRef)),
188 contextEffect: RcRef.get(rcRef),
189 invalidate: RcRef.invalidate(rcRef),
190 refresh
191 })
192 }
193)
194
195/**
196 * Service class shape produced by `LayerRef.Service`.
197 *
198 * **When to use**
199 *
200 * Use as the public type for classes returned by `LayerRef.Service` when an API
201 * needs to accept, return, or alias the generated service class and its static
202 * helpers.
203 *
204 * **Details**
205 *
206 * It combines a `Context.Service` tag for the `LayerRef` with default layers and
207 * helper accessors for retrieving, using, and invalidating the cached resource.
208 *
209 * @see {@link Service} for creating concrete `LayerRef` service classes
210 *
211 * @category services
212 * @since 4.0.0
213 */
214export interface TagClass<
215 in out Self,
216 in out Id extends string,
217 in out I,
218 in out E,
219 in out R,
220 in out LE,
221 in out Deps extends Layer.Layer<any, any, any>
222> extends Context.ServiceClass<Self, Id, LayerRef<I, E>> {
223 /**
224 * Default layer for the `LayerRef` service, with dependencies applied.
225 */
226 readonly layer: Layer.Layer<
227 Self,
228 (Deps extends Layer.Layer<infer _A, infer _E, infer _R> ? _E : never) | LE,
229 | Exclude<R, (Deps extends Layer.Layer<infer _A, infer _E, infer _R> ? _A : never)>
230 | (Deps extends Layer.Layer<infer _A, infer _E, infer _R> ? _R : never)
231 >
232
233 /**
234 * Default layer for the `LayerRef` service without provided dependencies.
235 */
236 readonly layerNoDeps: Layer.Layer<Self, LE, R>
237
238 /**
239 * Layer that provides the currently cached context, requiring this service.
240 */
241 readonly get: Layer.Layer<I, E, Self>
242
243 /**
244 * Scoped effect that retrieves the currently cached context through this service.
245 */
246 readonly contextEffect: Effect.Effect<Context.Context<I>, E, Scope.Scope | Self>
247
248 /**
249 * Invalidates the cached context through this service.
250 */
251 readonly invalidate: Effect.Effect<void, never, Self>
252
253 /**
254 * Invalidates the cached context through this service, and reacquires it.
255 */
256 readonly refresh: Effect.Effect<void, E, Self>
257}
258
259/**
260 * Creates a service class for a `LayerRef`.
261 *
262 * **When to use**
263 *
264 * Use when you want to name a shared layer reference as an application service
265 * and expose static helpers for providing, retrieving, and invalidating it.
266 *
267 * **Details**
268 *
269 * The returned class is a `Context.Service` whose value is a `LayerRef`. It also
270 * includes `.layer`, `.layerNoDeps`, `.get`, `.contextEffect`, and `.invalidate`
271 * helpers so callers do not need to access the `LayerRef` value directly.
272 *
273 * **Example** (Defining a refreshable service)
274 *
275 * ```ts import.meta.vitest
276 * import { Context, Effect, Layer, LayerRef } from "effect"
277 *
278 * class Database extends Context.Service<Database, {
279 * readonly query: Effect.Effect<string>
280 * }>()("Database") {}
281 *
282 * const databaseLayer = Layer.succeed(Database, {
283 * query: Effect.succeed("result")
284 * })
285 *
286 * class DatabaseRef extends LayerRef.Service<DatabaseRef>()("DatabaseRef", {
287 * layer: databaseLayer,
288 * preload: true
289 * }) {}
290 *
291 * const program = Effect.gen(function*() {
292 * const database = yield* Database
293 * return yield* database.query
294 * }).pipe(
295 * Effect.provide(DatabaseRef.get),
296 * Effect.provide(DatabaseRef.layer)
297 * )
298 *
299 * await Effect.runPromise(program) // => "result"
300 * ```
301 *
302 * @see {@link make} for creating a `LayerRef` value without defining a service class
303 *
304 * @category services
305 * @since 4.0.0
306 */
307export const Service = <Self>() =>
308<
309 const Id extends string,
310 I,
311 E,
312 R,
313 X,
314 const Deps extends ReadonlyArray<Layer.Layer<any, any, any>> = [],
315 const Preload extends boolean = never,
316 SE = never,
317 SR = never
318>(
319 id: Id,
320 options: {
321 /**
322 * Layer used to build the cached context.
323 */
324 readonly layer: Layer.Layer<I, E, R>
325 /**
326 * Layers used to satisfy dependencies of the `LayerRef` service layer.
327 */
328 readonly dependencies?: Deps | undefined
329 /**
330 * Duration to keep the resource alive after it is no longer used.
331 */
332 readonly idleTimeToLive?: Duration.Input | undefined
333 /**
334 * Whether to acquire the resource during creation.
335 */
336 readonly preload?: Preload | undefined
337 /**
338 * Schedule used to invalidate the cached resource. When `preload` is
339 * `true`, each scheduled invalidation also reacquires the resource.
340 */
341 readonly invalidationSchedule?: Schedule.Schedule<X, unknown, SE, SR> | undefined
342 }
343): TagClass<
344 Self,
345 Id,
346 I,
347 E,
348 R | SR,
349 [Preload] extends [true] ? E : never,
350 Deps[number]
351> => {
352 const limit = getStackTraceLimit()
353 let creationError: Error | undefined
354 if (limit !== 0) {
355 setStackTraceLimit(2)
356 creationError = new globalThis.Error()
357 setStackTraceLimit(limit)
358 }
359
360 function TagClass() {}
361 const TagClass_ = TagClass as any as Mutable<TagClass<Self, Id, any, any, any, any, any>>
362 Object.setPrototypeOf(TagClass, Object.getPrototypeOf(Context.Service<Self, any>(id)))
363 TagClass.key = id
364 Object.defineProperty(TagClass, "stack", {
365 get() {
366 return creationError?.stack
367 }
368 })
369
370 TagClass_.layerNoDeps = Layer.effect(TagClass_)(
371 make(options.layer, options)
372 )
373 TagClass_.layer = options.dependencies && options.dependencies.length > 0 ?
374 Layer.provide(TagClass_.layerNoDeps, options.dependencies as any) :
375 TagClass_.layerNoDeps
376
377 TagClass_.get = Layer.unwrap(TagClass_.useSync((ref) => ref.get))
378 TagClass_.contextEffect = TagClass_.use((ref) => ref.contextEffect)
379 TagClass_.invalidate = TagClass_.use((ref) => ref.invalidate)
380 TagClass_.refresh = TagClass_.use((ref) => ref.refresh)
381
382 return TagClass as any
383}