1import type { HostPowerSnapshot } from "@t3tools/contracts";
2import * as Context from "effect/Context";
3import * as DateTime from "effect/DateTime";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Option from "effect/Option";
7import * as PubSub from "effect/PubSub";
8import * as Ref from "effect/Ref";
9import * as Stream from "effect/Stream";
10
11import * as DesktopTelemetryReceiver from "../resourceTelemetry/DesktopTelemetryReceiver.ts";
12
13export class HostPowerMonitor extends Context.Service<
14 HostPowerMonitor,
15 {
16 readonly snapshot: Effect.Effect<HostPowerSnapshot>;
17 readonly report: (snapshot: HostPowerSnapshot) => Effect.Effect<void>;
18 readonly streamChanges: Stream.Stream<HostPowerSnapshot>;
19 }
20>()("t3/background/HostPowerMonitor") {}
21
22const makeUnknownSnapshot = (
23 source: HostPowerSnapshot["source"],
24 updatedAt: HostPowerSnapshot["updatedAt"],
25): HostPowerSnapshot => ({
26 source,
27 idle: "unknown",
28 idleSeconds: null,
29 locked: "unknown",
30 suspended: false,
31 onBattery: "unknown",
32 lowPowerMode: "unknown",
33 thermalState: "unknown",
34 stale: true,
35 updatedAt,
36});
37
38function samePowerState(left: HostPowerSnapshot, right: HostPowerSnapshot): boolean {
39 return (
40 left.source === right.source &&
41 left.idle === right.idle &&
42 left.locked === right.locked &&
43 left.suspended === right.suspended &&
44 left.onBattery === right.onBattery &&
45 left.lowPowerMode === right.lowPowerMode &&
46 left.thermalState === right.thermalState &&
47 left.stale === right.stale
48 );
49}
50
51export const make = Effect.fn("background.hostPower.make")(function* (
52 initialSnapshot?: HostPowerSnapshot,
53) {
54 const initial = initialSnapshot ?? makeUnknownSnapshot("unknown", yield* DateTime.now);
55 const latestRef = yield* Ref.make(initial);
56 const changes = yield* PubSub.sliding<HostPowerSnapshot>(1);
57
58 const report: HostPowerMonitor["Service"]["report"] = (snapshot) =>
59 Ref.modify(latestRef, (current) => {
60 if (DateTime.isLessThan(snapshot.updatedAt, current.updatedAt)) {
61 return [Option.none<HostPowerSnapshot>(), current] as const;
62 }
63 return [
64 samePowerState(current, snapshot) ? Option.none() : Option.some(snapshot),
65 snapshot,
66 ] as const;
67 }).pipe(
68 Effect.flatMap(
69 Option.match({
70 onNone: () => Effect.void,
71 onSome: (next) => PubSub.publish(changes, next),
72 }),
73 ),
74 Effect.asVoid,
75 );
76
77 return HostPowerMonitor.of({
78 snapshot: Ref.get(latestRef),
79 report,
80 streamChanges: Stream.fromPubSub(changes),
81 });
82});
83
84export const layer = Layer.effect(
85 HostPowerMonitor,
86 Effect.gen(function* () {
87 const desktopTelemetry = yield* DesktopTelemetryReceiver.DesktopTelemetryReceiver;
88 const desktopSubscription = yield* desktopTelemetry.subscribe;
89 const initial = desktopSubscription.latest;
90 const monitor = yield* Option.match(initial, {
91 onNone: () => make(),
92 onSome: (snapshot) => make(snapshot.power),
93 });
94 yield* desktopSubscription.changes.pipe(
95 Stream.map((snapshot) => snapshot.power),
96 Stream.runForEach(monitor.report),
97 Effect.forkScoped,
98 );
99 return monitor;
100 }),
101);