import * as Tracer from "effect/Tracer" import { SqlClient } from "effect/unstable/sql" import * as Effect from "./Effect.js" import * as Layer from "./Layer.js" import * as Option from "./Option.js" import { LocaleRef, RequestContext, spanAttributes } from "./RequestContext.js" import { NonEmptyString255 } from "./Schema.js" import { ContextMapContainer, storeId } from "./Store.js" const withSqlTransaction = (self: Effect.Effect): Effect.Effect => Effect.serviceOption(SqlClient.SqlClient).pipe( Effect.flatMap(Option.match({ onNone: () => self, onSome: (sql) => sql.withTransaction(self).pipe(Effect.orDie) })) ) export const getRequestContext = Effect .all({ span: Effect.currentSpan.pipe(Effect.orDie), locale: LocaleRef, namespace: storeId }) .pipe( Effect.map(({ locale, namespace, span }) => RequestContext.make({ span: Tracer.externalSpan(span), locale, namespace, name: NonEmptyString255(span.name) }) ) ) export const getRC = Effect.all({ locale: LocaleRef, namespace: storeId }) const withRequestSpan = (name = "request", options?: Tracer.SpanOptions) => (f: Effect.Effect) => Effect.andThen( getRC, (ctx) => f.pipe( Effect.withSpan(name, { ...options, attributes: { ...spanAttributes({ ...ctx, name: NonEmptyString255(name) }), ...options?.attributes } }, { captureStackTrace: options?.captureStackTrace ?? false }), // TODO: false // request context info is picked up directly in the logger for annotations. Effect.withLogSpan(name) ) ) export interface SetupRequestOptions { readonly withTransaction?: boolean } // Build `layer` against the ambient (request) scope rather than a sub-scope of the // returned Effect. Required when the returned value is a streaming HttpServerResponse: // the response body keeps producing chunks (and using layer-provided state) after the // Effect returns, so a sub-scope would close too early and run finalizers mid-stream. export const provideOnRequestScope = (layer: Layer.Layer) => (self: Effect.Effect) => Effect.gen(function*() { const requestScope = yield* Effect.scope // Fresh MemoMap per request: `Layer.buildWithScope` would otherwise reuse // the ambient MemoMap living on the HTTP server fiber, sharing the built // value (e.g. ContextMap) across every request handled by that server. const memoMap = yield* Layer.makeMemoMap const ctx = yield* Layer.buildWithMemoMap(layer, memoMap, requestScope) return yield* Effect.provide(self, ctx) }) export const setupRequestContextFromCurrent = (name = "request", options?: Tracer.SpanOptions & SetupRequestOptions) => (self: Effect.Effect) => self .pipe( options?.withTransaction === true ? withSqlTransaction : (_) => _, withRequestSpan(name, options), Effect.provide(ContextMapContainer.layer, { local: true }) ) // Streaming variant: binds ContextMapContainer to the ambient (request) scope so its // finalizer (clear()) runs only after the response body is fully drained, not when the // outer Effect returns its HttpServerResponse value. Use for handlers that return a // streaming HttpServerResponse (e.g. SSE) — see RequestContextMiddleware for context. export const setupStreamingRequestContextFromCurrent = (name = "request", options?: Tracer.SpanOptions & SetupRequestOptions) => (self: Effect.Effect) => self.pipe( options?.withTransaction === true ? withSqlTransaction : (_) => _, withRequestSpan(name, options), provideOnRequestScope(ContextMapContainer.layer) ) // TODO: consider integrating Effect.withParentSpan export function setupRequestContext( self: Effect.Effect, requestContext: RequestContext, options?: SetupRequestOptions ) { const layer = Layer.mergeAll( ContextMapContainer.layer, Layer.succeed(LocaleRef, requestContext.locale), Layer.succeed(storeId, requestContext.namespace) ) return self .pipe( options?.withTransaction === true ? withSqlTransaction : (_) => _, withRequestSpan(requestContext.name), Effect.provide(layer, { local: true }) ) } export function setupRequestContextWithCustomSpan( self: Effect.Effect, requestContext: RequestContext, name: string, options?: Tracer.SpanOptions & SetupRequestOptions ) { const layer = Layer.mergeAll( ContextMapContainer.layer, Layer.succeed(LocaleRef, requestContext.locale), Layer.succeed(storeId, requestContext.namespace) ) return self .pipe( options?.withTransaction === true ? withSqlTransaction : (_) => _, withRequestSpan(name, options), Effect.provide(layer, { local: true }) ) }