@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.
Files changed (82) hide show
  1. package/dist/.tsbuildinfo +1 -0
  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} +18 -18
  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 +64 -80
  17. package/dist/node/FileLogger.js.map +1 -1
  18. package/dist/node/cmd-log.d.ts +21 -0
  19. package/dist/node/cmd-log.d.ts.map +1 -0
  20. package/dist/node/cmd-log.js +58 -0
  21. package/dist/node/cmd-log.js.map +1 -0
  22. package/dist/node/cmd.d.ts +20 -14
  23. package/dist/node/cmd.d.ts.map +1 -1
  24. package/dist/node/cmd.js +248 -33
  25. package/dist/node/cmd.js.map +1 -1
  26. package/dist/node/cmd.test.d.ts +2 -0
  27. package/dist/node/cmd.test.d.ts.map +1 -0
  28. package/dist/node/cmd.test.js +103 -0
  29. package/dist/node/cmd.test.js.map +1 -0
  30. package/dist/node/mod.d.ts +5 -5
  31. package/dist/node/mod.d.ts.map +1 -1
  32. package/dist/node/mod.js +114 -66
  33. package/dist/node/mod.js.map +1 -1
  34. package/dist/node/workspace.d.ts +22 -0
  35. package/dist/node/workspace.d.ts.map +1 -0
  36. package/dist/node/workspace.js +26 -0
  37. package/dist/node/workspace.js.map +1 -0
  38. package/dist/node-vitest/Vitest.d.ts +17 -8
  39. package/dist/node-vitest/Vitest.d.ts.map +1 -1
  40. package/dist/node-vitest/Vitest.js +16 -16
  41. package/dist/node-vitest/Vitest.js.map +1 -1
  42. package/dist/node-vitest/Vitest.test.d.ts +6 -0
  43. package/dist/node-vitest/Vitest.test.d.ts.map +1 -1
  44. package/dist/node-vitest/Vitest.test.js +25 -10
  45. package/dist/node-vitest/Vitest.test.js.map +1 -1
  46. package/dist/wrangler/WranglerDevServer.d.ts +32 -20
  47. package/dist/wrangler/WranglerDevServer.d.ts.map +1 -1
  48. package/dist/wrangler/WranglerDevServer.js +79 -60
  49. package/dist/wrangler/WranglerDevServer.js.map +1 -1
  50. package/dist/wrangler/WranglerDevServer.test.js +31 -31
  51. package/dist/wrangler/WranglerDevServer.test.js.map +1 -1
  52. package/dist/wrangler/mod.d.ts +1 -1
  53. package/dist/wrangler/mod.d.ts.map +1 -1
  54. package/dist/wrangler/mod.js +1 -1
  55. package/dist/wrangler/mod.js.map +1 -1
  56. package/package.json +56 -21
  57. package/src/node/{DockerComposeService/DockerComposeService.test.ts → DockerCompose/DockerCompose.test.ts} +23 -20
  58. package/src/node/DockerCompose/DockerCompose.ts +328 -0
  59. package/src/node/DockerCompose/mod.ts +1 -0
  60. package/src/node/DockerCompose/test-fixtures/docker-compose.yml +4 -0
  61. package/src/node/FileLogger.ts +91 -115
  62. package/src/node/cmd-log.ts +95 -0
  63. package/src/node/cmd.test.ts +136 -0
  64. package/src/node/cmd.ts +431 -82
  65. package/src/node/mod.ts +152 -96
  66. package/src/node/workspace.ts +45 -0
  67. package/src/node-vitest/Vitest.test.ts +34 -10
  68. package/src/node-vitest/Vitest.ts +60 -66
  69. package/src/wrangler/WranglerDevServer.test.ts +38 -40
  70. package/src/wrangler/WranglerDevServer.ts +165 -108
  71. package/src/wrangler/fixtures/wrangler.toml +1 -1
  72. package/src/wrangler/mod.ts +1 -6
  73. package/dist/.tsbuildinfo.json +0 -1
  74. package/dist/node/DockerComposeService/DockerComposeService.d.ts +0 -58
  75. package/dist/node/DockerComposeService/DockerComposeService.d.ts.map +0 -1
  76. package/dist/node/DockerComposeService/DockerComposeService.js +0 -144
  77. package/dist/node/DockerComposeService/DockerComposeService.js.map +0 -1
  78. package/dist/node/DockerComposeService/DockerComposeService.test.d.ts +0 -2
  79. package/dist/node/DockerComposeService/DockerComposeService.test.d.ts.map +0 -1
  80. package/dist/node/DockerComposeService/DockerComposeService.test.js.map +0 -1
  81. package/src/node/DockerComposeService/DockerComposeService.ts +0 -328
  82. package/src/node/DockerComposeService/test-fixtures/docker-compose.yml +0 -4
@@ -1,44 +1,76 @@
1
1
  import * as path from 'node:path'
2
+
2
3
  import * as Toml from '@iarna/toml'
4
+ import * as wrangler from 'wrangler'
5
+
3
6
  import { IS_CI } from '@livestore/utils'
4
- import { Cause, Duration, Effect, FileSystem, HttpClient, Schedule, Schema } from '@livestore/utils/effect'
7
+ import {
8
+ Cause,
9
+ Context,
10
+ Duration,
11
+ Effect,
12
+ FileSystem,
13
+ HttpClient,
14
+ Layer,
15
+ Schedule,
16
+ Schema,
17
+ } from '@livestore/utils/effect'
5
18
  import { getFreePort } from '@livestore/utils/node'
6
- import * as wrangler from 'wrangler'
7
19
 
8
20
  /**
9
21
  * Error type for WranglerDevServer operations
10
22
  */
11
- export class WranglerDevServerError extends Schema.TaggedError<WranglerDevServerError>()('WranglerDevServerError', {
12
- cause: Schema.Unknown,
23
+ export class WranglerDevServerError extends Schema.TaggedError<WranglerDevServerError>(
24
+ '~@livestore/utils-dev/WranglerDevServerError',
25
+ )('WranglerDevServerError', {
26
+ cause: Schema.Defect(),
13
27
  message: Schema.String,
14
- port: Schema.Number,
28
+ port: Schema.Finite,
15
29
  }) {}
16
30
 
17
31
  /**
18
32
  * WranglerDevServer instance interface
19
33
  */
20
- export interface WranglerDevServer {
34
+ export interface Service {
21
35
  readonly port: number
22
36
  readonly url: string
23
37
  // readonly processId: number
24
38
  }
25
39
 
26
40
  /**
27
- * Configuration for starting WranglerDevServer
41
+ * Readiness and retry configuration for wrangler boot and HTTP health.
42
+ *
43
+ * Example: startupTimeout=20s, connectTimeout=5s, retrySchedule=recurs(1)
44
+ * - Give wrangler up to 20s to boot; if it succeeds, give the HTTP check up to 5s.
45
+ * - If wrangler fails/times out, retry boot once; each boot attempt gets its own 20s budget.
46
+ * connectTimeout should be shorter than startupTimeout because HTTP readiness should be fast after boot.
28
47
  */
29
- export interface StartWranglerDevServerArgs {
48
+ export interface WranglerReadinessOptions {
49
+ /** Max time to wait for wrangler to report ready before retrying. */
50
+ startupTimeout?: Duration.Input
51
+ /** Max time for the HTTP connectivity check after wrangler reports ready. */
52
+ connectTimeout?: Duration.Input
53
+ /** Retry policy for startup attempts (applies when startupTimeout elapses or wrangler throws). */
54
+ retrySchedule?: Schedule.Schedule<unknown>
55
+ }
56
+
57
+ export interface Options {
58
+ /** Path to wrangler.toml (defaults to cwd/wrangler.toml). */
30
59
  wranglerConfigPath?: string
60
+ /** Working directory wrangler should use. */
31
61
  cwd: string
32
62
  /** The port to try first. The dev server may bind a different port if unavailable. */
33
63
  preferredPort?: number
34
64
  /** @default false */
35
65
  showLogs?: boolean
66
+ /** Optional inspector port for wrangler dev. */
36
67
  inspectorPort?: number
37
- connectTimeout?: Duration.DurationInput
68
+ /** Readiness and retry configuration for bringing up wrangler and confirming connectivity. */
69
+ readiness?: WranglerReadinessOptions
38
70
  }
39
71
 
40
72
  /**
41
- * WranglerDevServer as an Effect.Service.
73
+ * WranglerDevServer as an Effect service.
42
74
  *
43
75
  * This service provides the WranglerDevServer properties and can be accessed
44
76
  * directly to get port and url.
@@ -46,96 +78,127 @@ export interface StartWranglerDevServerArgs {
46
78
  * TODO: Allow for config to be passed in via code instead of `wrangler.toml` file
47
79
  * (would need to be placed in temporary file as wrangler only accepts files as config)
48
80
  */
49
- export class WranglerDevServerService extends Effect.Service<WranglerDevServerService>()('WranglerDevServerService', {
50
- scoped: (args: StartWranglerDevServerArgs) =>
51
- Effect.gen(function* () {
52
- const showLogs = args.showLogs ?? false
53
-
54
- // Allocate preferred port (Wrangler may bind a different one if unavailable)
55
- const preferredPort =
56
- args.preferredPort ??
57
- (yield* getFreePort.pipe(
58
- Effect.mapError(
59
- (cause) => new WranglerDevServerError({ cause, message: 'Failed to get free port', port: -1 }),
60
- ),
61
- ))
62
-
63
- yield* Effect.annotateCurrentSpan({ preferredPort })
64
-
65
- const configPath = path.resolve(args.wranglerConfigPath ?? path.join(args.cwd, 'wrangler.toml'))
66
-
67
- const fs = yield* FileSystem.FileSystem
68
- const configContent = yield* fs.readFileString(configPath)
69
- const parsedConfig = yield* Effect.try(() => Toml.parse(configContent)).pipe(
70
- Effect.andThen(Schema.decodeUnknown(Schema.Struct({ main: Schema.String }))),
71
- Effect.mapError(
72
- (error) => new WranglerDevServerError({ cause: error, message: 'Failed to parse wrangler config', port: -1 }),
73
- ),
74
- )
75
- const resolvedMainPath = yield* Effect.try(() => path.resolve(args.cwd, parsedConfig.main))
76
-
77
- const devServer = yield* Effect.promise(() =>
78
- wrangler.unstable_dev(resolvedMainPath, {
79
- config: configPath,
80
- port: preferredPort,
81
- inspectorPort: args.inspectorPort ?? 0,
82
- persistTo: path.join(args.cwd, '.wrangler/state'),
83
- logLevel: showLogs ? 'debug' : 'none',
84
- experimental: {
85
- disableExperimentalWarning: true,
86
- },
87
- }),
88
- )
89
-
90
- yield* Effect.addFinalizer(
91
- Effect.fn(
92
- function* (exit) {
93
- if (exit._tag === 'Failure' && Cause.isInterruptedOnly(exit.cause) === false) {
94
- yield* Effect.logError('Closing wrangler dev server on failure', exit.cause)
95
- }
96
-
97
- yield* Effect.tryPromise(async () => {
98
- await devServer.stop()
99
- // TODO investigate whether we need to wait until exit (see workers-sdk repo/talk to Cloudflare team)
100
- // await devServer.waitUntilExit()
101
- })
102
- },
103
- Effect.timeout('5 seconds'),
104
- Effect.orDie,
105
- Effect.tapCauseLogPretty,
106
- Effect.withSpan('WranglerDevServerService:stopDevServer'),
107
- ),
108
- )
81
+ export class WranglerDevServer extends Context.Service<WranglerDevServer, Service>()(
82
+ '@livestore/utils-dev/WranglerDevServer',
83
+ ) {}
109
84
 
110
- const actualPort = devServer.port
111
- const actualHost = devServer.address
112
- const url = `http://${actualHost}:${actualPort}`
85
+ export const make = (args: Options) =>
86
+ Effect.gen(function* () {
87
+ const showLogs = args.showLogs ?? false
113
88
 
114
- // Use longer timeout in CI environments to account for slower startup times
115
- const defaultTimeout = Duration.seconds(IS_CI ? 30 : 5)
89
+ // Allocate preferred port (Wrangler may bind a different one if unavailable)
90
+ const preferredPort =
91
+ args.preferredPort ??
92
+ (yield* getFreePort.pipe(
93
+ Effect.mapError((cause) => new WranglerDevServerError({ cause, message: 'Failed to get free port', port: -1 })),
94
+ ))
116
95
 
117
- yield* verifyHttpConnectivity({ url, showLogs, connectTimeout: args.connectTimeout ?? defaultTimeout })
96
+ yield* Effect.annotateCurrentSpan({ preferredPort })
118
97
 
119
- if (showLogs) {
120
- yield* Effect.logDebug(
121
- `Wrangler dev server ready and accepting connections on port ${actualPort} (preferred: ${preferredPort})`,
122
- )
123
- }
98
+ const configPath = path.resolve(args.wranglerConfigPath ?? path.join(args.cwd, 'wrangler.toml'))
124
99
 
125
- return {
126
- port: actualPort,
127
- url,
128
- } satisfies WranglerDevServer
100
+ const fs = yield* FileSystem.FileSystem
101
+ const configContent = yield* fs.readFileString(configPath)
102
+ const parsedConfig = yield* Effect.try({
103
+ try: () => Toml.parse(configContent),
104
+ catch: (cause) => new Cause.UnknownError(cause),
129
105
  }).pipe(
106
+ Effect.andThen(Schema.decodeUnknownEffect(Schema.Struct({ main: Schema.String }))),
130
107
  Effect.mapError(
131
- (error) =>
132
- new WranglerDevServerError({ cause: error, message: 'Failed to start wrangler dev server', port: -1 }),
108
+ (error) => new WranglerDevServerError({ cause: error, message: 'Failed to parse wrangler config', port: -1 }),
133
109
  ),
134
- Effect.withSpan('WranglerDevServerService', {
135
- attributes: { preferredPort: args.preferredPort ?? 'auto', cwd: args.cwd },
110
+ )
111
+ const resolvedMainPath = yield* Effect.try({
112
+ try: () => path.resolve(args.cwd, parsedConfig.main),
113
+ catch: (cause) => new Cause.UnknownError(cause),
114
+ })
115
+
116
+ const readiness = args.readiness ?? {}
117
+ const startupTimeout = readiness.startupTimeout ?? Duration.seconds(IS_CI === true ? 30 : 10)
118
+ const startupTimeoutDuration = Duration.fromInputUnsafe(startupTimeout)
119
+ const devServer = yield* Effect.promise(() =>
120
+ wrangler.unstable_dev(resolvedMainPath, {
121
+ config: configPath,
122
+ port: preferredPort,
123
+ inspectorPort: args.inspectorPort ?? 0,
124
+ persistTo: path.join(args.cwd, '.wrangler/state'),
125
+ logLevel: showLogs === true ? 'debug' : 'none',
126
+ experimental: {
127
+ disableExperimentalWarning: true,
128
+ },
136
129
  }),
130
+ ).pipe(
131
+ Effect.timeout(startupTimeout),
132
+ Effect.mapError(
133
+ (cause) =>
134
+ new WranglerDevServerError({
135
+ cause,
136
+ message: `Failed to start wrangler dev server within ${Duration.format(startupTimeoutDuration)}`,
137
+ port: preferredPort,
138
+ }),
139
+ ),
140
+ Effect.tapError((error) =>
141
+ Effect.logError('Wrangler dev server failed to start', {
142
+ message: error.message,
143
+ preferredPort,
144
+ cwd: args.cwd,
145
+ }),
146
+ ),
147
+ Effect.retry(readiness.retrySchedule ?? Schedule.recurs(1)),
148
+ )
149
+
150
+ yield* Effect.addFinalizer(
151
+ Effect.fn(
152
+ function* (exit) {
153
+ if (exit._tag === 'Failure' && Cause.hasInterruptsOnly(exit.cause) === false) {
154
+ yield* Effect.logError('Closing wrangler dev server on failure', exit.cause)
155
+ }
156
+
157
+ yield* Effect.tryPromise(async () => {
158
+ await devServer.stop()
159
+ // TODO investigate whether we need to wait until exit (see workers-sdk repo/talk to Cloudflare team)
160
+ // await devServer.waitUntilExit()
161
+ })
162
+ },
163
+ Effect.timeout('5 seconds'),
164
+ Effect.orDie,
165
+ Effect.tapCauseLogPretty,
166
+ Effect.withSpan('WranglerDevServer:stopDevServer'),
167
+ ),
168
+ )
169
+
170
+ const actualPort = devServer.port
171
+ const actualHost = devServer.address
172
+ const url = `http://${actualHost}:${actualPort}`
173
+
174
+ // Use longer timeout in CI environments to account for slower HTTP readiness
175
+ const defaultConnectivityTimeout = Duration.seconds(IS_CI === true ? 30 : 5)
176
+ const connectivityTimeout = readiness.connectTimeout ?? defaultConnectivityTimeout
177
+
178
+ yield* verifyHttpConnectivity({ url, showLogs, connectTimeout: connectivityTimeout })
179
+
180
+ if (showLogs === true) {
181
+ yield* Effect.logDebug(
182
+ `Wrangler dev server ready and accepting connections on port ${actualPort} (preferred: ${preferredPort})`,
183
+ )
184
+ }
185
+
186
+ return WranglerDevServer.of({
187
+ port: actualPort,
188
+ url,
189
+ })
190
+ }).pipe(
191
+ Effect.mapError((error) =>
192
+ error instanceof WranglerDevServerError
193
+ ? error
194
+ : new WranglerDevServerError({ cause: error, message: 'Failed to start wrangler dev server', port: -1 }),
137
195
  ),
138
- }) {}
196
+ Effect.withSpan('WranglerDevServer', {
197
+ attributes: { preferredPort: args.preferredPort ?? 'auto', cwd: args.cwd },
198
+ }),
199
+ )
200
+
201
+ export const layer = (options: Options) => Layer.effect(WranglerDevServer, make(options))
139
202
 
140
203
  /**
141
204
  * Verifies the server is actually accepting HTTP connections by making a test request
@@ -147,33 +210,27 @@ const verifyHttpConnectivity = ({
147
210
  }: {
148
211
  url: string
149
212
  showLogs: boolean
150
- connectTimeout: Duration.DurationInput
213
+ connectTimeout: Duration.Input
151
214
  }): Effect.Effect<void, WranglerDevServerError, HttpClient.HttpClient> =>
152
215
  Effect.gen(function* () {
153
216
  const client = yield* HttpClient.HttpClient
154
217
 
155
- if (showLogs) {
218
+ if (showLogs === true) {
156
219
  yield* Effect.logDebug(`Verifying HTTP connectivity to ${url}`)
157
220
  }
158
221
 
159
- // Try to connect with retries using exponential backoff
160
222
  yield* client.get(url).pipe(
161
- Effect.retryOrElse(
162
- Schedule.exponential('50 millis', 2).pipe(
163
- Schedule.jittered,
164
- Schedule.intersect(Schedule.elapsed.pipe(Schedule.whileOutput(Duration.lessThanOrEqualTo(connectTimeout)))),
165
- Schedule.compose(Schedule.count),
166
- ),
167
- (error, attemptCount) =>
168
- Effect.fail(
169
- new WranglerDevServerError({
170
- cause: error,
171
- message: `Failed to establish HTTP connection to Wrangler server at ${url} after ${attemptCount} attempts (timeout: ${Duration.toMillis(connectTimeout)}ms)`,
172
- port: 0,
173
- }),
174
- ),
223
+ Effect.retry(Schedule.exponential('50 millis', 2).pipe(Schedule.jittered)),
224
+ Effect.timeout(connectTimeout),
225
+ Effect.mapError(
226
+ (error) =>
227
+ new WranglerDevServerError({
228
+ cause: error,
229
+ message: `Failed to establish HTTP connection to Wrangler server at ${url} (timeout: ${Duration.toMillis(Duration.fromInputUnsafe(connectTimeout))}ms)`,
230
+ port: 0,
231
+ }),
175
232
  ),
176
- Effect.tap(() => (showLogs ? Effect.logDebug(`HTTP connectivity verified for ${url}`) : Effect.void)),
233
+ Effect.tap(() => (showLogs === true ? Effect.logDebug(`HTTP connectivity verified for ${url}`) : Effect.void)),
177
234
  Effect.asVoid,
178
235
  Effect.withSpan('verifyHttpConnectivity'),
179
236
  )
@@ -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"]
@@ -1,6 +1 @@
1
- export {
2
- type StartWranglerDevServerArgs,
3
- type WranglerDevServer,
4
- WranglerDevServerError,
5
- WranglerDevServerService,
6
- } from './WranglerDevServer.ts'
1
+ export * as WranglerDevServer from './WranglerDevServer.ts'