@livestore/utils-dev 0.0.0-snapshot-92c31f56117b4fd377b66f55163ed565795c5e4c → 0.0.0-snapshot-6d1da0d93ffe8144f36fc2ec57c22db22a1d18d8
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.json +1 -1
- package/dist/node/WranglerDevServer/WranglerDevServer.d.ts +6 -3
- package/dist/node/WranglerDevServer/WranglerDevServer.d.ts.map +1 -1
- package/dist/node/WranglerDevServer/WranglerDevServer.js +65 -17
- package/dist/node/WranglerDevServer/WranglerDevServer.js.map +1 -1
- package/dist/node/WranglerDevServer/WranglerDevServer.test.js +19 -26
- package/dist/node/WranglerDevServer/WranglerDevServer.test.js.map +1 -1
- package/dist/node-vitest/Vitest.d.ts +2 -2
- package/dist/node-vitest/Vitest.d.ts.map +1 -1
- package/dist/node-vitest/Vitest.js.map +1 -1
- package/package.json +5 -7
- package/src/node/WranglerDevServer/WranglerDevServer.test.ts +32 -30
- package/src/node/WranglerDevServer/WranglerDevServer.ts +118 -22
- package/src/node-vitest/Vitest.ts +9 -4
|
@@ -1,6 +1,18 @@
|
|
|
1
|
+
import { error } from 'node:console'
|
|
1
2
|
import * as path from 'node:path'
|
|
2
|
-
|
|
3
|
-
import {
|
|
3
|
+
import { IS_CI, shouldNeverHappen } from '@livestore/utils'
|
|
4
|
+
import {
|
|
5
|
+
Command,
|
|
6
|
+
Duration,
|
|
7
|
+
Effect,
|
|
8
|
+
Exit,
|
|
9
|
+
HttpClient,
|
|
10
|
+
Option,
|
|
11
|
+
type PlatformError,
|
|
12
|
+
Schedule,
|
|
13
|
+
Schema,
|
|
14
|
+
Stream,
|
|
15
|
+
} from '@livestore/utils/effect'
|
|
4
16
|
import { getFreePort } from '@livestore/utils/node'
|
|
5
17
|
import { cleanupOrphanedProcesses, killProcessTree } from './process-tree-manager.ts'
|
|
6
18
|
|
|
@@ -28,9 +40,12 @@ export interface WranglerDevServer {
|
|
|
28
40
|
export interface StartWranglerDevServerArgs {
|
|
29
41
|
wranglerConfigPath?: string
|
|
30
42
|
cwd: string
|
|
31
|
-
port
|
|
43
|
+
/** The port to try first. The dev server may bind a different port if unavailable. */
|
|
44
|
+
preferredPort?: number
|
|
32
45
|
/** @default false */
|
|
33
46
|
showLogs?: boolean
|
|
47
|
+
inspectorPort?: number
|
|
48
|
+
connectTimeout?: Duration.DurationInput
|
|
34
49
|
}
|
|
35
50
|
|
|
36
51
|
/**
|
|
@@ -57,27 +72,31 @@ export class WranglerDevServerService extends Effect.Service<WranglerDevServerSe
|
|
|
57
72
|
Effect.ignore, // Don't fail startup if cleanup fails
|
|
58
73
|
)
|
|
59
74
|
|
|
60
|
-
// Allocate port
|
|
61
|
-
const
|
|
62
|
-
args.
|
|
75
|
+
// Allocate preferred port (Wrangler may bind a different one if unavailable)
|
|
76
|
+
const preferredPort =
|
|
77
|
+
args.preferredPort ??
|
|
63
78
|
(yield* getFreePort.pipe(
|
|
64
79
|
Effect.mapError(
|
|
65
80
|
(cause) => new WranglerDevServerError({ cause, message: 'Failed to get free port', port: -1 }),
|
|
66
81
|
),
|
|
67
82
|
))
|
|
68
83
|
|
|
69
|
-
yield* Effect.annotateCurrentSpan({
|
|
84
|
+
yield* Effect.annotateCurrentSpan({ preferredPort })
|
|
70
85
|
|
|
71
86
|
// Resolve config path
|
|
72
87
|
const configPath = path.resolve(args.wranglerConfigPath ?? path.join(args.cwd, 'wrangler.toml'))
|
|
73
88
|
|
|
89
|
+
const inspectorPort = args.inspectorPort ?? (yield* getFreePort)
|
|
90
|
+
|
|
74
91
|
// Start wrangler process using Effect Command
|
|
75
92
|
const process = yield* Command.make(
|
|
76
93
|
'bunx',
|
|
77
94
|
'wrangler',
|
|
78
95
|
'dev',
|
|
79
96
|
'--port',
|
|
80
|
-
|
|
97
|
+
preferredPort.toString(),
|
|
98
|
+
'--inspector-port',
|
|
99
|
+
inspectorPort.toString(),
|
|
81
100
|
'--config',
|
|
82
101
|
configPath,
|
|
83
102
|
).pipe(
|
|
@@ -90,7 +109,7 @@ export class WranglerDevServerService extends Effect.Service<WranglerDevServerSe
|
|
|
90
109
|
new WranglerDevServerError({
|
|
91
110
|
cause: error,
|
|
92
111
|
message: `Failed to start wrangler process in directory: ${args.cwd}`,
|
|
93
|
-
port,
|
|
112
|
+
port: preferredPort,
|
|
94
113
|
}),
|
|
95
114
|
),
|
|
96
115
|
Effect.withSpan('WranglerDevServerService:startProcess'),
|
|
@@ -154,26 +173,38 @@ export class WranglerDevServerService extends Effect.Service<WranglerDevServerSe
|
|
|
154
173
|
yield* Effect.logDebug(`Process ${processId} already terminated`)
|
|
155
174
|
}
|
|
156
175
|
}).pipe(
|
|
176
|
+
Effect.withSpan('WranglerDevServerService:cleanupProcess'),
|
|
157
177
|
Effect.timeout('5 seconds'), // Don't let cleanup hang forever
|
|
158
178
|
Effect.ignoreLogged,
|
|
159
179
|
),
|
|
160
180
|
)
|
|
161
181
|
|
|
162
|
-
// Wait for server to be ready
|
|
163
|
-
yield* waitForReady({ stdout, showLogs })
|
|
182
|
+
// Wait for server to be ready and parse the actual bound host:port from stdout
|
|
183
|
+
const readyInfo = yield* waitForReady({ stdout, showLogs })
|
|
184
|
+
|
|
185
|
+
const actualPort = readyInfo.port
|
|
186
|
+
const actualHost = readyInfo.host
|
|
187
|
+
const url = `http://${actualHost}:${actualPort}`
|
|
188
|
+
|
|
189
|
+
// Use longer timeout in CI environments to account for slower startup times
|
|
190
|
+
const defaultTimeout = Duration.seconds(IS_CI ? 30 : 5)
|
|
191
|
+
|
|
192
|
+
yield* verifyHttpConnectivity({ url, showLogs, connectTimeout: args.connectTimeout ?? defaultTimeout })
|
|
164
193
|
|
|
165
194
|
if (showLogs) {
|
|
166
|
-
yield* Effect.logDebug(
|
|
195
|
+
yield* Effect.logDebug(
|
|
196
|
+
`Wrangler dev server ready and accepting connections on port ${actualPort} (preferred: ${preferredPort})`,
|
|
197
|
+
)
|
|
167
198
|
}
|
|
168
199
|
|
|
169
200
|
return {
|
|
170
|
-
port,
|
|
171
|
-
url
|
|
201
|
+
port: actualPort,
|
|
202
|
+
url,
|
|
172
203
|
processId,
|
|
173
204
|
} satisfies WranglerDevServer
|
|
174
205
|
}).pipe(
|
|
175
206
|
Effect.withSpan('WranglerDevServerService', {
|
|
176
|
-
attributes: {
|
|
207
|
+
attributes: { preferredPort: args.preferredPort ?? 'auto', cwd: args.cwd },
|
|
177
208
|
}),
|
|
178
209
|
),
|
|
179
210
|
}) {}
|
|
@@ -187,20 +218,85 @@ const waitForReady = ({
|
|
|
187
218
|
}: {
|
|
188
219
|
stdout: Stream.Stream<Uint8Array, PlatformError.PlatformError, never>
|
|
189
220
|
showLogs: boolean
|
|
190
|
-
}): Effect.Effect<
|
|
221
|
+
}): Effect.Effect<{ host: string; port: number }, WranglerDevServerError, never> =>
|
|
191
222
|
stdout.pipe(
|
|
192
223
|
Stream.decodeText('utf8'),
|
|
193
224
|
Stream.splitLines,
|
|
194
225
|
Stream.tap((line) => (showLogs ? Effect.logDebug(`[wrangler] ${line}`) : Effect.void)),
|
|
195
|
-
|
|
196
|
-
Stream.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
226
|
+
// Find the first readiness line and try to parse the port from it
|
|
227
|
+
Stream.filterMap<string, { host: string; port: number }>((line) => {
|
|
228
|
+
if (line.includes('Ready on')) {
|
|
229
|
+
// Expect: "Ready on http://<host>:<port>"
|
|
230
|
+
const m = line.match(/https?:\/\/([^:\s]+):(\d+)/i)
|
|
231
|
+
if (!m) return shouldNeverHappen('Could not parse host:port from Wrangler "Ready on" line', line)
|
|
232
|
+
const host = m[1]! as string
|
|
233
|
+
const port = Number.parseInt(m[2]!, 10)
|
|
234
|
+
if (!Number.isFinite(port)) return shouldNeverHappen('Parsed non-finite port from Wrangler output', line)
|
|
235
|
+
return Option.some({ host, port } as const)
|
|
236
|
+
} else {
|
|
237
|
+
return Option.none()
|
|
238
|
+
}
|
|
239
|
+
}),
|
|
240
|
+
Stream.take(1),
|
|
241
|
+
Stream.runHead,
|
|
242
|
+
Effect.flatten,
|
|
243
|
+
Effect.orElse(
|
|
244
|
+
() =>
|
|
245
|
+
new WranglerDevServerError({
|
|
246
|
+
cause: 'WranglerReadyLineMissing',
|
|
247
|
+
message: 'Wrangler server did not emit a "Ready on" line',
|
|
248
|
+
port: 0,
|
|
249
|
+
}),
|
|
250
|
+
),
|
|
251
|
+
Effect.timeoutFail({
|
|
252
|
+
duration: '30 seconds',
|
|
253
|
+
onTimeout: () =>
|
|
200
254
|
new WranglerDevServerError({
|
|
201
255
|
cause: error,
|
|
202
256
|
message: 'Wrangler server failed to start within timeout',
|
|
203
257
|
port: 0,
|
|
204
258
|
}),
|
|
205
|
-
),
|
|
259
|
+
}),
|
|
206
260
|
)
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Verifies the server is actually accepting HTTP connections by making a test request
|
|
264
|
+
*/
|
|
265
|
+
const verifyHttpConnectivity = ({
|
|
266
|
+
url,
|
|
267
|
+
showLogs,
|
|
268
|
+
connectTimeout,
|
|
269
|
+
}: {
|
|
270
|
+
url: string
|
|
271
|
+
showLogs: boolean
|
|
272
|
+
connectTimeout: Duration.DurationInput
|
|
273
|
+
}): Effect.Effect<void, WranglerDevServerError, HttpClient.HttpClient> =>
|
|
274
|
+
Effect.gen(function* () {
|
|
275
|
+
const client = yield* HttpClient.HttpClient
|
|
276
|
+
|
|
277
|
+
if (showLogs) {
|
|
278
|
+
yield* Effect.logDebug(`Verifying HTTP connectivity to ${url}`)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Try to connect with retries using exponential backoff
|
|
282
|
+
yield* client.get(url).pipe(
|
|
283
|
+
Effect.retryOrElse(
|
|
284
|
+
Schedule.exponential('50 millis', 2).pipe(
|
|
285
|
+
Schedule.jittered,
|
|
286
|
+
Schedule.intersect(Schedule.elapsed.pipe(Schedule.whileOutput(Duration.lessThanOrEqualTo(connectTimeout)))),
|
|
287
|
+
Schedule.compose(Schedule.count),
|
|
288
|
+
),
|
|
289
|
+
(error, attemptCount) =>
|
|
290
|
+
Effect.fail(
|
|
291
|
+
new WranglerDevServerError({
|
|
292
|
+
cause: error,
|
|
293
|
+
message: `Failed to establish HTTP connection to Wrangler server at ${url} after ${attemptCount} attempts (timeout: ${Duration.toMillis(connectTimeout)}ms)`,
|
|
294
|
+
port: 0,
|
|
295
|
+
}),
|
|
296
|
+
),
|
|
297
|
+
),
|
|
298
|
+
Effect.tap(() => (showLogs ? Effect.logDebug(`HTTP connectivity verified for ${url}`) : Effect.void)),
|
|
299
|
+
Effect.asVoid,
|
|
300
|
+
Effect.withSpan('verifyHttpConnectivity'),
|
|
301
|
+
)
|
|
302
|
+
})
|
|
@@ -20,12 +20,17 @@ export * from '@effect/vitest'
|
|
|
20
20
|
|
|
21
21
|
export const DEBUGGER_ACTIVE = Boolean(process.env.DEBUGGER_ACTIVE ?? inspector.url() !== undefined)
|
|
22
22
|
|
|
23
|
-
export const makeWithTestCtx
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
export const makeWithTestCtx: <R1, E1>(
|
|
24
|
+
ctxParams: WithTestCtxParams<R1, E1>,
|
|
25
|
+
) => (
|
|
26
|
+
testContext: Vitest.TestContext,
|
|
27
|
+
) => <A, E>(
|
|
28
|
+
self: Effect.Effect<A, E, Scope.Scope | NoInfer<R1> | OtelTracer.OtelTracer>,
|
|
29
|
+
) => Effect.Effect<A, E1 | Cause.TimeoutException | E, Scope.Scope> =
|
|
30
|
+
(ctxParams) => (testContext: Vitest.TestContext) =>
|
|
26
31
|
withTestCtx(testContext, ctxParams)
|
|
27
32
|
|
|
28
|
-
export type WithTestCtxParams<R1
|
|
33
|
+
export type WithTestCtxParams<R1, E1> = {
|
|
29
34
|
suffix?: string
|
|
30
35
|
makeLayer?: (testContext: Vitest.TestContext) => Layer.Layer<R1, E1, Scope.Scope>
|
|
31
36
|
timeout?: Duration.DurationInput
|