| @@ -136,8 +136,9 @@ interface ServiceState { · 135 unmodified lines |
| 136 | 136 | const vendorPrefix = (platform: DevicePlatform) => |
| 137 | 137 | platform === "ios" ? "/vendor/serve-sim" : "/vendor/serve-emu"; |
| 138 | 138 | |
| 139 | | −export const make = Effect.gen(function* () { |
| 140 | | − const localHost = yield* DeviceHost.DeviceHost; |
| 139 | +export const makeWithHosts = Effect.fn("DeviceService.makeWithHosts")(function* ( |
| 140 | + hosts: ReadonlyMap<DeviceHostId, DeviceHost.DeviceHost["Service"]>, |
| 141 | +) { |
| 141 | 142 | const settings = yield* ServerSettings.ServerSettingsService; |
| 142 | 143 | const lifecycleLock = yield* Semaphore.make(1); |
| 143 | 144 | const readDeviceSettings = settings.getSettings.pipe( |
| @@ -152,9 +153,7 @@ export const make = Effect.gen(function* () { · 8 unmodified lines |
| 152 | 153 | ), |
| 153 | 154 | ); |
| 154 | 155 | const initialSettings = yield* readDeviceSettings; |
| 155 | | − const hosts: ReadonlyMap<DeviceHostId, DeviceHost.DeviceHost["Service"]> = new Map([ |
| 156 | | − [localHost.id, localHost], |
| 157 | | − ]); |
| 156 | + |
| 158 | 157 | const httpClient = (yield* HttpClient.HttpClient).pipe(HttpClient.withScope); |
| 159 | 158 | const statePubSub = yield* PubSub.unbounded<DeviceServiceState>(); |
| 160 | 159 | const initialHosts = yield* Effect.forEach(hosts.values(), (host) => host.summary); |
| @@ -162,6 +161,7 @@ export const make = Effect.gen(function* () { · 1 unmodified line |
| 162 | 161 | state: { |
| 163 | 162 | hosts: initialHosts, |
| 164 | 163 | hostStatus: initialSettings.enabled ? "idle" : "disabled", |
| 164 | + hostStatuses: {}, |
| 165 | 165 | devices: [], |
| 166 | 166 | sessions: [], |
| 167 | 167 | onboardingCompleted: initialSettings.onboardingCompleted, |
| @@ -187,6 +187,18 @@ export const make = Effect.gen(function* () { · 19 unmodified lines |
| 187 | 187 | return host; |
| 188 | 188 | }); |
| 189 | 189 | |
| 190 | + const setHostStatus = ( |
| 191 | + hostId: DeviceHostId, |
| 192 | + status: DeviceServiceState["hostStatuses"][string], |
| 193 | + ) => |
| 194 | + publish((state) => ({ |
| 195 | + ...state, |
| 196 | + ...(hostId === LOCAL_DEVICE_HOST_ID |
| 197 | + ? { hostStatus: status.status, hostStatusDetail: status.detail } |
| 198 | + : {}), |
| 199 | + hostStatuses: { ...state.hostStatuses, [hostId]: status }, |
| 200 | + })); |
| 201 | + |
| 190 | 202 | const readiness: DeviceService["Service"]["readiness"] = Effect.fn("DeviceService.readiness")( |
| 191 | 203 | function* (hostId) { |
| 192 | 204 | const host = yield* resolveHost(hostId); |
| @@ -198,34 +210,19 @@ export const make = Effect.gen(function* () { · 5 unmodified lines |
| 198 | 210 | }); |
| 199 | 211 | } |
| 200 | 212 | const ready = yield* host |
| 201 | | − .ensureReady((phase) => |
| 202 | | − publish((state) => ({ ...state, hostStatus: phase, hostStatusDetail: undefined })).pipe( |
| 203 | | − Effect.asVoid, |
| 204 | | − ), |
| 205 | | − ) |
| 213 | + .ensureReady((status) => setHostStatus(host.id, { status }).pipe(Effect.asVoid)) |
| 206 | 214 | .pipe( |
| 207 | 215 | Effect.tapError((error) => |
| 208 | | − publish((state) => ({ |
| 209 | | − ...state, |
| 210 | | − hostStatus: "failed", |
| 211 | | − hostStatusDetail: error.message, |
| 212 | | − })), |
| 216 | + setHostStatus(host.id, { status: "failed", detail: error.message }), |
| 213 | 217 | ), |
| 214 | 218 | Effect.mapError( |
| 215 | 219 | (error) => new DeviceHostUnavailableError({ hostId: host.id, reason: error.message }), |
| 216 | 220 | ), |
| 217 | 221 | ); |
| 218 | | − yield* SynchronizedRef.get(stateRef).pipe( |
| 219 | | − Effect.flatMap(({ state }) => |
| 220 | | − state.hostStatus === "ready" |
| 221 | | − ? Effect.void |
| 222 | | − : publish((current) => ({ |
| 223 | | − ...current, |
| 224 | | − hostStatus: "ready", |
| 225 | | − hostStatusDetail: undefined, |
| 226 | | − })), |
| 227 | | − ), |
| 228 | | − ); |
| 222 | + const { state } = yield* SynchronizedRef.get(stateRef); |
| 223 | + if (state.hostStatuses[host.id]?.status !== "ready") { |
| 224 | + yield* setHostStatus(host.id, { status: "ready" }); |
| 225 | + } |
| 229 | 226 | return { hostId: host.id, ...ready }; |
| 230 | 227 | }, |
| 231 | 228 | lifecycleLock.withPermit, |
| @@ -249,25 +246,18 @@ export const make = Effect.gen(function* () { · 17 unmodified lines |
| 249 | 246 | const summary = yield* host.summary; |
| 250 | 247 | if (!summary.platforms.some((platform) => platform.available)) return null; |
| 251 | 248 | const ready = yield* host |
| 252 | | − .ensureAgentReady((phase) => |
| 253 | | − publish((state) => ({ ...state, hostStatus: phase, hostStatusDetail: undefined })).pipe( |
| 254 | | − Effect.asVoid, |
| 255 | | − ), |
| 256 | | − ) |
| 249 | + .ensureAgentReady((phase) => setHostStatus(host.id, { status: phase }).pipe(Effect.asVoid)) |
| 257 | 250 | .pipe( |
| 258 | 251 | Effect.tapError((error) => |
| 259 | | − publish((state) => ({ |
| 260 | | − ...state, |
| 261 | | − hostStatus: "failed", |
| 262 | | − hostStatusDetail: error.message, |
| 263 | | − })), |
| 252 | + setHostStatus(host.id, { status: "failed", detail: error.message }), |
| 264 | 253 | ), |
| 265 | 254 | Effect.mapError( |
| 266 | 255 | (error) => new DeviceHostUnavailableError({ hostId: host.id, reason: error.message }), |
| 267 | 256 | ), |
| 268 | 257 | ); |
| 269 | 258 | const hostSummaries = yield* Effect.forEach(hosts.values(), (candidate) => candidate.summary); |
| 270 | | − yield* publish((state) => ({ ...state, hosts: hostSummaries, hostStatus: "ready" })); |
| 259 | + yield* publish((state) => ({ ...state, hosts: hostSummaries })); |
| 260 | + yield* setHostStatus(host.id, { status: "ready" }); |
| 271 | 261 | return { hostId: host.id, ...ready }; |
| 272 | 262 | }, lifecycleLock.withPermit); |
| 273 | 263 | |
| @@ -357,8 +347,15 @@ export const make = Effect.gen(function* () { · 83 unmodified lines |
| 357 | 347 | return yield* publish((state) => ({ |
| 358 | 348 | ...state, |
| 359 | 349 | hosts: hostSummaries, |
| 360 | | − devices, |
| 361 | | − hostStatusDetail: detail, |
| 350 | + ...(ready.hostId === LOCAL_DEVICE_HOST_ID ? { hostStatusDetail: detail } : {}), |
| 351 | + devices: [ |
| 352 | + ...state.devices.filter((device) => device.hostId !== ready.hostId), |
| 353 | + ...devices, |
| 354 | + ], |
| 355 | + hostStatuses: { |
| 356 | + ...state.hostStatuses, |
| 357 | + [ready.hostId]: { status: "ready", ...(detail ? { detail } : {}) }, |
| 358 | + }, |
| 362 | 359 | })); |
| 363 | 360 | }), |
| 364 | 361 | ); |
| @@ -366,18 +363,21 @@ export const make = Effect.gen(function* () { · 1 unmodified line |
| 366 | 363 | |
| 367 | 364 | const list: DeviceService["Service"]["list"] = Effect.gen(function* () { |
| 368 | 365 | if (!(yield* readDeviceSettings).enabled) return (yield* SynchronizedRef.get(stateRef)).state; |
| 369 | | − const ready = yield* readiness(); |
| 370 | | − return yield* refresh(ready); |
| 371 | | − }).pipe( |
| 372 | | − Effect.tapError((error) => |
| 373 | | − publish((state) => |
| 374 | | − state.hostStatus === "disabled" |
| 375 | | − ? state |
| 376 | | − : { ...state, hostStatus: "failed", hostStatusDetail: error.message }, |
| 377 | | − ), |
| 378 | | − ), |
| 379 | | − Effect.withSpan("DeviceService.list"), |
| 380 | | − ); |
| 366 | + yield* Effect.forEach( |
| 367 | + hosts.values(), |
| 368 | + (host) => |
| 369 | + Effect.gen(function* () { |
| 370 | + const ready = yield* readinessIfSupported(host.id); |
| 371 | + if (ready) yield* refresh(ready); |
| 372 | + }).pipe( |
| 373 | + Effect.catch((error) => |
| 374 | + setHostStatus(host.id, { status: "failed", detail: error.message }), |
| 375 | + ), |
| 376 | + ), |
| 377 | + { concurrency: 4 }, |
| 378 | + ); |
| 379 | + return (yield* SynchronizedRef.get(stateRef)).state; |
| 380 | + }).pipe(Effect.withSpan("DeviceService.list")); |
| 381 | 381 | |
| 382 | 382 | const configure: DeviceService["Service"]["configure"] = Effect.fn("DeviceService.configure")( |
| 383 | 383 | function* (input) { |
| @@ -415,6 +415,7 @@ export const make = Effect.gen(function* () { · 31 unmodified lines |
| 415 | 415 | ...state, |
| 416 | 416 | hostStatus: nextEnabled ? "idle" : "disabled", |
| 417 | 417 | hostStatusDetail: undefined, |
| 418 | + hostStatuses: {}, |
| 418 | 419 | devices: nextEnabled ? state.devices : [], |
| 419 | 420 | sessions: nextEnabled ? state.sessions : [], |
| 420 | 421 | bootingDevices: nextEnabled ? state.bootingDevices : [], |
| @@ -636,6 +637,7 @@ export const make = Effect.gen(function* () { · 215 unmodified lines |
| 636 | 637 | const closing = state.sessions.filter( |
| 637 | 638 | (session) => |
| 638 | 639 | session.threadId === input.threadId && |
| 640 | + (input.hostId === undefined || session.hostId === input.hostId) && |
| 639 | 641 | (input.deviceId === undefined || session.deviceId === input.deviceId), |
| 640 | 642 | ); |
| 641 | 643 | if (closing.length === 0) return; |
| @@ -749,6 +751,11 @@ export const make = Effect.gen(function* () { · 107 unmodified lines |
| 749 | 751 | }); |
| 750 | 752 | }); |
| 751 | 753 | |
| 754 | +export const make = Effect.gen(function* () { |
| 755 | + const host = yield* DeviceHost.DeviceHost; |
| 756 | + return yield* makeWithHosts(new Map([[host.id, host]])); |
| 757 | +}); |
| 758 | + |
| 752 | 759 | export const layer = Layer.effect(DeviceService, make).pipe(Layer.provide(LocalDeviceHost.layer)); |
| 753 | 760 | |
| 754 | 761 | /** State stream for WS subscribers: current snapshot first, then every change. */ |