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 Scope from "effect/Scope";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";import { subscribeBeforeSnapshotWithoutMutex } from "../utils/subscribeBeforeSnapshot.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 NativeTelemetryRequestTimedOut extends Schema.TaggedErrorClass<NativeTelemetryRequestTimedOut>()( "NativeTelemetryRequestTimedOut", { operation: Schema.Literals(["readHistory", "sampleNow"]), timeoutMs: Schema.Number, },) { override get message(): string { return `Resource monitor '${this.operation}' request 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 NativeTelemetryStreamClosed extends Schema.TaggedErrorClass<NativeTelemetryStreamClosed>()( "NativeTelemetryStreamClosed", {},) { override get message(): string { return "Resource monitor event stream closed unexpectedly."; }} 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 = | ResourceMonitorBinary.ResourceMonitorBinaryError | NativeTelemetrySpawnFailed | NativeTelemetryHandshakeTimedOut | NativeTelemetryRequestTimedOut | NativeTelemetryProtocolMismatch | NativeTelemetryDecodeFailed | NativeTelemetryCommandFailed | NativeTelemetryExited | NativeTelemetryStreamClosed | 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 NativeTelemetrySnapshot { readonly generation: number; readonly snapshot: ResourceMonitorSnapshotEvent;} export class NativeTelemetryClient extends Context.Service< NativeTelemetryClient, { readonly capabilities: Effect.Effect<ResourceMonitorCapabilities, NativeTelemetryClientError>; readonly snapshots: Stream.Stream<NativeTelemetrySnapshot, 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<NativeTelemetrySnapshot, NativeTelemetryClientError>; readonly retry: Effect.Effect<boolean>; readonly health: Effect.Effect<NativeTelemetryClientHealth>; readonly subscribeHealth: Effect.Effect< { readonly latest: NativeTelemetryClientHealth; readonly changes: Stream.Stream<NativeTelemetryClientHealth>; }, never, Scope.Scope >; }>()("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;} export 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 || snapshot.locked === "true" || snapshot.lowPowerMode === "true" || isThermallyConstrained(snapshot) ) { return CONSTRAINED_SAMPLE_INTERVAL_MS; } if (snapshot.onBattery === "true") return BATTERY_SAMPLE_INTERVAL_MS; return liveSubscriberCount > 0 ? SAMPLE_INTERVAL_MS : UNKNOWN_BACKGROUND_SAMPLE_INTERVAL_MS;} export function commitCollectionControlUpdate<E, R>( desiredState: Ref.Ref<CollectionControl>, appliedState: Ref.Ref<CollectionControl>, update: (current: CollectionControl) => CollectionControl, apply: (previous: CollectionControl, next: CollectionControl) => Effect.Effect<void, E, R>,): Effect.Effect<readonly [CollectionControl, CollectionControl], E, R> { return Effect.gen(function* () { const [previousDesired, next] = yield* Ref.modify(desiredState, (previous) => { const next = update(previous); return [[previous, next] as const, next]; }); const previousApplied = yield* Ref.get(appliedState); yield* apply(previousApplied, next); yield* Ref.set(appliedState, next); return [previousDesired, next] as const; });} export function synchronizeCollectionControlOnStart<E1, R1, E2, R2>( mutex: Semaphore.Semaphore, desiredState: Ref.Ref<CollectionControl>, appliedState: Ref.Ref<CollectionControl>, apply: (control: CollectionControl) => Effect.Effect<void, E1, R1>, markReady: Effect.Effect<void, E2, R2>,) { return mutex.withPermits(1)( Effect.gen(function* () { const control = yield* Ref.get(desiredState); yield* apply(control); yield* Ref.set(appliedState, control); yield* markReady; return control; }), );} 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);} export function retainRecentNativeTelemetryFailures( failures: ReadonlyArray<number>, now: number,): ReadonlyArray<number> { return failures.filter((failedAt) => now - failedAt <= FAILURE_WINDOW_MS);} function errorMessage(error: NativeTelemetryClientError): string { return error.message;} export function nativeTelemetrySupervisorFailureMessage(_cause: Cause.Cause<unknown>): string { return "Resource monitor supervisor stopped unexpectedly.";} export function canRequestNativeTelemetryRetry( status: ResourceTelemetrySourceStatus, hasHandle: boolean,): boolean { return status !== "healthy" && status !== "starting" && !hasHandle;} export function canCommandNativeTelemetrySidecar( status: ResourceTelemetrySourceStatus, hasHandle: boolean,): boolean { return hasHandle && (status === "healthy" || status === "degraded");} 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 appliedCollectionControl = yield* Ref.make(yield* Ref.get(collectionControl)); const externalProcesses = yield* Ref.make<ReadonlyArray<ResourceMonitorExternalProcess>>([]); const pendingSamples = yield* Ref.make( new Map<string, Deferred.Deferred<NativeTelemetrySnapshot, NativeTelemetryClientError>>(), ); const pendingHistories = yield* Ref.make(new Map<string, PendingHistoryRequest>()); const snapshots = yield* PubSub.sliding<NativeTelemetrySnapshot>(8); const healthChanges = yield* PubSub.sliding<NativeTelemetryClientHealth>(4); const retryQueue = yield* Queue.sliding<void>(1); const commandMutex = yield* Semaphore.make(1); const controlMutex = 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>, generation: number, ): 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 nativeSnapshot = { generation, snapshot: event } satisfies NativeTelemetrySnapshot; const sampledAt = DateTime.makeUnsafe(event.sampledAtUnixMs); const healthChanged = yield* Ref.modify(state, (current) => [ current.status !== "healthy" || Option.isSome(current.lastError), { ...current, status: "healthy" as const, lastSampleAt: Option.some(sampledAt), lastError: Option.none(), }, ]); if (healthChanged) yield* publishHealth; yield* PubSub.publish(snapshots, nativeSnapshot); 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, nativeSnapshot); } } }); case "historyChunk": return Effect.gen(function* () { const latestSnapshot = event.snapshots.at(-1); const healthChanged = yield* Ref.modify(state, (current) => [ current.status !== "healthy" || Option.isSome(current.lastError), { ...current, status: "healthy" as const, lastSampleAt: latestSnapshot ? Option.some(DateTime.makeUnsafe(latestSnapshot.sampledAtUnixMs)) : current.lastSampleAt, lastError: Option.none(), },Lines 1–500 of 1030. 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 Scope from "effect/Scope"; 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"; import { subscribeBeforeSnapshotWithoutMutex } from "../utils/subscribeBeforeSnapshot.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 NativeTelemetryRequestTimedOut extends Schema.TaggedErrorClass<NativeTelemetryRequestTimedOut>()( "NativeTelemetryRequestTimedOut", { operation: Schema.Literals(["readHistory", "sampleNow"]), timeoutMs: Schema.Number, }, ) { override get message(): string { return `Resource monitor '${this.operation}' request 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 NativeTelemetryStreamClosed extends Schema.TaggedErrorClass<NativeTelemetryStreamClosed>()( "NativeTelemetryStreamClosed", {}, ) { override get message(): string { return "Resource monitor event stream closed unexpectedly."; } } 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 = | ResourceMonitorBinary.ResourceMonitorBinaryError | NativeTelemetrySpawnFailed | NativeTelemetryHandshakeTimedOut | NativeTelemetryRequestTimedOut | NativeTelemetryProtocolMismatch | NativeTelemetryDecodeFailed | NativeTelemetryCommandFailed | NativeTelemetryExited | NativeTelemetryStreamClosed | 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 NativeTelemetrySnapshot { readonly generation: number; readonly snapshot: ResourceMonitorSnapshotEvent; } export class NativeTelemetryClient extends Context.Service< NativeTelemetryClient, { readonly capabilities: Effect.Effect<ResourceMonitorCapabilities, NativeTelemetryClientError>; readonly snapshots: Stream.Stream<NativeTelemetrySnapshot, 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<NativeTelemetrySnapshot, NativeTelemetryClientError>; readonly retry: Effect.Effect<boolean>; readonly health: Effect.Effect<NativeTelemetryClientHealth>; readonly subscribeHealth: Effect.Effect< { readonly latest: NativeTelemetryClientHealth; readonly changes: Stream.Stream<NativeTelemetryClientHealth>; }, never, Scope.Scope >; } >()("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; } export 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 || snapshot.locked === "true" || snapshot.lowPowerMode === "true" || isThermallyConstrained(snapshot) ) { return CONSTRAINED_SAMPLE_INTERVAL_MS; } if (snapshot.onBattery === "true") return BATTERY_SAMPLE_INTERVAL_MS; return liveSubscriberCount > 0 ? SAMPLE_INTERVAL_MS : UNKNOWN_BACKGROUND_SAMPLE_INTERVAL_MS; } export function commitCollectionControlUpdate<E, R>( desiredState: Ref.Ref<CollectionControl>, appliedState: Ref.Ref<CollectionControl>, update: (current: CollectionControl) => CollectionControl, apply: (previous: CollectionControl, next: CollectionControl) => Effect.Effect<void, E, R>, ): Effect.Effect<readonly [CollectionControl, CollectionControl], E, R> { return Effect.gen(function* () { const [previousDesired, next] = yield* Ref.modify(desiredState, (previous) => { const next = update(previous); return [[previous, next] as const, next]; }); const previousApplied = yield* Ref.get(appliedState); yield* apply(previousApplied, next); yield* Ref.set(appliedState, next); return [previousDesired, next] as const; }); } export function synchronizeCollectionControlOnStart<E1, R1, E2, R2>( mutex: Semaphore.Semaphore, desiredState: Ref.Ref<CollectionControl>, appliedState: Ref.Ref<CollectionControl>, apply: (control: CollectionControl) => Effect.Effect<void, E1, R1>, markReady: Effect.Effect<void, E2, R2>, ) { return mutex.withPermits(1)( Effect.gen(function* () { const control = yield* Ref.get(desiredState); yield* apply(control); yield* Ref.set(appliedState, control); yield* markReady; return control; }), ); } 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); } export function retainRecentNativeTelemetryFailures( failures: ReadonlyArray<number>, now: number, ): ReadonlyArray<number> { return failures.filter((failedAt) => now - failedAt <= FAILURE_WINDOW_MS); } function errorMessage(error: NativeTelemetryClientError): string { return error.message; } export function nativeTelemetrySupervisorFailureMessage(_cause: Cause.Cause<unknown>): string { return "Resource monitor supervisor stopped unexpectedly."; } export function canRequestNativeTelemetryRetry( status: ResourceTelemetrySourceStatus, hasHandle: boolean, ): boolean { return status !== "healthy" && status !== "starting" && !hasHandle; } export function canCommandNativeTelemetrySidecar( status: ResourceTelemetrySourceStatus, hasHandle: boolean, ): boolean { return hasHandle && (status === "healthy" || status === "degraded"); } 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 appliedCollectionControl = yield* Ref.make(yield* Ref.get(collectionControl)); const externalProcesses = yield* Ref.make<ReadonlyArray<ResourceMonitorExternalProcess>>([]); const pendingSamples = yield* Ref.make( new Map<string, Deferred.Deferred<NativeTelemetrySnapshot, NativeTelemetryClientError>>(), ); const pendingHistories = yield* Ref.make(new Map<string, PendingHistoryRequest>()); const snapshots = yield* PubSub.sliding<NativeTelemetrySnapshot>(8); const healthChanges = yield* PubSub.sliding<NativeTelemetryClientHealth>(4); const retryQueue = yield* Queue.sliding<void>(1); const commandMutex = yield* Semaphore.make(1); const controlMutex = 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>, generation: number, ): 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 nativeSnapshot = { generation, snapshot: event } satisfies NativeTelemetrySnapshot; const sampledAt = DateTime.makeUnsafe(event.sampledAtUnixMs); const healthChanged = yield* Ref.modify(state, (current) => [ current.status !== "healthy" || Option.isSome(current.lastError), { ...current, status: "healthy" as const, lastSampleAt: Option.some(sampledAt), lastError: Option.none(), }, ]); if (healthChanged) yield* publishHealth; yield* PubSub.publish(snapshots, nativeSnapshot); 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, nativeSnapshot); } } }); case "historyChunk": return Effect.gen(function* () { const latestSnapshot = event.snapshots.at(-1); const healthChanged = yield* Ref.modify(state, (current) => [ current.status !== "healthy" || Option.isSome(current.lastError), { ...current, status: "healthy" as const, lastSampleAt: latestSnapshot ? Option.some(DateTime.makeUnsafe(latestSnapshot.sampledAtUnixMs)) : current.lastSampleAt, lastError: Option.none(), },