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