@flighthq/power 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ export * from './power';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './power';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC"}
@@ -0,0 +1,21 @@
1
+ import type { Power, PowerBackend, PowerBatteryHealth, PowerIdleState, PowerKeepAwakeMode, PowerStatus, PowerThermalState } from '@flighthq/types';
2
+ export declare function attachPower(power: Power, idleThresholdSeconds?: number): void;
3
+ export declare function createPower(): Power;
4
+ export declare function createPowerBatteryHealth(): PowerBatteryHealth;
5
+ export declare function createPowerStatus(): PowerStatus;
6
+ export declare function createWebPowerBackend(): PowerBackend;
7
+ export declare function detachPower(power: Power): void;
8
+ export declare function disposePower(power: Power): void;
9
+ export declare function enablePowerSignals(): Power;
10
+ export declare function getPowerBackend(): PowerBackend;
11
+ export declare function getPowerBatteryHealth(out: PowerBatteryHealth): PowerBatteryHealth | null;
12
+ export declare function getPowerIdlePollingIntervalMs(): number;
13
+ export declare function getPowerStatus(out: PowerStatus): PowerStatus;
14
+ export declare function getPowerSystemIdleState(thresholdSeconds: number): PowerIdleState;
15
+ export declare function getPowerSystemIdleTime(): number;
16
+ export declare function getPowerThermalState(): PowerThermalState;
17
+ export declare function hasPowerKeepAwake(): boolean;
18
+ export declare function setPowerBackend(backend: PowerBackend | null): void;
19
+ export declare function setPowerIdlePollingIntervalMs(intervalMs: number): void;
20
+ export declare function setPowerKeepAwake(enabled: boolean, mode?: PowerKeepAwakeMode): boolean;
21
+ //# sourceMappingURL=power.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"power.d.ts","sourceRoot":"","sources":["../src/power.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,KAAK,EACL,YAAY,EACZ,kBAAkB,EAClB,cAAc,EACd,kBAAkB,EAClB,WAAW,EACX,iBAAiB,EAClB,MAAM,iBAAiB,CAAC;AASzB,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,oBAAoB,SAAK,GAAG,IAAI,CA+CzE;AAGD,wBAAgB,WAAW,IAAI,KAAK,CAanC;AAGD,wBAAgB,wBAAwB,IAAI,kBAAkB,CAQ7D;AAGD,wBAAgB,iBAAiB,IAAI,WAAW,CAW/C;AAID,wBAAgB,qBAAqB,IAAI,YAAY,CA0JpD;AAGD,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAM9C;AAID,wBAAgB,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAE/C;AAED,wBAAgB,kBAAkB,IAAI,KAAK,CAE1C;AAED,wBAAgB,eAAe,IAAI,YAAY,CAG9C;AAID,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,kBAAkB,GAAG,kBAAkB,GAAG,IAAI,CAExF;AAGD,wBAAgB,6BAA6B,IAAI,MAAM,CAEtD;AAGD,wBAAgB,cAAc,CAAC,GAAG,EAAE,WAAW,GAAG,WAAW,CAE5D;AAGD,wBAAgB,uBAAuB,CAAC,gBAAgB,EAAE,MAAM,GAAG,cAAc,CAEhF;AAGD,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C;AAGD,wBAAgB,oBAAoB,IAAI,iBAAiB,CAExD;AAGD,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAGD,wBAAgB,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,GAAG,IAAI,CAElE;AAKD,wBAAgB,6BAA6B,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAEtE;AAID,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAEtF"}
package/dist/power.js ADDED
@@ -0,0 +1,341 @@
1
+ import { createSignal, emitSignal, hasSignalSlots } from '@flighthq/signals';
2
+ // Begins delivering power changes to `power`'s signals by subscribing to the active backend. On each
3
+ // change it reads a fresh status and emits onChange plus onCharging/onDischarging on charging
4
+ // transitions. For onIdleStateChange, a polling interval is started at the rate set by
5
+ // setPowerIdlePollingIntervalMs (default 5000ms); the poll is guarded so it only emits when at least
6
+ // one listener is connected to onIdleStateChange. idleThresholdSeconds (default 60) controls what
7
+ // idle state the backend reports as 'Idle' vs 'Active'. Idempotent: a prior subscription is torn
8
+ // down first. Pair with detachPower/disposePower.
9
+ export function attachPower(power, idleThresholdSeconds = 60) {
10
+ detachPower(power);
11
+ const backend = getPowerBackend();
12
+ let wasCharging = backend.getStatus(_scratch).isCharging;
13
+ const unsubscribeChange = backend.subscribe(() => {
14
+ const status = backend.getStatus(_scratch);
15
+ emitSignal(power.onChange, status);
16
+ if (status.isCharging !== wasCharging) {
17
+ wasCharging = status.isCharging;
18
+ emitSignal(status.isCharging ? power.onCharging : power.onDischarging);
19
+ }
20
+ });
21
+ const unsubscribeLockScreen = backend.subscribeLockScreen(() => emitSignal(power.onLockScreen));
22
+ const unsubscribeLowPowerModeChange = backend.subscribeLowPowerModeChange(() => emitSignal(power.onLowPowerModeChange));
23
+ const unsubscribeResume = backend.subscribeResume(() => emitSignal(power.onResume));
24
+ const unsubscribeSuspend = backend.subscribeSuspend(() => emitSignal(power.onSuspend));
25
+ const unsubscribeThermalStateChange = backend.subscribeThermalStateChange(() => emitSignal(power.onThermalStateChange));
26
+ const unsubscribeUnlockScreen = backend.subscribeUnlockScreen(() => emitSignal(power.onUnlockScreen));
27
+ // Idle state polling: poll at the configured interval and emit onIdleStateChange on transitions.
28
+ // The poll emits only when at least one slot is connected, avoiding spurious allocations when
29
+ // nobody is listening. The interval still runs (cheaply) so state transitions are never missed
30
+ // when a listener connects after attach.
31
+ let lastIdleState = backend.getSystemIdleState(idleThresholdSeconds);
32
+ const idleIntervalId = setInterval(() => {
33
+ if (!hasSignalSlots(power.onIdleStateChange))
34
+ return;
35
+ const current = backend.getSystemIdleState(idleThresholdSeconds);
36
+ if (current !== lastIdleState) {
37
+ lastIdleState = current;
38
+ emitSignal(power.onIdleStateChange);
39
+ }
40
+ }, _idlePollingIntervalMs);
41
+ _subscriptions.set(power, () => {
42
+ unsubscribeChange();
43
+ unsubscribeLockScreen();
44
+ unsubscribeLowPowerModeChange();
45
+ unsubscribeResume();
46
+ unsubscribeSuspend();
47
+ unsubscribeThermalStateChange();
48
+ unsubscribeUnlockScreen();
49
+ clearInterval(idleIntervalId);
50
+ });
51
+ }
52
+ // Allocates a Power event entity with inert signals; call attachPower to start delivery.
53
+ export function createPower() {
54
+ return {
55
+ onChange: createSignal(),
56
+ onCharging: createSignal(),
57
+ onDischarging: createSignal(),
58
+ onIdleStateChange: createSignal(),
59
+ onLockScreen: createSignal(),
60
+ onLowPowerModeChange: createSignal(),
61
+ onResume: createSignal(),
62
+ onSuspend: createSignal(),
63
+ onThermalStateChange: createSignal(),
64
+ onUnlockScreen: createSignal(),
65
+ };
66
+ }
67
+ // Allocates a zeroed PowerBatteryHealth, suitable as the `out` for getPowerBatteryHealth.
68
+ export function createPowerBatteryHealth() {
69
+ return {
70
+ capacityWearLevel: -1,
71
+ cycleCount: -1,
72
+ healthState: 'Unknown',
73
+ temperatureCelsius: -1,
74
+ voltage: -1,
75
+ };
76
+ }
77
+ // Allocates a zeroed PowerStatus, suitable as the `out` for getPowerStatus.
78
+ export function createPowerStatus() {
79
+ return {
80
+ batteryLevel: -1,
81
+ chargingTime: -1,
82
+ dischargingTime: -1,
83
+ isBatteryLow: false,
84
+ isCharging: false,
85
+ isLowPower: false,
86
+ isOnBattery: false,
87
+ thermalState: 'Unknown',
88
+ };
89
+ }
90
+ // Builds the default web backend over the Battery Status API and the Screen Wake Lock API. Degrades
91
+ // to batteryLevel=-1 / not charging and a no-op subscription where the APIs are absent.
92
+ export function createWebPowerBackend() {
93
+ let cachedLevel = -1;
94
+ let cachedCharging = false;
95
+ let cachedChargingTime = -1;
96
+ let cachedDischargingTime = -1;
97
+ return {
98
+ getBatteryHealth(_out) {
99
+ // Browser Battery Status API does not expose health information.
100
+ return null;
101
+ },
102
+ isKeepAwakeActive() {
103
+ return _wakeLockSentinel !== null;
104
+ },
105
+ getStatus(out) {
106
+ // Read all input values first (alias-safe: out may be the same object as an input).
107
+ const level = cachedLevel;
108
+ const charging = cachedCharging;
109
+ const chargingTime = cachedChargingTime;
110
+ const dischargingTime = cachedDischargingTime;
111
+ out.batteryLevel = level;
112
+ out.chargingTime = chargingTime;
113
+ out.dischargingTime = dischargingTime;
114
+ out.isBatteryLow = level >= 0 && level <= 0.2 && !charging;
115
+ out.isCharging = charging;
116
+ // Web Battery Status API: isOnBattery = device has a battery and is not charging/AC.
117
+ // If level is -1 (API absent), we cannot determine the power source; use false as sentinel.
118
+ out.isOnBattery = level >= 0 && !charging;
119
+ // No real OS low-power-mode read available on web; use false as sentinel.
120
+ out.isLowPower = false;
121
+ out.thermalState = 'Unknown';
122
+ return out;
123
+ },
124
+ getSystemIdleState(_thresholdSeconds) {
125
+ // The Idle Detection API is permission-gated and not widely available; return sentinel.
126
+ return 'Unknown';
127
+ },
128
+ getSystemIdleTime() {
129
+ // No idle-time API available on web; return sentinel.
130
+ return -1;
131
+ },
132
+ setKeepAwake(enabled, mode) {
133
+ const resolvedMode = mode ?? 'PreventDisplaySleep';
134
+ // Web only supports display-sleep prevention via the Screen Wake Lock API.
135
+ if (resolvedMode === 'PreventAppSuspension')
136
+ return false;
137
+ if (typeof navigator === 'undefined')
138
+ return false;
139
+ const wakeLock = navigator.wakeLock;
140
+ if (wakeLock === undefined)
141
+ return false;
142
+ try {
143
+ if (!enabled) {
144
+ _wakeLockSentinel?.release?.().catch(() => { });
145
+ _wakeLockSentinel = null;
146
+ return true;
147
+ }
148
+ wakeLock
149
+ .request('screen')
150
+ .then((sentinel) => {
151
+ _wakeLockSentinel = sentinel;
152
+ // Re-acquire the lock after visibility is restored; browsers auto-release it when the
153
+ // tab goes hidden.
154
+ sentinel.addEventListener?.('release', () => {
155
+ if (_wakeLockSentinel === sentinel && !document.hidden) {
156
+ wakeLock
157
+ .request('screen')
158
+ .then((newSentinel) => {
159
+ if (_wakeLockSentinel === sentinel)
160
+ _wakeLockSentinel = newSentinel;
161
+ })
162
+ .catch(() => { });
163
+ }
164
+ });
165
+ })
166
+ .catch(() => { });
167
+ return true;
168
+ }
169
+ catch {
170
+ return false;
171
+ }
172
+ },
173
+ subscribe(listener) {
174
+ const battery = getWebBatteryManagerPromise();
175
+ if (battery === null)
176
+ return () => { };
177
+ let manager = null;
178
+ const onLevelChange = () => {
179
+ if (manager !== null)
180
+ cachedLevel = manager.level;
181
+ listener();
182
+ };
183
+ const onChargingChange = () => {
184
+ if (manager !== null)
185
+ cachedCharging = manager.charging;
186
+ listener();
187
+ };
188
+ const onChargingTimeChange = () => {
189
+ if (manager !== null) {
190
+ const t = manager.chargingTime;
191
+ cachedChargingTime = t === Infinity ? -1 : t;
192
+ }
193
+ listener();
194
+ };
195
+ const onDischargingTimeChange = () => {
196
+ if (manager !== null) {
197
+ const t = manager.dischargingTime;
198
+ cachedDischargingTime = t === Infinity ? -1 : t;
199
+ }
200
+ listener();
201
+ };
202
+ let cancelled = false;
203
+ battery
204
+ .then((m) => {
205
+ if (cancelled)
206
+ return;
207
+ manager = m;
208
+ cachedLevel = m.level;
209
+ cachedCharging = m.charging;
210
+ cachedChargingTime = m.chargingTime === Infinity ? -1 : m.chargingTime;
211
+ cachedDischargingTime = m.dischargingTime === Infinity ? -1 : m.dischargingTime;
212
+ m.addEventListener?.('levelchange', onLevelChange);
213
+ m.addEventListener?.('chargingchange', onChargingChange);
214
+ m.addEventListener?.('chargingtimechange', onChargingTimeChange);
215
+ m.addEventListener?.('dischargingtimechange', onDischargingTimeChange);
216
+ listener();
217
+ })
218
+ .catch(() => { });
219
+ return () => {
220
+ cancelled = true;
221
+ manager?.removeEventListener?.('levelchange', onLevelChange);
222
+ manager?.removeEventListener?.('chargingchange', onChargingChange);
223
+ manager?.removeEventListener?.('chargingtimechange', onChargingTimeChange);
224
+ manager?.removeEventListener?.('dischargingtimechange', onDischargingTimeChange);
225
+ manager = null;
226
+ };
227
+ },
228
+ subscribeLockScreen() {
229
+ // The Web Platform has no lock-screen API; only native hosts can detect screen lock.
230
+ return () => { };
231
+ },
232
+ subscribeLowPowerModeChange() {
233
+ // No OS low-power-mode API available on web; return a no-op unsubscriber.
234
+ return () => { };
235
+ },
236
+ subscribeResume(listener) {
237
+ if (typeof document === 'undefined')
238
+ return () => { };
239
+ document.addEventListener('resume', listener);
240
+ return () => document.removeEventListener('resume', listener);
241
+ },
242
+ subscribeSuspend(listener) {
243
+ if (typeof document === 'undefined')
244
+ return () => { };
245
+ document.addEventListener('freeze', listener);
246
+ return () => document.removeEventListener('freeze', listener);
247
+ },
248
+ subscribeThermalStateChange() {
249
+ // No thermal state API available on web; return a no-op unsubscriber.
250
+ return () => { };
251
+ },
252
+ subscribeUnlockScreen() {
253
+ // The Web Platform has no unlock-screen API; only native hosts can detect screen unlock.
254
+ return () => { };
255
+ },
256
+ };
257
+ }
258
+ // Stops delivery to `power` and forgets its subscription. Safe to call when not attached.
259
+ export function detachPower(power) {
260
+ const unsubscribe = _subscriptions.get(power);
261
+ if (unsubscribe !== undefined) {
262
+ unsubscribe();
263
+ _subscriptions.delete(power);
264
+ }
265
+ }
266
+ // Releases `power` for garbage collection by detaching its backend subscription. The signals remain
267
+ // plain GC-managed memory afterward.
268
+ export function disposePower(power) {
269
+ detachPower(power);
270
+ }
271
+ export function enablePowerSignals() {
272
+ return createPower();
273
+ }
274
+ export function getPowerBackend() {
275
+ if (_backend === null)
276
+ _backend = createWebPowerBackend();
277
+ return _backend;
278
+ }
279
+ // Returns the battery health detail from the active backend, writing into `out` and returning it,
280
+ // or returns null when the active backend does not support battery health reporting.
281
+ export function getPowerBatteryHealth(out) {
282
+ return getPowerBackend().getBatteryHealth(out);
283
+ }
284
+ // Returns the current idle-state polling interval in milliseconds (default 5000).
285
+ export function getPowerIdlePollingIntervalMs() {
286
+ return _idlePollingIntervalMs;
287
+ }
288
+ // Fills `out` with the current power snapshot and returns it.
289
+ export function getPowerStatus(out) {
290
+ return getPowerBackend().getStatus(out);
291
+ }
292
+ // Returns the current system idle state at the given threshold in seconds.
293
+ export function getPowerSystemIdleState(thresholdSeconds) {
294
+ return getPowerBackend().getSystemIdleState(thresholdSeconds);
295
+ }
296
+ // Returns the elapsed seconds since the last user input event, or -1 when unsupported.
297
+ export function getPowerSystemIdleTime() {
298
+ return getPowerBackend().getSystemIdleTime();
299
+ }
300
+ // Returns the current thermal state from the active backend.
301
+ export function getPowerThermalState() {
302
+ return getPowerBackend().getStatus(_scratch).thermalState;
303
+ }
304
+ // Returns true when a keep-awake lock is currently held by the active backend.
305
+ export function hasPowerKeepAwake() {
306
+ return getPowerBackend().isKeepAwakeActive();
307
+ }
308
+ // Installs a native host power backend; pass null to fall back to the web default.
309
+ export function setPowerBackend(backend) {
310
+ _backend = backend;
311
+ }
312
+ // Sets the interval at which attachPower polls the backend for idle state changes. The default is
313
+ // 5000ms (5 seconds); set lower for more responsive idle detection at the cost of more frequent
314
+ // backend calls. Only affects Power entities attached after this call.
315
+ export function setPowerIdlePollingIntervalMs(intervalMs) {
316
+ _idlePollingIntervalMs = intervalMs;
317
+ }
318
+ // Requests or releases a keep-awake lock for the given mode; returns whether honored.
319
+ // mode defaults to 'PreventDisplaySleep'.
320
+ export function setPowerKeepAwake(enabled, mode) {
321
+ return getPowerBackend().setKeepAwake(enabled, mode);
322
+ }
323
+ let _backend = null;
324
+ let _idlePollingIntervalMs = 5000;
325
+ let _wakeLockSentinel = null;
326
+ const _scratch = createPowerStatus();
327
+ const _subscriptions = new WeakMap();
328
+ function getWebBatteryManagerPromise() {
329
+ if (typeof navigator === 'undefined')
330
+ return null;
331
+ const nav = navigator;
332
+ if (typeof nav.getBattery !== 'function')
333
+ return null;
334
+ try {
335
+ return nav.getBattery();
336
+ }
337
+ catch {
338
+ return null;
339
+ }
340
+ }
341
+ //# sourceMappingURL=power.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"power.js","sourceRoot":"","sources":["../src/power.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@flighthq/power",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src/**/*.test.ts",
16
+ "!dist/**/*.test.js",
17
+ "!dist/**/*.test.d.ts",
18
+ "!dist/**/*.test.js.map",
19
+ "!dist/**/*.test.d.ts.map"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -b",
23
+ "clean": "tsc -b --clean",
24
+ "test": "vitest run --config vitest.config.ts",
25
+ "test:watch": "vitest --watch --config vitest.config.ts",
26
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
27
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
28
+ },
29
+ "dependencies": {
30
+ "@flighthq/signals": "0.1.0",
31
+ "@flighthq/types": "0.1.0"
32
+ },
33
+ "devDependencies": {
34
+ "typescript": "^5.3.0"
35
+ },
36
+ "description": "Battery level, charging state, low-power signals and screen keep-awake over a swappable web/native backend",
37
+ "sideEffects": false
38
+ }
@@ -0,0 +1,583 @@
1
+ import { connectSignal } from '@flighthq/signals';
2
+ import type { PowerBackend, PowerBatteryHealth, PowerStatus } from '@flighthq/types';
3
+
4
+ import {
5
+ attachPower,
6
+ createPower,
7
+ createPowerBatteryHealth,
8
+ createPowerStatus,
9
+ createWebPowerBackend,
10
+ detachPower,
11
+ disposePower,
12
+ enablePowerSignals,
13
+ getPowerBackend,
14
+ getPowerBatteryHealth,
15
+ getPowerIdlePollingIntervalMs,
16
+ getPowerStatus,
17
+ getPowerSystemIdleState,
18
+ getPowerSystemIdleTime,
19
+ getPowerThermalState,
20
+ hasPowerKeepAwake,
21
+ setPowerBackend,
22
+ setPowerIdlePollingIntervalMs,
23
+ setPowerKeepAwake,
24
+ } from './power';
25
+
26
+ function fakeBackend(): PowerBackend & {
27
+ charging: boolean;
28
+ keepAwake: boolean;
29
+ fire: () => void;
30
+ fireLockScreen: () => void;
31
+ fireLowPowerModeChange: () => void;
32
+ fireResume: () => void;
33
+ fireSuspend: () => void;
34
+ fireThermalStateChange: () => void;
35
+ fireUnlockScreen: () => void;
36
+ } {
37
+ let listener: (() => void) | null = null;
38
+ let lockListener: (() => void) | null = null;
39
+ let lowPowerModeListener: (() => void) | null = null;
40
+ let resumeListener: (() => void) | null = null;
41
+ let suspendListener: (() => void) | null = null;
42
+ let thermalListener: (() => void) | null = null;
43
+ let unlockListener: (() => void) | null = null;
44
+ return {
45
+ charging: false,
46
+ keepAwake: false,
47
+ getBatteryHealth(_out) {
48
+ return null;
49
+ },
50
+ isKeepAwakeActive() {
51
+ return this.keepAwake;
52
+ },
53
+ getStatus(out) {
54
+ out.batteryLevel = 0.5;
55
+ out.chargingTime = -1;
56
+ out.dischargingTime = 3600;
57
+ out.isBatteryLow = false;
58
+ out.isCharging = this.charging;
59
+ out.isLowPower = false;
60
+ out.isOnBattery = !this.charging;
61
+ out.thermalState = 'Nominal';
62
+ return out;
63
+ },
64
+ getSystemIdleState(_threshold) {
65
+ return 'Active';
66
+ },
67
+ getSystemIdleTime() {
68
+ return 42;
69
+ },
70
+ setKeepAwake(enabled) {
71
+ this.keepAwake = enabled;
72
+ return true;
73
+ },
74
+ subscribe(l) {
75
+ listener = l;
76
+ return () => {
77
+ listener = null;
78
+ };
79
+ },
80
+ subscribeLockScreen(l) {
81
+ lockListener = l;
82
+ return () => {
83
+ lockListener = null;
84
+ };
85
+ },
86
+ subscribeLowPowerModeChange(l) {
87
+ lowPowerModeListener = l;
88
+ return () => {
89
+ lowPowerModeListener = null;
90
+ };
91
+ },
92
+ subscribeResume(l) {
93
+ resumeListener = l;
94
+ return () => {
95
+ resumeListener = null;
96
+ };
97
+ },
98
+ subscribeSuspend(l) {
99
+ suspendListener = l;
100
+ return () => {
101
+ suspendListener = null;
102
+ };
103
+ },
104
+ subscribeThermalStateChange(l) {
105
+ thermalListener = l;
106
+ return () => {
107
+ thermalListener = null;
108
+ };
109
+ },
110
+ subscribeUnlockScreen(l) {
111
+ unlockListener = l;
112
+ return () => {
113
+ unlockListener = null;
114
+ };
115
+ },
116
+ fire() {
117
+ listener?.();
118
+ },
119
+ fireLockScreen() {
120
+ lockListener?.();
121
+ },
122
+ fireLowPowerModeChange() {
123
+ lowPowerModeListener?.();
124
+ },
125
+ fireResume() {
126
+ resumeListener?.();
127
+ },
128
+ fireSuspend() {
129
+ suspendListener?.();
130
+ },
131
+ fireThermalStateChange() {
132
+ thermalListener?.();
133
+ },
134
+ fireUnlockScreen() {
135
+ unlockListener?.();
136
+ },
137
+ };
138
+ }
139
+
140
+ afterEach(() => setPowerBackend(null));
141
+
142
+ describe('attachPower', () => {
143
+ it('emits onChange and the charging transition signals', () => {
144
+ const backend = fakeBackend();
145
+ setPowerBackend(backend);
146
+ const power = createPower();
147
+ let changes = 0;
148
+ let charging = 0;
149
+ connectSignal(power.onChange, () => changes++);
150
+ connectSignal(power.onCharging, () => charging++);
151
+ attachPower(power);
152
+ backend.charging = true;
153
+ backend.fire();
154
+ expect(changes).toBe(1);
155
+ expect(charging).toBe(1);
156
+ });
157
+
158
+ it('emits onSuspend and onResume when the backend fires them', () => {
159
+ const backend = fakeBackend();
160
+ setPowerBackend(backend);
161
+ const power = createPower();
162
+ let suspends = 0;
163
+ let resumes = 0;
164
+ connectSignal(power.onSuspend, () => suspends++);
165
+ connectSignal(power.onResume, () => resumes++);
166
+ attachPower(power);
167
+ backend.fireSuspend();
168
+ backend.fireResume();
169
+ expect(suspends).toBe(1);
170
+ expect(resumes).toBe(1);
171
+ });
172
+
173
+ it('emits onLockScreen and onUnlockScreen when the backend fires them', () => {
174
+ const backend = fakeBackend();
175
+ setPowerBackend(backend);
176
+ const power = createPower();
177
+ let locks = 0;
178
+ let unlocks = 0;
179
+ connectSignal(power.onLockScreen, () => locks++);
180
+ connectSignal(power.onUnlockScreen, () => unlocks++);
181
+ attachPower(power);
182
+ backend.fireLockScreen();
183
+ backend.fireUnlockScreen();
184
+ expect(locks).toBe(1);
185
+ expect(unlocks).toBe(1);
186
+ });
187
+
188
+ it('emits onLowPowerModeChange when the backend fires it', () => {
189
+ const backend = fakeBackend();
190
+ setPowerBackend(backend);
191
+ const power = createPower();
192
+ let lowPowerChanges = 0;
193
+ connectSignal(power.onLowPowerModeChange, () => lowPowerChanges++);
194
+ attachPower(power);
195
+ backend.fireLowPowerModeChange();
196
+ expect(lowPowerChanges).toBe(1);
197
+ });
198
+
199
+ it('emits onThermalStateChange when the backend fires it', () => {
200
+ const backend = fakeBackend();
201
+ setPowerBackend(backend);
202
+ const power = createPower();
203
+ let thermalChanges = 0;
204
+ connectSignal(power.onThermalStateChange, () => thermalChanges++);
205
+ attachPower(power);
206
+ backend.fireThermalStateChange();
207
+ expect(thermalChanges).toBe(1);
208
+ });
209
+
210
+ it('is idempotent — a second attach tears down the first subscription', () => {
211
+ const backend = fakeBackend();
212
+ setPowerBackend(backend);
213
+ const power = createPower();
214
+ let changes = 0;
215
+ connectSignal(power.onChange, () => changes++);
216
+ attachPower(power);
217
+ attachPower(power);
218
+ backend.fire();
219
+ expect(changes).toBe(1);
220
+ });
221
+
222
+ it('polls onIdleStateChange when a listener is connected and state transitions', () => {
223
+ vi.useFakeTimers();
224
+ let idleState = 'Active';
225
+ const backend = fakeBackend();
226
+ backend.getSystemIdleState = () => idleState as ReturnType<typeof backend.getSystemIdleState>;
227
+ setPowerBackend(backend);
228
+ const power = createPower();
229
+ let idleChanges = 0;
230
+ connectSignal(power.onIdleStateChange, () => idleChanges++);
231
+ setPowerIdlePollingIntervalMs(100);
232
+ attachPower(power, 30);
233
+ // Change state and advance timer.
234
+ idleState = 'Idle';
235
+ vi.advanceTimersByTime(200);
236
+ expect(idleChanges).toBeGreaterThanOrEqual(1);
237
+ disposePower(power);
238
+ vi.useRealTimers();
239
+ // Restore default interval.
240
+ setPowerIdlePollingIntervalMs(5000);
241
+ });
242
+
243
+ it('does not emit onIdleStateChange when no listener is connected', () => {
244
+ vi.useFakeTimers();
245
+ let idleState = 'Active';
246
+ const backend = fakeBackend();
247
+ backend.getSystemIdleState = () => idleState as ReturnType<typeof backend.getSystemIdleState>;
248
+ setPowerBackend(backend);
249
+ const power = createPower();
250
+ // No listener connected.
251
+ setPowerIdlePollingIntervalMs(100);
252
+ attachPower(power, 30);
253
+ idleState = 'Idle';
254
+ vi.advanceTimersByTime(200);
255
+ // No signal has slots so no emission, no error.
256
+ expect(power.onIdleStateChange.data).toBeNull();
257
+ disposePower(power);
258
+ vi.useRealTimers();
259
+ setPowerIdlePollingIntervalMs(5000);
260
+ });
261
+
262
+ it('stops the idle polling interval after detach', () => {
263
+ vi.useFakeTimers();
264
+ let idleState = 'Active';
265
+ let pollCount = 0;
266
+ const backend = fakeBackend();
267
+ backend.getSystemIdleState = () => {
268
+ pollCount++;
269
+ return idleState as ReturnType<typeof backend.getSystemIdleState>;
270
+ };
271
+ setPowerBackend(backend);
272
+ const power = createPower();
273
+ connectSignal(power.onIdleStateChange, () => {});
274
+ setPowerIdlePollingIntervalMs(100);
275
+ attachPower(power, 30);
276
+ vi.advanceTimersByTime(150);
277
+ const countAfterAttach = pollCount;
278
+ detachPower(power);
279
+ idleState = 'Idle';
280
+ vi.advanceTimersByTime(300);
281
+ // Polling must stop after detach: poll count should not grow after detach.
282
+ expect(pollCount).toBe(countAfterAttach);
283
+ vi.useRealTimers();
284
+ setPowerIdlePollingIntervalMs(5000);
285
+ });
286
+ });
287
+
288
+ describe('createPower', () => {
289
+ it('creates an entity with all signals defined', () => {
290
+ const power = createPower();
291
+ expect(power.onChange).toBeDefined();
292
+ expect(power.onCharging).toBeDefined();
293
+ expect(power.onDischarging).toBeDefined();
294
+ expect(power.onIdleStateChange).toBeDefined();
295
+ expect(power.onLockScreen).toBeDefined();
296
+ expect(power.onLowPowerModeChange).toBeDefined();
297
+ expect(power.onResume).toBeDefined();
298
+ expect(power.onSuspend).toBeDefined();
299
+ expect(power.onThermalStateChange).toBeDefined();
300
+ expect(power.onUnlockScreen).toBeDefined();
301
+ });
302
+ });
303
+
304
+ describe('createPowerBatteryHealth', () => {
305
+ it('allocates a zeroed battery health with all sentinel values', () => {
306
+ const health = createPowerBatteryHealth();
307
+ expect(health.capacityWearLevel).toBe(-1);
308
+ expect(health.cycleCount).toBe(-1);
309
+ expect(health.healthState).toBe('Unknown');
310
+ expect(health.temperatureCelsius).toBe(-1);
311
+ expect(health.voltage).toBe(-1);
312
+ });
313
+ });
314
+
315
+ describe('createPowerStatus', () => {
316
+ it('allocates a zeroed status', () => {
317
+ const status = createPowerStatus();
318
+ expect(status.batteryLevel).toBe(-1);
319
+ expect(status.chargingTime).toBe(-1);
320
+ expect(status.dischargingTime).toBe(-1);
321
+ expect(status.isBatteryLow).toBe(false);
322
+ expect(status.isCharging).toBe(false);
323
+ expect(status.isLowPower).toBe(false);
324
+ expect(status.isOnBattery).toBe(false);
325
+ expect(status.thermalState).toBe('Unknown');
326
+ });
327
+ });
328
+
329
+ describe('createWebPowerBackend', () => {
330
+ it('isKeepAwakeActive returns false when no lock is held', () => {
331
+ expect(createWebPowerBackend().isKeepAwakeActive()).toBe(false);
332
+ });
333
+
334
+ it('reads a status without throwing', () => {
335
+ const out = createPowerStatus();
336
+ expect(typeof createWebPowerBackend().getStatus(out).isCharging).toBe('boolean');
337
+ });
338
+
339
+ it('getStatus is alias-safe when out is the scratch object', () => {
340
+ const backend = createWebPowerBackend();
341
+ const out = createPowerStatus();
342
+ const result = backend.getStatus(out);
343
+ expect(result).toBe(out);
344
+ });
345
+
346
+ it('getBatteryHealth returns null on web', () => {
347
+ const backend = createWebPowerBackend();
348
+ const out = createPowerBatteryHealth();
349
+ expect(backend.getBatteryHealth(out)).toBeNull();
350
+ });
351
+
352
+ it('getSystemIdleTime returns -1 on web', () => {
353
+ expect(createWebPowerBackend().getSystemIdleTime()).toBe(-1);
354
+ });
355
+
356
+ it('getSystemIdleState returns Unknown on web', () => {
357
+ expect(createWebPowerBackend().getSystemIdleState(5)).toBe('Unknown');
358
+ });
359
+
360
+ it('subscribeLockScreen and subscribeUnlockScreen return inert no-ops on the web', () => {
361
+ const backend = createWebPowerBackend();
362
+ expect(() => backend.subscribeLockScreen(() => {})()).not.toThrow();
363
+ expect(() => backend.subscribeUnlockScreen(() => {})()).not.toThrow();
364
+ });
365
+
366
+ it('subscribeLowPowerModeChange returns an inert no-op on the web', () => {
367
+ const backend = createWebPowerBackend();
368
+ expect(() => backend.subscribeLowPowerModeChange(() => {})()).not.toThrow();
369
+ });
370
+
371
+ it('subscribeThermalStateChange returns an inert no-op on the web', () => {
372
+ const backend = createWebPowerBackend();
373
+ expect(() => backend.subscribeThermalStateChange(() => {})()).not.toThrow();
374
+ });
375
+
376
+ it('subscribes without throwing when battery API is absent', () => {
377
+ const unsubscribe = createWebPowerBackend().subscribe(() => {});
378
+ expect(() => unsubscribe()).not.toThrow();
379
+ });
380
+
381
+ it('toggles keep-awake without throwing', () => {
382
+ expect(typeof createWebPowerBackend().setKeepAwake(true)).toBe('boolean');
383
+ });
384
+
385
+ it('setKeepAwake returns false for PreventAppSuspension mode (web unsupported)', () => {
386
+ expect(createWebPowerBackend().setKeepAwake(true, 'PreventAppSuspension')).toBe(false);
387
+ });
388
+
389
+ it('setKeepAwake honors PreventDisplaySleep mode (web default path)', () => {
390
+ // Web may not have wakeLock, but should not throw.
391
+ expect(() => createWebPowerBackend().setKeepAwake(true, 'PreventDisplaySleep')).not.toThrow();
392
+ });
393
+ });
394
+
395
+ describe('detachPower', () => {
396
+ it('stops further delivery', () => {
397
+ const backend = fakeBackend();
398
+ setPowerBackend(backend);
399
+ const power = createPower();
400
+ let changes = 0;
401
+ connectSignal(power.onChange, () => changes++);
402
+ attachPower(power);
403
+ detachPower(power);
404
+ backend.fire();
405
+ expect(changes).toBe(0);
406
+ });
407
+
408
+ it('is safe when called on an unattached power entity', () => {
409
+ const power = createPower();
410
+ expect(() => detachPower(power)).not.toThrow();
411
+ });
412
+ });
413
+
414
+ describe('disposePower', () => {
415
+ it('detaches the subscription', () => {
416
+ const backend = fakeBackend();
417
+ setPowerBackend(backend);
418
+ const power = createPower();
419
+ attachPower(power);
420
+ expect(() => disposePower(power)).not.toThrow();
421
+ });
422
+
423
+ it('is idempotent', () => {
424
+ const power = createPower();
425
+ expect(() => {
426
+ disposePower(power);
427
+ disposePower(power);
428
+ }).not.toThrow();
429
+ });
430
+ });
431
+
432
+ describe('enablePowerSignals', () => {
433
+ it('returns a Power entity with all signals defined', () => {
434
+ const power = enablePowerSignals();
435
+ expect(power.onChange).toBeDefined();
436
+ expect(power.onCharging).toBeDefined();
437
+ expect(power.onDischarging).toBeDefined();
438
+ expect(power.onIdleStateChange).toBeDefined();
439
+ expect(power.onLockScreen).toBeDefined();
440
+ expect(power.onLowPowerModeChange).toBeDefined();
441
+ expect(power.onResume).toBeDefined();
442
+ expect(power.onSuspend).toBeDefined();
443
+ expect(power.onThermalStateChange).toBeDefined();
444
+ expect(power.onUnlockScreen).toBeDefined();
445
+ });
446
+ });
447
+
448
+ describe('getPowerBackend', () => {
449
+ it('falls back to a web backend', () => {
450
+ expect(getPowerBackend()).not.toBeNull();
451
+ });
452
+ });
453
+
454
+ describe('getPowerBatteryHealth', () => {
455
+ it('returns null from the default web backend', () => {
456
+ const out = createPowerBatteryHealth();
457
+ expect(getPowerBatteryHealth(out)).toBeNull();
458
+ });
459
+
460
+ it('returns the out object from a backend that supports health', () => {
461
+ const health: PowerBatteryHealth = {
462
+ capacityWearLevel: 0.9,
463
+ cycleCount: 120,
464
+ healthState: 'Good',
465
+ temperatureCelsius: 30,
466
+ voltage: 12.1,
467
+ };
468
+ setPowerBackend({
469
+ ...fakeBackend(),
470
+ getBatteryHealth(out) {
471
+ out.capacityWearLevel = health.capacityWearLevel;
472
+ out.cycleCount = health.cycleCount;
473
+ out.healthState = health.healthState;
474
+ out.temperatureCelsius = health.temperatureCelsius;
475
+ out.voltage = health.voltage;
476
+ return out;
477
+ },
478
+ });
479
+ const out = createPowerBatteryHealth();
480
+ expect(getPowerBatteryHealth(out)).toBe(out);
481
+ expect(out.healthState).toBe('Good');
482
+ expect(out.cycleCount).toBe(120);
483
+ });
484
+ });
485
+
486
+ describe('getPowerIdlePollingIntervalMs', () => {
487
+ it('returns the default 5000ms interval', () => {
488
+ expect(getPowerIdlePollingIntervalMs()).toBe(5000);
489
+ });
490
+ });
491
+
492
+ describe('getPowerStatus', () => {
493
+ it('fills the out parameter from the backend', () => {
494
+ setPowerBackend(fakeBackend());
495
+ const out = createPowerStatus();
496
+ expect(getPowerStatus(out)).toBe(out);
497
+ expect(out.batteryLevel).toBe(0.5);
498
+ });
499
+
500
+ it('fills new fields from the backend', () => {
501
+ setPowerBackend(fakeBackend());
502
+ const out = createPowerStatus();
503
+ getPowerStatus(out);
504
+ expect(out.dischargingTime).toBe(3600);
505
+ expect(out.isOnBattery).toBe(true);
506
+ expect(out.thermalState).toBe('Nominal');
507
+ });
508
+ });
509
+
510
+ describe('getPowerSystemIdleState', () => {
511
+ it('delegates to the backend', () => {
512
+ setPowerBackend(fakeBackend());
513
+ expect(getPowerSystemIdleState(30)).toBe('Active');
514
+ });
515
+ });
516
+
517
+ describe('getPowerSystemIdleTime', () => {
518
+ it('delegates to the backend', () => {
519
+ setPowerBackend(fakeBackend());
520
+ expect(getPowerSystemIdleTime()).toBe(42);
521
+ });
522
+ });
523
+
524
+ describe('getPowerThermalState', () => {
525
+ it('returns the thermal state from the backend', () => {
526
+ setPowerBackend(fakeBackend());
527
+ expect(getPowerThermalState()).toBe('Nominal');
528
+ });
529
+ });
530
+
531
+ describe('hasPowerKeepAwake', () => {
532
+ it('returns false when no lock is held by the web backend', () => {
533
+ expect(hasPowerKeepAwake()).toBe(false);
534
+ });
535
+
536
+ it('delegates to the active backend isKeepAwakeActive', () => {
537
+ const backend = fakeBackend();
538
+ setPowerBackend(backend);
539
+ backend.keepAwake = false;
540
+ expect(hasPowerKeepAwake()).toBe(false);
541
+ backend.keepAwake = true;
542
+ expect(hasPowerKeepAwake()).toBe(true);
543
+ });
544
+ });
545
+
546
+ describe('setPowerBackend', () => {
547
+ it('clears back to the web fallback when passed null', () => {
548
+ setPowerBackend(fakeBackend());
549
+ setPowerBackend(null);
550
+ expect(getPowerBackend()).not.toBeNull();
551
+ });
552
+ });
553
+
554
+ describe('setPowerIdlePollingIntervalMs', () => {
555
+ it('updates the polling interval returned by getPowerIdlePollingIntervalMs', () => {
556
+ setPowerIdlePollingIntervalMs(1000);
557
+ expect(getPowerIdlePollingIntervalMs()).toBe(1000);
558
+ // Restore default.
559
+ setPowerIdlePollingIntervalMs(5000);
560
+ });
561
+ });
562
+
563
+ describe('setPowerKeepAwake', () => {
564
+ it('delegates to the backend and returns its result', () => {
565
+ const backend = fakeBackend();
566
+ setPowerBackend(backend);
567
+ expect(setPowerKeepAwake(true)).toBe(true);
568
+ expect(backend.keepAwake).toBe(true);
569
+ });
570
+
571
+ it('passes the mode to the backend', () => {
572
+ let capturedMode: string | undefined;
573
+ const backend = fakeBackend();
574
+ const originalSetKeepAwake = backend.setKeepAwake.bind(backend);
575
+ backend.setKeepAwake = (enabled, mode) => {
576
+ capturedMode = mode;
577
+ return originalSetKeepAwake(enabled, mode);
578
+ };
579
+ setPowerBackend(backend);
580
+ setPowerKeepAwake(true, 'PreventDisplaySleep');
581
+ expect(capturedMode).toBe('PreventDisplaySleep');
582
+ });
583
+ });