Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: it runs in the browser!!! #7586

Merged
merged 5 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading