mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-26 19:13:20 +00:00
Fix more typings
This commit is contained in:
15 files changed
+113
-50
No files matched your search
@@ -3,6 +3,7 @@ import { P2P_DEFAULT_SETTINGS } from "@lib/common/types";
|
|||||||
import type { ServiceContext } from "@lib/services/base/ServiceBase";
|
import type { ServiceContext } from "@lib/services/base/ServiceBase";
|
||||||
import { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
|
import { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
|
||||||
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
|
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
|
||||||
|
import { LiveSyncError } from "@lib/common/LSError";
|
||||||
|
|
||||||
type CLIP2PPeer = {
|
type CLIP2PPeer = {
|
||||||
peerId: string;
|
peerId: string;
|
||||||
@@ -107,11 +108,14 @@ export async function syncWithPeer(
|
|||||||
|
|
||||||
const pullResult = await replicator.replicateFrom(targetPeer.peerId, false);
|
const pullResult = await replicator.replicateFrom(targetPeer.peerId, false);
|
||||||
if (pullResult && "error" in pullResult && pullResult.error) {
|
if (pullResult && "error" in pullResult && pullResult.error) {
|
||||||
throw pullResult.error;
|
throw pullResult.error instanceof Error ? pullResult.error : LiveSyncError.fromError(pullResult.error);
|
||||||
}
|
}
|
||||||
const pushResult = await replicator.requestSynchroniseToPeer(targetPeer.peerId);
|
const pushResult = await replicator.requestSynchroniseToPeer(targetPeer.peerId);
|
||||||
if (!pushResult || pushResult.ok !== true) {
|
if (!pushResult || pushResult.ok !== true) {
|
||||||
throw pushResult?.error ?? new Error("P2P sync failed while requesting remote sync");
|
const err = pushResult?.error;
|
||||||
|
throw err instanceof Error
|
||||||
|
? err
|
||||||
|
: LiveSyncError.fromError(err ?? "P2P sync failed while requesting remote sync");
|
||||||
}
|
}
|
||||||
|
|
||||||
return targetPeer;
|
return targetPeer;
|
||||||
|
|||||||
@@ -159,9 +159,13 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pollTimer = compatGlobal.setTimeout(poll, currentIntervalMs);
|
pollTimer = compatGlobal.setTimeout(() => {
|
||||||
|
void poll();
|
||||||
|
}, currentIntervalMs);
|
||||||
};
|
};
|
||||||
let pollTimer = compatGlobal.setTimeout(poll, currentIntervalMs);
|
let pollTimer = compatGlobal.setTimeout(() => {
|
||||||
|
void poll();
|
||||||
|
}, currentIntervalMs);
|
||||||
core.services.appLifecycle.onUnload.addHandler(async () => {
|
core.services.appLifecycle.onUnload.addHandler(async () => {
|
||||||
compatGlobal.clearTimeout(pollTimer);
|
compatGlobal.clearTimeout(pollTimer);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -2,13 +2,16 @@
|
|||||||
// eslint-disable -- This is the entry point for the CLI application.
|
// eslint-disable -- This is the entry point for the CLI application.
|
||||||
import * as polyfill from "werift";
|
import * as polyfill from "werift";
|
||||||
import { main } from "./main";
|
import { main } from "./main";
|
||||||
|
import { compatGlobal } from "@lib/common/coreEnvFunctions";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Polyfill
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Polyfill
|
||||||
const rtcPolyfillCtor = (polyfill as any).RTCPeerConnection;
|
const rtcPolyfillCtor = (polyfill as any).RTCPeerConnection;
|
||||||
if (typeof (global as any).RTCPeerConnection === "undefined" && typeof rtcPolyfillCtor === "function") {
|
if (
|
||||||
|
typeof (compatGlobal as unknown as Record<string, unknown>).RTCPeerConnection === "undefined" &&
|
||||||
|
typeof rtcPolyfillCtor === "function"
|
||||||
|
) {
|
||||||
// Fill only the standard WebRTC global in Node CLI runtime.
|
// Fill only the standard WebRTC global in Node CLI runtime.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Polyfill
|
(compatGlobal as unknown as Record<string, unknown>).RTCPeerConnection = rtcPolyfillCtor;
|
||||||
(global as any).RTCPeerConnection = rtcPolyfillCtor;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((error) => {
|
main().catch((error) => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { fs as nodeFs, path as nodePath } from "@/apps/cli/node-compat";
|
import { fs as nodeFs, path as nodePath } from "@/apps/cli/node-compat";
|
||||||
|
import { compatGlobal } from "@lib/common/coreEnvFunctions";
|
||||||
|
|
||||||
type LocalStorageShape = {
|
type LocalStorageShape = {
|
||||||
getItem(key: string): string | null;
|
getItem(key: string): string | null;
|
||||||
@@ -83,10 +84,12 @@ function createNodeLocalStorageShim(): LocalStorageShape {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ensureGlobalNodeLocalStorage() {
|
export function ensureGlobalNodeLocalStorage() {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Node.js global object manipulation
|
const g = compatGlobal as unknown as Record<string, unknown>;
|
||||||
if (!("localStorage" in global) || typeof (global as any).localStorage?.getItem !== "function") {
|
if (
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Node.js global object access
|
!("localStorage" in g) ||
|
||||||
(global as any).localStorage = createNodeLocalStorageShim();
|
typeof (g.localStorage as Record<string, unknown> | undefined)?.getItem !== "function"
|
||||||
|
) {
|
||||||
|
g.localStorage = createNodeLocalStorageShim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { UXFileInfoStub, UXFolderInfo } from "@lib/common/types";
|
import type { FilePath, UXFileInfoStub, UXFolderInfo } from "@lib/common/types";
|
||||||
import type { IConversionAdapter } from "@lib/serviceModules/adapters";
|
import type { IConversionAdapter } from "@lib/serviceModules/adapters";
|
||||||
import type { FSAPIFile, FSAPIFolder } from "./FSAPITypes";
|
import type { FSAPIFile, FSAPIFolder } from "./FSAPITypes";
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ export class FSAPIConversionAdapter implements IConversionAdapter<FSAPIFile, FSA
|
|||||||
path: folder.path,
|
path: folder.path,
|
||||||
isFolder: true,
|
isFolder: true,
|
||||||
children: [],
|
children: [],
|
||||||
parent: parentPath as any,
|
parent: parentPath as FilePath,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,7 +173,11 @@ export class FSAPIFileSystemAdapter implements IFileSystemAdapter<FSAPIFile, FSA
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use AsyncIterator instead of .values() for better compatibility
|
// Use AsyncIterator instead of .values() for better compatibility
|
||||||
for await (const [name, entry] of (currentHandle as any).entries()) {
|
for await (const [name, entry] of (
|
||||||
|
currentHandle as unknown as {
|
||||||
|
entries(): AsyncIterable<[string, FileSystemHandle]>;
|
||||||
|
}
|
||||||
|
).entries()) {
|
||||||
const entryPath = relativePath ? `${relativePath}/${name}` : name;
|
const entryPath = relativePath ? `${relativePath}/${name}` : name;
|
||||||
|
|
||||||
if (entry.kind === "directory") {
|
if (entry.kind === "directory") {
|
||||||
|
|||||||
@@ -195,7 +195,11 @@ export class FSAPIStorageAdapter implements IStorageAdapter<FSAPIStat> {
|
|||||||
const folders: string[] = [];
|
const folders: string[] = [];
|
||||||
|
|
||||||
// Use AsyncIterator instead of .values() for better compatibility
|
// Use AsyncIterator instead of .values() for better compatibility
|
||||||
for await (const [name, entry] of (dirHandle as any).entries()) {
|
for await (const [name, entry] of (
|
||||||
|
dirHandle as unknown as {
|
||||||
|
entries(): AsyncIterable<[string, FileSystemHandle]>;
|
||||||
|
}
|
||||||
|
).entries()) {
|
||||||
const entryPath = basePath ? `${basePath}/${name}` : name;
|
const entryPath = basePath ? `${basePath}/${name}` : name;
|
||||||
|
|
||||||
if (entry.kind === "directory") {
|
if (entry.kind === "directory") {
|
||||||
|
|||||||
@@ -5,13 +5,24 @@ import type { FSAPIFile, FSAPIFolder } from "./FSAPITypes";
|
|||||||
* Type guard adapter implementation for FileSystem API
|
* Type guard adapter implementation for FileSystem API
|
||||||
*/
|
*/
|
||||||
export class FSAPITypeGuardAdapter implements ITypeGuardAdapter<FSAPIFile, FSAPIFolder> {
|
export class FSAPITypeGuardAdapter implements ITypeGuardAdapter<FSAPIFile, FSAPIFolder> {
|
||||||
isFile(file: any): file is FSAPIFile {
|
isFile(file: unknown): file is FSAPIFile {
|
||||||
return (
|
return !!(
|
||||||
file && typeof file === "object" && "path" in file && "stat" in file && "handle" in file && !file.isFolder
|
file &&
|
||||||
|
typeof file === "object" &&
|
||||||
|
"path" in file &&
|
||||||
|
"stat" in file &&
|
||||||
|
"handle" in file &&
|
||||||
|
!(file as { isFolder?: boolean }).isFolder
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isFolder(item: any): item is FSAPIFolder {
|
isFolder(item: unknown): item is FSAPIFolder {
|
||||||
return item && typeof item === "object" && "path" in item && item.isFolder === true && "handle" in item;
|
return !!(
|
||||||
|
item &&
|
||||||
|
typeof item === "object" &&
|
||||||
|
"path" in item &&
|
||||||
|
(item as { isFolder?: boolean }).isFolder === true &&
|
||||||
|
"handle" in item
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,8 +116,7 @@ export class FSAPIVaultAdapter implements IVaultAdapter<FSAPIFile> {
|
|||||||
await this.delete(file, force);
|
await this.delete(file, force);
|
||||||
}
|
}
|
||||||
|
|
||||||
trigger(name: string, ...data: any[]): any {
|
trigger(name: string, ...data: unknown[]): void {
|
||||||
// No-op in webapp version (no event system yet)
|
// No-op in webapp version (no event system yet)
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,13 +121,11 @@ async function initializeVaultSelector(): Promise<void> {
|
|||||||
await renderHistoryList();
|
await renderHistoryList();
|
||||||
}
|
}
|
||||||
|
|
||||||
compatGlobal.addEventListener("load", async () => {
|
compatGlobal.addEventListener("load", () => {
|
||||||
try {
|
initializeVaultSelector().catch((error) => {
|
||||||
await initializeVaultSelector();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to initialize vault selector:", error);
|
console.error("Failed to initialize vault selector:", error);
|
||||||
setStatus("error", `Initialization failed: ${String(error)}`);
|
setStatus("error", `Initialization failed: ${String(error)}`);
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
compatGlobal.addEventListener("beforeunload", () => {
|
compatGlobal.addEventListener("beforeunload", () => {
|
||||||
|
|||||||
@@ -97,13 +97,15 @@ class LiveSyncWebApp {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// App lifecycle handlers
|
// App lifecycle handlers
|
||||||
this.serviceHub.appLifecycle.scheduleRestart.setHandler(async () => {
|
this.serviceHub.appLifecycle.scheduleRestart.setHandler(() => {
|
||||||
console.log("[AppLifecycle] Restart requested");
|
void (async () => {
|
||||||
await this.shutdown();
|
console.log("[AppLifecycle] Restart requested");
|
||||||
await this.initialize();
|
await this.shutdown();
|
||||||
compatGlobal.setTimeout(() => {
|
await this.initialize();
|
||||||
compatGlobal.location.reload();
|
compatGlobal.setTimeout(() => {
|
||||||
}, 1000);
|
compatGlobal.location.reload();
|
||||||
|
}, 1000);
|
||||||
|
})();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create LiveSync core
|
// Create LiveSync core
|
||||||
|
|||||||
@@ -207,9 +207,9 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (type === "appeared") {
|
if (type === "appeared") {
|
||||||
await handlers.onCreate(fileInfo, undefined);
|
handlers.onCreate(fileInfo, undefined);
|
||||||
} else {
|
} else {
|
||||||
await handlers.onChange(fileInfo, undefined);
|
handlers.onChange(fileInfo, undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (type === "disappeared") {
|
} else if (type === "disappeared") {
|
||||||
@@ -223,7 +223,7 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
|
|||||||
},
|
},
|
||||||
handle: null as unknown as FileSystemFileHandle, // No handle available for disappeared files
|
handle: null as unknown as FileSystemFileHandle, // No handle available for disappeared files
|
||||||
};
|
};
|
||||||
await handlers.onDelete(fileInfo, undefined);
|
handlers.onDelete(fileInfo, undefined);
|
||||||
} else if (type === "moved") {
|
} else if (type === "moved") {
|
||||||
// Handle as delete + create
|
// Handle as delete + create
|
||||||
// Note: FileSystemObserver provides both old and new paths in some cases
|
// Note: FileSystemObserver provides both old and new paths in some cases
|
||||||
@@ -240,7 +240,7 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
|
|||||||
},
|
},
|
||||||
handle: changedHandle,
|
handle: changedHandle,
|
||||||
};
|
};
|
||||||
await handlers.onChange(fileInfo, undefined);
|
handlers.onChange(fileInfo, undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -28,11 +28,38 @@ function stripPrefix(raw: string): string {
|
|||||||
return raw.replace(/^[^:]+:/, "");
|
return raw.replace(/^[^:]+:/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TestCore {
|
||||||
|
services?: {
|
||||||
|
replication?: {
|
||||||
|
databaseQueueCount?: { value: number };
|
||||||
|
storageApplyingCount?: { value: number };
|
||||||
|
};
|
||||||
|
fileProcessing?: {
|
||||||
|
totalQueued?: { value: number };
|
||||||
|
batched?: { value: number };
|
||||||
|
processing?: { value: number };
|
||||||
|
};
|
||||||
|
database?: {
|
||||||
|
localDatabase: {
|
||||||
|
findAllNormalDocs(options?: { conflicts?: boolean }): AsyncIterable<{
|
||||||
|
_deleted?: boolean;
|
||||||
|
deleted?: boolean;
|
||||||
|
path?: string;
|
||||||
|
_rev?: string;
|
||||||
|
_conflicts?: string[];
|
||||||
|
size?: number;
|
||||||
|
mtime?: number;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Poll every 300 ms until all known processing queues are drained, or until
|
* Poll every 300 ms until all known processing queues are drained, or until
|
||||||
* the timeout elapses. Mirrors `waitForIdle` in the existing vitest harness.
|
* the timeout elapses. Mirrors `waitForIdle` in the existing vitest harness.
|
||||||
*/
|
*/
|
||||||
async function waitForIdle(core: any, timeoutMs = 60_000): Promise<void> {
|
async function waitForIdle(core: TestCore, timeoutMs = 60_000): Promise<void> {
|
||||||
const deadline = Date.now() + timeoutMs;
|
const deadline = Date.now() + timeoutMs;
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
const q =
|
const q =
|
||||||
@@ -47,8 +74,8 @@ async function waitForIdle(core: any, timeoutMs = 60_000): Promise<void> {
|
|||||||
throw new Error(`waitForIdle timed out after ${timeoutMs} ms`);
|
throw new Error(`waitForIdle timed out after ${timeoutMs} ms`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCore(): any {
|
function getCore(): TestCore {
|
||||||
const core = (app as any)?.core;
|
const core = (app as unknown as { core: TestCore | null })?.core;
|
||||||
if (!core) throw new Error("Vault not initialised – call livesyncTest.init() first");
|
if (!core) throw new Error("Vault not initialised – call livesyncTest.init() first");
|
||||||
return core;
|
return core;
|
||||||
}
|
}
|
||||||
@@ -201,4 +228,4 @@ const livesyncTest: LiveSyncTestAPI = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Expose on window for Playwright page.evaluate() calls.
|
// Expose on window for Playwright page.evaluate() calls.
|
||||||
(compatGlobal as any).livesyncTest = livesyncTest;
|
(compatGlobal as unknown as Record<string, unknown>).livesyncTest = livesyncTest;
|
||||||
@@ -32,7 +32,10 @@ function randomId(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function hasReadWritePermission(handle: FileSystemDirectoryHandle, requestIfNeeded: boolean): Promise<boolean> {
|
async function hasReadWritePermission(handle: FileSystemDirectoryHandle, requestIfNeeded: boolean): Promise<boolean> {
|
||||||
const h = handle as any;
|
const h = handle as unknown as {
|
||||||
|
queryPermission?: (options: { mode: "readwrite" }) => Promise<PermissionState>;
|
||||||
|
requestPermission?: (options: { mode: "readwrite" }) => Promise<PermissionState>;
|
||||||
|
};
|
||||||
if (typeof h.queryPermission === "function") {
|
if (typeof h.queryPermission === "function") {
|
||||||
const queried = await h.queryPermission({ mode: "readwrite" });
|
const queried = await h.queryPermission({ mode: "readwrite" });
|
||||||
if (queried === "granted") {
|
if (queried === "granted") {
|
||||||
@@ -172,15 +175,17 @@ export class VaultHistoryStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async pickNewVault(): Promise<FileSystemDirectoryHandle> {
|
async pickNewVault(): Promise<FileSystemDirectoryHandle> {
|
||||||
const picker = (compatGlobal as any).showDirectoryPicker;
|
const picker = (compatGlobal as unknown as Record<string, unknown>).showDirectoryPicker as
|
||||||
|
| ((options?: { mode?: "readwrite" | "read"; startIn?: string }) => Promise<FileSystemDirectoryHandle>)
|
||||||
|
| undefined;
|
||||||
if (typeof picker !== "function") {
|
if (typeof picker !== "function") {
|
||||||
throw new Error("FileSystem API showDirectoryPicker is not supported in this browser");
|
throw new Error("FileSystem API showDirectoryPicker is not supported in this browser");
|
||||||
}
|
}
|
||||||
|
|
||||||
const handle = (await picker({
|
const handle = await picker({
|
||||||
mode: "readwrite",
|
mode: "readwrite",
|
||||||
startIn: "documents",
|
startIn: "documents",
|
||||||
})) as FileSystemDirectoryHandle;
|
});
|
||||||
|
|
||||||
const granted = await hasReadWritePermission(handle, true);
|
const granted = await hasReadWritePermission(handle, true);
|
||||||
if (!granted) {
|
if (!granted) {
|
||||||
|
|||||||
@@ -82,8 +82,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
|
|||||||
replicator,
|
replicator,
|
||||||
p2pLogCollector,
|
p2pLogCollector,
|
||||||
storeP2PStatusLine: p2pStatusLine,
|
storeP2PStatusLine: p2pStatusLine,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- hacky way. //TODO: check it later
|
} = useP2PReplicator({ services: this.services } as unknown as Parameters<typeof useP2PReplicator>[0]);
|
||||||
} = useP2PReplicator({ services: this.services } as any);
|
|
||||||
this._liveSyncReplicator = replicator;
|
this._liveSyncReplicator = replicator;
|
||||||
this.p2pLogCollector = p2pLogCollector;
|
this.p2pLogCollector = p2pLogCollector;
|
||||||
p2pLogCollector.p2pReplicationLine.onChanged((line) => {
|
p2pLogCollector.p2pReplicationLine.onChanged((line) => {
|
||||||
@@ -101,7 +100,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
|
|||||||
const repStore = SimpleStoreIDBv2.open<unknown>("p2p-livesync-web-peer");
|
const repStore = SimpleStoreIDBv2.open<unknown>("p2p-livesync-web-peer");
|
||||||
this._simpleStore = repStore;
|
this._simpleStore = repStore;
|
||||||
let _settings = { ...P2P_DEFAULT_SETTINGS, additionalSuffixOfDatabaseName: "" } as ObsidianLiveSyncSettings;
|
let _settings = { ...P2P_DEFAULT_SETTINGS, additionalSuffixOfDatabaseName: "" } as ObsidianLiveSyncSettings;
|
||||||
this.services.setting.settings = _settings as ObsidianLiveSyncSettings;
|
this.services.setting.settings = _settings;
|
||||||
(this.services.setting as InjectableSettingService<ServiceContext>).saveData.setHandler(async (data) => {
|
(this.services.setting as InjectableSettingService<ServiceContext>).saveData.setHandler(async (data) => {
|
||||||
await repStore.set("settings", data);
|
await repStore.set("settings", data);
|
||||||
eventHub.emitEvent(EVENT_SETTING_SAVED, data);
|
eventHub.emitEvent(EVENT_SETTING_SAVED, data);
|
||||||
@@ -281,7 +280,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
|
|||||||
}
|
}
|
||||||
await this.services.setting.applyExternalSettings(remoteConfig, true);
|
await this.services.setting.applyExternalSettings(remoteConfig, true);
|
||||||
if (yn !== DROP) {
|
if (yn !== DROP) {
|
||||||
await this.plugin.core.services.appLifecycle.scheduleRestart();
|
this.plugin.core.services.appLifecycle.scheduleRestart();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Logger(`Cancelled\nRemote config for ${peer.name} is not applied`, LOG_LEVEL_NOTICE);
|
Logger(`Cancelled\nRemote config for ${peer.name} is not applied`, LOG_LEVEL_NOTICE);
|
||||||
|
|||||||
Reference in new issue
Block a user