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

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 (76) 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 +73 -36
  24. package/dist/node/cmd.js.map +1 -1
  25. package/dist/node/cmd.test.js +17 -17
  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 +58 -55
  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 +17 -8
  36. package/dist/node-vitest/Vitest.d.ts.map +1 -1
  37. package/dist/node-vitest/Vitest.js +5 -5
  38. package/dist/node-vitest/Vitest.js.map +1 -1
  39. package/dist/node-vitest/Vitest.test.d.ts +3 -5
  40. package/dist/node-vitest/Vitest.test.d.ts.map +1 -1
  41. package/dist/node-vitest/Vitest.test.js +14 -3
  42. package/dist/node-vitest/Vitest.test.js.map +1 -1
  43. package/dist/wrangler/WranglerDevServer.d.ts +15 -20
  44. package/dist/wrangler/WranglerDevServer.d.ts.map +1 -1
  45. package/dist/wrangler/WranglerDevServer.js +77 -71
  46. package/dist/wrangler/WranglerDevServer.js.map +1 -1
  47. package/dist/wrangler/WranglerDevServer.test.js +28 -28
  48. package/dist/wrangler/WranglerDevServer.test.js.map +1 -1
  49. package/dist/wrangler/mod.d.ts +1 -1
  50. package/dist/wrangler/mod.d.ts.map +1 -1
  51. package/dist/wrangler/mod.js +1 -1
  52. package/dist/wrangler/mod.js.map +1 -1
  53. package/package.json +19 -37
  54. package/src/node/{DockerComposeService/DockerComposeService.test.ts → DockerCompose/DockerCompose.test.ts} +18 -18
  55. package/src/node/DockerCompose/DockerCompose.ts +328 -0
  56. package/src/node/DockerCompose/mod.ts +1 -0
  57. package/src/node/FileLogger.ts +89 -114
  58. package/src/node/cmd-log.ts +21 -10
  59. package/src/node/cmd.test.ts +19 -17
  60. package/src/node/cmd.ts +117 -86
  61. package/src/node/mod.ts +112 -104
  62. package/src/node/workspace.ts +9 -10
  63. package/src/node-vitest/Vitest.test.ts +22 -3
  64. package/src/node-vitest/Vitest.ts +32 -42
  65. package/src/wrangler/WranglerDevServer.test.ts +33 -37
  66. package/src/wrangler/WranglerDevServer.ts +145 -129
  67. package/src/wrangler/mod.ts +1 -6
  68. package/dist/node/DockerComposeService/DockerComposeService.d.ts +0 -63
  69. package/dist/node/DockerComposeService/DockerComposeService.d.ts.map +0 -1
  70. package/dist/node/DockerComposeService/DockerComposeService.js +0 -149
  71. package/dist/node/DockerComposeService/DockerComposeService.js.map +0 -1
  72. package/dist/node/DockerComposeService/DockerComposeService.test.d.ts +0 -2
  73. package/dist/node/DockerComposeService/DockerComposeService.test.d.ts.map +0 -1
  74. package/dist/node/DockerComposeService/DockerComposeService.test.js.map +0 -1
  75. package/src/node/DockerComposeService/DockerComposeService.ts +0 -343
  76. /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
  }
@@ -14,7 +14,7 @@ Vitest.describe('Vitest.asProp', () => {
14
14
 
15
15
  // Always-passing test - should only show initial phase
16
16
  Vitest.asProp(
17
- Vitest.scopedLive,
17
+ Vitest.live,
18
18
  'always-pass test (shows only initial runs)',
19
19
  [IntArbitrary],
20
20
  (properties, _ctx, enhanced) =>
@@ -38,10 +38,29 @@ Vitest.describe('Vitest.asProp', () => {
38
38
  { fastCheck: { numRuns: 4 } },
39
39
  )
40
40
 
41
+ Vitest.asProp(
42
+ Vitest.live,
43
+ 'converts record-form schemas to arbitraries',
44
+ {
45
+ count: Schema.Int,
46
+ payload: Schema.Struct({
47
+ label: Schema.String,
48
+ note: Schema.optional(Schema.String),
49
+ }),
50
+ },
51
+ ({ count, payload }) =>
52
+ Effect.sync(() => {
53
+ Vitest.expect(Number.isInteger(count)).toBe(true)
54
+ Vitest.expect(typeof payload.label).toBe('string')
55
+ Vitest.expect(payload.note === undefined || typeof payload.note === 'string').toBe(true)
56
+ }),
57
+ { fastCheck: { numRuns: 10 } },
58
+ )
59
+
41
60
  // Failing test - should show initial + shrinking phases
42
61
  let alreadyFailed = false
43
62
  Vitest.asProp(
44
- Vitest.scopedLive,
63
+ Vitest.live,
45
64
  'failing test (shows initial runs + shrinking)',
46
65
  [IntArbitrary],
47
66
  (properties, ctx, enhanced) =>
@@ -79,7 +98,7 @@ Vitest.describe('Vitest.asProp', () => {
79
98
 
80
99
  // Test with endOnFailure: true - should not show shrinking
81
100
  Vitest.asProp(
82
- Vitest.scopedLive,
101
+ Vitest.live,
83
102
  'failing test with endOnFailure (no shrinking)',
84
103
  [IntArbitrary],
85
104
  (properties, _ctx, enhanced) =>
@@ -23,24 +23,27 @@ export * from '@effect/vitest'
23
23
 
24
24
  export const DEBUGGER_ACTIVE = Boolean(process.env.DEBUGGER_ACTIVE ?? inspector.url() !== undefined)
25
25
 
26
+ type WithoutTestCtxServices<R> = Exclude<Exclude<R, OtelTracer.OtelTracer>, Scope.Scope>
27
+ type WithoutLayerAndTestCtxServices<R, ROut> = Exclude<Exclude<R, ROut | OtelTracer.OtelTracer>, Scope.Scope>
28
+
26
29
  export const makeWithTestCtx: <ROut = never, E1 = never, RIn = never>(
27
30
  ctxParams: WithTestCtxParams<ROut, E1, RIn>,
28
31
  ) => (testContext: Vitest.TestContext) => <A, E, R>(
29
32
  self: Effect.Effect<A, E, R>,
30
33
  ) => Effect.Effect<
31
34
  A,
32
- E | E1 | Cause.TimeoutException,
35
+ E | E1 | Cause.TimeoutError,
33
36
  // Exclude dependencies provided by `withTestCtx` from the layer dependencies
34
- | Exclude<RIn, OtelTracer.OtelTracer | Scope.Scope>
37
+ | WithoutTestCtxServices<RIn>
35
38
  // Exclude dependencies provided by `withTestCtx` **and** dependencies produced
36
39
  // by the layer from the effect dependencies
37
- | Exclude<R, ROut | OtelTracer.OtelTracer | Scope.Scope>
40
+ | WithoutLayerAndTestCtxServices<R, ROut>
38
41
  > = (ctxParams) => (testContext: Vitest.TestContext) => withTestCtx(testContext, ctxParams)
39
42
 
40
43
  export type WithTestCtxParams<ROut, E1, RIn> = {
41
44
  suffix?: string
42
45
  makeLayer?: (testContext: Vitest.TestContext) => Layer.Layer<ROut, E1, RIn | Scope.Scope>
43
- timeout?: Duration.DurationInput
46
+ timeout?: Duration.Input
44
47
  forceOtel?: boolean
45
48
  }
46
49
 
@@ -54,8 +57,8 @@ export const withTestCtx =
54
57
  forceOtel = false,
55
58
  }: {
56
59
  suffix?: string
57
- makeLayer?: (testContext: Vitest.TestContext) => Layer.Layer<ROut, E1, RIn>
58
- timeout?: Duration.DurationInput
60
+ makeLayer?: (testContext: Vitest.TestContext) => Layer.Layer<ROut, E1, RIn | Scope.Scope>
61
+ timeout?: Duration.Input
59
62
  forceOtel?: boolean
60
63
  } = {},
61
64
  ) =>
@@ -63,15 +66,19 @@ export const withTestCtx =
63
66
  self: Effect.Effect<A, E, R>,
64
67
  ): Effect.Effect<
65
68
  A,
66
- E | E1 | Cause.TimeoutException,
69
+ E | E1 | Cause.TimeoutError,
67
70
  // Exclude dependencies provided internally from the provided layer's dependencies
68
- | Exclude<RIn, OtelTracer.OtelTracer | Scope.Scope>
71
+ | WithoutTestCtxServices<RIn>
69
72
  // Exclude dependencies provided internally **and** dependencies produced by the
70
73
  // provided layer from the effect dependencies
71
- | Exclude<R, ROut | OtelTracer.OtelTracer | Scope.Scope>
74
+ | WithoutLayerAndTestCtxServices<R, ROut>
72
75
  > => {
73
76
  const spanName = `${testContext.task.suite?.name}:${testContext.task.name}${suffix !== undefined ? `:${suffix}` : ''}`
74
- const layer = makeLayer?.(testContext) ?? Layer.empty
77
+ // `Layer.empty` provides `never`, which is narrower than an arbitrary generic
78
+ // `ROut`; widen it for the no-layer branch so `Effect.provide` can typecheck.
79
+ const layer: Layer.Layer<ROut, E1, RIn | Scope.Scope> =
80
+ makeLayer?.(testContext) ?? (Layer.empty as unknown as Layer.Layer<ROut, E1, RIn | Scope.Scope>)
81
+ const timeoutDuration = Duration.fromInputUnsafe(timeout)
75
82
 
76
83
  const otelLayer =
77
84
  DEBUGGER_ACTIVE === true || forceOtel === true
@@ -84,14 +91,14 @@ export const withTestCtx =
84
91
  DEBUGGER_ACTIVE === true
85
92
  ? identity
86
93
  : Effect.logWarnIfTakesLongerThan({
87
- duration: Duration.toMillis(timeout) * 0.8,
88
- label: `${spanName} approaching timeout (timeout: ${Duration.format(timeout)})`,
94
+ duration: Duration.toMillis(timeoutDuration) * 0.8,
95
+ label: `${spanName} approaching timeout (timeout: ${Duration.format(timeoutDuration)})`,
89
96
  }),
90
97
  DEBUGGER_ACTIVE === true ? identity : Effect.timeout(timeout),
91
98
  Effect.provide(combinedLayer),
92
99
  Effect.scoped, // We need to scope the effect manually here because otherwise the span is not closed
93
100
  Effect.annotateLogs({ suffix }),
94
- ) as any
101
+ )
95
102
  }
96
103
 
97
104
  /**
@@ -121,19 +128,17 @@ export type EnhancedTestContext =
121
128
  /**
122
129
  * Normalizes propOptions to ensure @effect/vitest receives correct fastCheck structure
123
130
  */
131
+ type ArbitraryValues<Arbs extends Vitest.Vitest.Arbitraries> = {
132
+ [K in keyof Arbs]: Arbs[K] extends FC.Arbitrary<infer T> ? T : Arbs[K] extends Schema.Schema<infer T> ? T : never
133
+ }
134
+
135
+ type PropOptions<Arbs extends Vitest.Vitest.Arbitraries> = Omit<Vitest.TestOptions, 'fastCheck'> & {
136
+ fastCheck?: FC.Parameters<ArbitraryValues<Arbs>>
137
+ }
138
+
124
139
  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
- } => {
140
+ propOptions: number | PropOptions<Arbs>,
141
+ ): PropOptions<Arbs> => {
137
142
  // If it's a number, treat as timeout and add our default fastCheck
138
143
  if (Predicate.isObject(propOptions) === false) {
139
144
  return {
@@ -180,23 +185,8 @@ export const asProp = <Arbs extends Vitest.Vitest.Arbitraries, A, E, R>(
180
185
  api: Vitest.Vitest.Tester<R>,
181
186
  name: string,
182
187
  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
- }),
188
+ test: Vitest.Vitest.TestFunction<A, E, R, [ArbitraryValues<Arbs>, Vitest.TestContext, EnhancedTestContext]>,
189
+ propOptions: number | PropOptions<Arbs>,
200
190
  ) => {
201
191
  const normalizedPropOptions = normalizePropOptions(propOptions)
202
192
  const numRuns = normalizedPropOptions.fastCheck?.numRuns ?? 100