Compare commits

...
3 changed files with 36 additions and 13 deletions
@@ -54,11 +54,7 @@ function managedService(options: EnsureOptions) {
const reconnectOptions = { ...options, version: undefined }
return {
reconnect: () => Service.ensure(reconnectOptions),
restart: () =>
Effect.gen(function* () {
yield* Service.stop(options)
yield* Service.ensure(reconnectOptions)
}),
restart: () => Service.replace(reconnectOptions).pipe(Effect.asVoid),
}
}
+17 -4
View File
@@ -51,10 +51,20 @@ export const incumbent = Effect.fn("service.incumbent")(function* (
// becomes discoverable. A contender is never killed merely for slow startup.
/** Ensure a healthy, compatible local service is running. */
export const ensure = Effect.fn("service.ensure")(function* (options: EnsureOptions = {}) {
return yield* ensureService(options, false)
})
/** Replace the registered local service while preserving persistent terminals. */
export const replace = Effect.fn("service.replace")(function* (options: EnsureOptions = {}) {
return yield* ensureService(options, true)
})
const ensureService = Effect.fnUntraced(function* (options: EnsureOptions, forceReplacement: boolean) {
const timing = ensureTiming(options)
const contenders = new Set<ServiceContender>()
let timeouts: { readonly info: Info; readonly count: number } | undefined
let announced = false
let replaceCurrent = forceReplacement
let lastSpawn = 0
let spawnDelay = timing.spawnDelay
const announce = (reason: "missing" | "version-mismatch", previousVersion?: string) =>
@@ -88,6 +98,7 @@ export const ensure = Effect.fn("service.ensure")(function* (options: EnsureOpti
yield* Effect.logWarning("Background service is unresponsive; recovery cannot preserve persistent terminals")
yield* Effect.tryPromise(() => PtyHandoff.clear(options.file ?? fallback()))
yield* terminate(info, options, timing)
replaceCurrent = false
timeouts = undefined
lastSpawn = Date.now() - spawnDelay
}
@@ -95,14 +106,15 @@ export const ensure = Effect.fn("service.ensure")(function* (options: EnsureOpti
if (service !== undefined) {
spawnDelay = timing.spawnDelay
const compatible = !service.legacy && matchesVersion(service.version, options)
if (compatible && service.state === "ready") {
if (!replaceCurrent && compatible && service.state === "ready") {
yield* Effect.tryPromise(() => PtyHandoff.complete(options.file ?? fallback(), service.info))
return Option.some(service)
}
if (compatible && service.state === "failed")
if (!replaceCurrent && compatible && service.state === "failed")
return yield* Effect.fail(new Error("Background service failed to start"))
if (compatible) return Option.none<LocalService>()
if (!replaceCurrent && compatible) return Option.none<LocalService>()
yield* announce("version-mismatch", service.version)
replaceCurrent = false
if (!service.legacy && service.state === "ready")
yield* Effect.tryPromise(() =>
PtyHandoff.prepare(options.file ?? fallback(), service.info, timing.requestTimeout),
@@ -116,6 +128,7 @@ export const ensure = Effect.fn("service.ensure")(function* (options: EnsureOpti
lastSpawn = 0
return Option.none<LocalService>()
} else if (lastSpawn === 0 && info !== undefined) lastSpawn = Date.now()
else if (info === undefined) replaceCurrent = false
const finished = [...contenders].filter(contenderFinished)
const failure = finished.map(contenderFailure).find((error): error is Error => error !== undefined)
@@ -294,4 +307,4 @@ const terminate = Effect.fnUntraced(function* (info: Info, options: { readonly f
})
/** Effect-based local service lifecycle operations. */
export const Service = { discover, incumbent, ensure, stop, headers, Info }
export const Service = { discover, incumbent, ensure, replace, stop, headers, Info }
+18 -4
View File
@@ -31,11 +31,21 @@ export async function discover(options: DiscoverOptions = {}) {
/** Ensure a healthy, compatible local service is running. */
export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
return ensureService(options, false)
}
/** Replace the registered local service while preserving persistent terminals. */
export async function replace(options: EnsureOptions = {}): Promise<Endpoint> {
return ensureService(options, true)
}
async function ensureService(options: EnsureOptions, forceReplacement: boolean): Promise<Endpoint> {
const timing = ensureTiming(options)
const deadline = Date.now() + timing.promiseTimeout
const contenders = new Set<ServiceContender>()
let timeouts: { readonly info: Info; readonly count: number } | undefined
let announced = false
let replaceCurrent = forceReplacement
let lastSpawn = 0
let spawnDelay = timing.spawnDelay
@@ -68,6 +78,7 @@ export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
console.warn("Background service is unresponsive; recovery cannot preserve persistent terminals")
await PtyHandoff.clear(options.file ?? fallback())
await terminate(registration.info, options, timing)
replaceCurrent = false
timeouts = undefined
lastSpawn = Date.now() - spawnDelay
}
@@ -77,13 +88,15 @@ export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
spawnDelay = timing.spawnDelay
const service = registration.service
const compatible = !service.legacy && matchesVersion(service.version, options)
if (compatible && service.state === "ready") {
if (!replaceCurrent && compatible && service.state === "ready") {
await PtyHandoff.complete(options.file ?? fallback(), service.info)
return service.endpoint
}
if (compatible && service.state === "failed") throw new Error("Background service failed to start")
if (!compatible) {
if (!replaceCurrent && compatible && service.state === "failed")
throw new Error("Background service failed to start")
if (replaceCurrent || !compatible) {
announce("version-mismatch", service.version)
replaceCurrent = false
if (!service.legacy && service.state === "ready")
await PtyHandoff.prepare(options.file ?? fallback(), service.info, timing.requestTimeout)
else {
@@ -95,6 +108,7 @@ export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
lastSpawn = 0
}
} else {
if (registration.info === undefined) replaceCurrent = false
if (lastSpawn === 0 && registration.info !== undefined) lastSpawn = Date.now()
const finished = [...contenders].filter(contenderFinished)
const failure = finished.map(contenderFailure).find((error) => error !== undefined)
@@ -252,4 +266,4 @@ function delay(milliseconds: number) {
}
/** Promise-based local service lifecycle operations. */
export const Service = { discover, ensure, stop, headers }
export const Service = { discover, ensure, replace, stop, headers }