@livestore/utils-dev 0.4.0-dev.9 → 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.
- package/dist/.tsbuildinfo +1 -0
- package/dist/node/DockerCompose/DockerCompose.d.ts +52 -0
- package/dist/node/DockerCompose/DockerCompose.d.ts.map +1 -0
- package/dist/node/DockerCompose/DockerCompose.js +158 -0
- package/dist/node/DockerCompose/DockerCompose.js.map +1 -0
- package/dist/node/DockerCompose/DockerCompose.test.d.ts +2 -0
- package/dist/node/DockerCompose/DockerCompose.test.d.ts.map +1 -0
- package/dist/node/{DockerComposeService/DockerComposeService.test.js → DockerCompose/DockerCompose.test.js} +18 -18
- package/dist/node/DockerCompose/DockerCompose.test.js.map +1 -0
- package/dist/node/DockerCompose/mod.d.ts +2 -0
- package/dist/node/DockerCompose/mod.d.ts.map +1 -0
- package/dist/node/DockerCompose/mod.js +2 -0
- package/dist/node/DockerCompose/mod.js.map +1 -0
- package/dist/node/FileLogger.d.ts +15 -8
- package/dist/node/FileLogger.d.ts.map +1 -1
- package/dist/node/FileLogger.js +64 -80
- package/dist/node/FileLogger.js.map +1 -1
- package/dist/node/cmd-log.d.ts +21 -0
- package/dist/node/cmd-log.d.ts.map +1 -0
- package/dist/node/cmd-log.js +58 -0
- package/dist/node/cmd-log.js.map +1 -0
- package/dist/node/cmd.d.ts +20 -14
- package/dist/node/cmd.d.ts.map +1 -1
- package/dist/node/cmd.js +248 -33
- package/dist/node/cmd.js.map +1 -1
- package/dist/node/cmd.test.d.ts +2 -0
- package/dist/node/cmd.test.d.ts.map +1 -0
- package/dist/node/cmd.test.js +103 -0
- package/dist/node/cmd.test.js.map +1 -0
- package/dist/node/mod.d.ts +5 -5
- package/dist/node/mod.d.ts.map +1 -1
- package/dist/node/mod.js +114 -66
- package/dist/node/mod.js.map +1 -1
- package/dist/node/workspace.d.ts +22 -0
- package/dist/node/workspace.d.ts.map +1 -0
- package/dist/node/workspace.js +26 -0
- package/dist/node/workspace.js.map +1 -0
- package/dist/node-vitest/Vitest.d.ts +17 -8
- package/dist/node-vitest/Vitest.d.ts.map +1 -1
- package/dist/node-vitest/Vitest.js +16 -16
- package/dist/node-vitest/Vitest.js.map +1 -1
- package/dist/node-vitest/Vitest.test.d.ts +6 -0
- package/dist/node-vitest/Vitest.test.d.ts.map +1 -1
- package/dist/node-vitest/Vitest.test.js +25 -10
- package/dist/node-vitest/Vitest.test.js.map +1 -1
- package/dist/wrangler/WranglerDevServer.d.ts +32 -20
- package/dist/wrangler/WranglerDevServer.d.ts.map +1 -1
- package/dist/wrangler/WranglerDevServer.js +79 -60
- package/dist/wrangler/WranglerDevServer.js.map +1 -1
- package/dist/wrangler/WranglerDevServer.test.js +31 -31
- package/dist/wrangler/WranglerDevServer.test.js.map +1 -1
- package/dist/wrangler/mod.d.ts +1 -1
- package/dist/wrangler/mod.d.ts.map +1 -1
- package/dist/wrangler/mod.js +1 -1
- package/dist/wrangler/mod.js.map +1 -1
- package/package.json +56 -21
- package/src/node/{DockerComposeService/DockerComposeService.test.ts → DockerCompose/DockerCompose.test.ts} +23 -20
- package/src/node/DockerCompose/DockerCompose.ts +328 -0
- package/src/node/DockerCompose/mod.ts +1 -0
- package/src/node/DockerCompose/test-fixtures/docker-compose.yml +4 -0
- package/src/node/FileLogger.ts +91 -115
- package/src/node/cmd-log.ts +95 -0
- package/src/node/cmd.test.ts +136 -0
- package/src/node/cmd.ts +431 -82
- package/src/node/mod.ts +152 -96
- package/src/node/workspace.ts +45 -0
- package/src/node-vitest/Vitest.test.ts +34 -10
- package/src/node-vitest/Vitest.ts +60 -66
- package/src/wrangler/WranglerDevServer.test.ts +38 -40
- package/src/wrangler/WranglerDevServer.ts +165 -108
- package/src/wrangler/fixtures/wrangler.toml +1 -1
- package/src/wrangler/mod.ts +1 -6
- package/dist/.tsbuildinfo.json +0 -1
- package/dist/node/DockerComposeService/DockerComposeService.d.ts +0 -58
- package/dist/node/DockerComposeService/DockerComposeService.d.ts.map +0 -1
- package/dist/node/DockerComposeService/DockerComposeService.js +0 -144
- package/dist/node/DockerComposeService/DockerComposeService.js.map +0 -1
- package/dist/node/DockerComposeService/DockerComposeService.test.d.ts +0 -2
- package/dist/node/DockerComposeService/DockerComposeService.test.d.ts.map +0 -1
- package/dist/node/DockerComposeService/DockerComposeService.test.js.map +0 -1
- package/src/node/DockerComposeService/DockerComposeService.ts +0 -328
- package/src/node/DockerComposeService/test-fixtures/docker-compose.yml +0 -4
|
@@ -1,39 +1,42 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { expect } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { Vitest } from '@livestore/utils-dev/node-vitest'
|
|
2
6
|
import { Duration, Effect, Layer, Stream } from '@livestore/utils/effect'
|
|
3
7
|
import { PlatformNode } from '@livestore/utils/node'
|
|
4
|
-
|
|
5
|
-
import
|
|
6
|
-
import { type DockerComposeArgs, DockerComposeService } from './DockerComposeService.ts'
|
|
8
|
+
|
|
9
|
+
import * as DockerCompose from './DockerCompose.ts'
|
|
7
10
|
|
|
8
11
|
const testTimeout = 30_000
|
|
9
12
|
const testFixturePath = path.join(import.meta.dirname, 'test-fixtures')
|
|
10
13
|
|
|
11
|
-
const DockerComposeTest = (args: Partial<
|
|
12
|
-
|
|
14
|
+
const DockerComposeTest = (args: Partial<DockerCompose.Options> = {}) =>
|
|
15
|
+
DockerCompose.layer({
|
|
13
16
|
cwd: testFixturePath,
|
|
14
17
|
...args,
|
|
15
18
|
})
|
|
16
19
|
|
|
17
|
-
Vitest.describe('
|
|
20
|
+
Vitest.describe('DockerCompose', { timeout: testTimeout }, () => {
|
|
18
21
|
Vitest.describe('Basic Operations', () => {
|
|
19
|
-
const withBasicTest = (args: Partial<
|
|
22
|
+
const withBasicTest = (args: Partial<DockerCompose.Options> = {}) =>
|
|
20
23
|
Vitest.makeWithTestCtx({
|
|
21
24
|
timeout: testTimeout,
|
|
22
|
-
makeLayer: () => DockerComposeTest(args).pipe(Layer.provide(PlatformNode.
|
|
25
|
+
makeLayer: () => DockerComposeTest(args).pipe(Layer.provide(PlatformNode.NodeServices.layer)),
|
|
23
26
|
})
|
|
24
27
|
|
|
25
|
-
Vitest.
|
|
28
|
+
Vitest.live('can pull docker images', (test) =>
|
|
26
29
|
Effect.gen(function* () {
|
|
27
|
-
const dockerCompose = yield*
|
|
30
|
+
const dockerCompose = yield* DockerCompose.DockerCompose
|
|
28
31
|
|
|
29
32
|
// Test that pull operation works (should succeed for hello-world image)
|
|
30
33
|
yield* dockerCompose.pull
|
|
31
34
|
}).pipe(withBasicTest()(test)),
|
|
32
35
|
)
|
|
33
36
|
|
|
34
|
-
Vitest.
|
|
37
|
+
Vitest.live('can start and stop docker compose services', (test) =>
|
|
35
38
|
Effect.gen(function* () {
|
|
36
|
-
const dockerCompose = yield*
|
|
39
|
+
const dockerCompose = yield* DockerCompose.DockerCompose
|
|
37
40
|
|
|
38
41
|
// Start the service
|
|
39
42
|
yield* dockerCompose.start({ detached: true })
|
|
@@ -43,9 +46,9 @@ Vitest.describe('DockerComposeService', { timeout: testTimeout }, () => {
|
|
|
43
46
|
}).pipe(withBasicTest({ serviceName: 'hello-world' })(test)),
|
|
44
47
|
)
|
|
45
48
|
|
|
46
|
-
Vitest.
|
|
49
|
+
Vitest.live('can get logs from docker compose services', (test) =>
|
|
47
50
|
Effect.gen(function* () {
|
|
48
|
-
const dockerCompose = yield*
|
|
51
|
+
const dockerCompose = yield* DockerCompose.DockerCompose
|
|
49
52
|
|
|
50
53
|
// Start the service first
|
|
51
54
|
yield* dockerCompose.start({ detached: true })
|
|
@@ -62,15 +65,15 @@ Vitest.describe('DockerComposeService', { timeout: testTimeout }, () => {
|
|
|
62
65
|
})
|
|
63
66
|
|
|
64
67
|
Vitest.describe('Health Check Operations', () => {
|
|
65
|
-
const withHealthCheckTest = (args: Partial<
|
|
68
|
+
const withHealthCheckTest = (args: Partial<DockerCompose.Options> = {}) =>
|
|
66
69
|
Vitest.makeWithTestCtx({
|
|
67
70
|
timeout: testTimeout,
|
|
68
|
-
makeLayer: () => DockerComposeTest(args).pipe(Layer.provide(PlatformNode.
|
|
71
|
+
makeLayer: () => DockerComposeTest(args).pipe(Layer.provide(PlatformNode.NodeServices.layer)),
|
|
69
72
|
})
|
|
70
73
|
|
|
71
|
-
Vitest.
|
|
74
|
+
Vitest.live('handles health check timeout gracefully', (test) =>
|
|
72
75
|
Effect.gen(function* () {
|
|
73
|
-
const dockerCompose = yield*
|
|
76
|
+
const dockerCompose = yield* DockerCompose.DockerCompose
|
|
74
77
|
|
|
75
78
|
// Test starting with a health check that will timeout (invalid URL)
|
|
76
79
|
const result = yield* dockerCompose
|
|
@@ -81,10 +84,10 @@ Vitest.describe('DockerComposeService', { timeout: testTimeout }, () => {
|
|
|
81
84
|
timeout: Duration.seconds(2),
|
|
82
85
|
},
|
|
83
86
|
})
|
|
84
|
-
.pipe(Effect.
|
|
87
|
+
.pipe(Effect.result)
|
|
85
88
|
|
|
86
89
|
// Should fail due to health check timeout
|
|
87
|
-
expect(result._tag).toBe('
|
|
90
|
+
expect(result._tag).toBe('Failure')
|
|
88
91
|
}).pipe(withHealthCheckTest({ serviceName: 'hello-world' })(test)),
|
|
89
92
|
)
|
|
90
93
|
})
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { objectToString, omitUndefineds } from '@livestore/utils'
|
|
2
|
+
import {
|
|
3
|
+
ChildProcess,
|
|
4
|
+
ChildProcessSpawner,
|
|
5
|
+
Context,
|
|
6
|
+
Duration,
|
|
7
|
+
Effect,
|
|
8
|
+
Fiber,
|
|
9
|
+
Layer,
|
|
10
|
+
type PlatformError,
|
|
11
|
+
Schedule,
|
|
12
|
+
Schema,
|
|
13
|
+
type Scope,
|
|
14
|
+
Stream,
|
|
15
|
+
} from '@livestore/utils/effect'
|
|
16
|
+
|
|
17
|
+
export class DockerComposeError extends Schema.TaggedError<DockerComposeError>(
|
|
18
|
+
'~@livestore/utils-dev/DockerComposeError',
|
|
19
|
+
)('DockerComposeError', {
|
|
20
|
+
cause: Schema.Defect(),
|
|
21
|
+
note: Schema.String,
|
|
22
|
+
/** Compose env of the failed invocation, if any — structured instead of string-formatted into `note`. */
|
|
23
|
+
env: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
24
|
+
}) {}
|
|
25
|
+
|
|
26
|
+
export interface Options {
|
|
27
|
+
readonly cwd: string
|
|
28
|
+
readonly serviceName?: string
|
|
29
|
+
/** Unique project name to isolate this compose instance. If not provided, a random one is generated. */
|
|
30
|
+
readonly projectName?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface StartOptions {
|
|
34
|
+
readonly detached?: boolean
|
|
35
|
+
readonly env?: Record<string, string>
|
|
36
|
+
readonly healthCheck?: {
|
|
37
|
+
readonly url: string
|
|
38
|
+
readonly timeout?: Duration.Duration
|
|
39
|
+
readonly interval?: Duration.Duration
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface LogsOptions {
|
|
44
|
+
readonly follow?: boolean
|
|
45
|
+
readonly tail?: number
|
|
46
|
+
readonly since?: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface Service {
|
|
50
|
+
readonly pull: Effect.Effect<void, DockerComposeError | PlatformError.PlatformError>
|
|
51
|
+
readonly start: (
|
|
52
|
+
options?: StartOptions,
|
|
53
|
+
) => Effect.Effect<void, DockerComposeError | PlatformError.PlatformError, Scope.Scope>
|
|
54
|
+
readonly stop: Effect.Effect<void, DockerComposeError | PlatformError.PlatformError>
|
|
55
|
+
readonly down: (options?: {
|
|
56
|
+
readonly env?: Record<string, string>
|
|
57
|
+
readonly volumes?: boolean
|
|
58
|
+
readonly removeOrphans?: boolean
|
|
59
|
+
}) => Effect.Effect<void, DockerComposeError | PlatformError.PlatformError>
|
|
60
|
+
readonly logs: (
|
|
61
|
+
options?: LogsOptions,
|
|
62
|
+
) => Stream.Stream<string, DockerComposeError | PlatformError.PlatformError, Scope.Scope>
|
|
63
|
+
/** The unique project name used to isolate this compose instance */
|
|
64
|
+
readonly projectName: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const generateProjectName = (): string => `ls-test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
68
|
+
|
|
69
|
+
export class DockerCompose extends Context.Service<DockerCompose, Service>()('@livestore/utils-dev/DockerCompose') {}
|
|
70
|
+
|
|
71
|
+
export const make = (args: Options) =>
|
|
72
|
+
Effect.gen(function* () {
|
|
73
|
+
const { cwd, serviceName } = args
|
|
74
|
+
const projectName = args.projectName ?? generateProjectName()
|
|
75
|
+
|
|
76
|
+
const commandExecutorContext = yield* Effect.context<ChildProcessSpawner.ChildProcessSpawner>()
|
|
77
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
|
78
|
+
|
|
79
|
+
const baseComposeArgs = ['-p', projectName]
|
|
80
|
+
|
|
81
|
+
const composeCommand = (args: ReadonlyArray<string>, options: ChildProcess.CommandOptions = {}) =>
|
|
82
|
+
ChildProcess.make('docker', ['compose', ...baseComposeArgs, ...args], { ...options, cwd })
|
|
83
|
+
|
|
84
|
+
const envOptions = (
|
|
85
|
+
env: Record<string, string> | undefined,
|
|
86
|
+
): Pick<ChildProcess.CommandOptions, 'env' | 'extendEnv'> => (env === undefined ? {} : { env, extendEnv: true })
|
|
87
|
+
|
|
88
|
+
const pull = Effect.gen(function* () {
|
|
89
|
+
yield* Effect.log(`Pulling Docker Compose images in ${cwd}`)
|
|
90
|
+
|
|
91
|
+
const process = yield* spawner.spawn(composeCommand(['pull'], { stdout: 'pipe', stderr: 'pipe' }))
|
|
92
|
+
|
|
93
|
+
const stdoutFiber = yield* process.stdout.pipe(
|
|
94
|
+
Stream.decodeText({ encoding: 'utf8' }),
|
|
95
|
+
Stream.runFold(
|
|
96
|
+
() => '',
|
|
97
|
+
(acc, chunk) => acc + chunk,
|
|
98
|
+
),
|
|
99
|
+
// Drain subprocess output immediately so the child cannot block while we wait for its exit code.
|
|
100
|
+
Effect.forkChild,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
const stderrFiber = yield* process.stderr.pipe(
|
|
104
|
+
Stream.decodeText({ encoding: 'utf8' }),
|
|
105
|
+
Stream.runFold(
|
|
106
|
+
() => '',
|
|
107
|
+
(acc, chunk) => acc + chunk,
|
|
108
|
+
),
|
|
109
|
+
// Drain subprocess output immediately so the child cannot block while we wait for its exit code.
|
|
110
|
+
Effect.forkChild,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
const exitCode = yield* process.exitCode
|
|
114
|
+
const stdout = yield* Fiber.join(stdoutFiber)
|
|
115
|
+
const stderr = yield* Fiber.join(stderrFiber)
|
|
116
|
+
|
|
117
|
+
const exitCodeNumber = Number(exitCode)
|
|
118
|
+
|
|
119
|
+
if (exitCodeNumber !== 0) {
|
|
120
|
+
const stdoutLog = stdout.length > 0 ? stdout : '<empty stdout>'
|
|
121
|
+
const stderrLog = stderr.length > 0 ? stderr : '<empty stderr>'
|
|
122
|
+
const failureMessage = [
|
|
123
|
+
`Docker compose pull failed with exit code ${exitCodeNumber} in ${cwd}`,
|
|
124
|
+
`stdout:\n${stdoutLog}`,
|
|
125
|
+
`stderr:\n${stderrLog}`,
|
|
126
|
+
].join('\n')
|
|
127
|
+
|
|
128
|
+
yield* Effect.logError(failureMessage)
|
|
129
|
+
|
|
130
|
+
return yield* new DockerComposeError({
|
|
131
|
+
cause: new Error(`Docker compose pull failed with exit code ${exitCodeNumber}`),
|
|
132
|
+
note: failureMessage,
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
yield* Effect.log(`Successfully pulled Docker Compose images`)
|
|
137
|
+
}).pipe(
|
|
138
|
+
Effect.retry({
|
|
139
|
+
schedule: Schedule.exponentialBackoff10Sec,
|
|
140
|
+
while: Schema.is(DockerComposeError),
|
|
141
|
+
}),
|
|
142
|
+
Effect.withSpan('pullDockerComposeImages'),
|
|
143
|
+
Effect.scoped,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
const start = Effect.fn('startDockerCompose')(function* (options: StartOptions = {}) {
|
|
147
|
+
const { detached = true, healthCheck } = options
|
|
148
|
+
|
|
149
|
+
const startArgs = ['up']
|
|
150
|
+
if (detached === true) startArgs.push('-d')
|
|
151
|
+
if (serviceName !== undefined) startArgs.push(serviceName)
|
|
152
|
+
|
|
153
|
+
const process = yield* spawner
|
|
154
|
+
.spawn(composeCommand(startArgs, { ...envOptions(options.env), stderr: 'inherit', stdout: 'inherit' }))
|
|
155
|
+
.pipe(
|
|
156
|
+
Effect.mapError(
|
|
157
|
+
(cause) =>
|
|
158
|
+
new DockerComposeError({
|
|
159
|
+
cause,
|
|
160
|
+
note: `Failed to start Docker Compose services in ${cwd}`,
|
|
161
|
+
}),
|
|
162
|
+
),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
const exitCode = yield* process.exitCode
|
|
166
|
+
|
|
167
|
+
if (Number(exitCode) !== 0) {
|
|
168
|
+
return yield* new DockerComposeError({
|
|
169
|
+
cause: new Error(`Docker compose exited with code ${exitCode}`),
|
|
170
|
+
note: `Docker Compose failed to start with exit code ${exitCode}`,
|
|
171
|
+
env: options.env,
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (healthCheck !== undefined) {
|
|
176
|
+
yield* performHealthCheck(healthCheck).pipe(Effect.provide(commandExecutorContext))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
yield* Effect.log(`Docker Compose services started successfully in ${cwd}`)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
const stop = Effect.gen(function* () {
|
|
183
|
+
yield* Effect.log(`Stopping Docker Compose services in ${cwd}`)
|
|
184
|
+
|
|
185
|
+
const stopArgs = serviceName === undefined ? ['stop'] : ['stop', serviceName]
|
|
186
|
+
const exitCode = yield* spawner.exitCode(composeCommand(stopArgs))
|
|
187
|
+
|
|
188
|
+
if (Number(exitCode) !== 0) {
|
|
189
|
+
return yield* new DockerComposeError({
|
|
190
|
+
cause: new Error(`Docker compose stop exited with code ${exitCode}`),
|
|
191
|
+
note: `Failed to stop Docker Compose services`,
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
yield* Effect.log(`Docker Compose services stopped successfully`)
|
|
196
|
+
}).pipe(Effect.withSpan('stopDockerCompose'))
|
|
197
|
+
|
|
198
|
+
const logs = (options: LogsOptions = {}) =>
|
|
199
|
+
Effect.gen(function* () {
|
|
200
|
+
const { follow = false, tail, since } = options
|
|
201
|
+
|
|
202
|
+
const logsArgs = ['logs']
|
|
203
|
+
if (follow === true) logsArgs.push('-f')
|
|
204
|
+
if (tail !== undefined) logsArgs.push('--tail', tail.toString())
|
|
205
|
+
if (since !== undefined) logsArgs.push('--since', since)
|
|
206
|
+
if (serviceName !== undefined) logsArgs.push(serviceName)
|
|
207
|
+
|
|
208
|
+
const process = yield* spawner.spawn(composeCommand(logsArgs)).pipe(
|
|
209
|
+
Effect.mapError(
|
|
210
|
+
(cause) =>
|
|
211
|
+
new DockerComposeError({
|
|
212
|
+
cause,
|
|
213
|
+
note: `Failed to read Docker Compose logs in ${cwd}`,
|
|
214
|
+
}),
|
|
215
|
+
),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
return process.stdout.pipe(
|
|
219
|
+
Stream.decodeText({ encoding: 'utf8' }),
|
|
220
|
+
Stream.splitLines,
|
|
221
|
+
Stream.mapError(
|
|
222
|
+
(cause) =>
|
|
223
|
+
new DockerComposeError({
|
|
224
|
+
cause,
|
|
225
|
+
note: `Error reading Docker Compose logs in ${cwd}`,
|
|
226
|
+
}),
|
|
227
|
+
),
|
|
228
|
+
)
|
|
229
|
+
}).pipe(Stream.unwrap)
|
|
230
|
+
|
|
231
|
+
const down = Effect.fn('downDockerCompose')(function* (options?: {
|
|
232
|
+
readonly env?: Record<string, string>
|
|
233
|
+
readonly volumes?: boolean
|
|
234
|
+
readonly removeOrphans?: boolean
|
|
235
|
+
}) {
|
|
236
|
+
yield* Effect.log(`Tearing down Docker Compose services in ${cwd}`)
|
|
237
|
+
|
|
238
|
+
const downArgs = ['down']
|
|
239
|
+
if (options?.volumes === true) downArgs.push('-v')
|
|
240
|
+
if (options?.removeOrphans === true) downArgs.push('--remove-orphans')
|
|
241
|
+
if (serviceName !== undefined) downArgs.push(serviceName)
|
|
242
|
+
|
|
243
|
+
const exitCode = yield* spawner.exitCode(composeCommand(downArgs, envOptions(options?.env)))
|
|
244
|
+
|
|
245
|
+
if (Number(exitCode) !== 0) {
|
|
246
|
+
return yield* new DockerComposeError({
|
|
247
|
+
cause: new Error(`Docker compose down exited with code ${exitCode}`),
|
|
248
|
+
note: `Failed to tear down Docker Compose services`,
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
yield* Effect.log(`Docker Compose services torn down successfully`)
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
// Register cleanup finalizer to ensure containers are removed when scope closes
|
|
256
|
+
yield* Effect.addFinalizer(() =>
|
|
257
|
+
down({ volumes: true, removeOrphans: true }).pipe(
|
|
258
|
+
Effect.tap(() => Effect.log(`Docker Compose cleanup completed for project ${projectName}`)),
|
|
259
|
+
Effect.catchCause((cause) =>
|
|
260
|
+
Effect.log('Docker Compose cleanup failed for project', projectName, objectToString(cause)),
|
|
261
|
+
),
|
|
262
|
+
),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
return DockerCompose.of({ pull, start, stop, down, logs, projectName })
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
export const layer = (options: Options) => Layer.effect(DockerCompose, make(options))
|
|
269
|
+
|
|
270
|
+
const performHealthCheck = ({
|
|
271
|
+
url,
|
|
272
|
+
timeout = Duration.minutes(2),
|
|
273
|
+
interval = Duration.seconds(2),
|
|
274
|
+
}: {
|
|
275
|
+
url: string
|
|
276
|
+
timeout?: Duration.Duration
|
|
277
|
+
interval?: Duration.Duration
|
|
278
|
+
}): Effect.Effect<void, DockerComposeError, ChildProcessSpawner.ChildProcessSpawner | Scope.Scope> =>
|
|
279
|
+
Effect.gen(function* () {
|
|
280
|
+
yield* Effect.log(`Performing health check on ${url}`)
|
|
281
|
+
|
|
282
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
|
283
|
+
|
|
284
|
+
const checkHealth = spawner.exitCode(ChildProcess.make('curl', ['-f', '-s', url])).pipe(
|
|
285
|
+
Effect.map((code: number) => code === 0),
|
|
286
|
+
Effect.orElseSucceed(() => false),
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
const healthCheck = checkHealth.pipe(
|
|
290
|
+
Effect.repeat({
|
|
291
|
+
while: (healthy: boolean) => healthy === false,
|
|
292
|
+
schedule: Schedule.fixed(interval),
|
|
293
|
+
}),
|
|
294
|
+
Effect.timeout(timeout),
|
|
295
|
+
Effect.mapError(
|
|
296
|
+
() =>
|
|
297
|
+
new DockerComposeError({
|
|
298
|
+
cause: new Error('Health check timeout'),
|
|
299
|
+
note: `Health check failed for ${url} after ${Duration.toMillis(timeout)}ms`,
|
|
300
|
+
}),
|
|
301
|
+
),
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
yield* healthCheck
|
|
305
|
+
yield* Effect.log(`Health check passed for ${url}`)
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
// Convenience function for scoped Docker Compose operations with automatic cleanup
|
|
309
|
+
export const startDockerComposeServicesScoped = (
|
|
310
|
+
args: Options & {
|
|
311
|
+
healthCheck?: StartOptions['healthCheck']
|
|
312
|
+
},
|
|
313
|
+
): Effect.Effect<
|
|
314
|
+
void,
|
|
315
|
+
DockerComposeError | PlatformError.PlatformError,
|
|
316
|
+
DockerCompose | ChildProcessSpawner.ChildProcessSpawner | Scope.Scope
|
|
317
|
+
> =>
|
|
318
|
+
Effect.gen(function* () {
|
|
319
|
+
const dockerCompose = yield* DockerCompose
|
|
320
|
+
|
|
321
|
+
// Start the services
|
|
322
|
+
yield* dockerCompose.start({
|
|
323
|
+
...omitUndefineds({ healthCheck: args.healthCheck !== undefined ? args.healthCheck : undefined }),
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
// Add cleanup finalizer to the current scope
|
|
327
|
+
yield* Effect.addFinalizer(() => dockerCompose.stop.pipe(Effect.orDie))
|
|
328
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * as DockerCompose from './DockerCompose.ts'
|