import type { HostPowerSnapshot, ResourceMonitorCapabilities, ResourceMonitorCommand, ResourceMonitorEvent, ResourceMonitorExternalProcess, ResourceMonitorHelloEvent, ResourceMonitorSnapshotEvent, ResourceTelemetrySourceStatus,} from "@t3tools/contracts";import { RESOURCE_MONITOR_PROTOCOL_VERSION, ResourceMonitorCommand as ResourceMonitorCommandSchema, ResourceMonitorEvent as ResourceMonitorEventSchema,} from "@t3tools/contracts";import * as Cause from "effect/Cause";import * as Context from "effect/Context";import * as Crypto from "effect/Crypto";import * as DateTime from "effect/DateTime";import * as Deferred from "effect/Deferred";import * as Duration from "effect/Duration";import * as Effect from "effect/Effect";import * as Fiber from "effect/Fiber";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as PubSub from "effect/PubSub";import * as Queue from "effect/Queue";import * as Ref from "effect/Ref";import * as Result from "effect/Result";import * as Schema from "effect/Schema";import * as Semaphore from "effect/Semaphore";import * as Stream from "effect/Stream";import * as Ndjson from "effect/unstable/encoding/Ndjson";import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import * as ResourceMonitorBinary from "./ResourceMonitorBinary.ts";import { ServerConfig } from "../config.ts"; const SAMPLE_INTERVAL_MS = 1_000;const UNKNOWN_BACKGROUND_SAMPLE_INTERVAL_MS = 5_000;const BATTERY_SAMPLE_INTERVAL_MS = 5_000;const CONSTRAINED_SAMPLE_INTERVAL_MS = 15_000;const HANDSHAKE_TIMEOUT = Duration.seconds(5);const SAMPLE_REQUEST_TIMEOUT = Duration.seconds(5);const HISTORY_REQUEST_TIMEOUT = Duration.seconds(15);const INITIAL_RESTART_DELAY = Duration.millis(500);const MAX_RESTART_DELAY = Duration.seconds(10);const FAILURE_WINDOW_MS = 60_000;const MAX_FAILURES_PER_WINDOW = 5; export class NativeTelemetrySpawnFailed extends Schema.TaggedErrorClass<NativeTelemetrySpawnFailed>()( "NativeTelemetrySpawnFailed", { path: Schema.String, cause: Schema.Defect(), },) { override get message(): string { return `Failed to start resource monitor '${this.path}'.`; }} export class NativeTelemetryHandshakeTimedOut extends Schema.TaggedErrorClass<NativeTelemetryHandshakeTimedOut>()( "NativeTelemetryHandshakeTimedOut", { timeoutMs: Schema.Number, },) { override get message(): string { return `Resource monitor handshake timed out after ${this.timeoutMs}ms.`; }} export class NativeTelemetryProtocolMismatch extends Schema.TaggedErrorClass<NativeTelemetryProtocolMismatch>()( "NativeTelemetryProtocolMismatch", { expectedVersion: Schema.Number, receivedVersion: Schema.Number, },) { override get message(): string { return `Resource monitor protocol ${this.receivedVersion} is incompatible with expected protocol ${this.expectedVersion}.`; }} export class NativeTelemetryDecodeFailed extends Schema.TaggedErrorClass<NativeTelemetryDecodeFailed>()( "NativeTelemetryDecodeFailed", { cause: Schema.Defect(), },) { override get message(): string { return "Failed to decode resource monitor output."; }} export class NativeTelemetryCommandFailed extends Schema.TaggedErrorClass<NativeTelemetryCommandFailed>()( "NativeTelemetryCommandFailed", { operation: Schema.String, cause: Schema.Defect(), },) { override get message(): string { return `Resource monitor command '${this.operation}' failed.`; }} export class NativeTelemetryExited extends Schema.TaggedErrorClass<NativeTelemetryExited>()( "NativeTelemetryExited", { exitCode: Schema.Number, },) { override get message(): string { return `Resource monitor exited with code ${this.exitCode}.`; }} export class NativeTelemetryUnavailable extends Schema.TaggedErrorClass<NativeTelemetryUnavailable>()( "NativeTelemetryUnavailable", { reason: Schema.String, },) { override get message(): string { return `Resource monitor is unavailable: ${this.reason}`; }} export type NativeTelemetryClientError = | NativeTelemetrySpawnFailed | NativeTelemetryHandshakeTimedOut | NativeTelemetryProtocolMismatch | NativeTelemetryDecodeFailed | NativeTelemetryCommandFailed | NativeTelemetryExited | NativeTelemetryUnavailable; export interface NativeTelemetryClientHealth { readonly status: ResourceTelemetrySourceStatus; readonly hello: Option.Option<ResourceMonitorHelloEvent>; readonly lastSampleAt: Option.Option<DateTime.Utc>; readonly lastError: Option.Option<string>; readonly restartCount: number; readonly sampleIntervalMs: number;} export interface NativeTelemetryClientShape { readonly capabilities: Effect.Effect<ResourceMonitorCapabilities, NativeTelemetryClientError>; readonly snapshots: Stream.Stream<ResourceMonitorSnapshotEvent, NativeTelemetryClientError>; readonly readHistory: ( windowMs: number, ) => Effect.Effect<ReadonlyArray<ResourceMonitorSnapshotEvent>, NativeTelemetryClientError>; readonly setExternalProcesses: ( processes: ReadonlyArray<ResourceMonitorExternalProcess>, ) => Effect.Effect<void, NativeTelemetryClientError>; readonly setHostPowerState: ( snapshot: HostPowerSnapshot, ) => Effect.Effect<void, NativeTelemetryClientError>; readonly sampleNow: Effect.Effect<ResourceMonitorSnapshotEvent, NativeTelemetryClientError>; readonly retry: Effect.Effect<boolean>; readonly health: Effect.Effect<NativeTelemetryClientHealth>; readonly healthChanges: Stream.Stream<NativeTelemetryClientHealth>;} export class NativeTelemetryClient extends Context.Service< NativeTelemetryClient, NativeTelemetryClientShape>()("t3/resourceTelemetry/NativeTelemetryClient") {} interface ClientState { readonly status: ResourceTelemetrySourceStatus; readonly handle: Option.Option<ChildProcessSpawner.ChildProcessHandle>; readonly hello: Option.Option<ResourceMonitorHelloEvent>; readonly lastSampleAt: Option.Option<DateTime.Utc>; readonly lastError: Option.Option<string>; readonly restartCount: number;} interface CollectionControl { readonly hostPower: HostPowerSnapshot; readonly liveSubscriberCount: number; readonly sampleIntervalMs: number;} interface PendingHistoryRequest { readonly deferred: Deferred.Deferred< ReadonlyArray<ResourceMonitorSnapshotEvent>, NativeTelemetryClientError >; readonly snapshots: ReadonlyArray<ResourceMonitorSnapshotEvent>;} const initialState: ClientState = { status: "starting", handle: Option.none(), hello: Option.none(), lastSampleAt: Option.none(), lastError: Option.none(), restartCount: 0,}; function toHealth(state: ClientState, sampleIntervalMs: number): NativeTelemetryClientHealth { return { status: state.status, hello: state.hello, lastSampleAt: state.lastSampleAt, lastError: state.lastError, restartCount: state.restartCount, sampleIntervalMs, };} function isThermallyConstrained(snapshot: HostPowerSnapshot): boolean { return snapshot.thermalState === "serious" || snapshot.thermalState === "critical";} export function resolveNativeSampleIntervalMs( snapshot: HostPowerSnapshot, liveSubscriberCount: number,): number { if (snapshot.stale || snapshot.source === "unknown") { return liveSubscriberCount > 0 ? SAMPLE_INTERVAL_MS : UNKNOWN_BACKGROUND_SAMPLE_INTERVAL_MS; } if (snapshot.suspended) return 0; if ( snapshot.locked === "true" || snapshot.lowPowerMode === "true" || isThermallyConstrained(snapshot) ) { return CONSTRAINED_SAMPLE_INTERVAL_MS; } if (snapshot.onBattery === "true") return BATTERY_SAMPLE_INTERVAL_MS; return SAMPLE_INTERVAL_MS;} const decodeMonitorEvent: ( value: unknown,) => Effect.Effect<ResourceMonitorEvent, Schema.SchemaError> = Schema.decodeUnknownEffect( ResourceMonitorEventSchema,);const encodeMonitorCommand = Schema.encodeEffect( Schema.fromJsonString(ResourceMonitorCommandSchema),);const isProtocolMismatch = Schema.is(NativeTelemetryProtocolMismatch);const isDecodeFailed = Schema.is(NativeTelemetryDecodeFailed);const isCommandFailed = Schema.is(NativeTelemetryCommandFailed); function eventVersion(value: unknown): number | undefined { if (typeof value !== "object" || value === null) return undefined; const version = Reflect.get(value, "version"); return typeof version === "number" ? version : undefined;} function restartDelay(attempt: number): Duration.Duration { return Duration.min(Duration.times(INITIAL_RESTART_DELAY, 2 ** attempt), MAX_RESTART_DELAY);} function errorMessage(error: NativeTelemetryClientError): string { return error.message;} export const make = Effect.fn("resourceTelemetry.nativeTelemetryClient.make")(function* () { const binary = yield* ResourceMonitorBinary.ResourceMonitorBinary; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const crypto = yield* Crypto.Crypto; const config = yield* ServerConfig; const initializedAt = yield* DateTime.now; const state = yield* Ref.make(initialState); const collectionControl = yield* Ref.make<CollectionControl>({ hostPower: { source: "unknown", idle: "unknown", idleSeconds: null, locked: "unknown", suspended: false, onBattery: "unknown", lowPowerMode: "unknown", thermalState: "unknown", stale: true, updatedAt: initializedAt, }, liveSubscriberCount: 0, sampleIntervalMs: UNKNOWN_BACKGROUND_SAMPLE_INTERVAL_MS, }); const externalProcesses = yield* Ref.make<ReadonlyArray<ResourceMonitorExternalProcess>>([]); const pendingSamples = yield* Ref.make( new Map<string, Deferred.Deferred<ResourceMonitorSnapshotEvent, NativeTelemetryClientError>>(), ); const pendingHistories = yield* Ref.make(new Map<string, PendingHistoryRequest>()); const snapshots = yield* PubSub.sliding<ResourceMonitorSnapshotEvent>(8); const healthChanges = yield* PubSub.sliding<NativeTelemetryClientHealth>(4); const retryQueue = yield* Queue.sliding<void>(1); const commandMutex = yield* Semaphore.make(1); const currentHealth = Effect.all([Ref.get(state), Ref.get(collectionControl)]).pipe( Effect.map(([current, control]) => toHealth(current, control.sampleIntervalMs)), ); const publishHealth = currentHealth.pipe( Effect.flatMap((health) => PubSub.publish(healthChanges, health)), Effect.asVoid, ); const failPending = (error: NativeTelemetryClientError) => Effect.gen(function* () { const samples = yield* Ref.getAndSet(pendingSamples, new Map()); const histories = yield* Ref.getAndSet(pendingHistories, new Map()); yield* Effect.forEach(samples.values(), (deferred) => Deferred.fail(deferred, error), { discard: true, }); yield* Effect.forEach( histories.values(), (request) => Deferred.fail(request.deferred, error), { discard: true }, ); }); const writeCommand = ( handle: ChildProcessSpawner.ChildProcessHandle, command: ResourceMonitorCommand, ): Effect.Effect<void, NativeTelemetryClientError> => commandMutex.withPermits(1)( encodeMonitorCommand(command).pipe( Effect.map((encoded) => `${encoded}\n`), Effect.mapError( (cause) => new NativeTelemetryCommandFailed({ operation: command.type, cause, }), ), Effect.flatMap((encoded) => Stream.run(Stream.encodeText(Stream.make(encoded)), handle.stdin), ), Effect.mapError( (cause) => new NativeTelemetryCommandFailed({ operation: command.type, cause, }), ), ), ); const processEvent = ( event: ResourceMonitorEvent, helloDeferred: Deferred.Deferred<ResourceMonitorHelloEvent>, ): Effect.Effect<void, NativeTelemetryClientError> => { switch (event.type) { case "hello": return Ref.update(state, (current) => ({ ...current, status: "starting" as const, hello: Option.some(event), lastError: Option.none(), })).pipe( Effect.andThen(publishHealth), Effect.andThen(Deferred.succeed(helloDeferred, event)), Effect.asVoid, ); case "snapshot": return Effect.gen(function* () { const sampledAt = DateTime.makeUnsafe(event.sampledAtUnixMs); yield* Ref.update(state, (current) => ({ ...current, status: "healthy" as const, lastSampleAt: Option.some(sampledAt), lastError: Option.none(), })); yield* PubSub.publish(snapshots, event); if (event.requestId) { const deferred = yield* Ref.modify(pendingSamples, (pending) => { const next = new Map(pending); const current = next.get(event.requestId!); next.delete(event.requestId!); return [Option.fromUndefinedOr(current), next]; }); if (Option.isSome(deferred)) { yield* Deferred.succeed(deferred.value, event); } } }); case "historyChunk": return Effect.gen(function* () { const latestSnapshot = event.snapshots.at(-1); if (latestSnapshot) { yield* Ref.update(state, (current) => ({ ...current, status: "healthy" as const, lastSampleAt: Option.some(DateTime.makeUnsafe(latestSnapshot.sampledAtUnixMs)), lastError: Option.none(), })); } const completed = yield* Ref.modify(pendingHistories, (pending) => { const request = pending.get(event.requestId); if (!request) return [Option.none(), pending] as const; const snapshots = [...request.snapshots, ...event.snapshots]; const next = new Map(pending); if (event.done) { next.delete(event.requestId); return [Option.some({ deferred: request.deferred, snapshots }), next] as const; } next.set(event.requestId, { deferred: request.deferred, snapshots }); return [Option.none(), next] as const; }); if (Option.isSome(completed)) { yield* Deferred.succeed(completed.value.deferred, completed.value.snapshots); } }); case "error": return Ref.update(state, (current) => ({ ...current, status: "degraded" as const, lastError: Option.some(event.message), })).pipe( Effect.andThen(publishHealth), Effect.andThen( event.recoverable ? Effect.void : Effect.fail( new NativeTelemetryCommandFailed({ operation: event.code, cause: event.message, }), ), ), ); } }; const runAttempt: Effect.Effect<void, NativeTelemetryClientError> = Effect.scoped( Effect.gen(function* () { const executablePath = yield* binary.resolve.pipe( Effect.mapError( (error) => new NativeTelemetryUnavailable({ reason: error.message, }), ), ); const command = ChildProcess.make(executablePath, [], { cwd: config.cwd, stdin: { stream: "pipe", endOnDone: false, }, stdout: "pipe", stderr: "pipe", killSignal: "SIGTERM", forceKillAfter: Duration.seconds(2), }); const handle = yield* Effect.acquireRelease( spawner .spawn(command) .pipe( Effect.mapError( (cause) => new NativeTelemetrySpawnFailed({ path: executablePath, cause }), ), ), (child) => child.kill().pipe(Effect.ignore), ); yield* Ref.update(state, (current) => ({ ...current, status: "starting" as const, handle: Option.some(handle), hello: Option.none(), })); yield* publishHealth; const helloDeferred = yield* Deferred.make<ResourceMonitorHelloEvent>(); const eventFiber = yield* handle.stdout.pipe( Stream.pipeThroughChannel(Ndjson.decode({ ignoreEmptyLines: true })), Stream.mapEffect( ( value, ): Effect.Effect< ResourceMonitorEvent, NativeTelemetryProtocolMismatch | NativeTelemetryDecodeFailed > => { const version = eventVersion(value); if (version !== undefined && version !== RESOURCE_MONITOR_PROTOCOL_VERSION) { return Effect.fail( new NativeTelemetryProtocolMismatch({ expectedVersion: RESOURCE_MONITOR_PROTOCOL_VERSION, receivedVersion: version, }), ); } return decodeMonitorEvent(value).pipe( Effect.mapError((cause) => new NativeTelemetryDecodeFailed({ cause })), ); }, ), Stream.runForEach((event) => processEvent(event, helloDeferred)), Effect.mapError((cause) => isProtocolMismatch(cause) || isDecodeFailed(cause) || isCommandFailed(cause) ? cause : new NativeTelemetryDecodeFailed({ cause }), ), Effect.forkScoped,Lines 1–500 of 922. Load more
import type { HostPowerSnapshot, ResourceMonitorCapabilities, ResourceMonitorCommand, ResourceMonitorEvent, ResourceMonitorExternalProcess, ResourceMonitorHelloEvent, ResourceMonitorSnapshotEvent, ResourceTelemetrySourceStatus, } from "@t3tools/contracts"; import { RESOURCE_MONITOR_PROTOCOL_VERSION, ResourceMonitorCommand as ResourceMonitorCommandSchema, ResourceMonitorEvent as ResourceMonitorEventSchema, } from "@t3tools/contracts"; import * as Cause from "effect/Cause"; import * as Context from "effect/Context"; import * as Crypto from "effect/Crypto"; import * as DateTime from "effect/DateTime"; import * as Deferred from "effect/Deferred"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as PubSub from "effect/PubSub"; import * as Queue from "effect/Queue"; import * as Ref from "effect/Ref"; import * as Result from "effect/Result"; import * as Schema from "effect/Schema"; import * as Semaphore from "effect/Semaphore"; import * as Stream from "effect/Stream"; import * as Ndjson from "effect/unstable/encoding/Ndjson"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import * as ResourceMonitorBinary from "./ResourceMonitorBinary.ts"; import { ServerConfig } from "../config.ts"; const SAMPLE_INTERVAL_MS = 1_000; const UNKNOWN_BACKGROUND_SAMPLE_INTERVAL_MS = 5_000; const BATTERY_SAMPLE_INTERVAL_MS = 5_000; const CONSTRAINED_SAMPLE_INTERVAL_MS = 15_000; const HANDSHAKE_TIMEOUT = Duration.seconds(5); const SAMPLE_REQUEST_TIMEOUT = Duration.seconds(5); const HISTORY_REQUEST_TIMEOUT = Duration.seconds(15); const INITIAL_RESTART_DELAY = Duration.millis(500); const MAX_RESTART_DELAY = Duration.seconds(10); const FAILURE_WINDOW_MS = 60_000; const MAX_FAILURES_PER_WINDOW = 5; export class NativeTelemetrySpawnFailed extends Schema.TaggedErrorClass<NativeTelemetrySpawnFailed>()( "NativeTelemetrySpawnFailed", { path: Schema.String, cause: Schema.Defect(), }, ) { override get message(): string { return `Failed to start resource monitor '${this.path}'.`; } } export class NativeTelemetryHandshakeTimedOut extends Schema.TaggedErrorClass<NativeTelemetryHandshakeTimedOut>()( "NativeTelemetryHandshakeTimedOut", { timeoutMs: Schema.Number, }, ) { override get message(): string { return `Resource monitor handshake timed out after ${this.timeoutMs}ms.`; } } export class NativeTelemetryProtocolMismatch extends Schema.TaggedErrorClass<NativeTelemetryProtocolMismatch>()( "NativeTelemetryProtocolMismatch", { expectedVersion: Schema.Number, receivedVersion: Schema.Number, }, ) { override get message(): string { return `Resource monitor protocol ${this.receivedVersion} is incompatible with expected protocol ${this.expectedVersion}.`; } } export class NativeTelemetryDecodeFailed extends Schema.TaggedErrorClass<NativeTelemetryDecodeFailed>()( "NativeTelemetryDecodeFailed", { cause: Schema.Defect(), }, ) { override get message(): string { return "Failed to decode resource monitor output."; } } export class NativeTelemetryCommandFailed extends Schema.TaggedErrorClass<NativeTelemetryCommandFailed>()( "NativeTelemetryCommandFailed", { operation: Schema.String, cause: Schema.Defect(), }, ) { override get message(): string { return `Resource monitor command '${this.operation}' failed.`; } } export class NativeTelemetryExited extends Schema.TaggedErrorClass<NativeTelemetryExited>()( "NativeTelemetryExited", { exitCode: Schema.Number, }, ) { override get message(): string { return `Resource monitor exited with code ${this.exitCode}.`; } } export class NativeTelemetryUnavailable extends Schema.TaggedErrorClass<NativeTelemetryUnavailable>()( "NativeTelemetryUnavailable", { reason: Schema.String, }, ) { override get message(): string { return `Resource monitor is unavailable: ${this.reason}`; } } export type NativeTelemetryClientError = | NativeTelemetrySpawnFailed | NativeTelemetryHandshakeTimedOut | NativeTelemetryProtocolMismatch | NativeTelemetryDecodeFailed | NativeTelemetryCommandFailed | NativeTelemetryExited | NativeTelemetryUnavailable; export interface NativeTelemetryClientHealth { readonly status: ResourceTelemetrySourceStatus; readonly hello: Option.Option<ResourceMonitorHelloEvent>; readonly lastSampleAt: Option.Option<DateTime.Utc>; readonly lastError: Option.Option<string>; readonly restartCount: number; readonly sampleIntervalMs: number; } export interface NativeTelemetryClientShape { readonly capabilities: Effect.Effect<ResourceMonitorCapabilities, NativeTelemetryClientError>; readonly snapshots: Stream.Stream<ResourceMonitorSnapshotEvent, NativeTelemetryClientError>; readonly readHistory: ( windowMs: number, ) => Effect.Effect<ReadonlyArray<ResourceMonitorSnapshotEvent>, NativeTelemetryClientError>; readonly setExternalProcesses: ( processes: ReadonlyArray<ResourceMonitorExternalProcess>, ) => Effect.Effect<void, NativeTelemetryClientError>; readonly setHostPowerState: ( snapshot: HostPowerSnapshot, ) => Effect.Effect<void, NativeTelemetryClientError>; readonly sampleNow: Effect.Effect<ResourceMonitorSnapshotEvent, NativeTelemetryClientError>; readonly retry: Effect.Effect<boolean>; readonly health: Effect.Effect<NativeTelemetryClientHealth>; readonly healthChanges: Stream.Stream<NativeTelemetryClientHealth>; } export class NativeTelemetryClient extends Context.Service< NativeTelemetryClient, NativeTelemetryClientShape >()("t3/resourceTelemetry/NativeTelemetryClient") {} interface ClientState { readonly status: ResourceTelemetrySourceStatus; readonly handle: Option.Option<ChildProcessSpawner.ChildProcessHandle>; readonly hello: Option.Option<ResourceMonitorHelloEvent>; readonly lastSampleAt: Option.Option<DateTime.Utc>; readonly lastError: Option.Option<string>; readonly restartCount: number; } interface CollectionControl { readonly hostPower: HostPowerSnapshot; readonly liveSubscriberCount: number; readonly sampleIntervalMs: number; } interface PendingHistoryRequest { readonly deferred: Deferred.Deferred< ReadonlyArray<ResourceMonitorSnapshotEvent>, NativeTelemetryClientError >; readonly snapshots: ReadonlyArray<ResourceMonitorSnapshotEvent>; } const initialState: ClientState = { status: "starting", handle: Option.none(), hello: Option.none(), lastSampleAt: Option.none(), lastError: Option.none(), restartCount: 0, }; function toHealth(state: ClientState, sampleIntervalMs: number): NativeTelemetryClientHealth { return { status: state.status, hello: state.hello, lastSampleAt: state.lastSampleAt, lastError: state.lastError, restartCount: state.restartCount, sampleIntervalMs, }; } function isThermallyConstrained(snapshot: HostPowerSnapshot): boolean { return snapshot.thermalState === "serious" || snapshot.thermalState === "critical"; } export function resolveNativeSampleIntervalMs( snapshot: HostPowerSnapshot, liveSubscriberCount: number, ): number { if (snapshot.stale || snapshot.source === "unknown") { return liveSubscriberCount > 0 ? SAMPLE_INTERVAL_MS : UNKNOWN_BACKGROUND_SAMPLE_INTERVAL_MS; } if (snapshot.suspended) return 0; if ( snapshot.locked === "true" || snapshot.lowPowerMode === "true" || isThermallyConstrained(snapshot) ) { return CONSTRAINED_SAMPLE_INTERVAL_MS; } if (snapshot.onBattery === "true") return BATTERY_SAMPLE_INTERVAL_MS; return SAMPLE_INTERVAL_MS; } const decodeMonitorEvent: ( value: unknown, ) => Effect.Effect<ResourceMonitorEvent, Schema.SchemaError> = Schema.decodeUnknownEffect( ResourceMonitorEventSchema, ); const encodeMonitorCommand = Schema.encodeEffect( Schema.fromJsonString(ResourceMonitorCommandSchema), ); const isProtocolMismatch = Schema.is(NativeTelemetryProtocolMismatch); const isDecodeFailed = Schema.is(NativeTelemetryDecodeFailed); const isCommandFailed = Schema.is(NativeTelemetryCommandFailed); function eventVersion(value: unknown): number | undefined { if (typeof value !== "object" || value === null) return undefined; const version = Reflect.get(value, "version"); return typeof version === "number" ? version : undefined; } function restartDelay(attempt: number): Duration.Duration { return Duration.min(Duration.times(INITIAL_RESTART_DELAY, 2 ** attempt), MAX_RESTART_DELAY); } function errorMessage(error: NativeTelemetryClientError): string { return error.message; } export const make = Effect.fn("resourceTelemetry.nativeTelemetryClient.make")(function* () { const binary = yield* ResourceMonitorBinary.ResourceMonitorBinary; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const crypto = yield* Crypto.Crypto; const config = yield* ServerConfig; const initializedAt = yield* DateTime.now; const state = yield* Ref.make(initialState); const collectionControl = yield* Ref.make<CollectionControl>({ hostPower: { source: "unknown", idle: "unknown", idleSeconds: null, locked: "unknown", suspended: false, onBattery: "unknown", lowPowerMode: "unknown", thermalState: "unknown", stale: true, updatedAt: initializedAt, }, liveSubscriberCount: 0, sampleIntervalMs: UNKNOWN_BACKGROUND_SAMPLE_INTERVAL_MS, }); const externalProcesses = yield* Ref.make<ReadonlyArray<ResourceMonitorExternalProcess>>([]); const pendingSamples = yield* Ref.make( new Map<string, Deferred.Deferred<ResourceMonitorSnapshotEvent, NativeTelemetryClientError>>(), ); const pendingHistories = yield* Ref.make(new Map<string, PendingHistoryRequest>()); const snapshots = yield* PubSub.sliding<ResourceMonitorSnapshotEvent>(8); const healthChanges = yield* PubSub.sliding<NativeTelemetryClientHealth>(4); const retryQueue = yield* Queue.sliding<void>(1); const commandMutex = yield* Semaphore.make(1); const currentHealth = Effect.all([Ref.get(state), Ref.get(collectionControl)]).pipe( Effect.map(([current, control]) => toHealth(current, control.sampleIntervalMs)), ); const publishHealth = currentHealth.pipe( Effect.flatMap((health) => PubSub.publish(healthChanges, health)), Effect.asVoid, ); const failPending = (error: NativeTelemetryClientError) => Effect.gen(function* () { const samples = yield* Ref.getAndSet(pendingSamples, new Map()); const histories = yield* Ref.getAndSet(pendingHistories, new Map()); yield* Effect.forEach(samples.values(), (deferred) => Deferred.fail(deferred, error), { discard: true, }); yield* Effect.forEach( histories.values(), (request) => Deferred.fail(request.deferred, error), { discard: true }, ); }); const writeCommand = ( handle: ChildProcessSpawner.ChildProcessHandle, command: ResourceMonitorCommand, ): Effect.Effect<void, NativeTelemetryClientError> => commandMutex.withPermits(1)( encodeMonitorCommand(command).pipe( Effect.map((encoded) => `${encoded}\n`), Effect.mapError( (cause) => new NativeTelemetryCommandFailed({ operation: command.type, cause, }), ), Effect.flatMap((encoded) => Stream.run(Stream.encodeText(Stream.make(encoded)), handle.stdin), ), Effect.mapError( (cause) => new NativeTelemetryCommandFailed({ operation: command.type, cause, }), ), ), ); const processEvent = ( event: ResourceMonitorEvent, helloDeferred: Deferred.Deferred<ResourceMonitorHelloEvent>, ): Effect.Effect<void, NativeTelemetryClientError> => { switch (event.type) { case "hello": return Ref.update(state, (current) => ({ ...current, status: "starting" as const, hello: Option.some(event), lastError: Option.none(), })).pipe( Effect.andThen(publishHealth), Effect.andThen(Deferred.succeed(helloDeferred, event)), Effect.asVoid, ); case "snapshot": return Effect.gen(function* () { const sampledAt = DateTime.makeUnsafe(event.sampledAtUnixMs); yield* Ref.update(state, (current) => ({ ...current, status: "healthy" as const, lastSampleAt: Option.some(sampledAt), lastError: Option.none(), })); yield* PubSub.publish(snapshots, event); if (event.requestId) { const deferred = yield* Ref.modify(pendingSamples, (pending) => { const next = new Map(pending); const current = next.get(event.requestId!); next.delete(event.requestId!); return [Option.fromUndefinedOr(current), next]; }); if (Option.isSome(deferred)) { yield* Deferred.succeed(deferred.value, event); } } }); case "historyChunk": return Effect.gen(function* () { const latestSnapshot = event.snapshots.at(-1); if (latestSnapshot) { yield* Ref.update(state, (current) => ({ ...current, status: "healthy" as const, lastSampleAt: Option.some(DateTime.makeUnsafe(latestSnapshot.sampledAtUnixMs)), lastError: Option.none(), })); } const completed = yield* Ref.modify(pendingHistories, (pending) => { const request = pending.get(event.requestId); if (!request) return [Option.none(), pending] as const; const snapshots = [...request.snapshots, ...event.snapshots]; const next = new Map(pending); if (event.done) { next.delete(event.requestId); return [Option.some({ deferred: request.deferred, snapshots }), next] as const; } next.set(event.requestId, { deferred: request.deferred, snapshots }); return [Option.none(), next] as const; }); if (Option.isSome(completed)) { yield* Deferred.succeed(completed.value.deferred, completed.value.snapshots); } }); case "error": return Ref.update(state, (current) => ({ ...current, status: "degraded" as const, lastError: Option.some(event.message), })).pipe( Effect.andThen(publishHealth), Effect.andThen( event.recoverable ? Effect.void : Effect.fail( new NativeTelemetryCommandFailed({ operation: event.code, cause: event.message, }), ), ), ); } }; const runAttempt: Effect.Effect<void, NativeTelemetryClientError> = Effect.scoped( Effect.gen(function* () { const executablePath = yield* binary.resolve.pipe( Effect.mapError( (error) => new NativeTelemetryUnavailable({ reason: error.message, }), ), ); const command = ChildProcess.make(executablePath, [], { cwd: config.cwd, stdin: { stream: "pipe", endOnDone: false, }, stdout: "pipe", stderr: "pipe", killSignal: "SIGTERM", forceKillAfter: Duration.seconds(2), }); const handle = yield* Effect.acquireRelease( spawner .spawn(command) .pipe( Effect.mapError( (cause) => new NativeTelemetrySpawnFailed({ path: executablePath, cause }), ), ), (child) => child.kill().pipe(Effect.ignore), ); yield* Ref.update(state, (current) => ({ ...current, status: "starting" as const, handle: Option.some(handle), hello: Option.none(), })); yield* publishHealth; const helloDeferred = yield* Deferred.make<ResourceMonitorHelloEvent>(); const eventFiber = yield* handle.stdout.pipe( Stream.pipeThroughChannel(Ndjson.decode({ ignoreEmptyLines: true })), Stream.mapEffect( ( value, ): Effect.Effect< ResourceMonitorEvent, NativeTelemetryProtocolMismatch | NativeTelemetryDecodeFailed > => { const version = eventVersion(value); if (version !== undefined && version !== RESOURCE_MONITOR_PROTOCOL_VERSION) { return Effect.fail( new NativeTelemetryProtocolMismatch({ expectedVersion: RESOURCE_MONITOR_PROTOCOL_VERSION, receivedVersion: version, }), ); } return decodeMonitorEvent(value).pipe( Effect.mapError((cause) => new NativeTelemetryDecodeFailed({ cause })), ); }, ), Stream.runForEach((event) => processEvent(event, helloDeferred)), Effect.mapError((cause) => isProtocolMismatch(cause) || isDecodeFailed(cause) || isCommandFailed(cause) ? cause : new NativeTelemetryDecodeFailed({ cause }), ), Effect.forkScoped,