Skip to content

Commit

Permalink
feat: it runs in the browser!!! (#7586)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlCalzone authored Jan 31, 2025
1 parent 2824837 commit 57d3a7d
Show file tree
Hide file tree
Showing 31 changed files with 1,098 additions and 210 deletions.
43 changes: 28 additions & 15 deletions packages/config/src/ConfigManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
ZWaveErrorCodes,
isZWaveError,
} from "@zwave-js/core";
import { log as createZWaveLogContainer } from "@zwave-js/core/bindings/log/node";
import { getErrorMessage, pathExists } from "@zwave-js/shared";
import { type FileSystem } from "@zwave-js/shared/bindings";
import path from "pathe";
Expand Down Expand Up @@ -43,9 +42,8 @@ export interface ConfigManagerOptions {
export class ConfigManager {
public constructor(options: ConfigManagerOptions = {}) {
this._fs = options.bindings;
this.logger = new ConfigLogger(
options.logContainer ?? createZWaveLogContainer({ enabled: false }),
);
this._logContainer = options.logContainer;

this.deviceConfigPriorityDir = options.deviceConfigPriorityDir;
this.deviceConfigExternalDir = options.deviceConfigExternalDir;

Expand All @@ -63,7 +61,20 @@ export class ConfigManager {
return this._configVersion;
}

private logger: ConfigLogger;
private _logContainer: LogContainer | undefined;
private _logger: ConfigLogger | undefined;
private async getLogger(): Promise<ConfigLogger> {
if (!this._logContainer) {
this._logContainer =
(await import("@zwave-js/core/bindings/log/node")).log({
enabled: false,
});
}
if (!this._logger) {
this._logger = new ConfigLogger(this._logContainer);
}
return this._logger;
}

private _manufacturers: ManufacturersMap | undefined;
public get manufacturers(): ManufacturersMap {
Expand Down Expand Up @@ -92,6 +103,7 @@ export class ConfigManager {
}

public async loadAll(): Promise<void> {
const logger = await this.getLogger();
// If the environment option for an external config dir is set
// try to sync it and then use it
let syncResult: SyncExternalConfigDirResult | undefined;
Expand All @@ -100,21 +112,21 @@ export class ConfigManager {
syncResult = await syncExternalConfigDir(
await this.getFS(),
externalConfigDir,
this.logger,
logger,
);
}

if (syncResult?.success) {
this._useExternalConfig = true;
this.logger.print(
logger.print(
`Using external configuration dir ${externalConfigDir}`,
);
this._configVersion = syncResult.version;
} else {
this._useExternalConfig = false;
this._configVersion = PACKAGE_VERSION;
}
this.logger.print(`version ${this._configVersion}`, "info");
logger.print(`version ${this._configVersion}`, "info");

await this.loadManufacturers();
await this.loadDeviceIndex();
Expand All @@ -130,7 +142,7 @@ export class ConfigManager {
// If the config file is missing or invalid, don't try to find it again
if (isZWaveError(e) && e.code === ZWaveErrorCodes.Config_Invalid) {
if (process.env.NODE_ENV !== "test") {
this.logger.print(
(await this.getLogger()).print(
`Could not load manufacturers config: ${e.message}`,
"error",
);
Expand Down Expand Up @@ -193,11 +205,12 @@ export class ConfigManager {

public async loadDeviceIndex(): Promise<void> {
const fs = await this.getFS();
const logger = await this.getLogger();
try {
// The index of config files included in this package
const embeddedIndex = await loadDeviceIndexInternal(
fs,
this.logger,
logger,
this._useExternalConfig && this.externalConfigDir || undefined,
);
// A dynamic index of the user-defined priority device config files
Expand All @@ -208,11 +221,11 @@ export class ConfigManager {
...(await generatePriorityDeviceIndex(
fs,
this.deviceConfigPriorityDir,
this.logger,
logger,
)),
);
} else {
this.logger.print(
logger.print(
`Priority device configuration directory ${this.deviceConfigPriorityDir} not found`,
"warn",
);
Expand All @@ -230,7 +243,7 @@ export class ConfigManager {
// Fall back to no index on production systems
if (!this.index) this.index = [];
if (process.env.NODE_ENV !== "test") {
this.logger.print(
logger.print(
`Could not load or regenerate device config index: ${e.message}`,
"error",
);
Expand All @@ -250,7 +263,7 @@ export class ConfigManager {
public async loadFulltextDeviceIndex(): Promise<void> {
this.fulltextIndex = await loadFulltextDeviceIndexInternal(
await this.getFS(),
this.logger,
await this.getLogger(),
);
}

Expand Down Expand Up @@ -321,7 +334,7 @@ export class ConfigManager {
);
} catch (e) {
if (process.env.NODE_ENV !== "test") {
this.logger.print(
(await this.getLogger()).print(
`Error loading device config ${filePath}: ${
getErrorMessage(
e,
Expand Down
3 changes: 2 additions & 1 deletion packages/config/src/devices/DeviceConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
type JSONObject,
enumFilesRecursive,
formatId,
getenv,
num2hex,
padVersion,
pathExists,
Expand Down Expand Up @@ -191,7 +192,7 @@ async function generateIndex<T extends Record<string, unknown>>(
}`;
// Crash hard during tests, just print an error when in production systems.
// A user could have changed a config file
if (process.env.NODE_ENV === "test" || !!process.env.CI) {
if (process.env.NODE_ENV === "test" || !!getenv("CI")) {
throw new ZWaveError(message, ZWaveErrorCodes.Config_Invalid);
} else {
logger?.print(message, "error");
Expand Down
21 changes: 12 additions & 9 deletions packages/config/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
copyFilesRecursive,
formatId,
getenv,
padVersion,
readTextFile,
writeTextFile,
Expand All @@ -12,7 +13,7 @@ import {
type ReadFileSystemInfo,
type WriteFile,
} from "@zwave-js/shared/bindings";
import { createRequire } from "node:module";
import { fileURLToPath } from "node:url";
import path from "pathe";
import semverGte from "semver/functions/gte.js";
import semverInc from "semver/functions/inc.js";
Expand All @@ -23,18 +24,20 @@ import type { ConfigLogger } from "./Logger.js";
import { PACKAGE_VERSION } from "./_version.js";
import type { DeviceConfigIndexEntry } from "./devices/DeviceConfig.js";

const require = createRequire(import.meta.url);

/** The absolute path of the embedded configuration directory */
// FIXME: use import.meta.resolve after upgrading to node 20
export const configDir = path.resolve(
path.dirname(require.resolve("@zwave-js/config/package.json")),
"config",
);
export const configDir = import.meta.url.startsWith("file:")
? path.join(
path.dirname(fileURLToPath(import.meta.url)),
import.meta.url.endsWith("src/utils.ts")
? ".."
: "../..",
"config",
)
: import.meta.resolve("/config");

/** The (optional) absolute path of an external configuration directory */
export function getExternalConfigDirEnvVariable(): string | undefined {
return process.env.ZWAVEJS_EXTERNAL_CONFIG;
return getenv("ZWAVEJS_EXTERNAL_CONFIG");
}

export function getDeviceEntryPredicate(
Expand Down
8 changes: 5 additions & 3 deletions packages/core/src/bindings/log/node.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getenv } from "@zwave-js/shared";
import path from "pathe";
import { configs } from "triple-beam";
import winston from "winston";
Expand All @@ -23,7 +24,8 @@ const isUnitTest = process.env.NODE_ENV === "test";
const loglevels = configs.npm.levels;

function getTransportLoglevel(): string {
return process.env.LOGLEVEL! in loglevels ? process.env.LOGLEVEL! : "debug";
const loglevel = getenv("LOGLEVEL")!;
return loglevel in loglevels ? loglevel : "debug";
}

/** Performs a reverse lookup of the numeric loglevel */
Expand All @@ -44,9 +46,9 @@ class ZWaveLogContainer<TContext extends LogContext> extends winston.Container
private logConfig: LogConfig & { level: string } = {
enabled: true,
level: getTransportLoglevel(),
logToFile: !!process.env.LOGTOFILE,
logToFile: !!getenv("LOGTOFILE"),
maxFiles: 7,
nodeFilter: stringToNodeList(process.env.LOG_NODES),
nodeFilter: stringToNodeList(getenv("LOG_NODES")),
transports: undefined as any,
filename: path.join(process.cwd(), `zwavejs_%DATE%.log`),
forceConsole: false,
Expand Down
13 changes: 5 additions & 8 deletions packages/core/src/security/Manager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/** Management class and utils for Security S0 */

import { type Timer, setTimer } from "@zwave-js/shared";
import { encryptAES128ECB, randomBytes } from "../crypto/index.js";
import { ZWaveError, ZWaveErrorCodes } from "../error/ZWaveError.js";

Expand Down Expand Up @@ -86,7 +87,7 @@ export class SecurityManager {

private _nonceStore = new Map<string, NonceEntry>();
private _freeNonceIDs = new Set<string>();
private _nonceTimers = new Map<string, NodeJS.Timeout>();
private _nonceTimers = new Map<string, Timer>();

private normalizeId(id: number | NonceKey): string {
let ret: NonceKey;
Expand Down Expand Up @@ -127,14 +128,12 @@ export class SecurityManager {
{ free = true }: SetNonceOptions = {},
): void {
const key = this.normalizeId(id);
if (this._nonceTimers.has(key)) {
clearTimeout(this._nonceTimers.get(key));
}
this._nonceTimers.get(key)?.clear();
this._nonceStore.set(key, entry);
if (free) this._freeNonceIDs.add(key);
this._nonceTimers.set(
key,
setTimeout(() => {
setTimer(() => {
this.expireNonce(key);
}, this.nonceTimeout).unref(),
);
Expand All @@ -161,9 +160,7 @@ export class SecurityManager {
}

private deleteNonceInternal(key: string) {
if (this._nonceTimers.has(key)) {
clearTimeout(this._nonceTimers.get(key));
}
this._nonceTimers.get(key)?.clear();
this._nonceStore.delete(key);
this._nonceTimers.delete(key);
this._freeNonceIDs.delete(key);
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/util/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export function getDSTInfo(now: Date = new Date()): DSTInfo {

/** Returns a timestamp with nano-second precision */
export function highResTimestamp(): number {
if (process != undefined) {
if (typeof process !== "undefined") {
const [s, ns] = process.hrtime();
return s * 1e9 + ns;
} else if (performance != undefined) {
Expand Down
5 changes: 3 additions & 2 deletions packages/serial/src/mock/MockPort.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { log as createZWaveLogContainer } from "@zwave-js/core/bindings/log/node";
import type { UnderlyingSink, UnderlyingSource } from "node:stream/web";
import {
type ZWaveSerialBindingFactory,
Expand Down Expand Up @@ -74,7 +73,9 @@ export async function createAndOpenMockedZWaveSerialPort(): Promise<{
const port = new MockPort();
const factory = new ZWaveSerialStreamFactory(
port.factory(),
createZWaveLogContainer({ enabled: false }),
(await import("@zwave-js/core/bindings/log/node")).log({
enabled: false,
}),
);
const serial = await factory.createStream();
return { port, serial };
Expand Down
4 changes: 2 additions & 2 deletions packages/serial/src/plumbing/SerialModeSwitch.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Bytes } from "@zwave-js/shared";
import { Bytes, getenv } from "@zwave-js/shared";
import { bootloaderMenuPreamble } from "../parsers/BootloaderParsers.js";
import { ZWaveSerialMode } from "../serialport/definitions.js";

const IS_TEST = process.env.NODE_ENV === "test" || !!process.env.CI;
const IS_TEST = process.env.NODE_ENV === "test" || !!getenv("CI");

// A transform stream with two outputs, where only one of both is
// active at the same time
Expand Down
86 changes: 86 additions & 0 deletions packages/shared/src/Timers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
export class Timer {
readonly #callback: (...args: any[]) => void;
readonly #delay?: number;
readonly #args: any[];

readonly #inner: NodeJS.Timeout;
readonly #kind = "timeout";

/** @internal */
constructor(
callback: (...args: any[]) => void,
delay?: number,
...args: any[]
) {
this.#callback = callback;
this.#delay = delay;
this.#args = args;
this.#inner = globalThis.setTimeout(callback, delay, ...args);
}

/** Clears the timeout. */
public clear(): void {
globalThis.clearTimeout(this.#inner);
}

public unref(): this {
// Not supported in browsers
if (typeof this.#inner.unref === "function") {
this.#inner.unref();
}
return this;
}

public refresh(): this {
if (typeof this.#inner.refresh === "function") {
this.#inner.refresh();
} else {
globalThis.clearTimeout(this.#inner);
globalThis.setTimeout(this.#callback, this.#delay, ...this.#args);
}
return this;
}
}

export class Interval {
readonly #inner: NodeJS.Timeout;
readonly #kind = "interval";

/** @internal */
constructor(inner: NodeJS.Timeout) {
this.#inner = inner;
}

/** Clears the timeout. */
public clear(): void {
globalThis.clearInterval(this.#inner);
}

public unref(): this {
// Not supported in browsers
if (typeof this.#inner.unref === "function") {
this.#inner.unref();
}
return this;
}
}

export function setTimer<TArgs extends any[]>(
callback: (...args: TArgs) => void,
delay?: number,
...args: TArgs
): Timer {
return new Timer(
callback,
delay,
...args,
);
}

export function setInterval<TArgs extends any[]>(
callback: (...args: TArgs) => void,
delay?: number,
...args: TArgs
): Interval {
return new Interval(globalThis.setInterval(callback, delay, ...args));
}
Loading

0 comments on commit 57d3a7d

Please sign in to comment.