@livestore/utils-dev 0.4.0-dev.8 → 0.4.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 (59) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/node/DockerComposeService/DockerComposeService.d.ts +9 -4
  3. package/dist/node/DockerComposeService/DockerComposeService.d.ts.map +1 -1
  4. package/dist/node/DockerComposeService/DockerComposeService.js +74 -49
  5. package/dist/node/DockerComposeService/DockerComposeService.js.map +1 -1
  6. package/dist/node/DockerComposeService/DockerComposeService.test.js +2 -2
  7. package/dist/node/DockerComposeService/DockerComposeService.test.js.map +1 -1
  8. package/dist/node/FileLogger.d.ts.map +1 -1
  9. package/dist/node/FileLogger.js +3 -3
  10. package/dist/node/FileLogger.js.map +1 -1
  11. package/dist/node/cmd-log.d.ts +21 -0
  12. package/dist/node/cmd-log.d.ts.map +1 -0
  13. package/dist/node/cmd-log.js +47 -0
  14. package/dist/node/cmd-log.js.map +1 -0
  15. package/dist/node/cmd.d.ts +12 -4
  16. package/dist/node/cmd.d.ts.map +1 -1
  17. package/dist/node/cmd.js +207 -29
  18. package/dist/node/cmd.js.map +1 -1
  19. package/dist/node/cmd.test.d.ts +2 -0
  20. package/dist/node/cmd.test.d.ts.map +1 -0
  21. package/dist/node/cmd.test.js +103 -0
  22. package/dist/node/cmd.test.js.map +1 -0
  23. package/dist/node/mod.d.ts +3 -2
  24. package/dist/node/mod.d.ts.map +1 -1
  25. package/dist/node/mod.js +63 -18
  26. package/dist/node/mod.js.map +1 -1
  27. package/dist/node/workspace.d.ts +22 -0
  28. package/dist/node/workspace.d.ts.map +1 -0
  29. package/dist/node/workspace.js +26 -0
  30. package/dist/node/workspace.js.map +1 -0
  31. package/dist/node-vitest/Vitest.d.ts.map +1 -1
  32. package/dist/node-vitest/Vitest.js +11 -11
  33. package/dist/node-vitest/Vitest.js.map +1 -1
  34. package/dist/node-vitest/Vitest.test.d.ts +8 -0
  35. package/dist/node-vitest/Vitest.test.d.ts.map +1 -1
  36. package/dist/node-vitest/Vitest.test.js +11 -7
  37. package/dist/node-vitest/Vitest.test.js.map +1 -1
  38. package/dist/wrangler/WranglerDevServer.d.ts +21 -4
  39. package/dist/wrangler/WranglerDevServer.d.ts.map +1 -1
  40. package/dist/wrangler/WranglerDevServer.js +23 -10
  41. package/dist/wrangler/WranglerDevServer.js.map +1 -1
  42. package/dist/wrangler/WranglerDevServer.test.js +3 -3
  43. package/dist/wrangler/WranglerDevServer.test.js.map +1 -1
  44. package/package.json +74 -21
  45. package/src/node/DockerComposeService/DockerComposeService.test.ts +5 -2
  46. package/src/node/DockerComposeService/DockerComposeService.ts +152 -101
  47. package/src/node/DockerComposeService/test-fixtures/docker-compose.yml +1 -1
  48. package/src/node/FileLogger.ts +4 -3
  49. package/src/node/cmd-log.ts +84 -0
  50. package/src/node/cmd.test.ts +134 -0
  51. package/src/node/cmd.ts +380 -62
  52. package/src/node/mod.ts +69 -21
  53. package/src/node/workspace.ts +46 -0
  54. package/src/node-vitest/Vitest.test.ts +12 -7
  55. package/src/node-vitest/Vitest.ts +28 -24
  56. package/src/wrangler/WranglerDevServer.test.ts +5 -3
  57. package/src/wrangler/WranglerDevServer.ts +54 -13
  58. package/src/wrangler/fixtures/wrangler.toml +1 -1
  59. package/dist/.tsbuildinfo.json +0 -1
@@ -0,0 +1,46 @@
1
+ import path from 'node:path'
2
+
3
+ import { shouldNeverHappen } from '@livestore/utils'
4
+ import { Context, Effect, Layer } from '@livestore/utils/effect'
5
+
6
+ export type WorkspaceInfo = string
7
+
8
+ /** Current working directory. */
9
+ export class CurrentWorkingDirectory extends Context.Tag('CurrentWorkingDirectory')<
10
+ CurrentWorkingDirectory,
11
+ WorkspaceInfo
12
+ >() {
13
+ /** Layer that captures the process cwd once. */
14
+ static live = Layer.effect(
15
+ CurrentWorkingDirectory,
16
+ Effect.sync(() => process.cwd()),
17
+ )
18
+
19
+ /** Override CWD for tests or nested invocations. */
20
+ static fromPath = (cwd: string) => Layer.succeed(CurrentWorkingDirectory, cwd)
21
+ }
22
+
23
+ /** Livestore workspace root (env required). */
24
+ export class LivestoreWorkspace extends Context.Tag('LivestoreWorkspace')<LivestoreWorkspace, WorkspaceInfo>() {
25
+ /** Resolve from WORKSPACE_ROOT env. */
26
+ static live = Layer.effect(
27
+ LivestoreWorkspace,
28
+ Effect.sync(() => {
29
+ const root = process.env.WORKSPACE_ROOT ?? shouldNeverHappen('WORKSPACE_ROOT is not set')
30
+ return root
31
+ }),
32
+ )
33
+
34
+ /** Provide a fixed Livestore root. */
35
+ static fromPath = (root: string) => Layer.succeed(LivestoreWorkspace, root)
36
+
37
+ /** Derive a CurrentWorkingDirectory layer from the Livestore workspace root (with optional subpath) */
38
+ static toCwd = (/** Relative path to the Livestore workspace root */ subPath?: string) =>
39
+ Layer.effect(
40
+ CurrentWorkingDirectory,
41
+ Effect.gen(function* () {
42
+ const root = yield* LivestoreWorkspace
43
+ return path.join(root, subPath ?? '')
44
+ }),
45
+ )
46
+ }
@@ -1,6 +1,11 @@
1
- import { Effect, FastCheck } from '@livestore/utils/effect'
1
+ import { Effect, FastCheck, Schema } from '@livestore/utils/effect'
2
+
2
3
  import * as Vitest from './Vitest.ts'
3
4
 
5
+ export class TestError extends Schema.TaggedError<TestError>()('TestError', {
6
+ message: Schema.String,
7
+ }) {}
8
+
4
9
  // Demonstrate enhanced asProp functionality with clear shrinking progress
5
10
  // This showcases the fix for the "Run 26/6" bug and accurate shrinking detection
6
11
 
@@ -16,7 +21,7 @@ Vitest.describe('Vitest.asProp', () => {
16
21
  Effect.gen(function* () {
17
22
  const [value] = properties
18
23
  if (value === undefined) {
19
- return yield* Effect.fail(new Error('Value is undefined'))
24
+ return yield* new TestError({ message: 'Value is undefined' })
20
25
  }
21
26
 
22
27
  console.log(
@@ -43,7 +48,7 @@ Vitest.describe('Vitest.asProp', () => {
43
48
  Effect.gen(function* () {
44
49
  const [value] = properties
45
50
  if (value === undefined) {
46
- return yield* Effect.fail(new Error('Value is undefined'))
51
+ return yield* new TestError({ message: 'Value is undefined' })
47
52
  }
48
53
 
49
54
  const displayInfo =
@@ -59,10 +64,10 @@ Vitest.describe('Vitest.asProp', () => {
59
64
  // Fail when value is greater than 80 to trigger shrinking
60
65
  if (value > 80) {
61
66
  alreadyFailed = true
62
- return yield* Effect.fail(new Error(`Value ${value} is too large (> 80)`))
67
+ return yield* new TestError({ message: `Value ${value} is too large (> 80)` })
63
68
  }
64
69
 
65
- if (alreadyFailed && enhanced._tag === 'shrinking') {
70
+ if (alreadyFailed === true && enhanced._tag === 'shrinking') {
66
71
  ctx.skip("For the sake of this test, we don't want to fail but want to skip")
67
72
  return
68
73
  }
@@ -81,7 +86,7 @@ Vitest.describe('Vitest.asProp', () => {
81
86
  Effect.gen(function* () {
82
87
  const [value] = properties
83
88
  if (value === undefined) {
84
- return yield* Effect.fail(new Error('Value is undefined'))
89
+ return yield* new TestError({ message: 'Value is undefined' })
85
90
  }
86
91
 
87
92
  console.log(
@@ -91,7 +96,7 @@ Vitest.describe('Vitest.asProp', () => {
91
96
 
92
97
  // This will fail but shrinking is disabled
93
98
  if (value > 50) {
94
- yield* Effect.fail(new Error(`Value ${value} is too large (> 50) - but no shrinking!`))
99
+ return yield* new TestError({ message: `Value ${value} is too large (> 50) - but no shrinking!` })
95
100
  }
96
101
 
97
102
  return
@@ -1,5 +1,7 @@
1
1
  import * as inspector from 'node:inspector'
2
+
2
3
  import type * as Vitest from '@effect/vitest'
4
+
3
5
  import { IS_CI } from '@livestore/utils'
4
6
  import {
5
7
  type Cause,
@@ -14,6 +16,7 @@ import {
14
16
  type Scope,
15
17
  } from '@livestore/utils/effect'
16
18
  import { OtelLiveDummy } from '@livestore/utils/node'
19
+
17
20
  import { OtelLiveHttp } from '../node/mod.ts'
18
21
 
19
22
  export * from '@effect/vitest'
@@ -47,7 +50,7 @@ export const withTestCtx =
47
50
  {
48
51
  suffix,
49
52
  makeLayer,
50
- timeout = IS_CI ? 60_000 : 10_000,
53
+ timeout = IS_CI === true ? 60_000 : 10_000,
51
54
  forceOtel = false,
52
55
  }: {
53
56
  suffix?: string
@@ -67,24 +70,24 @@ export const withTestCtx =
67
70
  // provided layer from the effect dependencies
68
71
  | Exclude<R, ROut | OtelTracer.OtelTracer | Scope.Scope>
69
72
  > => {
70
- const spanName = `${testContext.task.suite?.name}:${testContext.task.name}${suffix ? `:${suffix}` : ''}`
73
+ const spanName = `${testContext.task.suite?.name}:${testContext.task.name}${suffix !== undefined ? `:${suffix}` : ''}`
71
74
  const layer = makeLayer?.(testContext) ?? Layer.empty
72
75
 
73
76
  const otelLayer =
74
- DEBUGGER_ACTIVE || forceOtel
77
+ DEBUGGER_ACTIVE === true || forceOtel === true
75
78
  ? OtelLiveHttp({ rootSpanName: spanName, serviceName: 'vitest-runner', skipLogUrl: false })
76
79
  : OtelLiveDummy
77
80
 
78
81
  const combinedLayer = layer.pipe(Layer.provideMerge(otelLayer))
79
82
 
80
83
  return self.pipe(
81
- DEBUGGER_ACTIVE
84
+ DEBUGGER_ACTIVE === true
82
85
  ? identity
83
86
  : Effect.logWarnIfTakesLongerThan({
84
87
  duration: Duration.toMillis(timeout) * 0.8,
85
88
  label: `${spanName} approaching timeout (timeout: ${Duration.format(timeout)})`,
86
89
  }),
87
- DEBUGGER_ACTIVE ? identity : Effect.timeout(timeout),
90
+ DEBUGGER_ACTIVE === true ? identity : Effect.timeout(timeout),
88
91
  Effect.provide(combinedLayer),
89
92
  Effect.scoped, // We need to scope the effect manually here because otherwise the span is not closed
90
93
  Effect.annotateLogs({ suffix }),
@@ -132,7 +135,7 @@ const normalizePropOptions = <Arbs extends Vitest.Vitest.Arbitraries>(
132
135
  }>
133
136
  } => {
134
137
  // If it's a number, treat as timeout and add our default fastCheck
135
- if (!Predicate.isObject(propOptions)) {
138
+ if (Predicate.isObject(propOptions) === false) {
136
139
  return {
137
140
  timeout: propOptions,
138
141
  fastCheck: { numRuns: 100 },
@@ -140,7 +143,7 @@ const normalizePropOptions = <Arbs extends Vitest.Vitest.Arbitraries>(
140
143
  }
141
144
 
142
145
  // If no fastCheck property, add it with our default numRuns
143
- if (!propOptions.fastCheck) {
146
+ if (propOptions.fastCheck == null) {
144
147
  return {
145
148
  ...propOptions,
146
149
  fastCheck: { numRuns: 100 },
@@ -148,7 +151,7 @@ const normalizePropOptions = <Arbs extends Vitest.Vitest.Arbitraries>(
148
151
  }
149
152
 
150
153
  // If fastCheck exists but no numRuns, add our default
151
- if (propOptions.fastCheck && !propOptions.fastCheck.numRuns) {
154
+ if (propOptions.fastCheck !== undefined && propOptions.fastCheck.numRuns == null) {
152
155
  return {
153
156
  ...propOptions,
154
157
  fastCheck: {
@@ -205,31 +208,32 @@ export const asProp = <Arbs extends Vitest.Vitest.Arbitraries, A, E, R>(
205
208
  name,
206
209
  arbitraries,
207
210
  (properties, ctx) => {
208
- if (ctx.signal.aborted) {
211
+ if (ctx.signal.aborted === true) {
209
212
  return ctx.skip('Test aborted')
210
213
  }
211
214
 
212
215
  totalExecutions++
213
216
  const isInShrinkingPhase = runIndex >= numRuns
214
217
 
215
- if (isInShrinkingPhase) {
218
+ if (isInShrinkingPhase === true) {
216
219
  shrinkAttempts++
217
220
  }
218
221
 
219
- const enhancedContext: EnhancedTestContext = isInShrinkingPhase
220
- ? {
221
- _tag: 'shrinking',
222
- numRuns,
223
- runIndex: runIndex++,
224
- shrinkAttempt: shrinkAttempts,
225
- totalExecutions,
226
- }
227
- : {
228
- _tag: 'initial',
229
- numRuns,
230
- runIndex: runIndex++,
231
- totalExecutions,
232
- }
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
+ }
233
237
 
234
238
  return test(properties, ctx, enhancedContext)
235
239
  },
@@ -1,7 +1,9 @@
1
+ import { expect } from 'vitest'
2
+
3
+ import { Vitest } from '@livestore/utils-dev/node-vitest'
1
4
  import { Effect, FetchHttpClient, Layer } from '@livestore/utils/effect'
2
5
  import { getFreePort, PlatformNode } from '@livestore/utils/node'
3
- import { Vitest } from '@livestore/utils-dev/node-vitest'
4
- import { expect } from 'vitest'
6
+
5
7
  import {
6
8
  type StartWranglerDevServerArgs,
7
9
  WranglerDevServerError,
@@ -59,7 +61,7 @@ Vitest.describe('WranglerDevServer', { timeout: testTimeout }, () => {
59
61
  WranglerDevServerTest({
60
62
  cwd: '/tmp',
61
63
  wranglerConfigPath: '/dev/null',
62
- connectTimeout: '500 millis',
64
+ readiness: { connectTimeout: '500 millis' },
63
65
  }).pipe(Layer.provide(PlatformNode.NodeContext.layer)),
64
66
  ),
65
67
  Effect.flip,
@@ -1,4 +1,5 @@
1
1
  import * as path from 'node:path'
2
+
2
3
  import * as Toml from '@iarna/toml'
3
4
  import { IS_CI } from '@livestore/utils'
4
5
  import { Cause, Duration, Effect, FileSystem, HttpClient, Schedule, Schema } from '@livestore/utils/effect'
@@ -8,7 +9,7 @@ import * as wrangler from 'wrangler'
8
9
  /**
9
10
  * Error type for WranglerDevServer operations
10
11
  */
11
- export class WranglerDevServerError extends Schema.TaggedError<WranglerDevServerError>()('WranglerDevServerError', {
12
+ export class WranglerDevServerError extends Schema.TaggedError<WranglerDevServerError>('~@livestore/utils-dev/WranglerDevServerError')('WranglerDevServerError', {
12
13
  cause: Schema.Unknown,
13
14
  message: Schema.String,
14
15
  port: Schema.Number,
@@ -24,17 +25,35 @@ export interface WranglerDevServer {
24
25
  }
25
26
 
26
27
  /**
27
- * Configuration for starting WranglerDevServer
28
+ * Readiness and retry configuration for wrangler boot and HTTP health.
29
+ *
30
+ * Example: startupTimeout=20s, connectTimeout=5s, retrySchedule=recurs(1)
31
+ * - Give wrangler up to 20s to boot; if it succeeds, give the HTTP check up to 5s.
32
+ * - If wrangler fails/times out, retry boot once; each boot attempt gets its own 20s budget.
33
+ * connectTimeout should be shorter than startupTimeout because HTTP readiness should be fast after boot.
28
34
  */
35
+ export interface WranglerReadinessOptions {
36
+ /** Max time to wait for wrangler to report ready before retrying. */
37
+ startupTimeout?: Duration.DurationInput
38
+ /** Max time for the HTTP connectivity check after wrangler reports ready. */
39
+ connectTimeout?: Duration.DurationInput
40
+ /** Retry policy for startup attempts (applies when startupTimeout elapses or wrangler throws). */
41
+ retrySchedule?: Schedule.Schedule<unknown>
42
+ }
43
+
29
44
  export interface StartWranglerDevServerArgs {
45
+ /** Path to wrangler.toml (defaults to cwd/wrangler.toml). */
30
46
  wranglerConfigPath?: string
47
+ /** Working directory wrangler should use. */
31
48
  cwd: string
32
49
  /** The port to try first. The dev server may bind a different port if unavailable. */
33
50
  preferredPort?: number
34
51
  /** @default false */
35
52
  showLogs?: boolean
53
+ /** Optional inspector port for wrangler dev. */
36
54
  inspectorPort?: number
37
- connectTimeout?: Duration.DurationInput
55
+ /** Readiness and retry configuration for bringing up wrangler and confirming connectivity. */
56
+ readiness?: WranglerReadinessOptions
38
57
  }
39
58
 
40
59
  /**
@@ -74,17 +93,37 @@ export class WranglerDevServerService extends Effect.Service<WranglerDevServerSe
74
93
  )
75
94
  const resolvedMainPath = yield* Effect.try(() => path.resolve(args.cwd, parsedConfig.main))
76
95
 
96
+ const readiness = args.readiness ?? {}
97
+ const startupTimeout = readiness.startupTimeout ?? Duration.seconds(IS_CI === true ? 30 : 10)
77
98
  const devServer = yield* Effect.promise(() =>
78
99
  wrangler.unstable_dev(resolvedMainPath, {
79
100
  config: configPath,
80
101
  port: preferredPort,
81
102
  inspectorPort: args.inspectorPort ?? 0,
82
103
  persistTo: path.join(args.cwd, '.wrangler/state'),
83
- logLevel: showLogs ? 'info' : 'none',
104
+ logLevel: showLogs === true ? 'debug' : 'none',
84
105
  experimental: {
85
106
  disableExperimentalWarning: true,
86
107
  },
87
108
  }),
109
+ ).pipe(
110
+ Effect.timeout(startupTimeout),
111
+ Effect.mapError(
112
+ (cause) =>
113
+ new WranglerDevServerError({
114
+ cause,
115
+ message: `Failed to start wrangler dev server within ${Duration.format(startupTimeout)}`,
116
+ port: preferredPort,
117
+ }),
118
+ ),
119
+ Effect.tapError((error) =>
120
+ Effect.logError('Wrangler dev server failed to start', {
121
+ message: error.message,
122
+ preferredPort,
123
+ cwd: args.cwd,
124
+ }),
125
+ ),
126
+ Effect.retry(readiness.retrySchedule ?? Schedule.recurs(1)),
88
127
  )
89
128
 
90
129
  yield* Effect.addFinalizer(
@@ -111,12 +150,13 @@ export class WranglerDevServerService extends Effect.Service<WranglerDevServerSe
111
150
  const actualHost = devServer.address
112
151
  const url = `http://${actualHost}:${actualPort}`
113
152
 
114
- // Use longer timeout in CI environments to account for slower startup times
115
- const defaultTimeout = Duration.seconds(IS_CI ? 30 : 5)
153
+ // Use longer timeout in CI environments to account for slower HTTP readiness
154
+ const defaultConnectivityTimeout = Duration.seconds(IS_CI === true ? 30 : 5)
155
+ const connectivityTimeout = readiness.connectTimeout ?? defaultConnectivityTimeout
116
156
 
117
- yield* verifyHttpConnectivity({ url, showLogs, connectTimeout: args.connectTimeout ?? defaultTimeout })
157
+ yield* verifyHttpConnectivity({ url, showLogs, connectTimeout: connectivityTimeout })
118
158
 
119
- if (showLogs) {
159
+ if (showLogs === true) {
120
160
  yield* Effect.logDebug(
121
161
  `Wrangler dev server ready and accepting connections on port ${actualPort} (preferred: ${preferredPort})`,
122
162
  )
@@ -127,9 +167,10 @@ export class WranglerDevServerService extends Effect.Service<WranglerDevServerSe
127
167
  url,
128
168
  } satisfies WranglerDevServer
129
169
  }).pipe(
130
- Effect.mapError(
131
- (error) =>
132
- new WranglerDevServerError({ cause: error, message: 'Failed to start wrangler dev server', port: -1 }),
170
+ Effect.mapError((error) =>
171
+ error instanceof WranglerDevServerError
172
+ ? error
173
+ : new WranglerDevServerError({ cause: error, message: 'Failed to start wrangler dev server', port: -1 }),
133
174
  ),
134
175
  Effect.withSpan('WranglerDevServerService', {
135
176
  attributes: { preferredPort: args.preferredPort ?? 'auto', cwd: args.cwd },
@@ -152,7 +193,7 @@ const verifyHttpConnectivity = ({
152
193
  Effect.gen(function* () {
153
194
  const client = yield* HttpClient.HttpClient
154
195
 
155
- if (showLogs) {
196
+ if (showLogs === true) {
156
197
  yield* Effect.logDebug(`Verifying HTTP connectivity to ${url}`)
157
198
  }
158
199
 
@@ -173,7 +214,7 @@ const verifyHttpConnectivity = ({
173
214
  }),
174
215
  ),
175
216
  ),
176
- Effect.tap(() => (showLogs ? Effect.logDebug(`HTTP connectivity verified for ${url}`) : Effect.void)),
217
+ Effect.tap(() => (showLogs === true ? Effect.logDebug(`HTTP connectivity verified for ${url}`) : Effect.void)),
177
218
  Effect.asVoid,
178
219
  Effect.withSpan('verifyHttpConnectivity'),
179
220
  )
@@ -8,4 +8,4 @@ class_name = "TestDO"
8
8
 
9
9
  [[migrations]]
10
10
  tag = "v1"
11
- new_sqlite_classes = ["TestDO"]
11
+ new_sqlite_classes = ["TestDO"]