@livestore/utils-dev 0.4.0 → 0.5.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/node/DockerCompose/DockerCompose.d.ts +52 -0
  3. package/dist/node/DockerCompose/DockerCompose.d.ts.map +1 -0
  4. package/dist/node/DockerCompose/DockerCompose.js +158 -0
  5. package/dist/node/DockerCompose/DockerCompose.js.map +1 -0
  6. package/dist/node/DockerCompose/DockerCompose.test.d.ts +2 -0
  7. package/dist/node/DockerCompose/DockerCompose.test.d.ts.map +1 -0
  8. package/dist/node/{DockerComposeService/DockerComposeService.test.js → DockerCompose/DockerCompose.test.js} +16 -16
  9. package/dist/node/DockerCompose/DockerCompose.test.js.map +1 -0
  10. package/dist/node/DockerCompose/mod.d.ts +2 -0
  11. package/dist/node/DockerCompose/mod.d.ts.map +1 -0
  12. package/dist/node/DockerCompose/mod.js +2 -0
  13. package/dist/node/DockerCompose/mod.js.map +1 -0
  14. package/dist/node/FileLogger.d.ts +15 -8
  15. package/dist/node/FileLogger.d.ts.map +1 -1
  16. package/dist/node/FileLogger.js +63 -79
  17. package/dist/node/FileLogger.js.map +1 -1
  18. package/dist/node/cmd-log.d.ts.map +1 -1
  19. package/dist/node/cmd-log.js +19 -8
  20. package/dist/node/cmd-log.js.map +1 -1
  21. package/dist/node/cmd.d.ts +10 -12
  22. package/dist/node/cmd.d.ts.map +1 -1
  23. package/dist/node/cmd.js +76 -39
  24. package/dist/node/cmd.js.map +1 -1
  25. package/dist/node/cmd.test.js +19 -19
  26. package/dist/node/cmd.test.js.map +1 -1
  27. package/dist/node/mod.d.ts +4 -5
  28. package/dist/node/mod.d.ts.map +1 -1
  29. package/dist/node/mod.js +63 -60
  30. package/dist/node/mod.js.map +1 -1
  31. package/dist/node/workspace.d.ts +2 -2
  32. package/dist/node/workspace.d.ts.map +1 -1
  33. package/dist/node/workspace.js +7 -7
  34. package/dist/node/workspace.js.map +1 -1
  35. package/dist/node-vitest/Vitest.d.ts +8 -40
  36. package/dist/node-vitest/Vitest.d.ts.map +1 -1
  37. package/dist/node-vitest/Vitest.js +7 -78
  38. package/dist/node-vitest/Vitest.js.map +1 -1
  39. package/dist/node-vitest/mod.js +2 -2
  40. package/dist/wrangler/WranglerDevServer.d.ts +15 -20
  41. package/dist/wrangler/WranglerDevServer.d.ts.map +1 -1
  42. package/dist/wrangler/WranglerDevServer.js +77 -71
  43. package/dist/wrangler/WranglerDevServer.js.map +1 -1
  44. package/dist/wrangler/WranglerDevServer.test.js +28 -28
  45. package/dist/wrangler/WranglerDevServer.test.js.map +1 -1
  46. package/dist/wrangler/fixtures/cf-worker.d.ts.map +1 -1
  47. package/dist/wrangler/mod.d.ts +1 -1
  48. package/dist/wrangler/mod.d.ts.map +1 -1
  49. package/dist/wrangler/mod.js +1 -1
  50. package/dist/wrangler/mod.js.map +1 -1
  51. package/package.json +40 -58
  52. package/src/node/{DockerComposeService/DockerComposeService.test.ts → DockerCompose/DockerCompose.test.ts} +18 -18
  53. package/src/node/DockerCompose/DockerCompose.ts +328 -0
  54. package/src/node/DockerCompose/mod.ts +1 -0
  55. package/src/node/FileLogger.ts +89 -114
  56. package/src/node/cmd-log.ts +21 -10
  57. package/src/node/cmd.test.ts +19 -17
  58. package/src/node/cmd.ts +117 -88
  59. package/src/node/mod.ts +112 -104
  60. package/src/node/workspace.ts +9 -10
  61. package/src/node-vitest/Vitest.ts +21 -172
  62. package/src/wrangler/WranglerDevServer.test.ts +33 -37
  63. package/src/wrangler/WranglerDevServer.ts +145 -129
  64. package/src/wrangler/mod.ts +1 -6
  65. package/dist/node/DockerComposeService/DockerComposeService.d.ts +0 -63
  66. package/dist/node/DockerComposeService/DockerComposeService.d.ts.map +0 -1
  67. package/dist/node/DockerComposeService/DockerComposeService.js +0 -149
  68. package/dist/node/DockerComposeService/DockerComposeService.js.map +0 -1
  69. package/dist/node/DockerComposeService/DockerComposeService.test.d.ts +0 -2
  70. package/dist/node/DockerComposeService/DockerComposeService.test.d.ts.map +0 -1
  71. package/dist/node/DockerComposeService/DockerComposeService.test.js.map +0 -1
  72. package/dist/node-vitest/Vitest.test.d.ts +0 -10
  73. package/dist/node-vitest/Vitest.test.d.ts.map +0 -1
  74. package/dist/node-vitest/Vitest.test.js +0 -74
  75. package/dist/node-vitest/Vitest.test.js.map +0 -1
  76. package/src/node/DockerComposeService/DockerComposeService.ts +0 -343
  77. package/src/node-vitest/Vitest.test.ts +0 -117
  78. /package/src/node/{DockerComposeService → DockerCompose}/test-fixtures/docker-compose.yml +0 -0
package/src/node/mod.ts CHANGED
@@ -1,29 +1,27 @@
1
1
  import { performance } from 'node:perf_hooks'
2
2
 
3
- import * as OtelNodeSdk from '@effect/opentelemetry/NodeSdk'
4
3
  import * as otel from '@opentelemetry/api'
5
- import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'
6
- import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
7
- import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'
8
- import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'
9
-
10
- import { IS_BUN, isNonEmptyString } from '@livestore/utils'
11
- import type { Tracer } from '@livestore/utils/effect'
12
- import { Config, Effect, FiberRef, Layer, LogLevel, OtelTracer, Schema } from '@livestore/utils/effect'
4
+
5
+ import { IS_BUN } from '@livestore/utils'
6
+ import {
7
+ type Tracer,
8
+ Config,
9
+ Context,
10
+ Effect,
11
+ Exit,
12
+ FetchHttpClient,
13
+ Layer,
14
+ Option,
15
+ OtelTracer,
16
+ Otlp,
17
+ Schema,
18
+ } from '@livestore/utils/effect'
13
19
  import { OtelLiveDummy } from '@livestore/utils/node'
14
20
 
15
21
  export { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'
16
22
  export { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
17
23
  export * from './cmd.ts'
18
- export {
19
- type DockerComposeArgs,
20
- DockerComposeError,
21
- type DockerComposeOperations,
22
- DockerComposeService,
23
- type LogsOptions,
24
- type StartOptions,
25
- startDockerComposeServicesScoped,
26
- } from './DockerComposeService/DockerComposeService.ts'
24
+ export * from './DockerCompose/mod.ts'
27
25
  export * as FileLogger from './FileLogger.ts'
28
26
  export * from './workspace.ts'
29
27
 
@@ -41,62 +39,45 @@ export const OtelLiveHttp = ({
41
39
  rootSpanAttributes?: Record<string, unknown>
42
40
  skipLogUrl?: boolean
43
41
  traceNodeBootstrap?: boolean
44
- } = {}): Layer.Layer<OtelTracer.OtelTracer | Tracer.ParentSpan> =>
42
+ } = {}): Layer.Layer<OtelTracer.OtelTracer | Tracer.Tracer | Tracer.ParentSpan> =>
45
43
  Effect.gen(function* () {
46
44
  const configRes = yield* Config.all({
47
- exporterUrl: Config.string('OTEL_EXPORTER_OTLP_ENDPOINT').pipe(
48
- Config.validate({ message: 'OTEL_EXPORTER_OTLP_ENDPOINT must be set', validation: isNonEmptyString }),
49
- ),
50
- serviceName: serviceName !== undefined
51
- ? Config.succeed(serviceName)
52
- : Config.string('OTEL_SERVICE_NAME').pipe(Config.withDefault('livestore-utils-dev')),
53
- rootSpanName: rootSpanName !== undefined
54
- ? Config.succeed(rootSpanName)
55
- : Config.string('OTEL_ROOT_SPAN_NAME').pipe(Config.withDefault('RootSpan')),
45
+ exporterUrl: Config.NonEmptyString('OTEL_EXPORTER_OTLP_ENDPOINT'),
46
+ serviceName:
47
+ serviceName !== undefined
48
+ ? Config.succeed(serviceName)
49
+ : Config.String('OTEL_SERVICE_NAME').pipe(Config.withDefault('livestore-utils-dev')),
50
+ rootSpanName:
51
+ rootSpanName !== undefined
52
+ ? Config.succeed(rootSpanName)
53
+ : Config.String('OTEL_ROOT_SPAN_NAME').pipe(Config.withDefault('RootSpan')),
56
54
  }).pipe(Effect.option)
57
55
 
58
56
  if (configRes._tag === 'None') {
59
57
  const RootSpanLive = Layer.span('DummyRoot', {})
60
- return RootSpanLive.pipe(Layer.provideMerge(OtelLiveDummy)) as any
58
+ return withTracerReference(RootSpanLive.pipe(Layer.provideMerge(OtelLiveDummy)))
61
59
  }
62
60
 
63
61
  const config = configRes.value
64
62
 
65
- const resource = { serviceName: config.serviceName }
66
-
67
- const metricReader = new PeriodicExportingMetricReader({
68
- exporter: new OTLPMetricExporter({ url: `${config.exporterUrl}/v1/metrics` }),
69
- exportIntervalMillis: 1000,
70
- })
71
-
72
- const OtelLive = OtelNodeSdk.layer(() => ({
73
- resource,
74
- metricReader,
75
- spanProcessor: new BatchSpanProcessor(
76
- new OTLPTraceExporter({ url: `${config.exporterUrl}/v1/traces`, headers: {} }),
77
- { scheduledDelayMillis: 50 },
78
- ),
79
- })).pipe(
80
- // If an OpenTelemetry backend is not available, the `OtelNodeSdk` layer
81
- // will ignore the error when attempting to connect and emit a debug log
82
- // stating the reason for the error (in this case `ECONNREFUSED`). This
83
- // can cause problems for programs which rely on clean `stdout` (e.g.
84
- // command-line applications). To remedy this, the below code sets the
85
- // minimum log level `FiberRef` to `"None"` for the duration of the
86
- // `OtelNodeSdk`'s layer constructor.
87
- //
88
- // This can likely be removed when Livestore is migrated to the Effect
89
- // native Otlp exporters.
90
- Layer.locally(FiberRef.currentMinimumLogLevel, LogLevel.None),
91
- )
63
+ const OtelLive = Otlp.layerJson({
64
+ baseUrl: config.exporterUrl,
65
+ metricsExportInterval: 1000,
66
+ tracerExportInterval: 50,
67
+ resource: { serviceName: config.serviceName },
68
+ }).pipe(Layer.provide(FetchHttpClient.layer))
69
+ const OtelTracerLive = Layer.succeed(OtelTracer.OtelTracer, otel.trace.getTracer(config.serviceName))
92
70
 
93
71
  const RootSpanLive = Layer.span(config.rootSpanName, {
94
72
  attributes: { config, ...rootSpanAttributes },
95
- onEnd: skipLogUrl === true ? undefined : (span: any) => logTraceUiUrlForSpan()(span.span),
73
+ onEnd: skipLogUrl === true ? undefined : (span) => logTraceUiUrlForTraceId()(span.traceId),
96
74
  parent: parentSpan,
97
75
  })
98
76
 
99
- const layer = yield* Layer.memoize(RootSpanLive.pipe(Layer.provideMerge(OtelLive)))
77
+ const baseLayer = RootSpanLive.pipe(Layer.provideMerge(Layer.mergeAll(OtelLive, OtelTracerLive)))
78
+
79
+ const memoMap = yield* Layer.makeMemoMap
80
+ const layer = Layer.fromBuild((_, scope) => Layer.buildWithMemoMap(baseLayer, memoMap, scope))
100
81
 
101
82
  if (traceNodeBootstrap === true && IS_BUN === false) {
102
83
  /**
@@ -104,35 +85,48 @@ export const OtelLiveHttp = ({
104
85
  * Note: Skipped in Bun since performance.nodeTiming is not properly supported.
105
86
  */
106
87
  yield* Effect.gen(function* () {
107
- const tracer = yield* OtelTracer.OtelTracer
108
- const currentSpan = yield* OtelTracer.currentOtelSpan
88
+ const tracer = yield* Effect.tracer
89
+ const currentSpan = yield* Effect.currentSpan
109
90
 
110
91
  const { nodeTiming, endAbs, durationAttr } = computeBootstrapTiming()
111
92
 
112
- const bootSpan = tracer.startSpan(
113
- 'node-bootstrap',
114
- {
115
- startTime: nodeTiming.nodeStart,
116
- attributes: {
117
- 'node.timing.nodeStart': nodeTiming.nodeStart,
118
- 'node.timing.environment': nodeTiming.environment,
119
- 'node.timing.bootstrapComplete': nodeTiming.bootstrapComplete,
120
- 'node.timing.loopStart': nodeTiming.loopStart,
121
- 'node.timing.duration': durationAttr,
122
- },
123
- },
124
- otel.trace.setSpanContext(otel.context.active(), currentSpan.spanContext()),
125
- )
126
-
127
- bootSpan.end(endAbs)
93
+ const bootSpan = tracer.span({
94
+ name: 'node-bootstrap',
95
+ parent: Option.some(currentSpan),
96
+ annotations: Context.empty(),
97
+ links: [],
98
+ startTime: millisToNanos(
99
+ IS_BUN === true && typeof nodeTiming.nodeStart === 'number'
100
+ ? nodeTiming.nodeStart
101
+ : performance.timeOrigin + nodeTiming.nodeStart,
102
+ ),
103
+ kind: 'internal',
104
+ root: false,
105
+ sampled: true,
106
+ })
107
+
108
+ for (const [key, value] of Object.entries({
109
+ 'node.timing.nodeStart': nodeTiming.nodeStart,
110
+ 'node.timing.environment': nodeTiming.environment,
111
+ 'node.timing.bootstrapComplete': nodeTiming.bootstrapComplete,
112
+ 'node.timing.loopStart': nodeTiming.loopStart,
113
+ 'node.timing.duration': durationAttr,
114
+ })) {
115
+ if (value !== undefined) bootSpan.attribute(key, value)
116
+ }
117
+
118
+ bootSpan.end(millisToNanos(endAbs), Exit.void)
128
119
  }).pipe(Effect.provide(layer), Effect.orDie)
129
120
  }
130
121
 
131
- return layer
132
- }).pipe(Layer.unwrapScoped) as any
122
+ return withTracerReference(layer)
123
+ }).pipe(Layer.unwrap)
133
124
 
134
125
  export const logTraceUiUrlForSpan = (printMsg?: (url: string) => string) => (span: otel.Span) =>
135
- getTracingBackendUrl(span).pipe(
126
+ logTraceUiUrlForTraceId(printMsg)(span.spanContext().traceId)
127
+
128
+ const logTraceUiUrlForTraceId = (printMsg?: (url: string) => string) => (traceId: string) =>
129
+ getTracingBackendUrl(traceId).pipe(
136
130
  Effect.tap((url) => {
137
131
  if (url === undefined) {
138
132
  return Effect.logWarning('No tracing backend url found')
@@ -146,21 +140,21 @@ export const logTraceUiUrlForSpan = (printMsg?: (url: string) => string) => (spa
146
140
  }),
147
141
  )
148
142
 
149
- export const getTracingBackendUrl = (span: otel.Span) =>
143
+ export const getTracingBackendUrl = (traceIdOrSpan: string | otel.Span) =>
150
144
  Effect.gen(function* () {
151
- const endpoint = yield* Config.string('GRAFANA_ENDPOINT').pipe(Config.option, Effect.orDie)
145
+ const endpoint = yield* Config.String('GRAFANA_ENDPOINT').pipe(Config.option)
152
146
  if (endpoint._tag === 'None') return
153
147
 
154
- const traceId = span.spanContext().traceId
148
+ const traceId = typeof traceIdOrSpan === 'string' ? traceIdOrSpan : traceIdOrSpan.spanContext().traceId
155
149
 
156
150
  // Grafana + Tempo
157
151
 
158
152
  const grafanaEndpoint = endpoint.value
159
- const left = yield* Schema.encode(Schema.parseJson())({
153
+ const left = yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))({
160
154
  datasource: 'tempo',
161
155
  queries: [{ query: traceId, queryType: 'traceql', refId: 'A' }],
162
156
  range: { from: 'now-1h', to: 'now' },
163
- }).pipe(Effect.orDie)
157
+ })
164
158
  const searchParams = new URLSearchParams({
165
159
  orgId: '1',
166
160
  left,
@@ -168,7 +162,7 @@ export const getTracingBackendUrl = (span: otel.Span) =>
168
162
 
169
163
  // TODO make dynamic via env var
170
164
  return `${grafanaEndpoint}/explore?${searchParams.toString()}`
171
- })
165
+ }).pipe(Effect.orDie)
172
166
 
173
167
  /**
174
168
  * Compute absolute start/end timestamps for the Node.js bootstrap span in a
@@ -190,29 +184,43 @@ const computeBootstrapTiming = () => {
190
184
  const nodeTiming = performance.nodeTiming
191
185
 
192
186
  // Absolute start time in ms since epoch.
193
- const startAbs = IS_BUN === true
194
- ? typeof nodeTiming.nodeStart === 'number'
195
- ? nodeTiming.nodeStart
196
- : performance.timeOrigin
197
- : performance.timeOrigin + nodeTiming.nodeStart
187
+ const startAbs =
188
+ IS_BUN === true
189
+ ? typeof nodeTiming.nodeStart === 'number'
190
+ ? nodeTiming.nodeStart
191
+ : performance.timeOrigin
192
+ : performance.timeOrigin + nodeTiming.nodeStart
198
193
 
199
194
  // Absolute end time.
200
- const endAbs = IS_BUN === true
201
- ? (() => {
202
- const { loopStart, bootstrapComplete } = nodeTiming
203
- if (typeof loopStart === 'number' && loopStart > 0) return startAbs + loopStart
204
- if (typeof bootstrapComplete === 'number' && bootstrapComplete >= startAbs) return bootstrapComplete
205
- return startAbs + 1
206
- })()
207
- : startAbs + nodeTiming.duration
195
+ const endAbs =
196
+ IS_BUN === true
197
+ ? (() => {
198
+ const { loopStart, bootstrapComplete } = nodeTiming
199
+ if (typeof loopStart === 'number' && loopStart > 0) return startAbs + loopStart
200
+ if (typeof bootstrapComplete === 'number' && bootstrapComplete >= startAbs) return bootstrapComplete
201
+ return startAbs + 1
202
+ })()
203
+ : startAbs + nodeTiming.duration
208
204
 
209
205
  // Duration attribute value for the span.
210
- const durationAttr = IS_BUN === true
211
- ? (() => {
212
- const { loopStart } = nodeTiming
213
- return typeof loopStart === 'number' && loopStart > 0 ? loopStart : 0
214
- })()
215
- : nodeTiming.duration
206
+ const durationAttr =
207
+ IS_BUN === true
208
+ ? (() => {
209
+ const { loopStart } = nodeTiming
210
+ return typeof loopStart === 'number' && loopStart > 0 ? loopStart : 0
211
+ })()
212
+ : nodeTiming.duration
216
213
 
217
214
  return { nodeTiming, startAbs, endAbs, durationAttr } as const
218
215
  }
216
+
217
+ const millisToNanos = (millis: number): bigint => BigInt(Math.round(millis * 1_000_000))
218
+
219
+ /**
220
+ * Effect v4 installs `Tracer.Tracer` through a `Context.Reference` fiber ref.
221
+ * The OTEL layers set that reference at runtime, but their layer output type is
222
+ * `never`, so we localize the public compatibility assertion here.
223
+ */
224
+ const withTracerReference = <ROut, E, RIn>(
225
+ layer: Layer.Layer<ROut, E, RIn>,
226
+ ): Layer.Layer<ROut | Tracer.Tracer, E, RIn> => layer as Layer.Layer<ROut | Tracer.Tracer, E, RIn>
@@ -6,33 +6,32 @@ import { Context, Effect, Layer } from '@livestore/utils/effect'
6
6
  export type WorkspaceInfo = string
7
7
 
8
8
  /** Current working directory. */
9
- export class CurrentWorkingDirectory extends Context.Tag('CurrentWorkingDirectory')<
10
- CurrentWorkingDirectory,
11
- WorkspaceInfo
12
- >() {
9
+ export class CurrentWorkingDirectory extends Context.Service<CurrentWorkingDirectory, WorkspaceInfo>()(
10
+ 'CurrentWorkingDirectory',
11
+ ) {
13
12
  /** Layer that captures the process cwd once. */
14
13
  static live = Layer.effect(
15
14
  CurrentWorkingDirectory,
16
- Effect.sync(() => process.cwd()),
15
+ Effect.sync(() => CurrentWorkingDirectory.of(process.cwd())),
17
16
  )
18
17
 
19
18
  /** Override CWD for tests or nested invocations. */
20
- static fromPath = (cwd: string) => Layer.succeed(CurrentWorkingDirectory, cwd)
19
+ static fromPath = (cwd: string) => Layer.succeed(CurrentWorkingDirectory, CurrentWorkingDirectory.of(cwd))
21
20
  }
22
21
 
23
22
  /** Livestore workspace root (env required). */
24
- export class LivestoreWorkspace extends Context.Tag('LivestoreWorkspace')<LivestoreWorkspace, WorkspaceInfo>() {
23
+ export class LivestoreWorkspace extends Context.Service<LivestoreWorkspace, WorkspaceInfo>()('LivestoreWorkspace') {
25
24
  /** Resolve from WORKSPACE_ROOT env. */
26
25
  static live = Layer.effect(
27
26
  LivestoreWorkspace,
28
27
  Effect.sync(() => {
29
28
  const root = process.env.WORKSPACE_ROOT ?? shouldNeverHappen('WORKSPACE_ROOT is not set')
30
- return root
29
+ return LivestoreWorkspace.of(root)
31
30
  }),
32
31
  )
33
32
 
34
33
  /** Provide a fixed Livestore root. */
35
- static fromPath = (root: string) => Layer.succeed(LivestoreWorkspace, root)
34
+ static fromPath = (root: string) => Layer.succeed(LivestoreWorkspace, LivestoreWorkspace.of(root))
36
35
 
37
36
  /** Derive a CurrentWorkingDirectory layer from the Livestore workspace root (with optional subpath) */
38
37
  static toCwd = (/** Relative path to the Livestore workspace root */ subPath?: string) =>
@@ -40,7 +39,7 @@ export class LivestoreWorkspace extends Context.Tag('LivestoreWorkspace')<Livest
40
39
  CurrentWorkingDirectory,
41
40
  Effect.gen(function* () {
42
41
  const root = yield* LivestoreWorkspace
43
- return path.join(root, subPath ?? '')
42
+ return CurrentWorkingDirectory.of(path.join(root, subPath ?? ''))
44
43
  }),
45
44
  )
46
45
  }
@@ -3,18 +3,7 @@ import * as inspector from 'node:inspector'
3
3
  import type * as Vitest from '@effect/vitest'
4
4
 
5
5
  import { IS_CI } from '@livestore/utils'
6
- import {
7
- type Cause,
8
- Duration,
9
- Effect,
10
- type FastCheck as FC,
11
- identity,
12
- Layer,
13
- type OtelTracer,
14
- Predicate,
15
- type Schema,
16
- type Scope,
17
- } from '@livestore/utils/effect'
6
+ import { type Cause, Duration, Effect, identity, Layer, type OtelTracer, type Scope } from '@livestore/utils/effect'
18
7
  import { OtelLiveDummy } from '@livestore/utils/node'
19
8
 
20
9
  import { OtelLiveHttp } from '../node/mod.ts'
@@ -23,24 +12,27 @@ export * from '@effect/vitest'
23
12
 
24
13
  export const DEBUGGER_ACTIVE = Boolean(process.env.DEBUGGER_ACTIVE ?? inspector.url() !== undefined)
25
14
 
15
+ type WithoutTestCtxServices<R> = Exclude<Exclude<R, OtelTracer.OtelTracer>, Scope.Scope>
16
+ type WithoutLayerAndTestCtxServices<R, ROut> = Exclude<Exclude<R, ROut | OtelTracer.OtelTracer>, Scope.Scope>
17
+
26
18
  export const makeWithTestCtx: <ROut = never, E1 = never, RIn = never>(
27
19
  ctxParams: WithTestCtxParams<ROut, E1, RIn>,
28
20
  ) => (testContext: Vitest.TestContext) => <A, E, R>(
29
21
  self: Effect.Effect<A, E, R>,
30
22
  ) => Effect.Effect<
31
23
  A,
32
- E | E1 | Cause.TimeoutException,
24
+ E | E1 | Cause.TimeoutError,
33
25
  // Exclude dependencies provided by `withTestCtx` from the layer dependencies
34
- | Exclude<RIn, OtelTracer.OtelTracer | Scope.Scope>
26
+ | WithoutTestCtxServices<RIn>
35
27
  // Exclude dependencies provided by `withTestCtx` **and** dependencies produced
36
28
  // by the layer from the effect dependencies
37
- | Exclude<R, ROut | OtelTracer.OtelTracer | Scope.Scope>
29
+ | WithoutLayerAndTestCtxServices<R, ROut>
38
30
  > = (ctxParams) => (testContext: Vitest.TestContext) => withTestCtx(testContext, ctxParams)
39
31
 
40
32
  export type WithTestCtxParams<ROut, E1, RIn> = {
41
33
  suffix?: string
42
34
  makeLayer?: (testContext: Vitest.TestContext) => Layer.Layer<ROut, E1, RIn | Scope.Scope>
43
- timeout?: Duration.DurationInput
35
+ timeout?: Duration.Input
44
36
  forceOtel?: boolean
45
37
  }
46
38
 
@@ -54,8 +46,8 @@ export const withTestCtx =
54
46
  forceOtel = false,
55
47
  }: {
56
48
  suffix?: string
57
- makeLayer?: (testContext: Vitest.TestContext) => Layer.Layer<ROut, E1, RIn>
58
- timeout?: Duration.DurationInput
49
+ makeLayer?: (testContext: Vitest.TestContext) => Layer.Layer<ROut, E1, RIn | Scope.Scope>
50
+ timeout?: Duration.Input
59
51
  forceOtel?: boolean
60
52
  } = {},
61
53
  ) =>
@@ -63,15 +55,19 @@ export const withTestCtx =
63
55
  self: Effect.Effect<A, E, R>,
64
56
  ): Effect.Effect<
65
57
  A,
66
- E | E1 | Cause.TimeoutException,
58
+ E | E1 | Cause.TimeoutError,
67
59
  // Exclude dependencies provided internally from the provided layer's dependencies
68
- | Exclude<RIn, OtelTracer.OtelTracer | Scope.Scope>
60
+ | WithoutTestCtxServices<RIn>
69
61
  // Exclude dependencies provided internally **and** dependencies produced by the
70
62
  // provided layer from the effect dependencies
71
- | Exclude<R, ROut | OtelTracer.OtelTracer | Scope.Scope>
63
+ | WithoutLayerAndTestCtxServices<R, ROut>
72
64
  > => {
73
65
  const spanName = `${testContext.task.suite?.name}:${testContext.task.name}${suffix !== undefined ? `:${suffix}` : ''}`
74
- const layer = makeLayer?.(testContext) ?? Layer.empty
66
+ // `Layer.empty` provides `never`, which is narrower than an arbitrary generic
67
+ // `ROut`; widen it for the no-layer branch so `Effect.provide` can typecheck.
68
+ const layer: Layer.Layer<ROut, E1, RIn | Scope.Scope> =
69
+ makeLayer?.(testContext) ?? (Layer.empty as unknown as Layer.Layer<ROut, E1, RIn | Scope.Scope>)
70
+ const timeoutDuration = Duration.fromInputUnsafe(timeout)
75
71
 
76
72
  const otelLayer =
77
73
  DEBUGGER_ACTIVE === true || forceOtel === true
@@ -84,159 +80,12 @@ export const withTestCtx =
84
80
  DEBUGGER_ACTIVE === true
85
81
  ? identity
86
82
  : Effect.logWarnIfTakesLongerThan({
87
- duration: Duration.toMillis(timeout) * 0.8,
88
- label: `${spanName} approaching timeout (timeout: ${Duration.format(timeout)})`,
83
+ duration: Duration.toMillis(timeoutDuration) * 0.8,
84
+ label: `${spanName} approaching timeout (timeout: ${Duration.format(timeoutDuration)})`,
89
85
  }),
90
86
  DEBUGGER_ACTIVE === true ? identity : Effect.timeout(timeout),
91
87
  Effect.provide(combinedLayer),
92
88
  Effect.scoped, // We need to scope the effect manually here because otherwise the span is not closed
93
89
  Effect.annotateLogs({ suffix }),
94
- ) as any
95
- }
96
-
97
- /**
98
- * Shared properties for all enhanced test context phases
99
- */
100
- export interface EnhancedTestContextBase {
101
- numRuns: number
102
- /** 0-based index */
103
- runIndex: number
104
- /** Total number of executions including initial runs and shrinking attempts */
105
- totalExecutions: number
106
- }
107
-
108
- /**
109
- * Enhanced context for property-based tests that includes shrinking phase information
110
- */
111
- export type EnhancedTestContext =
112
- | (EnhancedTestContextBase & {
113
- _tag: 'initial'
114
- })
115
- | (EnhancedTestContextBase & {
116
- _tag: 'shrinking'
117
- /** Number of shrinking attempts */
118
- shrinkAttempt: number
119
- })
120
-
121
- /**
122
- * Normalizes propOptions to ensure @effect/vitest receives correct fastCheck structure
123
- */
124
- const normalizePropOptions = <Arbs extends Vitest.Vitest.Arbitraries>(
125
- propOptions:
126
- | number
127
- | (Vitest.TestOptions & {
128
- fastCheck?: FC.Parameters<{
129
- [K in keyof Arbs]: Arbs[K] extends FC.Arbitrary<infer T> ? T : Schema.Schema.Type<Arbs[K]>
130
- }>
131
- }),
132
- ): Vitest.TestOptions & {
133
- fastCheck?: FC.Parameters<{
134
- [K in keyof Arbs]: Arbs[K] extends FC.Arbitrary<infer T> ? T : Schema.Schema.Type<Arbs[K]>
135
- }>
136
- } => {
137
- // If it's a number, treat as timeout and add our default fastCheck
138
- if (Predicate.isObject(propOptions) === false) {
139
- return {
140
- timeout: propOptions,
141
- fastCheck: { numRuns: 100 },
142
- }
143
- }
144
-
145
- // If no fastCheck property, add it with our default numRuns
146
- if (propOptions.fastCheck == null) {
147
- return {
148
- ...propOptions,
149
- fastCheck: { numRuns: 100 },
150
- }
151
- }
152
-
153
- // If fastCheck exists but no numRuns, add our default
154
- if (propOptions.fastCheck !== undefined && propOptions.fastCheck.numRuns == null) {
155
- return {
156
- ...propOptions,
157
- fastCheck: {
158
- ...propOptions.fastCheck,
159
- numRuns: 100,
160
- },
161
- }
90
+ )
162
91
  }
163
-
164
- // If everything is properly structured, pass through
165
- return propOptions
166
- }
167
-
168
- /**
169
- * Equivalent to Vitest.prop but provides enhanced context including shrinking progress visibility
170
- *
171
- * This function enhances the standard property-based testing by providing clear information about
172
- * whether FastCheck is in the initial testing phase or the shrinking phase, solving the confusion
173
- * where tests show "Run 26/6" when FastCheck's shrinking algorithm is active.
174
- *
175
- * TODO: allow for upper timelimit instead of / additional to `numRuns`
176
- *
177
- * TODO: Upstream to Effect
178
- */
179
- export const asProp = <Arbs extends Vitest.Vitest.Arbitraries, A, E, R>(
180
- api: Vitest.Vitest.Tester<R>,
181
- name: string,
182
- arbitraries: Arbs,
183
- test: Vitest.Vitest.TestFunction<
184
- A,
185
- E,
186
- R,
187
- [
188
- { [K in keyof Arbs]: Arbs[K] extends FC.Arbitrary<infer T> ? T : Schema.Schema.Type<Arbs[K]> },
189
- Vitest.TestContext,
190
- EnhancedTestContext,
191
- ]
192
- >,
193
- propOptions:
194
- | number
195
- | (Vitest.TestOptions & {
196
- fastCheck?: FC.Parameters<{
197
- [K in keyof Arbs]: Arbs[K] extends FC.Arbitrary<infer T> ? T : Schema.Schema.Type<Arbs[K]>
198
- }>
199
- }),
200
- ) => {
201
- const normalizedPropOptions = normalizePropOptions(propOptions)
202
- const numRuns = normalizedPropOptions.fastCheck?.numRuns ?? 100
203
- let runIndex = 0
204
- let shrinkAttempts = 0
205
- let totalExecutions = 0
206
-
207
- return api.prop(
208
- name,
209
- arbitraries,
210
- (properties, ctx) => {
211
- if (ctx.signal.aborted === true) {
212
- return ctx.skip('Test aborted')
213
- }
214
-
215
- totalExecutions++
216
- const isInShrinkingPhase = runIndex >= numRuns
217
-
218
- if (isInShrinkingPhase === true) {
219
- shrinkAttempts++
220
- }
221
-
222
- const enhancedContext: EnhancedTestContext =
223
- isInShrinkingPhase === true
224
- ? {
225
- _tag: 'shrinking',
226
- numRuns,
227
- runIndex: runIndex++,
228
- shrinkAttempt: shrinkAttempts,
229
- totalExecutions,
230
- }
231
- : {
232
- _tag: 'initial',
233
- numRuns,
234
- runIndex: runIndex++,
235
- totalExecutions,
236
- }
237
-
238
- return test(properties, ctx, enhancedContext)
239
- },
240
- normalizedPropOptions,
241
- )
242
- }