From cb851bda3e1f2ea643ff2c62e290ee120c7975bc Mon Sep 17 00:00:00 2001 From: kwasniow Date: Tue, 24 Jun 2025 10:50:59 +0200 Subject: [PATCH 1/9] feat: support system info driven by pressure api --- src/index.ts | 3 +- src/system-info.ts | 129 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 src/system-info.ts diff --git a/src/index.ts b/src/index.ts index e5fc96d..ac1bd5d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ export * from './browser-info'; -export * from './web-capabilities'; export * from './cpu-info'; +export * from './system-info'; +export * from './web-capabilities'; diff --git a/src/system-info.ts b/src/system-info.ts new file mode 100644 index 0000000..c4fbdc2 --- /dev/null +++ b/src/system-info.ts @@ -0,0 +1,129 @@ +/* eslint-disable max-classes-per-file */ +import { EventEmitter } from 'events'; + +// https://w3c.github.io/compute-pressure/#pressure-states +// ⚪ Nominal: Work is minimal and the system is running on lower clock speed to preserve power. +// +// 🟢 Fair: The system is doing fine, everything is smooth and it can take on additional work without issues. +// +// 🟡 Serious: There is some serious pressure on the system, but it is sustainable and the system is doing well, +// but it is getting close to its limits: +// +// Clock speed (depending on AC or DC power) is consistently high +// Thermals are high but system can handle it +// At this point, if you add more work the system may move into critical. +// +// 🔴 Critical: The system is now about to reach its limits, but it hasn’t reached the limit yet. +// Critical doesn’t mean that the system is being actively throttled, +// but this state is not sustainable for the long run and might result in throttling if the workload remains the same. +// This signal is the last call for the web application to lighten its workload. + +type PressureState = 'nominal' | 'fair' | 'serious' | 'critical'; + +interface PressureRecord { + source: string; + state: PressureState; + time: number; +} + +export enum ComputePressureEvents { + CpuPressureStateChange = 'cpu-pressure-state-change', +} + +/** + * Events emitted by the ComputePressureObserver. + */ +class ComputePressureObserver extends EventEmitter { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private observer?: any; + + /** + * Creates an instance of ComputePressureObserver. + */ + constructor() { + super(); + + if ('PressureObserver' in window) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this.observer = new PressureObserver(this.onUpdate.bind(this)); + + this.observer.observe('cpu', { + sampleInterval: 1000, + }); + } + } + + /** + * Handles updates from the PressureObserver. + * + * @param records - The records from the PressureObserver. + */ + private onUpdate(records: PressureRecord[]) { + records.forEach((record: PressureRecord) => { + if (record.source === 'cpu') { + this.emit(ComputePressureEvents.CpuPressureStateChange, record.state); + } + }); + } +} + +/** + * SystemInfo class to manage system information and pressure states. + */ +export default class SystemInfo { + private static observer?: ComputePressureObserver = undefined; + + private static lastCpuPressure?: PressureState = undefined; + + /** + * Retrieves the ComputePressureObserver instance, creating it if it doesn't exist. + * + * @returns The ComputePressureObserver instance. + */ + private static getObserver(): ComputePressureObserver | undefined { + if (this.observer) { + return this.observer; + } + + if ('PressureObserver' in window) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this.observer = new ComputePressureObserver(); + this.observer.on(ComputePressureEvents.CpuPressureStateChange, (state: PressureState) => { + this.lastCpuPressure = state; + }); + } + + return this.observer; + } + + /** + * Gets the current CPU pressure state. + * + * @returns The current CPU pressure state, or undefined if API is not supported. + */ + static async getCpuPressure(): Promise { + if (this.lastCpuPressure) { + return Promise.resolve(this.lastCpuPressure); + } + + const observer = this.getObserver(); + if (!observer) { + return Promise.resolve(undefined); + } + + return new Promise((resolve) => { + // eslint-disable-next-line jsdoc/require-jsdoc + const handleFirstUpdate = (records: PressureState) => { + this.lastCpuPressure = records; + + observer.removeListener('cpu-pressure-state-change', handleFirstUpdate); + + resolve(records); + }; + + observer.addListener(ComputePressureEvents.CpuPressureStateChange, handleFirstUpdate); + }); + } +} From 0e3ee478cd7c76c8b5ae6c126ee9fcb3e48028c3 Mon Sep 17 00:00:00 2001 From: kwasniow Date: Tue, 24 Jun 2025 12:30:38 +0200 Subject: [PATCH 2/9] feat: different approach --- src/system-info.ts | 89 ++++++++++++---------------------------------- 1 file changed, 23 insertions(+), 66 deletions(-) diff --git a/src/system-info.ts b/src/system-info.ts index c4fbdc2..f3df9b6 100644 --- a/src/system-info.ts +++ b/src/system-info.ts @@ -1,6 +1,4 @@ /* eslint-disable max-classes-per-file */ -import { EventEmitter } from 'events'; - // https://w3c.github.io/compute-pressure/#pressure-states // ⚪ Nominal: Work is minimal and the system is running on lower clock speed to preserve power. // @@ -18,7 +16,7 @@ import { EventEmitter } from 'events'; // but this state is not sustainable for the long run and might result in throttling if the workload remains the same. // This signal is the last call for the web application to lighten its workload. -type PressureState = 'nominal' | 'fair' | 'serious' | 'critical'; +export type PressureState = 'nominal' | 'fair' | 'serious' | 'critical'; interface PressureRecord { source: string; @@ -26,31 +24,25 @@ interface PressureRecord { time: number; } -export enum ComputePressureEvents { - CpuPressureStateChange = 'cpu-pressure-state-change', -} - /** - * Events emitted by the ComputePressureObserver. + * SystemInfo class to manage system information and pressure states. */ -class ComputePressureObserver extends EventEmitter { +class SystemInfoInternal { // eslint-disable-next-line @typescript-eslint/no-explicit-any private observer?: any; + private lastCpuPressure?: PressureState = undefined; + /** - * Creates an instance of ComputePressureObserver. + * Creates an instance of SystemInfo. */ constructor() { - super(); - if ('PressureObserver' in window) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - this.observer = new PressureObserver(this.onUpdate.bind(this)); + this.observer = new PressureObserver(this.handleStateChange.bind(this)); - this.observer.observe('cpu', { - sampleInterval: 1000, - }); + this.observer.observe('cpu'); } } @@ -59,71 +51,36 @@ class ComputePressureObserver extends EventEmitter { * * @param records - The records from the PressureObserver. */ - private onUpdate(records: PressureRecord[]) { + private handleStateChange(records: PressureRecord[]) { records.forEach((record: PressureRecord) => { if (record.source === 'cpu') { - this.emit(ComputePressureEvents.CpuPressureStateChange, record.state); + this.lastCpuPressure = record.state; } }); } -} - -/** - * SystemInfo class to manage system information and pressure states. - */ -export default class SystemInfo { - private static observer?: ComputePressureObserver = undefined; - - private static lastCpuPressure?: PressureState = undefined; /** - * Retrieves the ComputePressureObserver instance, creating it if it doesn't exist. + * Gets the current CPU pressure state. * - * @returns The ComputePressureObserver instance. + * @returns The current CPU pressure state, or undefined if API is not supported. */ - private static getObserver(): ComputePressureObserver | undefined { - if (this.observer) { - return this.observer; - } - - if ('PressureObserver' in window) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - this.observer = new ComputePressureObserver(); - this.observer.on(ComputePressureEvents.CpuPressureStateChange, (state: PressureState) => { - this.lastCpuPressure = state; - }); - } - - return this.observer; + getCpuPressure(): PressureState | undefined { + return this.lastCpuPressure; } +} + +const systemInfo = new SystemInfoInternal(); +/** + * SystemInfo class to provide static methods for system information. + */ +export class SystemInfo { /** * Gets the current CPU pressure state. * * @returns The current CPU pressure state, or undefined if API is not supported. */ - static async getCpuPressure(): Promise { - if (this.lastCpuPressure) { - return Promise.resolve(this.lastCpuPressure); - } - - const observer = this.getObserver(); - if (!observer) { - return Promise.resolve(undefined); - } - - return new Promise((resolve) => { - // eslint-disable-next-line jsdoc/require-jsdoc - const handleFirstUpdate = (records: PressureState) => { - this.lastCpuPressure = records; - - observer.removeListener('cpu-pressure-state-change', handleFirstUpdate); - - resolve(records); - }; - - observer.addListener(ComputePressureEvents.CpuPressureStateChange, handleFirstUpdate); - }); + static getCpuPressure(): PressureState | undefined { + return systemInfo.getCpuPressure(); } } From 63f1ca75d0f9f094ad5b54de574ffc12cec5e00c Mon Sep 17 00:00:00 2001 From: kwasniow Date: Wed, 25 Jun 2025 08:22:30 +0200 Subject: [PATCH 3/9] feat: add possibility to listen for changes --- src/system-info.ts | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/system-info.ts b/src/system-info.ts index f3df9b6..dc4cdcb 100644 --- a/src/system-info.ts +++ b/src/system-info.ts @@ -1,4 +1,7 @@ /* eslint-disable max-classes-per-file */ + +import { EventEmitter } from 'events'; + // https://w3c.github.io/compute-pressure/#pressure-states // ⚪ Nominal: Work is minimal and the system is running on lower clock speed to preserve power. // @@ -18,6 +21,10 @@ export type PressureState = 'nominal' | 'fair' | 'serious' | 'critical'; +export enum SystemInfoEvents { + CpuPressureStateChange = 'cpu-pressure-state-change', +} + interface PressureRecord { source: string; state: PressureState; @@ -27,7 +34,7 @@ interface PressureRecord { /** * SystemInfo class to manage system information and pressure states. */ -class SystemInfoInternal { +class SystemInfoInternal extends EventEmitter { // eslint-disable-next-line @typescript-eslint/no-explicit-any private observer?: any; @@ -37,6 +44,8 @@ class SystemInfoInternal { * Creates an instance of SystemInfo. */ constructor() { + super(); + if ('PressureObserver' in window) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore @@ -53,8 +62,10 @@ class SystemInfoInternal { */ private handleStateChange(records: PressureRecord[]) { records.forEach((record: PressureRecord) => { - if (record.source === 'cpu') { + if (record.source === 'cpu' && record.state !== this.lastCpuPressure) { this.lastCpuPressure = record.state; + + this.emit(SystemInfoEvents.CpuPressureStateChange, record.state); } }); } @@ -83,4 +94,13 @@ export class SystemInfo { static getCpuPressure(): PressureState | undefined { return systemInfo.getCpuPressure(); } + + /** + * Registers a callback to be called when the CPU pressure state changes. + * + * @param callback - Callback to be called when the CPU pressure state changes. + */ + static onCpuPressureChange(callback: (state: PressureState) => void): void { + systemInfo.on(SystemInfoEvents.CpuPressureStateChange, callback); + } } From 4331b9a460c0b120beb327927dd83044f45b18e7 Mon Sep 17 00:00:00 2001 From: kwasniow Date: Wed, 25 Jun 2025 10:01:09 +0200 Subject: [PATCH 4/9] feat: add unit tests --- src/system-info.spec.ts | 167 ++++++++++++++++++++++++++++++++++++++++ src/system-info.ts | 37 ++++++++- 2 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 src/system-info.spec.ts diff --git a/src/system-info.spec.ts b/src/system-info.spec.ts new file mode 100644 index 0000000..b9319e4 --- /dev/null +++ b/src/system-info.spec.ts @@ -0,0 +1,167 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let pressureObserverCallback: any; + +/** + * Mock implementation of PressureObserver to simulate CPU pressure states. + */ +class MockPressureObserver { + /** + * Mock implementation of PressureObserver to simulate CPU pressure states. + * + * @param callback - The callback to be called with pressure records. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(callback: (list: any) => void) { + pressureObserverCallback = callback; + } + + /** + * Attaches the observer to a source to observe state changes. + */ + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-empty-function + observe() {} +} + +Object.defineProperty(window, 'PressureObserver', { + writable: true, + configurable: true, + value: MockPressureObserver, +}); + +// Import the SystemInfo class after defining the PressureObserver mock +// This ensures that the mock is available when SystemInfo is imported. +// eslint-disable-next-line import/first +import { SystemInfo } from './system-info'; + +// Extend the Window interface to include PressureObserver +// NOTE: This is needed for TypeScript to recognize PressureObserver +// since it is not a standard part of the Window interface yet. +declare global { + interface Window { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + PressureObserver?: new (callback: (records: any[]) => void) => { + observe: (source: string) => void; + disconnect: () => void; + }; + } +} + +describe('SystemInfo', () => { + describe('isPressureObserverSupported', () => { + it('should return true when PressureObserver is supported', () => { + expect.hasAssertions(); + + Object.defineProperty(window, 'PressureObserver', { + writable: true, + configurable: true, + value: {}, + }); + + expect(SystemInfo.isPressureObserverSupported()).toBe(true); + }); + + it('should return false when PressureObserver is not supported', () => { + expect.hasAssertions(); + + // Ensure that PressureObserver is not defined + delete window.PressureObserver; + + expect(SystemInfo.isPressureObserverSupported()).toBe(false); + }); + }); + + describe('getCpuPressure', () => { + describe('when PressureObserver is supported', () => { + beforeEach(() => { + Object.defineProperty(window, 'PressureObserver', { + writable: true, + configurable: true, + value: MockPressureObserver, + }); + }); + + it('should return undefined when information is not available', () => { + expect.hasAssertions(); + + expect(SystemInfo.getCpuPressure()).toBeUndefined(); + }); + + ['nominal', 'fair', 'serious', 'critical'].forEach((state) => { + it(`should return the last CPU pressure state as ${state}`, () => { + expect.hasAssertions(); + + pressureObserverCallback([{ source: 'cpu', state }]); + + expect(SystemInfo.getCpuPressure()).toBe(state); + }); + }); + }); + + describe('when PressureObserver is not supported', () => { + beforeEach(() => { + // Ensure that PressureObserver is not defined + delete window.PressureObserver; + }); + + it('should return undefined', () => { + expect.hasAssertions(); + + expect(SystemInfo.getCpuPressure()).toBeUndefined(); + }); + }); + }); + + describe('onCpuPressureChange', () => { + describe('when PressureObserver is supported', () => { + beforeEach(() => { + Object.defineProperty(window, 'PressureObserver', { + writable: true, + configurable: true, + value: MockPressureObserver, + }); + }); + + it('should call the callback when CPU pressure state changes', () => { + expect.hasAssertions(); + + const callback = jest.fn(); + SystemInfo.onCpuPressureChange(callback); + + pressureObserverCallback([{ source: 'cpu', state: 'nominal' }]); + expect(callback).toHaveBeenCalledWith('nominal'); + }); + }); + + describe('when PressureObserver is not supported', () => { + beforeEach(() => { + // Ensure that PressureObserver is not defined + delete window.PressureObserver; + }); + + it('should throw', () => { + expect.hasAssertions(); + + const callback = jest.fn(); + expect(() => SystemInfo.onCpuPressureChange(callback)).toThrow(expect.anything()); + }); + }); + }); + + describe('getNumLogicalCores', () => { + it('should return the number of logical CPU cores when the information is available', () => { + expect.assertions(1); + + jest.spyOn(Navigator.prototype, 'hardwareConcurrency', 'get').mockReturnValue(1); + + expect(SystemInfo.getNumLogicalCores()).toBe(1); + }); + + it('should return undefined when the logical CPU cores information is not available', () => { + expect.assertions(1); + + jest.spyOn(Navigator.prototype, 'hardwareConcurrency', 'get').mockImplementation(); + + expect(SystemInfo.getNumLogicalCores()).toBeUndefined(); + }); + }); +}); diff --git a/src/system-info.ts b/src/system-info.ts index dc4cdcb..a13b4ca 100644 --- a/src/system-info.ts +++ b/src/system-info.ts @@ -46,7 +46,7 @@ class SystemInfoInternal extends EventEmitter { constructor() { super(); - if ('PressureObserver' in window) { + if (SystemInfoInternal.isPressureObserverSupported()) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore this.observer = new PressureObserver(this.handleStateChange.bind(this)); @@ -78,6 +78,15 @@ class SystemInfoInternal extends EventEmitter { getCpuPressure(): PressureState | undefined { return this.lastCpuPressure; } + + /** + * Checks if the Compute Pressure API is supported in the current environment. + * + * @returns True if the Compute Pressure API is supported, false otherwise. + */ + static isPressureObserverSupported(): boolean { + return 'PressureObserver' in window; + } } const systemInfo = new SystemInfoInternal(); @@ -86,12 +95,25 @@ const systemInfo = new SystemInfoInternal(); * SystemInfo class to provide static methods for system information. */ export class SystemInfo { + /** + * Checks if the Compute Pressure API is supported in the current environment. + * + * @returns True if the Compute Pressure API is supported, false otherwise. + */ + static isPressureObserverSupported(): boolean { + return SystemInfoInternal.isPressureObserverSupported(); + } + /** * Gets the current CPU pressure state. * * @returns The current CPU pressure state, or undefined if API is not supported. */ static getCpuPressure(): PressureState | undefined { + if (!SystemInfo.isPressureObserverSupported()) { + return undefined; + } + return systemInfo.getCpuPressure(); } @@ -101,6 +123,19 @@ export class SystemInfo { * @param callback - Callback to be called when the CPU pressure state changes. */ static onCpuPressureChange(callback: (state: PressureState) => void): void { + if (!SystemInfo.isPressureObserverSupported()) { + throw new Error('PressureObserver is not supported in this environment.'); + } + systemInfo.on(SystemInfoEvents.CpuPressureStateChange, callback); } + + /** + * Gets the number of logical CPU cores. + * + * @returns The number of logical CPU cores, or undefined if not available. + */ + static getNumLogicalCores(): number | undefined { + return navigator.hardwareConcurrency; + } } From aeaa1c8dec07eb631b0facb284c6d19963ae0654 Mon Sep 17 00:00:00 2001 From: kwasniow Date: Wed, 25 Jun 2025 10:20:23 +0200 Subject: [PATCH 5/9] feat: add option to deregister listener --- cspell.json | 1 + src/system-info.spec.ts | 33 +++++++++++++++++++++++++++++++++ src/system-info.ts | 13 +++++++++++++ 3 files changed, 47 insertions(+) diff --git a/cspell.json b/cspell.json index 6ff0d7f..629754d 100644 --- a/cspell.json +++ b/cspell.json @@ -46,6 +46,7 @@ "transcoding", "transpiled", "typedoc", + "Unregisters", "untracked", "videostateupdate", "VITE", diff --git a/src/system-info.spec.ts b/src/system-info.spec.ts index b9319e4..2b9a199 100644 --- a/src/system-info.spec.ts +++ b/src/system-info.spec.ts @@ -130,6 +130,39 @@ describe('SystemInfo', () => { pressureObserverCallback([{ source: 'cpu', state: 'nominal' }]); expect(callback).toHaveBeenCalledWith('nominal'); }); + + it('should not call the callback if the CPU pressure state does not change', () => { + expect.hasAssertions(); + + const callback = jest.fn(); + SystemInfo.onCpuPressureChange(callback); + + // Call with the same state + pressureObserverCallback([{ source: 'cpu', state: 'nominal' }]); + expect(callback).not.toHaveBeenCalled(); + + // Call with a different state + pressureObserverCallback([{ source: 'cpu', state: 'fair' }]); + expect(callback).toHaveBeenCalledWith('fair'); + }); + + it('should not emit if callback was deregistered', () => { + expect.hasAssertions(); + + const callback = jest.fn(); + SystemInfo.onCpuPressureChange(callback); + + // Call with the same state + pressureObserverCallback([{ source: 'cpu', state: 'nominal' }]); + expect(callback).toHaveBeenCalledWith('nominal'); + + // Deregister the callback + SystemInfo.offCpuPressureChange(callback); + + // Call with a different state + pressureObserverCallback([{ source: 'cpu', state: 'fair' }]); + expect(callback).toHaveBeenCalledTimes(1); // Should not be called again + }); }); describe('when PressureObserver is not supported', () => { diff --git a/src/system-info.ts b/src/system-info.ts index a13b4ca..6be977a 100644 --- a/src/system-info.ts +++ b/src/system-info.ts @@ -130,6 +130,19 @@ export class SystemInfo { systemInfo.on(SystemInfoEvents.CpuPressureStateChange, callback); } + /** + * Unregisters a callback that was registered to be called when the CPU pressure state changes. + * + * @param callback - Callback to be called when the CPU pressure state changes. + */ + static offCpuPressureChange(callback: (state: PressureState) => void): void { + if (!SystemInfo.isPressureObserverSupported()) { + throw new Error('PressureObserver is not supported in this environment.'); + } + + systemInfo.off(SystemInfoEvents.CpuPressureStateChange, callback); + } + /** * Gets the number of logical CPU cores. * From dae4371b366002d254ac54b9f5abaa3f2df62de6 Mon Sep 17 00:00:00 2001 From: kwasniow Date: Thu, 26 Jun 2025 13:06:50 +0200 Subject: [PATCH 6/9] feat: better typing --- src/system-info.spec.ts | 21 ++++++--------------- src/system-info.ts | 39 +++++++++++++++++++++++++++++++-------- 2 files changed, 37 insertions(+), 23 deletions(-) diff --git a/src/system-info.spec.ts b/src/system-info.spec.ts index 2b9a199..a8b027c 100644 --- a/src/system-info.spec.ts +++ b/src/system-info.spec.ts @@ -33,19 +33,6 @@ Object.defineProperty(window, 'PressureObserver', { // eslint-disable-next-line import/first import { SystemInfo } from './system-info'; -// Extend the Window interface to include PressureObserver -// NOTE: This is needed for TypeScript to recognize PressureObserver -// since it is not a standard part of the Window interface yet. -declare global { - interface Window { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - PressureObserver?: new (callback: (records: any[]) => void) => { - observe: (source: string) => void; - disconnect: () => void; - }; - } -} - describe('SystemInfo', () => { describe('isPressureObserverSupported', () => { it('should return true when PressureObserver is supported', () => { @@ -137,9 +124,11 @@ describe('SystemInfo', () => { const callback = jest.fn(); SystemInfo.onCpuPressureChange(callback); + expect(callback).toHaveBeenCalledWith('nominal'); + // Call with the same state pressureObserverCallback([{ source: 'cpu', state: 'nominal' }]); - expect(callback).not.toHaveBeenCalled(); + expect(callback).toHaveBeenCalledTimes(1); // Should not be called again // Call with a different state pressureObserverCallback([{ source: 'cpu', state: 'fair' }]); @@ -151,17 +140,19 @@ describe('SystemInfo', () => { const callback = jest.fn(); SystemInfo.onCpuPressureChange(callback); + expect(callback).toHaveBeenCalledTimes(1); // Call with the same state pressureObserverCallback([{ source: 'cpu', state: 'nominal' }]); expect(callback).toHaveBeenCalledWith('nominal'); + expect(callback).toHaveBeenCalledTimes(2); // Deregister the callback SystemInfo.offCpuPressureChange(callback); // Call with a different state pressureObserverCallback([{ source: 'cpu', state: 'fair' }]); - expect(callback).toHaveBeenCalledTimes(1); // Should not be called again + expect(callback).toHaveBeenCalledTimes(2); // Should not be called again }); }); diff --git a/src/system-info.ts b/src/system-info.ts index 6be977a..f07361d 100644 --- a/src/system-info.ts +++ b/src/system-info.ts @@ -19,11 +19,9 @@ import { EventEmitter } from 'events'; // but this state is not sustainable for the long run and might result in throttling if the workload remains the same. // This signal is the last call for the web application to lighten its workload. -export type PressureState = 'nominal' | 'fair' | 'serious' | 'critical'; +export type PressureSource = 'cpu'; -export enum SystemInfoEvents { - CpuPressureStateChange = 'cpu-pressure-state-change', -} +export type PressureState = 'nominal' | 'fair' | 'serious' | 'critical'; interface PressureRecord { source: string; @@ -31,12 +29,28 @@ interface PressureRecord { time: number; } +interface PressureObserver { + observe(source: PressureSource): Promise; + unobserve(source: PressureSource): void; + disconnect(): void; + takeRecords(): PressureRecord[]; +} + +declare global { + interface Window { + PressureObserver?: PressureObserver; + } +} + +export enum SystemInfoEvents { + CpuPressureStateChange = 'cpu-pressure-state-change', +} + /** * SystemInfo class to manage system information and pressure states. */ class SystemInfoInternal extends EventEmitter { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private observer?: any; + private observer?: PressureObserver; private lastCpuPressure?: PressureState = undefined; @@ -50,8 +64,9 @@ class SystemInfoInternal extends EventEmitter { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore this.observer = new PressureObserver(this.handleStateChange.bind(this)); - - this.observer.observe('cpu'); + if (this.observer) { + this.observer.observe('cpu'); + } } } @@ -128,6 +143,14 @@ export class SystemInfo { } systemInfo.on(SystemInfoEvents.CpuPressureStateChange, callback); + + // There might be possibility that the CPU pressure state has already changed + // before the callback was registered, so we check the current state + // and call the callback immediately if the state is available. + const state = SystemInfo.getCpuPressure(); + if (state !== undefined) { + callback(state); + } } /** From ea8716b54546ccb78a1c063d4186133302734725 Mon Sep 17 00:00:00 2001 From: kwasniow Date: Thu, 26 Jun 2025 17:03:38 +0200 Subject: [PATCH 7/9] fix: deprecate cpu-info and add comment about types --- src/cpu-info.ts | 2 ++ src/system-info.ts | 24 ++++++++++++++++-------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/cpu-info.ts b/src/cpu-info.ts index 042dd3f..aed89b2 100644 --- a/src/cpu-info.ts +++ b/src/cpu-info.ts @@ -1,9 +1,11 @@ /** * A class that provides information about the CPU. + * @deprecated Use `SystemInfo` instead. */ export class CpuInfo { /** * Gets the number of logical CPU cores. + * @deprecated Use `SystemInfo.getNumLogicalCores()` instead. * * @returns The number of logical CPU cores, or undefined if not available. */ diff --git a/src/system-info.ts b/src/system-info.ts index f07361d..6900aaf 100644 --- a/src/system-info.ts +++ b/src/system-info.ts @@ -2,6 +2,13 @@ import { EventEmitter } from 'events'; +// PressureObserver is a W3C standard API that provides information about the system's pressure state. +// It allows web applications to observe the pressure state of the system, which can help them adapt +// their behavior based on the system's performance and resource availability. +// Pressure API is supported in modern browsers, but it is not available in all environments. +// Currently, it's not supported by TypeScript, so we need to define the types ourselves. +// NOTE: Consider removing this once TypeScript supports PressureObserver. + // https://w3c.github.io/compute-pressure/#pressure-states // ⚪ Nominal: Work is minimal and the system is running on lower clock speed to preserve power. // @@ -47,9 +54,10 @@ export enum SystemInfoEvents { } /** - * SystemInfo class to manage system information and pressure states. + * PressureObserverHelper class to wrap the PressureObserver API + * and provide a simple interface for observing CPU pressure state changes. */ -class SystemInfoInternal extends EventEmitter { +class PressureObserverHelper extends EventEmitter { private observer?: PressureObserver; private lastCpuPressure?: PressureState = undefined; @@ -60,7 +68,7 @@ class SystemInfoInternal extends EventEmitter { constructor() { super(); - if (SystemInfoInternal.isPressureObserverSupported()) { + if (PressureObserverHelper.isPressureObserverSupported()) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore this.observer = new PressureObserver(this.handleStateChange.bind(this)); @@ -104,7 +112,7 @@ class SystemInfoInternal extends EventEmitter { } } -const systemInfo = new SystemInfoInternal(); +const pressureObserverHelper = new PressureObserverHelper(); /** * SystemInfo class to provide static methods for system information. @@ -116,7 +124,7 @@ export class SystemInfo { * @returns True if the Compute Pressure API is supported, false otherwise. */ static isPressureObserverSupported(): boolean { - return SystemInfoInternal.isPressureObserverSupported(); + return PressureObserverHelper.isPressureObserverSupported(); } /** @@ -129,7 +137,7 @@ export class SystemInfo { return undefined; } - return systemInfo.getCpuPressure(); + return pressureObserverHelper.getCpuPressure(); } /** @@ -142,7 +150,7 @@ export class SystemInfo { throw new Error('PressureObserver is not supported in this environment.'); } - systemInfo.on(SystemInfoEvents.CpuPressureStateChange, callback); + pressureObserverHelper.on(SystemInfoEvents.CpuPressureStateChange, callback); // There might be possibility that the CPU pressure state has already changed // before the callback was registered, so we check the current state @@ -163,7 +171,7 @@ export class SystemInfo { throw new Error('PressureObserver is not supported in this environment.'); } - systemInfo.off(SystemInfoEvents.CpuPressureStateChange, callback); + pressureObserverHelper.off(SystemInfoEvents.CpuPressureStateChange, callback); } /** From eba10022971a9f8a5afb0f77fe22d683d15dce97 Mon Sep 17 00:00:00 2001 From: kwasniow Date: Thu, 26 Jun 2025 17:11:11 +0200 Subject: [PATCH 8/9] fix: missed rename --- src/system-info.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/system-info.ts b/src/system-info.ts index 6900aaf..00bb641 100644 --- a/src/system-info.ts +++ b/src/system-info.ts @@ -55,7 +55,7 @@ export enum SystemInfoEvents { /** * PressureObserverHelper class to wrap the PressureObserver API - * and provide a simple interface for observing CPU pressure state changes. + * and provide a simple interface for observing pressure state changes. */ class PressureObserverHelper extends EventEmitter { private observer?: PressureObserver; @@ -63,7 +63,7 @@ class PressureObserverHelper extends EventEmitter { private lastCpuPressure?: PressureState = undefined; /** - * Creates an instance of SystemInfo. + * Creates an instance of PressureObserverHelper. */ constructor() { super(); From 92ed33d6db92040b6c1d5a3e3cb7342345ac7717 Mon Sep 17 00:00:00 2001 From: kwasniow Date: Fri, 27 Jun 2025 08:25:22 +0200 Subject: [PATCH 9/9] fix: do not throw --- src/system-info.spec.ts | 7 +++++-- src/system-info.ts | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/system-info.spec.ts b/src/system-info.spec.ts index a8b027c..09aaf33 100644 --- a/src/system-info.spec.ts +++ b/src/system-info.spec.ts @@ -162,11 +162,14 @@ describe('SystemInfo', () => { delete window.PressureObserver; }); - it('should throw', () => { + it('should not attach listener', () => { expect.hasAssertions(); const callback = jest.fn(); - expect(() => SystemInfo.onCpuPressureChange(callback)).toThrow(expect.anything()); + + SystemInfo.onCpuPressureChange(callback); + + expect(callback).toHaveBeenCalledTimes(0); }); }); }); diff --git a/src/system-info.ts b/src/system-info.ts index 00bb641..6ddcc14 100644 --- a/src/system-info.ts +++ b/src/system-info.ts @@ -147,7 +147,7 @@ export class SystemInfo { */ static onCpuPressureChange(callback: (state: PressureState) => void): void { if (!SystemInfo.isPressureObserverSupported()) { - throw new Error('PressureObserver is not supported in this environment.'); + return; } pressureObserverHelper.on(SystemInfoEvents.CpuPressureStateChange, callback); @@ -168,7 +168,7 @@ export class SystemInfo { */ static offCpuPressureChange(callback: (state: PressureState) => void): void { if (!SystemInfo.isPressureObserverSupported()) { - throw new Error('PressureObserver is not supported in this environment.'); + return; } pressureObserverHelper.off(SystemInfoEvents.CpuPressureStateChange, callback);