From 1b2f9dd17196c7faa93282adcc23c37aa9b7255d Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Thu, 16 Dec 2021 19:06:42 +0900 Subject: [PATCH] Refactored and fixed: - Refactored, linted, fixed potential problems, enabled 'use strict' Fixed: - Added "Enable plugin synchronization" option (Plugins and settings had been run always) Implemented: - Sync preset implemented. - "Check integrity on saving" implemented. - "Sanity check" implemented It's mainly for debugging. --- .eslintignore | 3 + .eslintrc | 19 + main.ts | 4054 ----------------------------- manifest.json | 2 +- package-lock.json | 4036 +++++++++++++++++++++++++++- package.json | 10 +- rollup.config.js | 2 +- src/ConflictResolveModal.ts | 74 + src/LocalPouchDB.ts | 1229 +++++++++ src/LogDisplayModal.ts | 37 + src/ObsidianLiveSyncSettingTab.ts | 1093 ++++++++ src/e2ee.ts | 168 ++ src/logger.ts | 13 + src/main.ts | 1300 +++++++++ src/types.ts | 224 ++ src/utils.ts | 197 ++ src/utils_couchdb.ts | 70 + styles.css | 4 + tsconfig.json | 10 +- 19 files changed, 8476 insertions(+), 4069 deletions(-) create mode 100644 .eslintignore create mode 100644 .eslintrc delete mode 100644 main.ts create mode 100644 src/ConflictResolveModal.ts create mode 100644 src/LocalPouchDB.ts create mode 100644 src/LogDisplayModal.ts create mode 100644 src/ObsidianLiveSyncSettingTab.ts create mode 100644 src/e2ee.ts create mode 100644 src/logger.ts create mode 100644 src/main.ts create mode 100644 src/types.ts create mode 100644 src/utils.ts create mode 100644 src/utils_couchdb.ts diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..543aaac --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +npm node_modules +build +.eslintrc.js.bak \ No newline at end of file diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..733f079 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,19 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], + "extends": ["eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended"], + "parserOptions": { + "sourceType": "module" + }, + "rules": { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], + "@typescript-eslint/ban-ts-comment": "off", + "no-prototype-builtins": "off", + "@typescript-eslint/no-empty-function": "off", + "require-await": "warn", + "no-async-promise-executor": "off", + "@typescript-eslint/no-explicit-any": "off" + } +} diff --git a/main.ts b/main.ts deleted file mode 100644 index 4240686..0000000 --- a/main.ts +++ /dev/null @@ -1,4054 +0,0 @@ -import { App, debounce, Modal, Notice, Plugin, PluginSettingTab, Setting, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest } from "obsidian"; -import { PouchDB } from "./pouchdb-browser-webpack/dist/pouchdb-browser"; -import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch"; -import xxhash from "xxhash-wasm"; - -// docs should be encoded as base64, so 1 char -> 1 bytes -// and cloudant limitation is 1MB , we use 900kb; -// const MAX_DOC_SIZE = 921600; -const MAX_DOC_SIZE = 1000; // for .md file, but if delimiters exists. use that before. -const MAX_DOC_SIZE_BIN = 102400; // 100kb -const VER = 10; - -const RECENT_MOFIDIED_DOCS_QTY = 30; -const LEAF_WAIT_TIMEOUT = 90000; // in synchronization, waiting missing leaf time out. -const LOG_LEVEL = { - VERBOSE: 1, - INFO: 10, - NOTICE: 100, - URGENT: 1000, -} as const; -type LOG_LEVEL = typeof LOG_LEVEL[keyof typeof LOG_LEVEL]; - -const VERSIONINFO_DOCID = "obsydian_livesync_version"; -const MILSTONE_DOCID = "_local/obsydian_livesync_milestone"; -const NODEINFO_DOCID = "_local/obsydian_livesync_nodeinfo"; - -interface ObsidianLiveSyncSettings { - couchDB_URI: string; - couchDB_USER: string; - couchDB_PASSWORD: string; - couchDB_DBNAME: string; - liveSync: boolean; - syncOnSave: boolean; - syncOnStart: boolean; - syncOnFileOpen: boolean; - savingDelay: number; - lessInformationInLog: boolean; - gcDelay: number; - versionUpFlash: string; - minimumChunkSize: number; - longLineThreshold: number; - showVerboseLog: boolean; - suspendFileWatching: boolean; - trashInsteadDelete: boolean; - periodicReplication: boolean; - periodicReplicationInterval: number; - encrypt: boolean; - passphrase: string; - workingEncrypt: boolean; - workingPassphrase: string; - doNotDeleteFolder: boolean; - resolveConflictsByNewerFile: boolean; - batchSave: boolean; - deviceAndVaultName: string; - usePluginSettings: boolean; - showOwnPlugins: boolean; - showStatusOnEditor: boolean; - autoSweepPlugins: boolean; - autoSweepPluginsPeriodic: boolean; - notifyPluginOrSettingUpdated: boolean; -} - -const DEFAULT_SETTINGS: ObsidianLiveSyncSettings = { - couchDB_URI: "", - couchDB_USER: "", - couchDB_PASSWORD: "", - couchDB_DBNAME: "", - liveSync: false, - syncOnSave: false, - syncOnStart: false, - savingDelay: 200, - lessInformationInLog: false, - gcDelay: 300, - versionUpFlash: "", - minimumChunkSize: 20, - longLineThreshold: 250, - showVerboseLog: false, - suspendFileWatching: false, - trashInsteadDelete: true, - periodicReplication: false, - periodicReplicationInterval: 60, - syncOnFileOpen: false, - encrypt: false, - passphrase: "", - workingEncrypt: false, - workingPassphrase: "", - doNotDeleteFolder: false, - resolveConflictsByNewerFile: false, - batchSave: false, - deviceAndVaultName: "", - usePluginSettings: false, - showOwnPlugins: false, - showStatusOnEditor: false, - autoSweepPlugins: false, - autoSweepPluginsPeriodic: false, - notifyPluginOrSettingUpdated: false, -}; - -const PERIODIC_PLUGIN_SWEEP = 60; - -interface Entry { - _id: string; - data: string; - _rev?: string; - ctime: number; - mtime: number; - size: number; - _deleted?: boolean; - _conflicts?: string[]; - type?: "notes"; -} -interface NewEntry { - _id: string; - children: string[]; - _rev?: string; - ctime: number; - mtime: number; - size: number; - _deleted?: boolean; - _conflicts?: string[]; - NewNote: true; - type: "newnote"; -} -interface PlainEntry { - _id: string; - children: string[]; - _rev?: string; - ctime: number; - mtime: number; - size: number; - _deleted?: boolean; - NewNote: true; - _conflicts?: string[]; - type: "plain"; -} -type LoadedEntry = Entry & { - children: string[]; - datatype: "plain" | "newnote"; -}; - -interface PluginDataEntry { - _id: string; - deviceVaultName: string; - mtime: number; - manifest: PluginManifest; - mainJs: string; - manifestJson: string; - styleCss?: string; - // it must be encrypted. - dataJson?: string; - _rev?: string; - _deleted?: boolean; - _conflicts?: string[]; - type: "plugin"; -} - -interface EntryLeaf { - _id: string; - data: string; - _deleted?: boolean; - type: "leaf"; - _rev?: string; -} - -interface EntryVersionInfo { - _id: typeof VERSIONINFO_DOCID; - _rev?: string; - type: "versioninfo"; - version: number; - _deleted?: boolean; -} - -interface EntryMilestoneInfo { - _id: typeof MILSTONE_DOCID; - _rev?: string; - type: "milestoneinfo"; - _deleted?: boolean; - created: number; - accepted_nodes: string[]; - locked: boolean; -} - -interface EntryNodeInfo { - _id: typeof NODEINFO_DOCID; - _rev?: string; - _deleted?: boolean; - type: "nodeinfo"; - nodeid: string; -} - -type EntryBody = Entry | NewEntry | PlainEntry; -type EntryDoc = EntryBody | LoadedEntry | EntryLeaf | EntryVersionInfo | EntryMilestoneInfo | EntryNodeInfo | PluginDataEntry; - -type diff_result_leaf = { - rev: string; - data: string; - ctime: number; - mtime: number; -}; -type dmp_result = Array<[number, string]>; - -type diff_result = { - left: diff_result_leaf; - right: diff_result_leaf; - diff: dmp_result; -}; -type diff_check_result = boolean | diff_result; - -type Credential = { - username: string; - password: string; -}; - -type EntryDocResponse = EntryDoc & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta; - -//-->Functions. -function arrayBufferToBase64Old(buffer: ArrayBuffer) { - var binary = ""; - var bytes = new Uint8Array(buffer); - var len = bytes.byteLength; - for (var i = 0; i < len; i++) { - binary += String.fromCharCode(bytes[i]); - } - return window.btoa(binary); -} -// Ten times faster. -function arrayBufferToBase64(buffer: ArrayBuffer): Promise { - return new Promise((res) => { - var blob = new Blob([buffer], { type: "application/octet-binary" }); - var reader = new FileReader(); - reader.onload = function (evt) { - var dataurl = evt.target.result.toString(); - res(dataurl.substr(dataurl.indexOf(",") + 1)); - }; - reader.readAsDataURL(blob); - }); -} - -function base64ToArrayBuffer(base64: string): ArrayBuffer { - try { - var binary_string = window.atob(base64); - var len = binary_string.length; - var bytes = new Uint8Array(len); - for (var i = 0; i < len; i++) { - bytes[i] = binary_string.charCodeAt(i); - } - return bytes.buffer; - } catch (ex) { - try { - return new Uint16Array( - [].map.call(base64, function (c: string) { - return c.charCodeAt(0); - }) - ).buffer; - } catch (ex2) { - return null; - } - } -} -function base64ToString(base64: string): string { - try { - var binary_string = window.atob(base64); - var len = binary_string.length; - var bytes = new Uint8Array(len); - for (var i = 0; i < len; i++) { - bytes[i] = binary_string.charCodeAt(i); - } - return new TextDecoder().decode(bytes); - } catch (ex) { - return base64; - } -} - -const escapeStringToHTML = (str: string) => { - if (!str) return; - return str.replace(/[<>&"'`]/g, (match) => { - const escape: any = { - "<": "<", - ">": ">", - "&": "&", - '"': """, - "'": "'", - "`": "`", - }; - return escape[match]; - }); -}; - -function resolveWithIgnoreKnownError(p: Promise, def: T): Promise { - return new Promise((res, rej) => { - p.then(res).catch((ex) => (ex.status && ex.status == 404 ? res(def) : rej(ex))); - }); -} - -const isValidRemoteCouchDBURI = (uri: string): boolean => { - if (uri.startsWith("https://")) return true; - if (uri.startsWith("http://")) return true; - return false; -}; -const connectRemoteCouchDB = async (uri: string, auth: { username: string; password: string }): Promise => { - if (!isValidRemoteCouchDBURI(uri)) return "Remote URI is not valid"; - let db = new PouchDB(uri, { - auth, - }); - try { - let info = await db.info(); - return { db: db, info: info }; - } catch (ex) { - let msg = `${ex.name}:${ex.message}`; - if (ex.name == "TypeError" && ex.message == "Failed to fetch") { - msg += "\n**Note** This error caused by many reasons. The only sure thing is you didn't touch the server.\nTo check details, open inspector."; - } - Logger(ex, LOG_LEVEL.VERBOSE); - return msg; - } -}; -// check the version of remote. -// if remote is higher than current(or specified) version, return false. -const checkRemoteVersion = async (db: PouchDB.Database, migrate: (from: number, to: number) => Promise, barrier: number = VER): Promise => { - try { - let versionInfo = (await db.get(VERSIONINFO_DOCID)) as EntryVersionInfo; - if (versionInfo.type != "versioninfo") { - return false; - } - - let version = versionInfo.version; - if (version < barrier) { - try { - let versionUpResult = await migrate(version, barrier); - if (versionUpResult) { - await bumpRemoteVersion(db); - return true; - } - } catch (ex) { - throw ex; - } - } - if (version == barrier) return true; - return false; - } catch (ex) { - if (ex.status && ex.status == 404) { - if (await bumpRemoteVersion(db)) { - return true; - } - return false; - } - throw ex; - } -}; -const bumpRemoteVersion = async (db: PouchDB.Database, barrier: number = VER): Promise => { - let vi: EntryVersionInfo = { - _id: VERSIONINFO_DOCID, - version: barrier, - type: "versioninfo", - }; - let versionInfo = (await resolveWithIgnoreKnownError(db.get(VERSIONINFO_DOCID), vi)) as EntryVersionInfo; - if (versionInfo.type != "versioninfo") { - return false; - } - vi._rev = versionInfo._rev; - await db.put(vi); - return true; -}; - -function isValidPath(filename: string): boolean { - let regex = /[\u0000-\u001f]|[\\"':?<>|*]/g; - let x = filename.replace(regex, "_"); - let win = /(\\|\/)(COM\d|LPT\d|CON|PRN|AUX|NUL|CLOCK$)($|\.)/gi; - let sx = (x = x.replace(win, "/_")); - return sx == filename; -} - -// For backward compatibility, using the path for determining id. -// Only CouchDB nonacceptable ID (that starts with an underscore) has been prefixed with "/". -// The first slash will be deleted when the path is normalized. -function path2id(filename: string): string { - let x = normalizePath(filename); - if (x.startsWith("_")) x = "/" + x; - return x; -} -function id2path(filename: string): string { - return normalizePath(filename); -} - -// Default Logger. -let Logger: (message: any, levlel?: LOG_LEVEL) => Promise = async (message, _) => { - let timestamp = new Date().toLocaleString(); - let messagecontent = typeof message == "string" ? message : message instanceof Error ? `${message.name}:${message.message}` : JSON.stringify(message, null, 2); - let newmessage = timestamp + "->" + messagecontent; - console.log(newmessage); -}; - -type DatabaseConnectingStatus = "NOT_CONNECTED" | "PAUSED" | "CONNECTED" | "COMPLETED" | "CLOSED" | "ERRORED"; - -// --> Encryption. -//NOTE: I have to split source. -type encodedData = [encryptedData: string, iv: string, salt: string]; -type KeyBuffer = { - index: string; - key: CryptoKey; - salt: Uint8Array; -}; - -let KeyBuffs: KeyBuffer[] = []; - -const KEY_RECYCLE_COUNT = 100; -let recycleCount = KEY_RECYCLE_COUNT; - -async function getKeyForEncrypt(passphrase: string): Promise<[CryptoKey, Uint8Array]> { - // For performance, the plugin reuses the key KEY_RECYCLE_COUNT times. - let f = KeyBuffs.find((e) => e.index == passphrase); - if (f) { - recycleCount--; - if (recycleCount > 0) { - return [f.key, f.salt]; - } - KeyBuffs.remove(f); - recycleCount = KEY_RECYCLE_COUNT; - } - let xpassphrase = new TextEncoder().encode(passphrase); - let digest = await crypto.subtle.digest({ name: "SHA-256" }, xpassphrase); - let keyMaterial = await crypto.subtle.importKey("raw", digest, { name: "PBKDF2" }, false, ["deriveKey"]); - const salt = crypto.getRandomValues(new Uint8Array(16)); - let key = await crypto.subtle.deriveKey( - { - name: "PBKDF2", - salt, - iterations: 100000, - hash: "SHA-256", - }, - keyMaterial, - { name: "AES-GCM", length: 256 }, - false, - ["encrypt"] - ); - KeyBuffs.push({ - index: passphrase, - key, - salt, - }); - while (KeyBuffs.length > 50) { - KeyBuffs.shift(); - } - return [key, salt]; -} - -let decKeyBuffs: KeyBuffer[] = []; - -async function getKeyForDecryption(passphrase: string, salt: Uint8Array): Promise<[CryptoKey, Uint8Array]> { - let bufKey = passphrase + uint8ArrayToHexString(salt); - let f = decKeyBuffs.find((e) => e.index == bufKey); - if (f) { - return [f.key, f.salt]; - } - let xpassphrase = new TextEncoder().encode(passphrase); - let digest = await crypto.subtle.digest({ name: "SHA-256" }, xpassphrase); - let keyMaterial = await crypto.subtle.importKey("raw", digest, { name: "PBKDF2" }, false, ["deriveKey"]); - let key = await crypto.subtle.deriveKey( - { - name: "PBKDF2", - salt, - iterations: 100000, - hash: "SHA-256", - }, - keyMaterial, - { name: "AES-GCM", length: 256 }, - false, - ["decrypt"] - ); - decKeyBuffs.push({ - index: bufKey, - key, - salt, - }); - while (decKeyBuffs.length > 50) { - decKeyBuffs.shift(); - } - return [key, salt]; -} -let semiStaticFieldBuffer: Uint8Array = null; -function getSemiStaticField(reset?: boolean) { - // return fixed field of iv. - if (semiStaticFieldBuffer != null && !reset) { - return semiStaticFieldBuffer; - } - semiStaticFieldBuffer = crypto.getRandomValues(new Uint8Array(12)); - return semiStaticFieldBuffer; -} - -let nonceBuffer: Uint32Array = new Uint32Array(1); -function getNonce() { - // This is nonce, so do not send same thing. - nonceBuffer[0]++; - if (nonceBuffer[0] > 10000) { - // reset semi-static field. - getSemiStaticField(true); - } - return nonceBuffer; -} - -function uint8ArrayToHexString(src: Uint8Array): string { - return Array.from(src) - .map((e: number): string => `00${e.toString(16)}`.slice(-2)) - .join(""); -} -function hexStringToUint8Array(src: string): Uint8Array { - const srcArr = [...src]; - const arr = srcArr.reduce((acc, _, i) => (i % 2 ? acc : [...acc, srcArr.slice(i, i + 2).join("")]), []).map((e) => parseInt(e, 16)); - return Uint8Array.from(arr); -} -async function encrypt(input: string, passphrase: string) { - let key: CryptoKey; - let salt: Uint8Array; - [key, salt] = await getKeyForEncrypt(passphrase); - // Create initial vector with semifixed part and incremental part - // I think it's not good against related-key attacks. - const fixedPart = getSemiStaticField(); - const invocationPart = getNonce(); - const iv = Uint8Array.from([...fixedPart, ...new Uint8Array(invocationPart.buffer)]); - const plainStringified: string = JSON.stringify(input); - const plainStringBuffer: Uint8Array = new TextEncoder().encode(plainStringified); - const encryptedDataArrayBuffer = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, plainStringBuffer); - - const encryptedData = window.btoa(Array.from(new Uint8Array(encryptedDataArrayBuffer), (char) => String.fromCharCode(char)).join("")); - - //return data with iv and salt. - const response: encodedData = [encryptedData, uint8ArrayToHexString(iv), uint8ArrayToHexString(salt)]; - const ret = JSON.stringify(response); - return ret; -} - -async function decrypt(encryptedResult: string, passphrase: string): Promise { - try { - let [encryptedData, ivString, salt]: encodedData = JSON.parse(encryptedResult); - let [key, _] = await getKeyForDecryption(passphrase, hexStringToUint8Array(salt)); - let iv = hexStringToUint8Array(ivString); - // decode base 64, it should increase speed and i should with in MAX_DOC_SIZE_BIN, so it won't OOM. - let encryptedDataBin = window.atob(encryptedData); - let encryptedDataArrayBuffer = Uint8Array.from(encryptedDataBin.split(""), (char) => char.charCodeAt(0)); - let plainStringBuffer: ArrayBuffer = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, encryptedDataArrayBuffer); - let plainStringified = new TextDecoder().decode(plainStringBuffer); - let plain = JSON.parse(plainStringified); - return plain; - } catch (ex) { - Logger("Couldn't decode! You should wrong the passphrases", LOG_LEVEL.VERBOSE); - Logger(ex, LOG_LEVEL.VERBOSE); - throw ex; - } -} - -async function testCrypt() { - let src = "supercalifragilisticexpialidocious"; - let encoded = await encrypt(src, "passwordTest"); - let decrypted = await decrypt(encoded, "passwordTest"); - if (src != decrypted) { - Logger("WARNING! Your device would not support encryption.", LOG_LEVEL.VERBOSE); - return false; - } else { - Logger("CRYPT LOGIC OK", LOG_LEVEL.VERBOSE); - return true; - } -} -// <-- Encryption -const delay = (ms: number): Promise => { - return new Promise((res) => { - setTimeout(() => { - res(); - }, ms); - }); -}; - -//<--Functions -class LocalPouchDB { - auth: Credential; - dbname: string; - settings: ObsidianLiveSyncSettings; - localDatabase: PouchDB.Database; - nodeid: string = ""; - isReady: boolean = false; - - recentModifiedDocs: string[] = []; - h32: (input: string, seed?: number) => string; - h64: (input: string, seedHigh?: number, seedLow?: number) => string; - h32Raw: (input: Uint8Array, seed?: number) => number; - hashCache: { - [key: string]: string; - } = {}; - hashCacheRev: { - [key: string]: string; - } = {}; - - corruptedEntries: { [key: string]: EntryDoc } = {}; - remoteLocked = false; - remoteLockedAndDeviceNotAccepted = false; - - constructor(settings: ObsidianLiveSyncSettings, dbname: string) { - this.auth = { - username: "", - password: "", - }; - this.dbname = dbname; - this.settings = settings; - - // this.initializeDatabase(); - } - close() { - Logger("Database closed (by close)"); - this.isReady = false; - if (this.changeHandler != null) { - this.changeHandler.cancel(); - this.changeHandler.removeAllListeners(); - } - if (this.localDatabase != null) { - this.localDatabase.close(); - } - } - status() { - if (this.syncHandler == null) { - return "connected"; - } - return "disabled"; - } - disposeHashCache() { - this.hashCache = {}; - this.hashCacheRev = {}; - } - - updateRecentModifiedDocs(id: string, rev: string, deleted: boolean) { - let idrev = id + rev; - if (deleted) { - this.recentModifiedDocs = this.recentModifiedDocs.filter((e) => e != idrev); - } else { - this.recentModifiedDocs.push(idrev); - this.recentModifiedDocs = this.recentModifiedDocs.slice(0 - RECENT_MOFIDIED_DOCS_QTY); - } - } - isSelfModified(id: string, rev: string): boolean { - let idrev = id + rev; - return this.recentModifiedDocs.indexOf(idrev) !== -1; - } - - changeHandler: PouchDB.Core.Changes<{}> = null; - async initializeDatabase() { - await this.prepareHashFunctions(); - if (this.localDatabase != null) this.localDatabase.close(); - if (this.changeHandler != null) { - this.changeHandler.cancel(); - } - this.localDatabase = null; - this.localDatabase = new PouchDB(this.dbname + "-livesync", { - auto_compaction: true, - revs_limit: 100, - deterministic_revs: true, - }); - - Logger("Database Info"); - Logger(await this.localDatabase.info(), LOG_LEVEL.VERBOSE); - // initialize local node information. - let nodeinfo: EntryNodeInfo = await resolveWithIgnoreKnownError(this.localDatabase.get(NODEINFO_DOCID), { - _id: NODEINFO_DOCID, - type: "nodeinfo", - nodeid: "", - }); - if (nodeinfo.nodeid == "") { - nodeinfo.nodeid = Math.random().toString(36).slice(-10); - await this.localDatabase.put(nodeinfo); - } - this.localDatabase.on("close", () => { - Logger("Database closed."); - this.isReady = false; - }); - this.nodeid = nodeinfo.nodeid; - - // Traceing the leaf id - let changes = this.localDatabase - .changes({ - since: "now", - live: true, - filter: (doc) => doc.type == "leaf", - }) - .on("change", (e) => { - if (e.deleted) return; - this.leafArrived(e.id); - this.docSeq = `${e.seq}`; - }); - this.changeHandler = changes; - this.isReady = true; - Logger("Database is now ready."); - } - - async prepareHashFunctions() { - if (this.h32 != null) return; - const { h32, h64, h32Raw } = await xxhash(); - this.h32 = h32; - this.h64 = h64; - this.h32Raw = h32Raw; - } - - // leaf waiting - leafArrivedCallbacks: { [key: string]: (() => void)[] } = {}; - - leafArrived(id: string) { - if (typeof this.leafArrivedCallbacks[id] !== "undefined") { - for (let func of this.leafArrivedCallbacks[id]) { - func(); - } - delete this.leafArrivedCallbacks[id]; - } - } - // wait - waitForLeafReady(id: string): Promise { - return new Promise((res, rej) => { - // Set timeout. - let timer = setTimeout(() => rej(new Error(`Leaf timed out:${id}`)), LEAF_WAIT_TIMEOUT); - if (typeof this.leafArrivedCallbacks[id] == "undefined") { - this.leafArrivedCallbacks[id] = []; - } - this.leafArrivedCallbacks[id].push(() => { - clearTimeout(timer); - res(true); - }); - }); - } - - async getDBLeaf(id: string, waitForReady: boolean): Promise { - await this.waitForGCComplete(); - // when in cache, use that. - if (this.hashCacheRev[id]) { - return this.hashCacheRev[id]; - } - try { - let w = await this.localDatabase.get(id); - if (w.type == "leaf") { - if (id.startsWith("h:+")) { - try { - w.data = await decrypt(w.data, this.settings.passphrase); - } catch (e) { - Logger("The element of the document has been encrypted, but decryption failed.", LOG_LEVEL.NOTICE); - throw e; - } - } - this.hashCache[w.data] = id; - this.hashCacheRev[id] = w.data; - return w.data; - } - throw new Error(`retrive leaf, but it was not leaf.`); - } catch (ex) { - if (ex.status && ex.status == 404 && waitForReady) { - // just leaf is not ready. - // wait for on - if ((await this.waitForLeafReady(id)) === false) { - throw new Error(`time out (waiting leaf)`); - } - try { - // retrive again. - let w = await this.localDatabase.get(id); - if (w.type == "leaf") { - if (id.startsWith("h:+")) { - try { - w.data = await decrypt(w.data, this.settings.passphrase); - } catch (e) { - Logger("The element of the document has been encrypted, but decryption failed.", LOG_LEVEL.NOTICE); - throw e; - } - } - this.hashCache[w.data] = id; - this.hashCacheRev[id] = w.data; - return w.data; - } - throw new Error(`retrive leaf, but it was not leaf.`); - } catch (ex) { - if (ex.status && ex.status == 404) { - throw new Error("leaf is not found"); - } - Logger(`Something went wrong on retriving leaf`); - throw ex; - } - } else { - Logger(`Something went wrong on retriving leaf`); - throw ex; - } - } - } - - async getDBEntryMeta(path: string, opt?: PouchDB.Core.GetOptions): Promise { - await this.waitForGCComplete(); - let id = path2id(path); - try { - let obj: EntryDocResponse = null; - if (opt) { - obj = await this.localDatabase.get(id, opt); - } else { - obj = await this.localDatabase.get(id); - } - - if (obj.type && obj.type == "leaf") { - //do nothing for leaf; - return false; - } - - // retrieve metadata only - if (!obj.type || (obj.type && obj.type == "notes") || obj.type == "newnote" || obj.type == "plain") { - let note = obj as Entry; - let children: string[] = []; - if (obj.type == "newnote" || obj.type == "plain") { - children = obj.children; - } - let doc: LoadedEntry & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta = { - data: "", - _id: note._id, - ctime: note.ctime, - mtime: note.mtime, - size: note.size, - _deleted: obj._deleted, - _rev: obj._rev, - _conflicts: obj._conflicts, - children: children, - datatype: "newnote", - }; - return doc; - } - } catch (ex) { - if (ex.status && ex.status == 404) { - return false; - } - throw ex; - } - return false; - } - async getDBEntry(path: string, opt?: PouchDB.Core.GetOptions, dump = false, waitForReady = true): Promise { - await this.waitForGCComplete(); - let id = path2id(path); - try { - let obj: EntryDocResponse = null; - if (opt) { - obj = await this.localDatabase.get(id, opt); - } else { - obj = await this.localDatabase.get(id); - } - - if (obj.type && obj.type == "leaf") { - //do nothing for leaf; - return false; - } - - //Check it out and fix docs to regular case - if (!obj.type || (obj.type && obj.type == "notes")) { - let note = obj as Entry; - let doc: LoadedEntry & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta = { - data: note.data, - _id: note._id, - ctime: note.ctime, - mtime: note.mtime, - size: note.size, - _deleted: obj._deleted, - _rev: obj._rev, - _conflicts: obj._conflicts, - children: [], - datatype: "newnote", - }; - if (typeof this.corruptedEntries[doc._id] != "undefined") { - delete this.corruptedEntries[doc._id]; - } - if (dump) { - Logger(`Simple doc`); - Logger(doc); - } - - return doc; - // simple note - } - if (obj.type == "newnote" || obj.type == "plain") { - // search childrens - try { - if (dump) { - Logger(`Enhanced doc`); - Logger(obj); - } - let childrens: string[]; - try { - childrens = await Promise.all(obj.children.map((e) => this.getDBLeaf(e, waitForReady))); - if (dump) { - Logger(`childrens:`); - Logger(childrens); - } - } catch (ex) { - Logger(`Something went wrong on reading elements of ${obj._id} from database.`, LOG_LEVEL.NOTICE); - Logger(ex, LOG_LEVEL.VERBOSE); - this.corruptedEntries[obj._id] = obj; - return false; - } - let data = childrens.join(""); - let doc: LoadedEntry & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta = { - data: data, - _id: obj._id, - ctime: obj.ctime, - mtime: obj.mtime, - size: obj.size, - _deleted: obj._deleted, - _rev: obj._rev, - children: obj.children, - datatype: obj.type, - _conflicts: obj._conflicts, - }; - if (dump) { - Logger(`therefore:`); - Logger(doc); - } - if (typeof this.corruptedEntries[doc._id] != "undefined") { - delete this.corruptedEntries[doc._id]; - } - return doc; - } catch (ex) { - if (ex.status && ex.status == 404) { - Logger(`Missing document content!, could not read ${obj._id} from database.`, LOG_LEVEL.NOTICE); - return false; - } - Logger(`Something went wrong on reading ${obj._id} from database.`, LOG_LEVEL.NOTICE); - Logger(ex); - } - } - } catch (ex) { - if (ex.status && ex.status == 404) { - return false; - } - throw ex; - } - return false; - } - async deleteDBEntry(path: string, opt?: PouchDB.Core.GetOptions): Promise { - await this.waitForGCComplete(); - let id = path2id(path); - try { - let obj: EntryDocResponse = null; - if (opt) { - obj = await this.localDatabase.get(id, opt); - } else { - obj = await this.localDatabase.get(id); - } - - if (obj.type && obj.type == "leaf") { - //do nothing for leaf; - return false; - } - //Check it out and fix docs to regular case - if (!obj.type || (obj.type && obj.type == "notes")) { - obj._deleted = true; - let r = await this.localDatabase.put(obj); - this.updateRecentModifiedDocs(r.id, r.rev, true); - if (typeof this.corruptedEntries[obj._id] != "undefined") { - delete this.corruptedEntries[obj._id]; - } - return true; - // simple note - } - if (obj.type == "newnote" || obj.type == "plain") { - obj._deleted = true; - let r = await this.localDatabase.put(obj); - Logger(`entry removed:${obj._id}-${r.rev}`); - this.updateRecentModifiedDocs(r.id, r.rev, true); - if (typeof this.corruptedEntries[obj._id] != "undefined") { - delete this.corruptedEntries[obj._id]; - } - return true; - } - } catch (ex) { - if (ex.status && ex.status == 404) { - return false; - } - throw ex; - } - } - async deleteDBEntryPrefix(prefixSrc: string): Promise { - await this.waitForGCComplete(); - // delete database entries by prefix. - // it called from folder deletion. - let c = 0; - let readCount = 0; - let delDocs: string[] = []; - let prefix = path2id(prefixSrc); - do { - let result = await this.localDatabase.allDocs({ include_docs: false, skip: c, limit: 100, conflicts: true }); - readCount = result.rows.length; - if (readCount > 0) { - //there are some result - for (let v of result.rows) { - // let doc = v.doc; - if (v.id.startsWith(prefix) || v.id.startsWith("/" + prefix)) { - delDocs.push(v.id); - // console.log("!" + v.id); - } else { - if (!v.id.startsWith("h:")) { - // console.log("?" + v.id); - } - } - } - } - c += readCount; - } while (readCount != 0); - // items collected. - //bulk docs to delete? - let deleteCount = 0; - let notfound = 0; - for (let v of delDocs) { - try { - let item = await this.localDatabase.get(v); - item._deleted = true; - await this.localDatabase.put(item); - this.updateRecentModifiedDocs(item._id, item._rev, true); - deleteCount++; - } catch (ex) { - if (ex.status && ex.status == 404) { - notfound++; - // NO OP. It should be timing problem. - } else { - throw ex; - } - } - } - Logger(`deleteDBEntryPrefix:deleted ${deleteCount} items, skipped ${notfound}`); - return true; - } - isPlainText(filename: string): boolean { - if (filename.endsWith(".md")) return true; - if (filename.endsWith(".txt")) return true; - if (filename.endsWith(".svg")) return true; - if (filename.endsWith(".html")) return true; - if (filename.endsWith(".csv")) return true; - if (filename.endsWith(".css")) return true; - if (filename.endsWith(".js")) return true; - if (filename.endsWith(".xml")) return true; - - return false; - } - async putDBEntry(note: LoadedEntry) { - await this.waitForGCComplete(); - let leftData = note.data; - let savenNotes = []; - let processed = 0; - let made = 0; - let skiped = 0; - let pieceSize = MAX_DOC_SIZE_BIN; - let plainSplit = false; - let cacheUsed = 0; - let userpasswordHash = this.h32Raw(new TextEncoder().encode(this.settings.passphrase)); - if (this.isPlainText(note._id)) { - pieceSize = MAX_DOC_SIZE; - plainSplit = true; - } - let newLeafs: EntryLeaf[] = []; - do { - // To keep low bandwith and database size, - // Dedup pieces on database. - // from 0.1.10, for best performance. we use markdown delimiters - // 1. \n[^\n]{longLineThreshold}[^\n]*\n -> long sentence shuld break. - // 2. \n\n shold break - // 3. \r\n\r\n should break - // 4. \n# should break. - let cPieceSize = pieceSize; - if (plainSplit) { - let minimumChunkSize = this.settings.minimumChunkSize; - if (minimumChunkSize < 10) minimumChunkSize = 10; - let longLineThreshold = this.settings.longLineThreshold; - if (longLineThreshold < 100) longLineThreshold = 100; - cPieceSize = 0; - // lookup for next splittion . - // we're standing on "\n" - // debugger - do { - let n1 = leftData.indexOf("\n", cPieceSize + 1); - let n2 = leftData.indexOf("\n\n", cPieceSize + 1); - let n3 = leftData.indexOf("\r\n\r\n", cPieceSize + 1); - let n4 = leftData.indexOf("\n#", cPieceSize + 1); - if (n1 == -1 && n2 == -1 && n3 == -1 && n4 == -1) { - cPieceSize = MAX_DOC_SIZE; - break; - } - - if (n1 > longLineThreshold) { - // long sentence is an established piece - cPieceSize = n1; - } else { - // cPieceSize = Math.min.apply([n2, n3, n4].filter((e) => e > 1)); - // ^ heavy. - if (n1 > 0 && cPieceSize < n1) cPieceSize = n1; - if (n2 > 0 && cPieceSize < n2) cPieceSize = n2 + 1; - if (n3 > 0 && cPieceSize < n3) cPieceSize = n3 + 3; - // Choose shorter, empty line and \n# - if (n4 > 0 && cPieceSize > n4) cPieceSize = n4 + 0; - cPieceSize++; - } - } while (cPieceSize < minimumChunkSize); - } - - // piece size determined. - let piece = leftData.substring(0, cPieceSize); - leftData = leftData.substring(cPieceSize); - processed++; - let leafid = ""; - // Get hash of piece. - let hashedPiece: string = ""; - let hashQ: number = 0; // if hash collided, **IF**, count it up. - let tryNextHash = false; - let needMake = true; - if (typeof this.hashCache[piece] !== "undefined") { - hashedPiece = ""; - leafid = this.hashCache[piece]; - needMake = false; - skiped++; - cacheUsed++; - } else { - if (this.settings.encrypt) { - // When encryption has been enabled, make hash to be different between each passphrase to avoid inferring password. - hashedPiece = "+" + (this.h32Raw(new TextEncoder().encode(piece)) ^ userpasswordHash).toString(16); - } else { - hashedPiece = this.h32(piece); - } - leafid = "h:" + hashedPiece; - do { - let nleafid = leafid; - try { - nleafid = `${leafid}${hashQ}`; - let pieceData = await this.localDatabase.get(nleafid); - //try decode - if (pieceData._id.startsWith("h:+")) { - try { - pieceData.data = await decrypt(pieceData.data, this.settings.passphrase); - } catch (e) { - Logger("Decode failed !"); - throw e; - } - } - if (pieceData.type == "leaf" && pieceData.data == piece) { - leafid = nleafid; - needMake = false; - tryNextHash = false; - this.hashCache[piece] = leafid; - this.hashCacheRev[leafid] = piece; - } else if (pieceData.type == "leaf") { - Logger("hash:collision!!"); - hashQ++; - tryNextHash = true; - } else { - leafid = nleafid; - tryNextHash = false; - } - } catch (ex) { - if (ex.status && ex.status == 404) { - //not found, we can use it. - leafid = nleafid; - needMake = true; - tryNextHash = false; - } else { - needMake = false; - tryNextHash = false; - throw ex; - } - } - } while (tryNextHash); - if (needMake) { - //have to make - let savePiece = piece; - if (this.settings.encrypt) { - let passphrase = this.settings.passphrase; - savePiece = await encrypt(piece, passphrase); - } - let d: EntryLeaf = { - _id: leafid, - data: savePiece, - type: "leaf", - }; - newLeafs.push(d); - this.hashCache[piece] = leafid; - this.hashCacheRev[leafid] = piece; - made++; - } else { - skiped++; - } - } - savenNotes.push(leafid); - } while (leftData != ""); - let saved = true; - if (newLeafs.length > 0) { - try { - let result = await this.localDatabase.bulkDocs(newLeafs); - for (let item of result) { - if ((item as any).ok) { - this.updateRecentModifiedDocs(item.id, item.rev, false); - Logger(`save ok:id:${item.id} rev:${item.rev}`, LOG_LEVEL.VERBOSE); - } else { - if ((item as any).status && (item as any).status == 409) { - // conflicted, but it would be ok in childrens. - } else { - Logger(`save failed:id:${item.id} rev:${item.rev}`, LOG_LEVEL.NOTICE); - Logger(item); - // this.disposeHashCache(); - saved = false; - } - } - } - } catch (ex) { - Logger("ERROR ON SAVING LEAVES "); - Logger(ex); - saved = false; - } - } - if (saved) { - Logger(`note content saven, pieces:${processed} new:${made}, skip:${skiped}, cache:${cacheUsed}`); - let newDoc: PlainEntry | NewEntry = { - NewNote: true, - children: savenNotes, - _id: note._id, - ctime: note.ctime, - mtime: note.mtime, - size: note.size, - type: plainSplit ? "plain" : "newnote", - }; - // Here for upsert logic, - try { - let old = await this.localDatabase.get(newDoc._id); - if (!old.type || old.type == "notes" || old.type == "newnote" || old.type == "plain") { - // simple use rev for new doc - newDoc._rev = old._rev; - } - } catch (ex) { - if (ex.status && ex.status == 404) { - // NO OP/ - } else { - throw ex; - } - } - let r = await this.localDatabase.put(newDoc, { force: true }); - this.updateRecentModifiedDocs(r.id, r.rev, newDoc._deleted); - if (typeof this.corruptedEntries[note._id] != "undefined") { - delete this.corruptedEntries[note._id]; - } - Logger(`note saven:${newDoc._id}:${r.rev}`); - } else { - Logger(`note coud not saved:${note._id}`); - } - } - - syncHandler: PouchDB.Replication.Sync<{}> = null; - syncStatus: DatabaseConnectingStatus = "NOT_CONNECTED"; - docArrived: number = 0; - docSent: number = 0; - docSeq: string = ""; - updateInfo: () => void = () => { - console.log("default updinfo"); - }; - async migrate(from: number, to: number): Promise { - Logger(`Database updated from ${from} to ${to}`, LOG_LEVEL.NOTICE); - // no op now, - return true; - } - replicateAllToServer(setting: ObsidianLiveSyncSettings, showingNotice?: boolean) { - return new Promise(async (res, rej) => { - await this.waitForGCComplete(); - this.closeReplication(); - Logger("send all data to server", LOG_LEVEL.NOTICE); - let notice: Notice = null; - if (showingNotice) { - notice = new Notice("Initializing", 0); - } - this.syncStatus = "CLOSED"; - this.updateInfo(); - let uri = setting.couchDB_URI + (setting.couchDB_DBNAME == "" ? "" : "/" + setting.couchDB_DBNAME); - let auth: Credential = { - username: setting.couchDB_USER, - password: setting.couchDB_PASSWORD, - }; - let dbret = await connectRemoteCouchDB(uri, auth); - if (typeof dbret === "string") { - Logger(`could not connect to ${uri}:${dbret}`, LOG_LEVEL.NOTICE); - if (notice != null) notice.hide(); - return rej(`could not connect to ${uri}:${dbret}`); - } - - let syncOptionBase: PouchDB.Replication.SyncOptions = { - batch_size: 250, - batches_limit: 40, - }; - - let db = dbret.db; - let totalCount = (await this.localDatabase.info()).doc_count; - //replicate once - let replicate = this.localDatabase.replicate.to(db, syncOptionBase); - replicate - .on("active", () => { - this.syncStatus = "CONNECTED"; - this.updateInfo(); - if (notice) { - notice.setMessage("CONNECTED"); - } - }) - .on("change", async (e) => { - // no op. - this.docSent += e.docs.length; - this.updateInfo(); - notice.setMessage(`SENDING:${e.docs_written}/${totalCount}`); - Logger(`replicateAllToServer: sending..:${e.docs.length}`); - }) - .on("complete", async (info) => { - this.syncStatus = "COMPLETED"; - this.updateInfo(); - Logger("replicateAllToServer: Completed", LOG_LEVEL.NOTICE); - replicate.cancel(); - replicate.removeAllListeners(); - if (notice != null) notice.hide(); - res(true); - }) - .on("error", (e) => { - this.syncStatus = "ERRORED"; - this.updateInfo(); - Logger("replicateAllToServer: Pulling Replication error", LOG_LEVEL.INFO); - Logger(e); - replicate.cancel(); - replicate.removeAllListeners(); - if (notice != null) notice.hide(); - rej(e); - }); - }); - } - - async openReplication(setting: ObsidianLiveSyncSettings, keepAlive: boolean, showResult: boolean, callback: (e: PouchDB.Core.ExistingDocument<{}>[]) => Promise) { - if (!this.isReady) { - Logger("Database is not ready."); - return false; - } - - await this.waitForGCComplete(); - if (setting.versionUpFlash != "") { - new Notice("Open settings and check message, please."); - return; - } - let uri = setting.couchDB_URI + (setting.couchDB_DBNAME == "" ? "" : "/" + setting.couchDB_DBNAME); - let auth: Credential = { - username: setting.couchDB_USER, - password: setting.couchDB_PASSWORD, - }; - if (this.syncHandler != null) { - Logger("Another replication running."); - return false; - } - let dbret = await connectRemoteCouchDB(uri, auth); - if (typeof dbret === "string") { - Logger(`could not connect to ${uri}:${dbret}`, LOG_LEVEL.NOTICE); - return; - } - - if (!(await checkRemoteVersion(dbret.db, this.migrate.bind(this), VER))) { - Logger("Remote database is newer or corrupted, make sure to latest version of self-hosted-livesync installed", LOG_LEVEL.NOTICE); - return; - } - - let defMilestonePoint: EntryMilestoneInfo = { - _id: MILSTONE_DOCID, - type: "milestoneinfo", - created: (new Date() as any) / 1, - locked: false, - accepted_nodes: [this.nodeid], - }; - - let remoteMilestone: EntryMilestoneInfo = await resolveWithIgnoreKnownError(dbret.db.get(MILSTONE_DOCID), defMilestonePoint); - this.remoteLocked = remoteMilestone.locked; - this.remoteLockedAndDeviceNotAccepted = remoteMilestone.locked && remoteMilestone.accepted_nodes.indexOf(this.nodeid) == -1; - - if (remoteMilestone.locked && remoteMilestone.accepted_nodes.indexOf(this.nodeid) == -1) { - Logger("Remote database marked as 'Auto Sync Locked'. And this devide does not marked as resolved device. see settings dialog.", LOG_LEVEL.NOTICE); - return; - } - if (typeof remoteMilestone._rev == "undefined") { - await dbret.db.put(remoteMilestone); - } - - let syncOptionBase: PouchDB.Replication.SyncOptions = { - batch_size: 250, - batches_limit: 40, - }; - let syncOption: PouchDB.Replication.SyncOptions = keepAlive ? { live: true, retry: true, heartbeat: 30000, ...syncOptionBase } : { ...syncOptionBase }; - let notice: Notice = null; - if (showResult) { - notice = new Notice("Replicating", 0); - } - let db = dbret.db; - //replicate once - this.syncStatus = "CONNECTED"; - Logger("Pull before replicate."); - Logger(await this.localDatabase.info(), LOG_LEVEL.VERBOSE); - Logger(await db.info(), LOG_LEVEL.VERBOSE); - let replicate = this.localDatabase.replicate.from(db, syncOptionBase); - replicate - .on("active", () => { - this.syncStatus = "CONNECTED"; - this.updateInfo(); - Logger("Replication pull activated."); - }) - .on("change", async (e) => { - // when in first run, replication will send us tombstone data - // and in normal cases, all leavs should sent before the entry that contains these item. - // so skip to completed all, we should treat all changes. - try { - callback(e.docs); - this.docArrived += e.docs.length; - this.updateInfo(); - Logger(`pulled ${e.docs.length} doc(s)`); - if (notice != null) { - notice.setMessage(`Replication pulled:${e.docs_read}`); - } - } catch (ex) { - Logger("Replication callback error"); - Logger(ex); - } - }) - .on("complete", async (info) => { - this.syncStatus = "COMPLETED"; - this.updateInfo(); - replicate.cancel(); - replicate.removeAllListeners(); - this.syncHandler = null; - if (this.syncHandler != null) { - this.syncHandler.cancel(); - this.syncHandler.removeAllListeners(); - } - Logger("Replication pull completed."); - this.syncHandler = this.localDatabase.sync(db, syncOption); - this.syncHandler - .on("active", () => { - this.syncStatus = "CONNECTED"; - this.updateInfo(); - Logger("Replication activated"); - }) - .on("change", async (e) => { - try { - if (e.direction == "pull") { - callback(e.change.docs); - Logger(`replicated ${e.change.docs_read} doc(s)`); - this.docArrived += e.change.docs.length; - } else { - this.docSent += e.change.docs.length; - } - if (notice != null) { - notice.setMessage(`↑${e.change.docs_written} ↓${e.change.docs_read}`); - } - this.updateInfo(); - } catch (ex) { - Logger("Replication callback error"); - Logger(ex); - } - }) - .on("complete", (e) => { - this.syncStatus = "COMPLETED"; - this.updateInfo(); - Logger("Replication completed", showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO); - this.syncHandler = null; - if (notice != null) notice.hide(); - }) - .on("denied", (e) => { - this.syncStatus = "ERRORED"; - this.updateInfo(); - if (notice != null) notice.hide(); - Logger("Replication denied", LOG_LEVEL.NOTICE); - // Logger(e); - }) - .on("error", (e) => { - this.syncStatus = "ERRORED"; - this.updateInfo(); - if (notice != null) notice.hide(); - Logger("Replication error", LOG_LEVEL.NOTICE); - // Logger(e); - }) - .on("paused", (e) => { - this.syncStatus = "PAUSED"; - this.updateInfo(); - if (notice != null) notice.hide(); - Logger("replication paused", LOG_LEVEL.VERBOSE); - // Logger(e); - }); - }) - .on("error", (e) => { - this.syncStatus = "ERRORED"; - this.updateInfo(); - Logger("Pulling Replication error", LOG_LEVEL.INFO); - replicate.cancel(); - replicate.removeAllListeners(); - this.syncHandler.cancel(); - this.syncHandler.removeAllListeners(); - this.syncHandler = null; - if (notice != null) notice.hide(); - // debugger; - Logger(e); - }); - } - - closeReplication() { - if (this.syncHandler == null) { - return; - } - this.syncStatus = "CLOSED"; - this.updateInfo(); - this.syncHandler.cancel(); - this.syncHandler.removeAllListeners(); - this.syncHandler = null; - Logger("Replication closed"); - } - - async resetDatabase() { - await this.waitForGCComplete(); - if (this.changeHandler != null) { - this.changeHandler.removeAllListeners(); - this.changeHandler.cancel(); - } - await this.closeReplication(); - Logger("Database closed for reset Database."); - this.isReady = false; - await this.localDatabase.destroy(); - this.localDatabase = null; - await this.initializeDatabase(); - this.disposeHashCache(); - Logger("Local Database Reset", LOG_LEVEL.NOTICE); - } - async tryResetRemoteDatabase(setting: ObsidianLiveSyncSettings) { - await this.closeReplication(); - let uri = setting.couchDB_URI + (setting.couchDB_DBNAME == "" ? "" : "/" + setting.couchDB_DBNAME); - let auth: Credential = { - username: setting.couchDB_USER, - password: setting.couchDB_PASSWORD, - }; - let con = await connectRemoteCouchDB(uri, auth); - if (typeof con == "string") return; - try { - await con.db.destroy(); - Logger("Remote Database Destroyed", LOG_LEVEL.NOTICE); - await this.tryCreateRemoteDatabase(setting); - } catch (ex) { - Logger("something happend on Remote Database Destory", LOG_LEVEL.NOTICE); - } - } - async tryCreateRemoteDatabase(setting: ObsidianLiveSyncSettings) { - await this.closeReplication(); - let uri = setting.couchDB_URI + (setting.couchDB_DBNAME == "" ? "" : "/" + setting.couchDB_DBNAME); - let auth: Credential = { - username: setting.couchDB_USER, - password: setting.couchDB_PASSWORD, - }; - let con2 = await connectRemoteCouchDB(uri, auth); - if (typeof con2 === "string") return; - Logger("Remote Database Created or Connected", LOG_LEVEL.NOTICE); - } - async markRemoteLocked(setting: ObsidianLiveSyncSettings, locked: boolean) { - let uri = setting.couchDB_URI + (setting.couchDB_DBNAME == "" ? "" : "/" + setting.couchDB_DBNAME); - let auth: Credential = { - username: setting.couchDB_USER, - password: setting.couchDB_PASSWORD, - }; - let dbret = await connectRemoteCouchDB(uri, auth); - if (typeof dbret === "string") { - Logger(`could not connect to ${uri}:${dbret}`, LOG_LEVEL.NOTICE); - return; - } - - if (!(await checkRemoteVersion(dbret.db, this.migrate.bind(this), VER))) { - Logger("Remote database is newer or corrupted, make sure to latest version of self-hosted-livesync installed", LOG_LEVEL.NOTICE); - return; - } - let defInitPoint: EntryMilestoneInfo = { - _id: MILSTONE_DOCID, - type: "milestoneinfo", - created: (new Date() as any) / 1, - locked: locked, - accepted_nodes: [this.nodeid], - }; - - let remoteMilestone: EntryMilestoneInfo = await resolveWithIgnoreKnownError(dbret.db.get(MILSTONE_DOCID), defInitPoint); - remoteMilestone.accepted_nodes = [this.nodeid]; - remoteMilestone.locked = locked; - if (locked) { - Logger("Lock remote database to prevent data corruption", LOG_LEVEL.NOTICE); - } else { - Logger("Unlock remote database to prevent data corruption", LOG_LEVEL.NOTICE); - } - await dbret.db.put(remoteMilestone); - } - async markRemoteResolved(setting: ObsidianLiveSyncSettings) { - let uri = setting.couchDB_URI + (setting.couchDB_DBNAME == "" ? "" : "/" + setting.couchDB_DBNAME); - let auth: Credential = { - username: setting.couchDB_USER, - password: setting.couchDB_PASSWORD, - }; - let dbret = await connectRemoteCouchDB(uri, auth); - if (typeof dbret === "string") { - Logger(`could not connect to ${uri}:${dbret}`, LOG_LEVEL.NOTICE); - return; - } - - if (!(await checkRemoteVersion(dbret.db, this.migrate.bind(this), VER))) { - Logger("Remote database is newer or corrupted, make sure to latest version of self-hosted-livesync installed", LOG_LEVEL.NOTICE); - return; - } - let defInitPoint: EntryMilestoneInfo = { - _id: MILSTONE_DOCID, - type: "milestoneinfo", - created: (new Date() as any) / 1, - locked: false, - accepted_nodes: [this.nodeid], - }; - // check local database hash status and remote replicate hash status - let remoteMilestone: EntryMilestoneInfo = await resolveWithIgnoreKnownError(dbret.db.get(MILSTONE_DOCID), defInitPoint); - // remoteMilestone.locked = false; - remoteMilestone.accepted_nodes = Array.from(new Set([...remoteMilestone.accepted_nodes, this.nodeid])); - // this.remoteLocked = false; - Logger("Mark this device as 'resolved'.", LOG_LEVEL.NOTICE); - await dbret.db.put(remoteMilestone); - } - gcRunning = false; - async waitForGCComplete() { - while (this.gcRunning) { - Logger("Waiting for Garbage Collection completed."); - await delay(1000); - } - } - async garbageCollect() { - if (this.gcRunning) return; - this.gcRunning = true; - try { - // get all documents of NewEntry2 - // we don't use queries , just use allDocs(); - this.disposeHashCache(); - let c = 0; - let readCount = 0; - let hashPieces: string[] = []; - let usedPieces: string[] = []; - Logger("Collecting Garbage"); - do { - let result = await this.localDatabase.allDocs({ include_docs: true, skip: c, limit: 500, conflicts: true }); - readCount = result.rows.length; - Logger("checked:" + readCount); - if (readCount > 0) { - //there are some result - for (let v of result.rows) { - let doc = v.doc; - if (doc.type == "newnote" || doc.type == "plain") { - // used pieces memo. - usedPieces = Array.from(new Set([...usedPieces, ...doc.children])); - if (doc._conflicts) { - for (let cid of doc._conflicts) { - let p = await this.localDatabase.get(doc._id, { rev: cid }); - if (p.type == "newnote" || p.type == "plain") { - usedPieces = Array.from(new Set([...usedPieces, ...p.children])); - } - } - } - } - if (doc.type == "leaf") { - // all pieces. - hashPieces = Array.from(new Set([...hashPieces, doc._id])); - } - } - } - c += readCount; - } while (readCount != 0); - // items collected. - Logger("Finding unused pieces"); - this.disposeHashCache(); - const garbages = hashPieces.filter((e) => usedPieces.indexOf(e) == -1); - let deleteCount = 0; - Logger("we have to delete:" + garbages.length); - let deleteDoc: EntryDoc[] = []; - for (let v of garbages) { - try { - let item = await this.localDatabase.get(v); - item._deleted = true; - deleteDoc.push(item); - if (deleteDoc.length > 50) { - await this.localDatabase.bulkDocs(deleteDoc); - deleteDoc = []; - Logger("delete:" + deleteCount); - } - deleteCount++; - } catch (ex) { - if (ex.status && ex.status == 404) { - // NO OP. It should be timing problem. - } else { - throw ex; - } - } - } - if (deleteDoc.length > 0) { - await this.localDatabase.bulkDocs(deleteDoc); - } - Logger(`GC:deleted ${deleteCount} items.`); - } finally { - this.gcRunning = false; - } - this.disposeHashCache(); - } -} - -export default class ObsidianLiveSyncPlugin extends Plugin { - settings: ObsidianLiveSyncSettings; - localDatabase: LocalPouchDB; - logMessage: string[] = []; - statusBar: HTMLElement; - statusBar2: HTMLElement; - suspended: boolean; - - async onload() { - Logger = this.addLog.bind(this); // Logger moved to global. - Logger("loading plugin"); - const lsname = "obsidian-live-sync-ver" + this.app.vault.getName(); - const last_version = localStorage.getItem(lsname); - await this.loadSettings(); - if (!last_version || Number(last_version) < VER) { - this.settings.liveSync = false; - this.settings.syncOnSave = false; - this.settings.syncOnStart = false; - this.settings.syncOnFileOpen = false; - this.settings.periodicReplication = false; - this.settings.versionUpFlash = "I changed specifications incompatiblly, so when you enable sync again, be sure to made version up all nother devides."; - this.saveSettings(); - } - localStorage.setItem(lsname, `${VER}`); - await this.openDatabase(); - - addIcon( - "replicate", - ` - - - - - ` - ); - addIcon( - "view-log", - ` - - - ` - ); - this.addRibbonIcon("replicate", "Replicate", async () => { - await this.replicate(true); - }); - - let x = this.addRibbonIcon("view-log", "Show log", () => { - new LogDisplayModal(this.app, this).open(); - }); - - this.statusBar = this.addStatusBarItem(); - this.statusBar.addClass("syncstatusbar"); - this.refreshStatusText = this.refreshStatusText.bind(this); - - this.statusBar2 = this.addStatusBarItem(); - // this.watchVaultChange = debounce(this.watchVaultChange.bind(this), delay, false); - // this.watchVaultDelete = debounce(this.watchVaultDelete.bind(this), delay, false); - // this.watchVaultRename = debounce(this.watchVaultRename.bind(this), delay, false); - - this.watchVaultChange = this.watchVaultChange.bind(this); - this.watchVaultCreate = this.watchVaultCreate.bind(this); - this.watchVaultDelete = this.watchVaultDelete.bind(this); - this.watchVaultRename = this.watchVaultRename.bind(this); - this.watchWorkspaceOpen = debounce(this.watchWorkspaceOpen.bind(this), 1000, false); - this.watchWindowVisiblity = debounce(this.watchWindowVisiblity.bind(this), 1000, false); - - this.parseReplicationResult = this.parseReplicationResult.bind(this); - - this.periodicSync = this.periodicSync.bind(this); - this.setPeriodicSync = this.setPeriodicSync.bind(this); - - // this.registerWatchEvents(); - this.addSettingTab(new ObsidianLiveSyncSettingTab(this.app, this)); - - this.app.workspace.onLayoutReady(async () => { - try { - await this.initializeDatabase(); - await this.realizeSettingSyncMode(); - this.registerWatchEvents(); - } catch (ex) { - Logger("Error while loading Self-hosted LiveSync", LOG_LEVEL.NOTICE); - Logger(ex, LOG_LEVEL.VERBOSE); - } - }); - this.addCommand({ - id: "livesync-replicate", - name: "Replicate now", - callback: () => { - this.replicate(); - }, - }); - this.addCommand({ - id: "livesync-dump", - name: "Dump informations of this doc ", - editorCallback: (editor: Editor, view: MarkdownView) => { - //this.replicate(); - this.localDatabase.getDBEntry(view.file.path, {}, true, false); - }, - }); - this.addCommand({ - id: "livesync-gc", - name: "garbage collect now", - callback: () => { - this.garbageCollect(); - }, - }); - this.addCommand({ - id: "livesync-toggle", - name: "Toggle LiveSync", - callback: async () => { - if (this.settings.liveSync) { - this.settings.liveSync = false; - Logger("LiveSync Disabled.", LOG_LEVEL.NOTICE); - } else { - this.settings.liveSync = true; - Logger("LiveSync Enabled.", LOG_LEVEL.NOTICE); - } - await this.realizeSettingSyncMode(); - this.saveSettings(); - }, - }); - this.addCommand({ - id: "livesync-suspendall", - name: "Toggle All Sync.", - callback: async () => { - if (this.suspended) { - this.suspended = false; - Logger("Self-hosted LiveSync resumed", LOG_LEVEL.NOTICE); - } else { - this.suspended = true; - Logger("Self-hosted LiveSync suspended", LOG_LEVEL.NOTICE); - } - await this.realizeSettingSyncMode(); - this.saveSettings(); - }, - }); - this.triggerRealizeSettingSyncMode = debounce(this.triggerRealizeSettingSyncMode.bind(this), 1000); - this.triggerCheckPluginUpdate = debounce(this.triggerCheckPluginUpdate.bind(this), 3000); - } - onunload() { - if (this.gcTimerHandler != null) { - clearTimeout(this.gcTimerHandler); - this.gcTimerHandler = null; - } - this.clearPeriodicSync(); - this.localDatabase.closeReplication(); - this.localDatabase.close(); - window.removeEventListener("visibilitychange", this.watchWindowVisiblity); - Logger("unloading plugin"); - } - - async openDatabase() { - if (this.localDatabase != null) { - this.localDatabase.close(); - } - let vaultName = this.app.vault.getName(); - Logger("Open Database..."); - this.localDatabase = new LocalPouchDB(this.settings, vaultName); - this.localDatabase.updateInfo = () => { - this.refreshStatusText(); - }; - await this.localDatabase.initializeDatabase(); - } - async garbageCollect() { - await this.localDatabase.garbageCollect(); - } - - async loadSettings() { - this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); - this.settings.workingEncrypt = this.settings.encrypt; - this.settings.workingPassphrase = this.settings.passphrase; - } - - triggerRealizeSettingSyncMode() { - (async () => await this.realizeSettingSyncMode())(); - } - async saveSettings() { - await this.saveData(this.settings); - this.localDatabase.settings = this.settings; - this.triggerRealizeSettingSyncMode(); - } - gcTimerHandler: any = null; - gcHook() { - if (this.settings.gcDelay == 0) return; - const GC_DELAY = this.settings.gcDelay * 1000; // if leaving opening window, try GC, - if (this.gcTimerHandler != null) { - clearTimeout(this.gcTimerHandler); - this.gcTimerHandler = null; - } - this.gcTimerHandler = setTimeout(() => { - this.gcTimerHandler = null; - this.garbageCollect(); - }, GC_DELAY); - } - registerWatchEvents() { - this.registerEvent(this.app.vault.on("modify", this.watchVaultChange)); - this.registerEvent(this.app.vault.on("delete", this.watchVaultDelete)); - this.registerEvent(this.app.vault.on("rename", this.watchVaultRename)); - this.registerEvent(this.app.vault.on("create", this.watchVaultChange)); - this.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen)); - window.addEventListener("visibilitychange", this.watchWindowVisiblity); - } - - watchWindowVisiblity() { - this.watchWindowVisiblityAsync(); - } - async watchWindowVisiblityAsync() { - if (this.settings.suspendFileWatching) return; - // if (this.suspended) return; - let isHidden = document.hidden; - await this.applyBatchChange(); - if (isHidden) { - this.localDatabase.closeReplication(); - this.clearPeriodicSync(); - } else { - // suspend all temporary. - if (this.suspended) return; - if (this.settings.autoSweepPlugins) { - await this.sweepPlugin(false); - } - if (this.settings.liveSync) { - await this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult); - } - if (this.settings.syncOnStart) { - await this.localDatabase.openReplication(this.settings, false, false, this.parseReplicationResult); - } - if (this.settings.periodicReplication) { - this.setPeriodicSync(); - } - } - this.gcHook(); - } - - watchWorkspaceOpen(file: TFile) { - if (this.settings.suspendFileWatching) return; - this.watchWorkspaceOpenAsync(file); - } - async watchWorkspaceOpenAsync(file: TFile) { - await this.applyBatchChange(); - if (file == null) return; - if (this.settings.syncOnFileOpen && !this.suspended) { - await this.replicate(); - } - this.localDatabase.disposeHashCache(); - await this.showIfConflicted(file); - this.gcHook(); - } - watchVaultCreate(file: TFile, ...args: any[]) { - if (this.settings.suspendFileWatching) return; - this.watchVaultChangeAsync(file, ...args); - } - watchVaultChange(file: TFile, ...args: any[]) { - if (this.settings.suspendFileWatching) return; - // If batchsave is enabled, queue all changes and do nothing. - if (this.settings.batchSave) { - this.batchFileChange = Array.from(new Set([...this.batchFileChange, file.path])); - this.refreshStatusText(); - return; - } - this.watchVaultChangeAsync(file, ...args); - } - applyBatchChange() { - let batchItems = JSON.parse(JSON.stringify(this.batchFileChange)) as string[]; - this.batchFileChange = []; - let files = this.app.vault.getFiles(); - let promises = batchItems.map(async (e) => { - try { - if (await this.app.vault.adapter.exists(normalizePath(e))) { - let f = files.find((f) => f.path == e); - if (f) { - await this.updateIntoDB(f); - Logger(`Batch save:${e}`); - } - } - } catch (ex) { - Logger(`Batch save error:${e}`, LOG_LEVEL.NOTICE); - Logger(ex, LOG_LEVEL.VERBOSE); - } - }); - this.refreshStatusText(); - return Promise.all(promises); - } - batchFileChange: string[] = []; - async watchVaultChangeAsync(file: TFile, ...args: any[]) { - if (file instanceof TFile) { - await this.updateIntoDB(file); - this.gcHook(); - } - } - watchVaultDelete(file: TFile | TFolder) { - // When save is delayed, it should be cancelled. - this.batchFileChange = this.batchFileChange.filter((e) => e == file.path); - if (this.settings.suspendFileWatching) return; - this.watchVaultDeleteAsync(file); - } - async watchVaultDeleteAsync(file: TFile | TFolder) { - if (file instanceof TFile) { - await this.deleteFromDB(file); - } else if (file instanceof TFolder) { - await this.deleteFolderOnDB(file); - } - this.gcHook(); - } - GetAllFilesRecursively(file: TAbstractFile): TFile[] { - if (file instanceof TFile) { - return [file]; - } else if (file instanceof TFolder) { - let result: TFile[] = []; - for (var v of file.children) { - result.push(...this.GetAllFilesRecursively(v)); - } - return result; - } else { - Logger(`Filetype error:${file.path}`, LOG_LEVEL.NOTICE); - throw new Error(`Filetype error:${file.path}`); - } - } - watchVaultRename(file: TFile | TFolder, oldFile: any) { - if (this.settings.suspendFileWatching) return; - this.watchVaultRenameAsync(file, oldFile); - } - getFilePath(file: TAbstractFile): string { - if (file instanceof TFolder) { - if (file.isRoot()) return ""; - return this.getFilePath(file.parent) + "/" + file.name; - } - if (file instanceof TFile) { - return this.getFilePath(file.parent) + "/" + file.name; - } - } - async watchVaultRenameAsync(file: TFile | TFolder, oldFile: any) { - Logger(`${oldFile} renamed to ${file.path}`, LOG_LEVEL.VERBOSE); - await this.applyBatchChange(); - if (file instanceof TFolder) { - const newFiles = this.GetAllFilesRecursively(file); - // for guard edge cases. this won't happen and each file's event will be raise. - for (const i of newFiles) { - let newFilePath = normalizePath(this.getFilePath(i)); - let newFile = this.app.vault.getAbstractFileByPath(newFilePath); - if (newFile instanceof TFile) { - Logger(`save ${newFile.path} into db`); - await this.updateIntoDB(newFile); - } - } - Logger(`delete below ${oldFile} from db`); - await this.deleteFromDBbyPath(oldFile); - } else if (file instanceof TFile) { - Logger(`file save ${file.path} into db`); - await this.updateIntoDB(file); - Logger(`deleted ${oldFile} into db`); - await this.deleteFromDBbyPath(oldFile); - } - this.gcHook(); - } - addLogHook: () => void = null; - //--> Basic document Functions - notifies: { [key: string]: { notice: Notice; timer: NodeJS.Timeout; count: number } } = {}; - async addLog(message: any, level: LOG_LEVEL = LOG_LEVEL.INFO) { - if (level < LOG_LEVEL.INFO && this.settings && this.settings.lessInformationInLog) { - return; - } - if (this.settings && !this.settings.showVerboseLog && level == LOG_LEVEL.VERBOSE) { - return; - } - let valutName = this.app.vault.getName(); - let timestamp = new Date().toLocaleString(); - let messagecontent = typeof message == "string" ? message : message instanceof Error ? `${message.name}:${message.message}` : JSON.stringify(message, null, 2); - let newmessage = timestamp + "->" + messagecontent; - - this.logMessage = [].concat(this.logMessage).concat([newmessage]).slice(-100); - console.log(valutName + ":" + newmessage); - // if (this.statusBar2 != null) { - // this.statusBar2.setText(newmessage.substring(0, 60)); - // } - - if (level >= LOG_LEVEL.NOTICE) { - if (messagecontent in this.notifies) { - clearTimeout(this.notifies[messagecontent].timer); - this.notifies[messagecontent].count++; - this.notifies[messagecontent].notice.setMessage(`(${this.notifies[messagecontent].count}):${messagecontent}`); - this.notifies[messagecontent].timer = setTimeout(() => { - const notify = this.notifies[messagecontent].notice; - delete this.notifies[messagecontent]; - try { - notify.hide(); - } catch (ex) { - // NO OP - } - }, 5000); - } else { - let notify = new Notice(messagecontent, 0); - this.notifies[messagecontent] = { - count: 0, - notice: notify, - timer: setTimeout(() => { - delete this.notifies[messagecontent]; - notify.hide(); - }, 5000), - }; - } - } - if (this.addLogHook != null) this.addLogHook(); - } - - async ensureDirectory(fullpath: string) { - let pathElements = fullpath.split("/"); - pathElements.pop(); - let c = ""; - for (var v of pathElements) { - c += v; - try { - await this.app.vault.createFolder(c); - } catch (ex) { - // basically skip exceptions. - if (ex.message && ex.message == "Folder already exists.") { - // especialy this message is. - } else { - Logger("Folder Create Error"); - Logger(ex); - } - } - c += "/"; - } - } - - async doc2storage_create(docEntry: EntryBody, force?: boolean) { - let pathSrc = id2path(docEntry._id); - let doc = await this.localDatabase.getDBEntry(pathSrc, { rev: docEntry._rev }); - if (doc === false) return; - let path = id2path(doc._id); - if (doc.datatype == "newnote") { - let bin = base64ToArrayBuffer(doc.data); - if (bin != null) { - if (!isValidPath(path)) { - Logger(`The file that having platform dependent name has been arrived. This file has skipped: ${path}`, LOG_LEVEL.NOTICE); - return; - } - await this.ensureDirectory(path); - try { - let newfile = await this.app.vault.createBinary(normalizePath(path), bin, { ctime: doc.ctime, mtime: doc.mtime }); - Logger("live : write to local (newfile:b) " + path); - await this.app.vault.trigger("create", newfile); - } catch (ex) { - Logger("could not write to local (newfile:bin) " + path, LOG_LEVEL.NOTICE); - Logger(ex, LOG_LEVEL.VERBOSE); - } - } - } else if (doc.datatype == "plain") { - if (!isValidPath(path)) { - Logger(`The file that having platform dependent name has been arrived. This file has skipped: ${path}`, LOG_LEVEL.NOTICE); - return; - } - await this.ensureDirectory(path); - try { - let newfile = await this.app.vault.create(normalizePath(path), doc.data, { ctime: doc.ctime, mtime: doc.mtime }); - Logger("live : write to local (newfile:p) " + path); - await this.app.vault.trigger("create", newfile); - } catch (ex) { - Logger("could not write to local (newfile:plain) " + path, LOG_LEVEL.NOTICE); - Logger(ex, LOG_LEVEL.VERBOSE); - } - } else { - Logger("live : New data imcoming, but we cound't parse that." + doc.datatype, LOG_LEVEL.NOTICE); - } - } - - async deleteVaultItem(file: TFile | TFolder) { - let dir = file.parent; - if (this.settings.trashInsteadDelete) { - await this.app.vault.trash(file, false); - } else { - await this.app.vault.delete(file); - } - Logger(`deleted:${file.path}`); - Logger(`other items:${dir.children.length}`); - if (dir.children.length == 0) { - if (!this.settings.doNotDeleteFolder) { - Logger(`all files deleted by replication, so delete dir`); - await this.deleteVaultItem(dir); - } - } - } - async doc2storate_modify(docEntry: EntryBody, file: TFile, force?: boolean) { - let pathSrc = id2path(docEntry._id); - if (docEntry._deleted) { - //basically pass. - //but if there're no docs left, delete file. - let lastDocs = await this.localDatabase.getDBEntry(pathSrc); - if (lastDocs === false) { - await this.deleteVaultItem(file); - } else { - // it perhaps delete some revisions. - // may be we have to reload this - await this.pullFile(pathSrc, null, true); - Logger(`delete skipped:${lastDocs._id}`); - } - return; - } - let localMtime = ~~(file.stat.mtime / 1000); - let docMtime = ~~(docEntry.mtime / 1000); - if (localMtime < docMtime || force) { - let doc = await this.localDatabase.getDBEntry(pathSrc); - let msg = "livesync : newer local files so write to local:" + file.path; - if (force) msg = "livesync : force write to local:" + file.path; - if (doc === false) return; - let path = id2path(doc._id); - if (doc.datatype == "newnote") { - let bin = base64ToArrayBuffer(doc.data); - if (bin != null) { - if (!isValidPath(path)) { - Logger(`The file that having platform dependent name has been arrived. This file has skipped: ${path}`, LOG_LEVEL.NOTICE); - return; - } - await this.ensureDirectory(path); - try { - await this.app.vault.modifyBinary(file, bin, { ctime: doc.ctime, mtime: doc.mtime }); - Logger(msg); - await this.app.vault.trigger("modify", file); - } catch (ex) { - Logger("could not write to local (modify:bin) " + path, LOG_LEVEL.NOTICE); - } - } - } else if (doc.datatype == "plain") { - if (!isValidPath(path)) { - Logger(`The file that having platform dependent name has been arrived. This file has skipped: ${path}`, LOG_LEVEL.NOTICE); - return; - } - await this.ensureDirectory(path); - try { - await this.app.vault.modify(file, doc.data, { ctime: doc.ctime, mtime: doc.mtime }); - Logger(msg); - await this.app.vault.trigger("modify", file); - } catch (ex) { - Logger("could not write to local (modify:plain) " + path, LOG_LEVEL.NOTICE); - } - } else { - Logger("live : New data imcoming, but we cound't parse that.:" + doc.datatype + "-", LOG_LEVEL.NOTICE); - } - } else if (localMtime > docMtime) { - // newer local file. - // ? - } else { - //Nothing have to op. - //eq.case - } - } - async handleDBChanged(change: EntryBody) { - let allfiles = this.app.vault.getFiles(); - let targetFiles = allfiles.filter((e) => e.path == id2path(change._id)); - if (targetFiles.length == 0) { - if (change._deleted) { - return; - } - let doc = change; - await this.doc2storage_create(doc); - } - if (targetFiles.length == 1) { - let doc = change; - let file = targetFiles[0]; - await this.doc2storate_modify(doc, file); - await this.showIfConflicted(file); - } - } - - periodicSyncHandler: NodeJS.Timer = null; - //---> Sync - async parseReplicationResult(docs: Array>): Promise { - this.refreshStatusText(); - for (var change of docs) { - if (this.localDatabase.isSelfModified(change._id, change._rev)) { - continue; - } - if (change._id.startsWith("ps:")) { - if (this.settings.notifyPluginOrSettingUpdated) { - this.triggerCheckPluginUpdate(); - } - continue; - } - if (change._id.startsWith("h:")) { - continue; - } - if (change.type != "leaf" && change.type != "versioninfo" && change.type != "milestoneinfo" && change.type != "nodeinfo" && change.type != "plugin") { - Logger("replication change arrived", LOG_LEVEL.VERBOSE); - await this.handleDBChanged(change); - } - if (change.type == "versioninfo") { - if (change.version > VER) { - this.localDatabase.closeReplication(); - Logger(`Remote database updated to incompatible version. update your self-hosted-livesync plugin.`, LOG_LEVEL.NOTICE); - } - } - this.gcHook(); - } - } - triggerCheckPluginUpdate() { - (async () => await this.checkPluginUpdate())(); - } - async checkPluginUpdate() { - await this.sweepPlugin(false); - const { allPlugins, thisDevicePlugins } = await this.getPluginList(); - const arrPlugins = Object.values(allPlugins); - for (const plugin of arrPlugins) { - let currentPlugin = thisDevicePlugins[plugin.manifest.id]; - if (currentPlugin) { - const thisVersion = plugin.manifest.version - .split(".") - .reverse() - .map((e, i) => ((e as any) / 1) * 1000 ** i) - .reduce((prev, current) => prev + current, 0); - const currentVersion = currentPlugin.manifest.version - .split(".") - .reverse() - .map((e, i) => ((e as any) / 1) * 1000 ** i) - .reduce((prev, current) => prev + current, 0); - if (thisVersion > currentVersion) { - Logger(`the device ${plugin.deviceVaultName} has the newer plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE); - } - if (plugin.mtime > currentPlugin.mtime) { - Logger(`the device ${plugin.deviceVaultName} has the newer settings of the plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE); - } - } else { - Logger(`the device ${plugin.deviceVaultName} has the new plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE); - } - } - } - clearPeriodicSync() { - if (this.periodicSyncHandler != null) { - clearInterval(this.periodicSyncHandler); - this.periodicSyncHandler = null; - } - } - setPeriodicSync() { - if (this.settings.periodicReplication && this.settings.periodicReplicationInterval > 0) { - this.clearPeriodicSync(); - this.periodicSyncHandler = setInterval(async () => await this.periodicSync(), Math.max(this.settings.periodicReplicationInterval, 30) * 1000); - } - } - async periodicSync() { - await this.replicate(); - } - periodicPluginSweepHandler: NodeJS.Timer = null; - clearPluginSweep() { - if (this.periodicPluginSweepHandler != null) { - clearInterval(this.periodicPluginSweepHandler); - this.periodicPluginSweepHandler = null; - } - } - setPluginSweep() { - if (this.settings.autoSweepPluginsPeriodic) { - this.clearPluginSweep(); - this.periodicPluginSweepHandler = setInterval(async () => await this.periodicPluginSweep(), PERIODIC_PLUGIN_SWEEP * 1000); - } - } - async periodicPluginSweep() { - await this.sweepPlugin(false); - } - async realizeSettingSyncMode() { - this.localDatabase.closeReplication(); - this.clearPeriodicSync(); - this.clearPluginSweep(); - await this.applyBatchChange(); - // disable all sync temporary. - if (this.suspended) return; - if (this.settings.autoSweepPlugins) { - await this.sweepPlugin(false); - } - if (this.settings.liveSync) { - this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult); - this.refreshStatusText(); - } - this.setPeriodicSync(); - this.setPluginSweep(); - } - lastMessage = ""; - refreshStatusText() { - let sent = this.localDatabase.docSent; - let arrived = this.localDatabase.docArrived; - let w = ""; - switch (this.localDatabase.syncStatus) { - case "CLOSED": - case "COMPLETED": - case "NOT_CONNECTED": - w = "⏹"; - break; - case "PAUSED": - w = "💤"; - break; - - case "CONNECTED": - w = "⚡"; - break; - case "ERRORED": - w = "⚠"; - break; - default: - w = "?"; - } - this.statusBar.title = this.localDatabase.syncStatus; - let waiting = ""; - if (this.settings.batchSave) { - waiting = " " + this.batchFileChange.map((e) => "🛫").join(""); - waiting = waiting.replace(/🛫{10}/g, "🚀"); - } - const message = `Sync:${w} ↑${sent} ↓${arrived}${waiting}`; - this.setStatusBarText(message); - } - setStatusBarText(message: string) { - if (this.lastMessage != message) { - this.statusBar.setText(message); - if (this.settings.showStatusOnEditor) { - const root = document.documentElement; - root.style.setProperty("--slsmessage", '"' + message + '"'); - } else { - const root = document.documentElement; - root.style.setProperty("--slsmessage", '""'); - } - this.lastMessage = message; - } - } - async replicate(showMessage?: boolean) { - if (this.settings.versionUpFlash != "") { - new Notice("Open settings and check message, please."); - return; - } - await this.applyBatchChange(); - if (this.settings.autoSweepPlugins) { - await this.sweepPlugin(false); - } - this.localDatabase.openReplication(this.settings, false, showMessage, this.parseReplicationResult); - } - - async initializeDatabase(showingNotice?: boolean) { - await this.openDatabase(); - await this.syncAllFiles(showingNotice); - } - async replicateAllToServer(showingNotice?: boolean) { - if (this.settings.autoSweepPlugins) { - await this.sweepPlugin(showingNotice); - } - return await this.localDatabase.replicateAllToServer(this.settings, showingNotice); - } - async markRemoteLocked() { - return await this.localDatabase.markRemoteLocked(this.settings, true); - } - async markRemoteUnlocked() { - return await this.localDatabase.markRemoteLocked(this.settings, false); - } - async markRemoteResolved() { - return await this.localDatabase.markRemoteResolved(this.settings); - } - async syncAllFiles(showingNotice?: boolean) { - // synchronize all files between database and storage. - let notice: Notice = null; - if (showingNotice) { - notice = new Notice("Initializing", 0); - } - const filesStorage = this.app.vault.getFiles(); - const filesStorageName = filesStorage.map((e) => e.path); - const wf = await this.localDatabase.localDatabase.allDocs(); - const filesDatabase = wf.rows.filter((e) => !e.id.startsWith("h:") && !e.id.startsWith("ps:") && e.id != "obsydian_livesync_version").map((e) => id2path(e.id)); - - const onlyInStorage = filesStorage.filter((e) => filesDatabase.indexOf(e.path) == -1); - const onlyInDatabase = filesDatabase.filter((e) => filesStorageName.indexOf(e) == -1); - - const onlyInStorageNames = onlyInStorage.map((e) => e.path); - - const syncFiles = filesStorage.filter((e) => onlyInStorageNames.indexOf(e.path) == -1); - Logger("Initialize and checking database files"); - Logger("Updating database by new files"); - this.setStatusBarText(`UPDATE DATABASE`); - let _this = this; - async function runAll(procedurename: string, objects: T[], callback: (arg: T) => Promise) { - const count = objects.length; - Logger(procedurename); - let i = 0; - // let lastTicks = performance.now() + 2000; - let procs = objects.map(async (e) => { - try { - // debugger; - // Logger("hello?") - await callback(e); - i++; - if (i % 25 == 0) { - const notify = `${procedurename} : ${i}/${count}`; - if (notice != null) notice.setMessage(notify); - Logger(notify); - // lastTicks = performance.now() + 2000; - _this.setStatusBarText(notify); - } - } catch (ex) { - Logger(`Error while ${procedurename}`, LOG_LEVEL.NOTICE); - Logger(ex); - } - }); - if (!Promise.allSettled) { - await Promise.all( - procs.map((p) => - p - .then((value) => ({ - status: "fulfilled", - value, - })) - .catch((reason) => ({ - status: "rejected", - reason, - })) - ) - ); - } else { - await Promise.allSettled(procs); - } - } - await runAll("UPDATE DATABASE", onlyInStorage, async (e) => { - Logger(`Update into ${e.path}`); - await this.updateIntoDB(e); - }); - await runAll("UPDATE STORAGE", onlyInDatabase, async (e) => { - Logger(`Pull from db:${e}`); - await this.pullFile(e, filesStorage, false, null, false); - }); - await runAll("CHECK FILE STATUS", syncFiles, async (e) => { - await this.syncFileBetweenDBandStorage(e, filesStorage); - }); - this.setStatusBarText(`NOW TRACKING!`); - Logger("Initialized,NOW TRACKING!"); - if (showingNotice) { - notice.hide(); - Logger("Initialize done!", LOG_LEVEL.NOTICE); - } - } - async deleteFolderOnDB(folder: TFolder) { - Logger(`delete folder:${folder.path}`); - await this.localDatabase.deleteDBEntryPrefix(folder.path + "/"); - for (var v of folder.children) { - let entry = v as TFile & TFolder; - Logger(`->entry:${entry.path}`, LOG_LEVEL.VERBOSE); - if (entry.children) { - Logger(`->is dir`, LOG_LEVEL.VERBOSE); - await this.deleteFolderOnDB(entry); - try { - if (this.settings.trashInsteadDelete) { - await this.app.vault.trash(entry, false); - } else { - await this.app.vault.delete(entry); - } - } catch (ex) { - if (ex.code && ex.code == "ENOENT") { - //NO OP. - } else { - Logger(`error while delete folder:${entry.path}`, LOG_LEVEL.NOTICE); - Logger(ex); - } - } - } else { - Logger(`->is file`, LOG_LEVEL.VERBOSE); - await this.deleteFromDB(entry); - } - } - try { - if (this.settings.trashInsteadDelete) { - await this.app.vault.trash(folder, false); - } else { - await this.app.vault.delete(folder); - } - } catch (ex) { - if (ex.code && ex.code == "ENOENT") { - //NO OP. - } else { - Logger(`error while delete filder:${folder.path}`, LOG_LEVEL.NOTICE); - Logger(ex); - } - } - } - - async renameFolder(folder: TFolder, oldFile: any) { - for (var v of folder.children) { - let entry = v as TFile & TFolder; - if (entry.children) { - await this.deleteFolderOnDB(entry); - if (this.settings.trashInsteadDelete) { - await this.app.vault.trash(entry, false); - } else { - await this.app.vault.delete(entry); - } - } else { - await this.deleteFromDB(entry); - } - } - } - - // --> conflict resolving - async getConflictedDoc(path: string, rev: string): Promise { - try { - let doc = await this.localDatabase.getDBEntry(path, { rev: rev }); - if (doc === false) return false; - let data = doc.data; - if (doc.datatype == "newnote") { - data = base64ToString(doc.data); - } else if (doc.datatype == "plain") { - data = doc.data; - } - return { - ctime: doc.ctime, - mtime: doc.mtime, - rev: rev, - data: data, - }; - } catch (ex) { - if (ex.status && ex.status == 404) { - return false; - } - } - return false; - } - /** - * Getting file conflicted status. - * @param path the file location - * @returns true -> resolved, false -> nothing to do, or check result. - */ - async getConflictedStatus(path: string): Promise { - let test = await this.localDatabase.getDBEntry(path, { conflicts: true }); - if (test === false) return false; - if (test == null) return false; - if (!test._conflicts) return false; - if (test._conflicts.length == 0) return false; - // should be one or more conflicts; - let leftLeaf = await this.getConflictedDoc(path, test._rev); - let rightLeaf = await this.getConflictedDoc(path, test._conflicts[0]); - if (leftLeaf == false) { - // what's going on.. - Logger(`could not get current revisions:${path}`, LOG_LEVEL.NOTICE); - return false; - } - if (rightLeaf == false) { - // Conflicted item could not load, delete this. - await this.localDatabase.deleteDBEntry(path, { rev: test._conflicts[0] }); - await this.pullFile(path, null, true); - Logger(`could not get old revisions, automaticaly used newer one:${path}`, LOG_LEVEL.NOTICE); - return true; - } - // first,check for same contents - if (leftLeaf.data == rightLeaf.data) { - let leaf = leftLeaf; - if (leftLeaf.mtime > rightLeaf.mtime) { - leaf = rightLeaf; - } - await this.localDatabase.deleteDBEntry(path, { rev: leaf.rev }); - await this.pullFile(path, null, true); - Logger(`automaticaly merged:${path}`); - return true; - } - if (this.settings.resolveConflictsByNewerFile) { - let lmtime = ~~(leftLeaf.mtime / 1000); - let rmtime = ~~(rightLeaf.mtime / 1000); - let loser = leftLeaf; - if (lmtime > rmtime) { - loser = rightLeaf; - } - await this.localDatabase.deleteDBEntry(path, { rev: loser.rev }); - await this.pullFile(path, null, true); - Logger(`Automaticaly merged (newerFileResolve) :${path}`, LOG_LEVEL.NOTICE); - return true; - } - // make diff. - let dmp = new diff_match_patch(); - var diff = dmp.diff_main(leftLeaf.data, rightLeaf.data); - dmp.diff_cleanupSemantic(diff); - Logger(`conflict(s) found:${path}`); - return { - left: leftLeaf, - right: rightLeaf, - diff: diff, - }; - } - async showIfConflicted(file: TFile) { - let conflictCheckResult = await this.getConflictedStatus(file.path); - if (conflictCheckResult === false) return; //nothign to do. - if (conflictCheckResult === true) { - //auto resolved, but need check again; - setTimeout(() => { - this.showIfConflicted(file); - }, 500); - return; - } - //there conflicts, and have to resolve ; - let leaf = this.app.workspace.activeLeaf; - if (leaf) { - new ConflictResolveModal(this.app, conflictCheckResult, async (selected) => { - let testDoc = await this.localDatabase.getDBEntry(file.path, { conflicts: true }); - if (testDoc === false) return; - if (!testDoc._conflicts) { - Logger("something went wrong on merging.", LOG_LEVEL.NOTICE); - return; - } - let toDelete = selected; - if (toDelete == null) { - //concat both, - if (conflictCheckResult !== false && conflictCheckResult !== true) { - // write data,and delete both old rev. - let p = conflictCheckResult.diff.map((e) => e[1]).join(""); - await this.app.vault.modify(file, p); - await this.localDatabase.deleteDBEntry(file.path, { rev: conflictCheckResult.left.rev }); - await this.localDatabase.deleteDBEntry(file.path, { rev: conflictCheckResult.right.rev }); - } - return; - } - if (toDelete == "") { - return; - } - Logger(`resolved conflict:${file.path}`); - await this.localDatabase.deleteDBEntry(file.path, { rev: toDelete }); - await this.pullFile(file.path, null, true); - setTimeout(() => { - //resolved, check again. - this.showIfConflicted(file); - }, 500); - }).open(); - } - } - async pullFile(filename: string, fileList?: TFile[], force?: boolean, rev?: string, waitForReady: boolean = true) { - if (!fileList) { - fileList = this.app.vault.getFiles(); - } - let targetFiles = fileList.filter((e) => e.path == id2path(filename)); - if (targetFiles.length == 0) { - //have to create; - let doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady); - if (doc === false) return; - await this.doc2storage_create(doc, force); - } else if (targetFiles.length == 1) { - //normal case - let file = targetFiles[0]; - let doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady); - if (doc === false) return; - await this.doc2storate_modify(doc, file, force); - } else { - Logger(`target files:${filename} is two or more files in your vault`); - //something went wrong.. - } - //when to opened file; - } - async syncFileBetweenDBandStorage(file: TFile, fileList?: TFile[]) { - let doc = await this.localDatabase.getDBEntryMeta(file.path); - if (doc === false) return; - - let storageMtime = ~~(file.stat.mtime / 1000); - let docMtime = ~~(doc.mtime / 1000); - if (storageMtime > docMtime) { - //newer local file. - Logger("STORAGE -> DB :" + file.path); - Logger(`${storageMtime} > ${docMtime}`); - await this.updateIntoDB(file); - } else if (storageMtime < docMtime) { - //newer database file. - Logger("STORAGE <- DB :" + file.path); - Logger(`${storageMtime} < ${docMtime}`); - let docx = await this.localDatabase.getDBEntry(file.path, null, false, false); - if (docx != false) { - await this.doc2storate_modify(docx, file); - } - } else { - // Logger("EVEN :" + file.path, LOG_LEVEL.VERBOSE); - // Logger(`${storageMtime} = ${docMtime}`, LOG_LEVEL.VERBOSE); - //eq.case - } - } - - async updateIntoDB(file: TFile) { - await this.localDatabase.waitForGCComplete(); - let content = ""; - let datatype: "plain" | "newnote" = "newnote"; - if (file.extension != "md") { - let contentBin = await this.app.vault.readBinary(file); - content = await arrayBufferToBase64(contentBin); - datatype = "newnote"; - } else { - content = await this.app.vault.read(file); - datatype = "plain"; - } - let fullpath = path2id(file.path); - let d: LoadedEntry = { - _id: fullpath, - data: content, - ctime: file.stat.ctime, - mtime: file.stat.mtime, - size: file.stat.size, - children: [], - datatype: datatype, - }; - //From here - let old = await this.localDatabase.getDBEntry(fullpath, null, false, false); - if (old !== false) { - let oldData = { data: old.data, deleted: old._deleted }; - let newData = { data: d.data, deleted: d._deleted }; - if (JSON.stringify(oldData) == JSON.stringify(newData)) { - Logger("not changed:" + fullpath + (d._deleted ? " (deleted)" : ""), LOG_LEVEL.VERBOSE); - return; - } - // d._rev = old._rev; - } - let ret = await this.localDatabase.putDBEntry(d); - - Logger("put database:" + fullpath + "(" + datatype + ") "); - if (this.settings.syncOnSave && !this.suspended) { - await this.replicate(); - } - } - async deleteFromDB(file: TFile) { - let fullpath = file.path; - Logger(`deleteDB By path:${fullpath}`); - await this.deleteFromDBbyPath(fullpath); - if (this.settings.syncOnSave && !this.suspended) { - await this.replicate(); - } - } - async deleteFromDBbyPath(fullpath: string) { - await this.localDatabase.deleteDBEntry(fullpath); - if (this.settings.syncOnSave && !this.suspended) { - await this.replicate(); - } - } - - async resetLocalDatabase() { - await this.localDatabase.resetDatabase(); - } - async tryResetRemoteDatabase() { - await this.localDatabase.tryResetRemoteDatabase(this.settings); - } - async tryCreateRemoteDatabase() { - await this.localDatabase.tryCreateRemoteDatabase(this.settings); - } - async getPluginList(): Promise<{ plugins: { [key: string]: PluginDataEntry[] }; allPlugins: { [key: string]: PluginDataEntry }; thisDevicePlugins: { [key: string]: PluginDataEntry } }> { - const db = this.localDatabase.localDatabase; - let docList = await db.allDocs({ startkey: `ps:`, endkey: `ps;`, include_docs: false }); - let oldDocs: PluginDataEntry[] = ((await Promise.all(docList.rows.map(async (e) => await this.localDatabase.getDBEntry(e.id)))).filter((e) => e !== false) as LoadedEntry[]).map((e) => JSON.parse(e.data)); - let plugins: { [key: string]: PluginDataEntry[] } = {}; - let allPlugins: { [key: string]: PluginDataEntry } = {}; - let thisDevicePlugins: { [key: string]: PluginDataEntry } = {}; - for (let v of oldDocs) { - if (typeof plugins[v.deviceVaultName] === "undefined") { - plugins[v.deviceVaultName] = []; - } - plugins[v.deviceVaultName].push(v); - allPlugins[v._id] = v; - if (v.deviceVaultName == this.settings.deviceAndVaultName) { - thisDevicePlugins[v.manifest.id] = v; - } - } - return { plugins, allPlugins, thisDevicePlugins }; - } - async sweepPlugin(showMessage = false) { - const logLevel = showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO; - if (!this.settings.encrypt) { - Logger("You have to encrypt the database to use plugin setting sync.", LOG_LEVEL.NOTICE); - return; - } - if (!this.settings.deviceAndVaultName) { - Logger("You have to set your device and vault name.", LOG_LEVEL.NOTICE); - return; - } - Logger("Sweeping plugins", logLevel); - const db = this.localDatabase.localDatabase; - let oldDocs = await db.allDocs({ startkey: `ps:${this.settings.deviceAndVaultName}-`, endkey: `ps:${this.settings.deviceAndVaultName}.`, include_docs: true }); - Logger("OLD DOCS.", LOG_LEVEL.VERBOSE); - // sweep current plugin. - // @ts-ignore - const pl = this.app.plugins; - const manifests: PluginManifest[] = Object.values(pl.manifests); - for (let m of manifests) { - Logger(`Reading plugin:${m.name}(${m.id})`, LOG_LEVEL.VERBOSE); - let path = normalizePath(m.dir) + "/"; - const adapter = this.app.vault.adapter; - let files = ["manifest.json", "main.js", "styles.css", "data.json"]; - let pluginData: { [key: string]: string } = {}; - for (let file of files) { - let thePath = path + file; - if (await adapter.exists(thePath)) { - pluginData[file] = await adapter.read(thePath); - } - } - let mtime = 0; - if (await adapter.exists(path + "/data.json")) { - mtime = (await adapter.stat(path + "/data.json")).mtime; - } - let p: PluginDataEntry = { - _id: `ps:${this.settings.deviceAndVaultName}-${m.id}`, - dataJson: pluginData["data.json"], - deviceVaultName: this.settings.deviceAndVaultName, - mainJs: pluginData["main.js"], - styleCss: pluginData["styles.css"], - manifest: m, - manifestJson: pluginData["manifest.json"], - mtime: mtime, - type: "plugin", - }; - let d: LoadedEntry = { - _id: p._id, - data: JSON.stringify(p), - ctime: mtime, - mtime: mtime, - size: 0, - children: [], - datatype: "plain", - }; - Logger(`check diff:${m.name}(${m.id})`, LOG_LEVEL.VERBOSE); - let old = await this.localDatabase.getDBEntry(p._id, null, false, false); - if (old !== false) { - let oldData = { data: old.data, deleted: old._deleted }; - let newData = { data: d.data, deleted: d._deleted }; - if (JSON.stringify(oldData) == JSON.stringify(newData)) { - oldDocs.rows = oldDocs.rows.filter((e) => e.id != d._id); - Logger(`Nothing changed:${m.name}`); - continue; - } - } - await this.localDatabase.putDBEntry(d); - oldDocs.rows = oldDocs.rows.filter((e) => e.id != d._id); - Logger(`Plugin saved:${m.name}`, logLevel); - //remove saved plugin data. - } - Logger(`Deleting old plugins`, LOG_LEVEL.VERBOSE); - let delDocs = oldDocs.rows.map((e) => { - e.doc._deleted = true; - return e.doc; - }); - await db.bulkDocs(delDocs); - Logger(`Sweep plugin done.`, logLevel); - } - async applyPluginData(plugin: PluginDataEntry) { - const pluginTargetFolderPath = normalizePath(plugin.manifest.dir) + "/"; - const adapter = this.app.vault.adapter; - // @ts-ignore - let stat = this.app.plugins.enabledPlugins[plugin.manifest.id]; - if (stat) { - // @ts-ignore - await this.app.plugins.unloadPlugin(plugin.manifest.id); - Logger(`Unload plugin:${plugin.manifest.id}`, LOG_LEVEL.NOTICE); - } - if (plugin.dataJson) await adapter.write(pluginTargetFolderPath + "data.json", plugin.dataJson); - Logger("wrote:" + pluginTargetFolderPath + "data.json", LOG_LEVEL.NOTICE); - // @ts-ignore - if (stat) { - // @ts-ignore - await this.app.plugins.loadPlugin(plugin.manifest.id); - Logger(`Load plugin:${plugin.manifest.id}`, LOG_LEVEL.NOTICE); - } - } - async applyPlugin(plugin: PluginDataEntry) { - // @ts-ignore - let stat = this.app.plugins.enabledPlugins[plugin.manifest.id]; - if (stat) { - // @ts-ignore - await this.app.plugins.unloadPlugin(plugin.manifest.id); - Logger(`Unload plugin:${plugin.manifest.id}`, LOG_LEVEL.NOTICE); - } - - const pluginTargetFolderPath = normalizePath(plugin.manifest.dir) + "/"; - const adapter = this.app.vault.adapter; - if ((await adapter.exists(pluginTargetFolderPath)) === false) { - await adapter.mkdir(pluginTargetFolderPath); - } - await adapter.write(pluginTargetFolderPath + "main.js", plugin.mainJs); - await adapter.write(pluginTargetFolderPath + "manifest.json", plugin.manifestJson); - if (plugin.styleCss) await adapter.write(pluginTargetFolderPath + "styles.css", plugin.styleCss); - // if (plugin.dataJson) await adapter.write(pluginTargetFolderPath + "data.json", plugin.dataJson); - if (stat) { - // @ts-ignore - await this.app.plugins.loadPlugin(plugin.manifest.id); - Logger(`Load plugin:${plugin.manifest.id}`, LOG_LEVEL.NOTICE); - } - } -} -class LogDisplayModal extends Modal { - plugin: ObsidianLiveSyncPlugin; - logEl: HTMLDivElement; - constructor(app: App, plugin: ObsidianLiveSyncPlugin) { - super(app); - this.plugin = plugin; - } - updateLog() { - let msg = ""; - for (var v of this.plugin.logMessage) { - msg += escapeStringToHTML(v) + "
"; - } - this.logEl.innerHTML = msg; - } - onOpen() { - let { contentEl } = this; - - contentEl.empty(); - contentEl.createEl("h2", { text: "Sync Status" }); - let div = contentEl.createDiv(""); - div.addClass("op-scrollable"); - div.addClass("op-pre"); - this.logEl = div; - this.updateLog = this.updateLog.bind(this); - this.plugin.addLogHook = this.updateLog; - this.updateLog(); - } - onClose() { - let { contentEl } = this; - contentEl.empty(); - this.plugin.addLogHook = null; - } -} -class ConflictResolveModal extends Modal { - // result: Array<[number, string]>; - result: diff_result; - callback: (remove_rev: string) => Promise; - constructor(app: App, diff: diff_result, callback: (remove_rev: string) => Promise) { - super(app); - this.result = diff; - this.callback = callback; - } - - onOpen() { - let { contentEl } = this; - - contentEl.empty(); - - contentEl.createEl("h2", { text: "This document has conflicted changes." }); - let div = contentEl.createDiv(""); - div.addClass("op-scrollable"); - let diff = ""; - // const showContents = this.result.map((e) => (e[0] == 1 ? "" + htmlEscape(e[1]) + "" : e[0] == -1 ? "" + htmlEscape(e[1]) + "" : "" + htmlEscape(e[1]) + "")); - for (let v of this.result.diff) { - let x1 = v[0]; - let x2 = v[1]; - if (x1 == DIFF_DELETE) { - diff += "" + escapeStringToHTML(x2) + ""; - } else if (x1 == DIFF_EQUAL) { - diff += "" + escapeStringToHTML(x2) + ""; - } else if (x1 == DIFF_INSERT) { - diff += "" + escapeStringToHTML(x2) + ""; - } - } - - diff = diff.replace(/\n/g, "
"); - div.innerHTML = diff; - let div2 = contentEl.createDiv(""); - let date1 = new Date(this.result.left.mtime).toLocaleString(); - let date2 = new Date(this.result.right.mtime).toLocaleString(); - div2.innerHTML = ` -A:${date1}
B:${date2}
- `; - contentEl.createEl("button", { text: "Keep A" }, (e) => { - e.addEventListener("click", async () => { - await this.callback(this.result.right.rev); - this.close(); - }); - }); - contentEl.createEl("button", { text: "Keep B" }, (e) => { - e.addEventListener("click", async () => { - await this.callback(this.result.left.rev); - this.close(); - }); - }); - contentEl.createEl("button", { text: "Concat both" }, (e) => { - e.addEventListener("click", async () => { - await this.callback(null); - this.close(); - }); - }); - contentEl.createEl("button", { text: "Not now" }, (e) => { - e.addEventListener("click", async () => { - this.close(); - }); - }); - } - - onClose() { - let { contentEl } = this; - contentEl.empty(); - } -} - -class ObsidianLiveSyncSettingTab extends PluginSettingTab { - plugin: ObsidianLiveSyncPlugin; - - constructor(app: App, plugin: ObsidianLiveSyncPlugin) { - super(app, plugin); - this.plugin = plugin; - } - async testConnection(): Promise { - let db = await connectRemoteCouchDB(this.plugin.settings.couchDB_URI + (this.plugin.settings.couchDB_DBNAME == "" ? "" : "/" + this.plugin.settings.couchDB_DBNAME), { - username: this.plugin.settings.couchDB_USER, - password: this.plugin.settings.couchDB_PASSWORD, - }); - if (typeof db === "string") { - this.plugin.addLog(`could not connect to ${this.plugin.settings.couchDB_URI} : ${this.plugin.settings.couchDB_DBNAME} \n(${db})`, LOG_LEVEL.NOTICE); - return; - } - this.plugin.addLog(`Connected to ${db.info.db_name}`, LOG_LEVEL.NOTICE); - } - display(): void { - let { containerEl } = this; - - containerEl.empty(); - - containerEl.createEl("h2", { text: "Settings for Self-hosted LiveSync." }); - - const w = containerEl.createDiv(""); - const screenElements: { [key: string]: HTMLElement[] } = {}; - const addScreenElement = (key: string, element: HTMLElement) => { - if (!(key in screenElements)) { - screenElements[key] = []; - } - screenElements[key].push(element); - }; - w.addClass("sls-setting-menu"); - w.innerHTML = ` - - - - - - - - - `; - const menutabs = w.querySelectorAll(".sls-setting-label"); - const changeDisplay = (screen: string) => { - for (var k in screenElements) { - if (k == screen) { - screenElements[k].forEach((element) => element.removeClass("setting-collapsed")); - } else { - screenElements[k].forEach((element) => element.addClass("setting-collapsed")); - } - } - }; - menutabs.forEach((element) => { - const e = element.querySelector(".sls-setting-tab"); - if (!e) return; - e.addEventListener("change", (event: any) => { - menutabs.forEach((element) => element.removeClass("selected")); - changeDisplay(event.target.value); - element.addClass("selected"); - }); - }); - - const containerRemoteDatabaseEl = containerEl.createDiv(); - containerRemoteDatabaseEl.createEl("h3", { text: "Remote Database configuration" }); - let syncWarn = containerRemoteDatabaseEl.createEl("div", { text: "The remote configuration is locked while any synchronization is enabled." }); - syncWarn.addClass("op-warn"); - syncWarn.addClass("sls-hidden"); - - const isAnySyncEnabled = (): boolean => { - if (this.plugin.settings.liveSync) return true; - if (this.plugin.settings.periodicReplication) return true; - if (this.plugin.settings.syncOnFileOpen) return true; - if (this.plugin.settings.syncOnSave) return true; - if (this.plugin.settings.syncOnStart) return true; - }; - const applyDisplayEnabled = () => { - if (isAnySyncEnabled()) { - dbsettings.forEach((e) => { - e.setDisabled(true).setTooltip("When any sync is enabled, It cound't be changed."); - }); - syncWarn.removeClass("sls-hidden"); - } else { - dbsettings.forEach((e) => { - e.setDisabled(false).setTooltip(""); - }); - syncWarn.addClass("sls-hidden"); - } - if (this.plugin.settings.liveSync) { - syncNonLive.forEach((e) => { - e.setDisabled(true).setTooltip(""); - }); - syncLive.forEach((e) => { - e.setDisabled(false).setTooltip(""); - }); - } else if (this.plugin.settings.syncOnFileOpen || this.plugin.settings.syncOnSave || this.plugin.settings.syncOnStart || this.plugin.settings.periodicReplication) { - syncNonLive.forEach((e) => { - e.setDisabled(false).setTooltip(""); - }); - syncLive.forEach((e) => { - e.setDisabled(true).setTooltip(""); - }); - } else { - syncNonLive.forEach((e) => { - e.setDisabled(false).setTooltip(""); - }); - syncLive.forEach((e) => { - e.setDisabled(false).setTooltip(""); - }); - } - }; - - let dbsettings: Setting[] = []; - dbsettings.push( - new Setting(containerRemoteDatabaseEl).setName("URI").addText((text) => - text - .setPlaceholder("https://........") - .setValue(this.plugin.settings.couchDB_URI) - .onChange(async (value) => { - this.plugin.settings.couchDB_URI = value; - await this.plugin.saveSettings(); - }) - ), - new Setting(containerRemoteDatabaseEl) - .setName("Username") - .setDesc("username") - .addText((text) => - text - .setPlaceholder("") - .setValue(this.plugin.settings.couchDB_USER) - .onChange(async (value) => { - this.plugin.settings.couchDB_USER = value; - await this.plugin.saveSettings(); - }) - ), - new Setting(containerRemoteDatabaseEl) - .setName("Password") - .setDesc("password") - .addText((text) => { - text.setPlaceholder("") - .setValue(this.plugin.settings.couchDB_PASSWORD) - .onChange(async (value) => { - this.plugin.settings.couchDB_PASSWORD = value; - await this.plugin.saveSettings(); - }); - text.inputEl.setAttribute("type", "password"); - }), - new Setting(containerRemoteDatabaseEl).setName("Database name").addText((text) => - text - .setPlaceholder("") - .setValue(this.plugin.settings.couchDB_DBNAME) - .onChange(async (value) => { - this.plugin.settings.couchDB_DBNAME = value; - await this.plugin.saveSettings(); - }) - ) - ); - - new Setting(containerRemoteDatabaseEl) - .setName("Test Database Connection") - .setDesc("Open database connection. If the remote database is not found and you have the privilege to create a database, the database will be created.") - .addButton((button) => - button - .setButtonText("Test") - .setDisabled(false) - .onClick(async () => { - await this.testConnection(); - }) - ); - - addScreenElement("0", containerRemoteDatabaseEl); - const containerLocalDatabaseEl = containerEl.createDiv(); - containerLocalDatabaseEl.createEl("h3", { text: "Local Database configuration" }); - - new Setting(containerLocalDatabaseEl) - .setName("Batch database update (beta)") - .setDesc("Delay all changes, save once before replication or opening another file.") - .addToggle((toggle) => - toggle.setValue(this.plugin.settings.batchSave).onChange(async (value) => { - if (value && this.plugin.settings.liveSync) { - Logger("LiveSync and Batch database update cannot be used at the same time.", LOG_LEVEL.NOTICE); - toggle.setValue(false); - return; - } - this.plugin.settings.batchSave = value; - await this.plugin.saveSettings(); - }) - ); - - new Setting(containerLocalDatabaseEl) - .setName("Auto Garbage Collection delay") - .setDesc("(seconds), if you set zero, you have to run manually.") - .addText((text) => { - text.setPlaceholder("") - .setValue(this.plugin.settings.gcDelay + "") - .onChange(async (value) => { - let v = Number(value); - if (isNaN(v) || v > 5000) { - return 0; - } - this.plugin.settings.gcDelay = v; - await this.plugin.saveSettings(); - }); - text.inputEl.setAttribute("type", "number"); - }); - new Setting(containerLocalDatabaseEl).setName("Manual Garbage Collect").addButton((button) => - button - .setButtonText("Collect now") - .setDisabled(false) - .onClick(async () => { - await this.plugin.garbageCollect(); - }) - ); - new Setting(containerLocalDatabaseEl) - .setName("End to End Encryption") - .setDesc("Encrypting contents on the database.") - .addToggle((toggle) => - toggle.setValue(this.plugin.settings.workingEncrypt).onChange(async (value) => { - this.plugin.settings.workingEncrypt = value; - phasspharase.setDisabled(!value); - await this.plugin.saveSettings(); - }) - ); - let phasspharase = new Setting(containerLocalDatabaseEl) - .setName("Passphrase") - .setDesc("Encrypting passphrase") - .addText((text) => { - text.setPlaceholder("") - .setValue(this.plugin.settings.workingPassphrase) - .onChange(async (value) => { - this.plugin.settings.workingPassphrase = value; - await this.plugin.saveSettings(); - }); - text.inputEl.setAttribute("type", "password"); - }); - phasspharase.setDisabled(!this.plugin.settings.workingEncrypt); - containerLocalDatabaseEl.createEl("div", { - text: "When you change any encryption enabled or passphrase, you have to reset all databases to make sure that the last password is unused and erase encrypted data from anywhere. This operation will not lost your vault if you are fully synced.", - }); - const applyEncryption = async (sendToServer: boolean) => { - if (this.plugin.settings.workingEncrypt && this.plugin.settings.workingPassphrase == "") { - Logger("If you enable encryption, you have to set the passphrase", LOG_LEVEL.NOTICE); - return; - } - if (this.plugin.settings.workingEncrypt && !(await testCrypt())) { - Logger("WARNING! Your device would not support encryption.", LOG_LEVEL.NOTICE); - return; - } - if (!this.plugin.settings.workingEncrypt) { - this.plugin.settings.workingPassphrase = ""; - } - this.plugin.settings.liveSync = false; - this.plugin.settings.periodicReplication = false; - this.plugin.settings.syncOnSave = false; - this.plugin.settings.syncOnStart = false; - this.plugin.settings.syncOnFileOpen = false; - this.plugin.settings.encrypt = this.plugin.settings.workingEncrypt; - this.plugin.settings.passphrase = this.plugin.settings.workingPassphrase; - - await this.plugin.saveSettings(); - await this.plugin.resetLocalDatabase(); - if (sendToServer) { - await this.plugin.initializeDatabase(true); - await this.plugin.markRemoteLocked(); - await this.plugin.tryResetRemoteDatabase(); - await this.plugin.markRemoteLocked(); - await this.plugin.replicateAllToServer(true); - } else { - await this.plugin.markRemoteResolved(); - await this.plugin.replicate(true); - } - }; - new Setting(containerLocalDatabaseEl) - .setName("Apply") - .setDesc("apply encryption settinngs, and re-initialize database") - .addButton((button) => - button - .setButtonText("Apply and send") - .setWarning() - .setDisabled(false) - .setClass("sls-btn-left") - .onClick(async () => { - await applyEncryption(true); - }) - ) - .addButton((button) => - button - .setButtonText("Apply and receive") - .setWarning() - .setDisabled(false) - .setClass("sls-btn-right") - .onClick(async () => { - await applyEncryption(false); - }) - ); - - addScreenElement("10", containerLocalDatabaseEl); - const containerGeneralSettingsEl = containerEl.createDiv(); - containerGeneralSettingsEl.createEl("h3", { text: "General Settings" }); - - new Setting(containerGeneralSettingsEl) - .setName("Do not show low-priority Log") - .setDesc("Reduce log infomations") - .addToggle((toggle) => - toggle.setValue(this.plugin.settings.lessInformationInLog).onChange(async (value) => { - this.plugin.settings.lessInformationInLog = value; - await this.plugin.saveSettings(); - }) - ); - new Setting(containerGeneralSettingsEl) - .setName("Verbose Log") - .setDesc("Show verbose log ") - .addToggle((toggle) => - toggle.setValue(this.plugin.settings.showVerboseLog).onChange(async (value) => { - this.plugin.settings.showVerboseLog = value; - await this.plugin.saveSettings(); - }) - ); - - addScreenElement("20", containerGeneralSettingsEl); - const containerSyncSettingEl = containerEl.createDiv(); - containerSyncSettingEl.createEl("h3", { text: "Sync setting" }); - - if (this.plugin.settings.versionUpFlash != "") { - let c = containerSyncSettingEl.createEl("div", { text: this.plugin.settings.versionUpFlash }); - c.createEl("button", { text: "I got it and updated." }, (e) => { - e.addClass("mod-cta"); - e.addEventListener("click", async () => { - this.plugin.settings.versionUpFlash = ""; - await this.plugin.saveSettings(); - applyDisplayEnabled(); - c.remove(); - }); - }); - c.addClass("op-warn"); - } - - let syncLive: Setting[] = []; - let syncNonLive: Setting[] = []; - syncLive.push( - new Setting(containerSyncSettingEl) - .setName("LiveSync") - .setDesc("Sync realtime") - .addToggle((toggle) => - toggle.setValue(this.plugin.settings.liveSync).onChange(async (value) => { - if (value && this.plugin.settings.batchSave) { - Logger("LiveSync and Batch database update cannot be used at the same time.", LOG_LEVEL.NOTICE); - toggle.setValue(false); - return; - } - - this.plugin.settings.liveSync = value; - // ps.setDisabled(value); - await this.plugin.saveSettings(); - applyDisplayEnabled(); - await this.plugin.realizeSettingSyncMode(); - }) - ) - ); - - syncNonLive.push( - new Setting(containerSyncSettingEl) - .setName("Periodic Sync") - .setDesc("Sync periodically") - .addToggle((toggle) => - toggle.setValue(this.plugin.settings.periodicReplication).onChange(async (value) => { - this.plugin.settings.periodicReplication = value; - await this.plugin.saveSettings(); - applyDisplayEnabled(); - }) - ), - new Setting(containerSyncSettingEl) - .setName("Periodic sync intreval") - .setDesc("Interval (sec)") - .addText((text) => { - text.setPlaceholder("") - .setValue(this.plugin.settings.periodicReplicationInterval + "") - .onChange(async (value) => { - let v = Number(value); - if (isNaN(v) || v > 5000) { - return 0; - } - this.plugin.settings.periodicReplicationInterval = v; - await this.plugin.saveSettings(); - applyDisplayEnabled(); - }); - text.inputEl.setAttribute("type", "number"); - }), - - new Setting(containerSyncSettingEl) - .setName("Sync on Save") - .setDesc("When you save file, sync automatically") - .addToggle((toggle) => - toggle.setValue(this.plugin.settings.syncOnSave).onChange(async (value) => { - this.plugin.settings.syncOnSave = value; - await this.plugin.saveSettings(); - applyDisplayEnabled(); - }) - ), - new Setting(containerSyncSettingEl) - .setName("Sync on File Open") - .setDesc("When you open file, sync automatically") - .addToggle((toggle) => - toggle.setValue(this.plugin.settings.syncOnFileOpen).onChange(async (value) => { - this.plugin.settings.syncOnFileOpen = value; - await this.plugin.saveSettings(); - applyDisplayEnabled(); - }) - ), - new Setting(containerSyncSettingEl) - .setName("Sync on Start") - .setDesc("Start synchronization on Obsidian started.") - .addToggle((toggle) => - toggle.setValue(this.plugin.settings.syncOnStart).onChange(async (value) => { - this.plugin.settings.syncOnStart = value; - await this.plugin.saveSettings(); - applyDisplayEnabled(); - }) - ) - ); - - new Setting(containerSyncSettingEl) - .setName("Use Trash for deleted files") - .setDesc("Do not delete files that deleted in remote, just move to trash.") - .addToggle((toggle) => - toggle.setValue(this.plugin.settings.trashInsteadDelete).onChange(async (value) => { - this.plugin.settings.trashInsteadDelete = value; - await this.plugin.saveSettings(); - }) - ); - - new Setting(containerSyncSettingEl) - .setName("Do not delete empty folder") - .setDesc("Normally, folder is deleted When the folder became empty by replication. enable this, leave it as is") - .addToggle((toggle) => - toggle.setValue(this.plugin.settings.doNotDeleteFolder).onChange(async (value) => { - this.plugin.settings.doNotDeleteFolder = value; - await this.plugin.saveSettings(); - }) - ); - - new Setting(containerSyncSettingEl) - .setName("Use newer file if conflicted (beta)") - .setDesc("Resolve conflicts by newer files automatically.") - .addToggle((toggle) => - toggle.setValue(this.plugin.settings.resolveConflictsByNewerFile).onChange(async (value) => { - this.plugin.settings.resolveConflictsByNewerFile = value; - await this.plugin.saveSettings(); - }) - ); - new Setting(containerSyncSettingEl) - .setName("Minimum chunk size") - .setDesc("(letters), minimum chunk size.") - .addText((text) => { - text.setPlaceholder("") - .setValue(this.plugin.settings.minimumChunkSize + "") - .onChange(async (value) => { - let v = Number(value); - if (isNaN(v) || v < 10 || v > 1000) { - return 10; - } - this.plugin.settings.minimumChunkSize = v; - await this.plugin.saveSettings(); - }); - text.inputEl.setAttribute("type", "number"); - }); - - new Setting(containerSyncSettingEl) - .setName("LongLine Threshold") - .setDesc("(letters), If the line is longer than this, make the line to chunk") - .addText((text) => { - text.setPlaceholder("") - .setValue(this.plugin.settings.longLineThreshold + "") - .onChange(async (value) => { - let v = Number(value); - if (isNaN(v) || v < 10 || v > 1000) { - return 10; - } - this.plugin.settings.longLineThreshold = v; - await this.plugin.saveSettings(); - }); - text.inputEl.setAttribute("type", "number"); - }); - - addScreenElement("30", containerSyncSettingEl); - const containerMiscellaneousEl = containerEl.createDiv(); - containerMiscellaneousEl.createEl("h3", { text: "Miscellaneous" }); - new Setting(containerMiscellaneousEl) - .setName("Show status inside editor") - .setDesc("") - .addToggle((toggle) => - toggle.setValue(this.plugin.settings.showStatusOnEditor).onChange(async (value) => { - this.plugin.settings.showStatusOnEditor = value; - await this.plugin.saveSettings(); - }) - ); - addScreenElement("40", containerMiscellaneousEl); - - const containerHatchEl = containerEl.createDiv(); - - containerHatchEl.createEl("h3", { text: "Hatch" }); - - if (this.plugin.localDatabase.remoteLockedAndDeviceNotAccepted) { - let c = containerHatchEl.createEl("div", { - text: "To prevent unwanted vault corruption, the remote database has been locked for synchronization, and this device was not marked as 'resolved'. it caused by some operations like this. re-initialized. Local database initialization should be required. please back your vault up, reset local database, and press 'Mark this device as resolved'. ", - }); - c.createEl("button", { text: "I'm ready, mark this device 'resolved'" }, (e) => { - e.addClass("mod-warning"); - e.addEventListener("click", async () => { - await this.plugin.markRemoteResolved(); - c.remove(); - }); - }); - c.addClass("op-warn"); - } else { - if (this.plugin.localDatabase.remoteLocked) { - let c = containerHatchEl.createEl("div", { - text: "To prevent unwanted vault corruption, the remote database has been locked for synchronization. (This device is marked 'resolved') When all your devices are marked 'resolved', unlock the database.", - }); - c.createEl("button", { text: "I'm ready, unlock the database" }, (e) => { - e.addClass("mod-warning"); - e.addEventListener("click", async () => { - await this.plugin.markRemoteUnlocked(); - c.remove(); - }); - }); - c.addClass("op-warn"); - } - } - const dropHistory = async (sendToServer: boolean) => { - this.plugin.settings.liveSync = false; - this.plugin.settings.periodicReplication = false; - this.plugin.settings.syncOnSave = false; - this.plugin.settings.syncOnStart = false; - this.plugin.settings.syncOnFileOpen = false; - - await this.plugin.saveSettings(); - applyDisplayEnabled(); - await this.plugin.resetLocalDatabase(); - if (sendToServer) { - await this.plugin.initializeDatabase(true); - await this.plugin.markRemoteLocked(); - await this.plugin.tryResetRemoteDatabase(); - await this.plugin.markRemoteLocked(); - await this.plugin.replicateAllToServer(true); - } else { - await this.plugin.markRemoteResolved(); - await this.plugin.replicate(true); - } - }; - new Setting(containerHatchEl) - .setName("Reread all files") - .setDesc("Reread all files and update database without dropping history") - .addButton((button) => - button - .setButtonText("Reread") - .setDisabled(false) - .setWarning() - .onClick(async () => { - const files = this.app.vault.getFiles(); - Logger("Reread all files started", LOG_LEVEL.NOTICE); - let notice = new Notice("", 0); - let i = 0; - for (const file of files) { - i++; - Logger(`Update into ${file.path}`); - notice.setMessage(`${i}/${files.length}\n${file.path}`); - try { - await this.plugin.updateIntoDB(file); - } catch (ex) { - Logger("could not update:"); - Logger(ex); - } - } - notice.hide(); - Logger("done", LOG_LEVEL.NOTICE); - }) - ); - - new Setting(containerHatchEl) - .setName("Drop History") - .setDesc("Initialize local and remote database, and send all or retrieve all again.") - .addButton((button) => - button - .setButtonText("Drop and send") - .setWarning() - .setDisabled(false) - .setClass("sls-btn-left") - .onClick(async () => { - await dropHistory(true); - }) - ) - .addButton((button) => - button - .setButtonText("Drop and receive") - .setWarning() - .setDisabled(false) - .setClass("sls-btn-right") - .onClick(async () => { - await dropHistory(false); - }) - ); - - new Setting(containerHatchEl) - .setName("Lock remote database") - .setDesc("Lock remote database for synchronize") - .addButton((button) => - button - .setButtonText("Lock") - .setDisabled(false) - .setWarning() - .onClick(async () => { - await this.plugin.markRemoteLocked(); - }) - ); - - new Setting(containerHatchEl) - .setName("Suspend file watching") - .setDesc("if enables it, all file operations are ignored.") - .addToggle((toggle) => - toggle.setValue(this.plugin.settings.suspendFileWatching).onChange(async (value) => { - this.plugin.settings.suspendFileWatching = value; - await this.plugin.saveSettings(); - }) - ); - - new Setting(containerHatchEl) - .setName("Reset remote database") - .setDesc("Reset remote database, this affects only database. If you replicate again, remote database will restored by local database.") - .addButton((button) => - button - .setButtonText("Reset") - .setDisabled(false) - .setWarning() - .onClick(async () => { - await this.plugin.tryResetRemoteDatabase(); - }) - ); - new Setting(containerHatchEl) - .setName("Reset local database") - .setDesc("Reset local database, this affects only database. If you replicate again, local database will restored by remote database.") - .addButton((button) => - button - .setButtonText("Reset") - .setDisabled(false) - .setWarning() - .onClick(async () => { - await this.plugin.resetLocalDatabase(); - }) - ); - new Setting(containerHatchEl) - .setName("Initialize local database again") - .setDesc("WARNING: Reset local database and reconstruct by storage data. It affects local database, but if you replicate remote as is, remote data will be merged or corrupted.") - .addButton((button) => - button - .setButtonText("INITIALIZE") - .setWarning() - .setDisabled(false) - .onClick(async () => { - await this.plugin.resetLocalDatabase(); - await this.plugin.initializeDatabase(); - }) - ); - - addScreenElement("50", containerHatchEl); - // With great respect, thank you TfTHacker! - // refered: https://github.com/TfTHacker/obsidian42-brat/blob/main/src/features/BetaPlugins.ts - - const containerPluginSettings = containerEl.createDiv(); - containerPluginSettings.createEl("h3", { text: "Plugins and settings (bleeding edge)" }); - - const updateDisabledOfDeviceAndVaultName = () => { - vaultName.setDisabled(this.plugin.settings.autoSweepPlugins || this.plugin.settings.autoSweepPluginsPeriodic); - vaultName.setTooltip(this.plugin.settings.autoSweepPlugins || this.plugin.settings.autoSweepPluginsPeriodic ? "You could not change when you enabling auto sweep." : ""); - }; - new Setting(containerPluginSettings).setName("Show own plugins and settings").addToggle((toggle) => - toggle.setValue(this.plugin.settings.showOwnPlugins).onChange(async (value) => { - this.plugin.settings.showOwnPlugins = value; - await this.plugin.saveSettings(); - updatePluginPane(); - }) - ); - - new Setting(containerPluginSettings) - .setName("Sweep plugins automatically") - .setDesc("Sweep plugins before replicating.") - .addToggle((toggle) => - toggle.setValue(this.plugin.settings.autoSweepPlugins).onChange(async (value) => { - this.plugin.settings.autoSweepPlugins = value; - updateDisabledOfDeviceAndVaultName(); - await this.plugin.saveSettings(); - }) - ); - - new Setting(containerPluginSettings) - .setName("Sweep plugins periodically") - .setDesc("Sweep plugins each 1 minutes.") - .addToggle((toggle) => - toggle.setValue(this.plugin.settings.autoSweepPluginsPeriodic).onChange(async (value) => { - this.plugin.settings.autoSweepPluginsPeriodic = value; - updateDisabledOfDeviceAndVaultName(); - await this.plugin.saveSettings(); - }) - ); - - new Setting(containerPluginSettings) - .setName("Notify updates") - .setDesc("Notify when any device has a newer plugin or its setting.") - .addToggle((toggle) => - toggle.setValue(this.plugin.settings.notifyPluginOrSettingUpdated).onChange(async (value) => { - this.plugin.settings.notifyPluginOrSettingUpdated = value; - await this.plugin.saveSettings(); - }) - ); - const vaultName = new Setting(containerPluginSettings) - .setName("Device and Vault name") - .setDesc("") - .addText((text) => { - text.setPlaceholder("desktop-main") - .setValue(this.plugin.settings.deviceAndVaultName) - .onChange(async (value) => { - this.plugin.settings.deviceAndVaultName = value; - await this.plugin.saveSettings(); - }); - // text.inputEl.setAttribute("type", "password"); - }); - - updateDisabledOfDeviceAndVaultName(); - const sweepPlugin = async (showMessage: boolean) => { - await this.plugin.sweepPlugin(showMessage); - updatePluginPane(); - }; - const updatePluginPane = async () => { - const { plugins, allPlugins, thisDevicePlugins } = await this.plugin.getPluginList(); - let html = ` -
- - `; - for (let vaults in plugins) { - if (!this.plugin.settings.showOwnPlugins && vaults == this.plugin.settings.deviceAndVaultName) continue; - html += ` - - - - `; - for (let v of plugins[vaults]) { - let mtime = v.mtime == 0 ? "-" : new Date(v.mtime).toLocaleString(); - let settingApplyable: boolean | string = "-"; - let settingFleshness: string = ""; - let isSameVersion = false; - let isSameContents = false; - if (thisDevicePlugins[v.manifest.id]) { - if (thisDevicePlugins[v.manifest.id].manifest.version == v.manifest.version) { - isSameVersion = true; - } - if (thisDevicePlugins[v.manifest.id].styleCss == v.styleCss && thisDevicePlugins[v.manifest.id].mainJs == v.mainJs && thisDevicePlugins[v.manifest.id].manifestJson == v.manifestJson) { - isSameContents = true; - } - } - if (thisDevicePlugins[v.manifest.id] && v.dataJson) { - // have this plugin. - let localSetting = thisDevicePlugins[v.manifest.id].dataJson || null; - - try { - let remoteSetting = v.dataJson; - if (!localSetting) { - settingFleshness = "newer"; - settingApplyable = true; - } else if (localSetting == remoteSetting) { - settingApplyable = "even"; - } else { - if (v.mtime > thisDevicePlugins[v.manifest.id].mtime) { - settingFleshness = "newer"; - } else { - settingFleshness = "older"; - } - settingApplyable = true; - } - } catch (ex) { - settingApplyable = "could not decrypt"; - } - } else if (!v.dataJson) { - settingApplyable = "N/A"; - } - // very ugly way. - let piece = ` - - - - - - - - - - - - `; - html += piece; - } - html += ` - - - -`; - } - html += "
${escapeStringToHTML(vaults)} - - - -
${escapeStringToHTML(v.manifest.name)}${isSameContents ? "even" : ``}
${escapeStringToHTML(mtime)}${settingApplyable === true ? "" : settingApplyable}
"; - pluginConfig.innerHTML = html; - pluginConfig.querySelectorAll(".apply-plugin-data").forEach((e) => - e.addEventListener("click", async (evt) => { - let plugin = allPlugins[e.attributes.getNamedItem("data-key").value]; - Logger(`Updating plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE); - await this.plugin.applyPluginData(plugin); - Logger(`Setting done:${plugin.manifest.name}`, LOG_LEVEL.NOTICE); - await sweepPlugin(true); - }) - ); - pluginConfig.querySelectorAll(".apply-plugin-version").forEach((e) => - e.addEventListener("click", async (evt) => { - let plugin = allPlugins[e.attributes.getNamedItem("data-key").value]; - Logger(`Setting plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE); - await this.plugin.applyPlugin(plugin); - Logger(`Updated plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE); - await sweepPlugin(true); - }) - ); - pluginConfig.querySelectorAll(".sls-plugin-apply-all-newer-plugin").forEach((e) => - e.addEventListener("click", async (evt) => { - Logger("Apply all newer plugins.", LOG_LEVEL.NOTICE); - const vaultname = e.attributes.getNamedItem("data-key").value; - let plugins = Object.values(allPlugins).filter((e) => e.deviceVaultName == vaultname && e.manifest.id != "obsidian-livesync"); - for (const plugin of plugins) { - let currentPlugin = thisDevicePlugins[plugin.manifest.id]; - if (currentPlugin) { - const thisVersion = plugin.manifest.version - .split(".") - .reverse() - .map((e, i) => ((e as any) / 1) * 1000 ** i) - .reduce((prev, current) => prev + current, 0); - const currentVersion = currentPlugin.manifest.version - .split(".") - .reverse() - .map((e, i) => ((e as any) / 1) * 1000 ** i) - .reduce((prev, current) => prev + current, 0); - if (thisVersion > currentVersion) { - Logger(`Updating plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE); - await this.plugin.applyPlugin(plugin); - Logger(`Updated plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE); - } else { - Logger(`Plugin ${plugin.manifest.name} is not new`); - } - } else { - Logger(`Updating plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE); - await this.plugin.applyPlugin(plugin); - Logger(`Updated plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE); - } - } - await sweepPlugin(true); - Logger("Done", LOG_LEVEL.NOTICE); - }) - ); - pluginConfig.querySelectorAll(".sls-plugin-apply-all-newer-setting").forEach((e) => - e.addEventListener("click", async (evt) => { - Logger("Apply all newer settings.", LOG_LEVEL.NOTICE); - const vaultname = e.attributes.getNamedItem("data-key").value; - let plugins = Object.values(allPlugins).filter((e) => e.deviceVaultName == vaultname && e.manifest.id != "obsidian-livesync"); - for (const plugin of plugins) { - let currentPlugin = thisDevicePlugins[plugin.manifest.id]; - if (currentPlugin) { - const thisVersion = plugin.mtime; - const currentVersion = currentPlugin.mtime; - if (thisVersion > currentVersion) { - Logger(`Setting plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE); - await this.plugin.applyPluginData(plugin); - Logger(`Setting done:${plugin.manifest.name}`, LOG_LEVEL.NOTICE); - } else { - Logger(`Setting ${plugin.manifest.name} is not new`); - } - } else { - Logger(`Setting plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE); - await this.plugin.applyPluginData(plugin); - Logger(`Setting done:${plugin.manifest.name}`, LOG_LEVEL.NOTICE); - } - } - await sweepPlugin(true); - Logger("Done", LOG_LEVEL.NOTICE); - }) - ); - pluginConfig.querySelectorAll(".sls-plugin-delete").forEach((e) => - e.addEventListener("click", async (evt) => { - const db = this.plugin.localDatabase.localDatabase; - const vaultname = e.attributes.getNamedItem("data-key").value; - let oldDocs = await db.allDocs({ startkey: `ps:${vaultname}-`, endkey: `ps:${vaultname}.`, include_docs: true }); - Logger(`Deleting ${vaultname}`, LOG_LEVEL.NOTICE); - let delDocs = oldDocs.rows.map((e) => { - e.doc._deleted = true; - return e.doc; - }); - await db.bulkDocs(delDocs); - Logger(`Deleted ${vaultname}`, LOG_LEVEL.NOTICE); - await this.plugin.replicate(true); - await updatePluginPane(); - }) - ); - }; - - let pluginConfig = containerPluginSettings.createEl("div"); - - new Setting(containerPluginSettings) - .setName("Reload") - .setDesc("Replicate once and reload the list") - .addButton((button) => - button - .setButtonText("Reload") - .setDisabled(false) - .onClick(async () => { - await this.plugin.replicate(true); - await updatePluginPane(); - }) - ); - new Setting(containerPluginSettings) - .setName("Save plugins into the database") - .setDesc("") - .addButton((button) => - button - .setButtonText("Save plugins") - .setDisabled(false) - .onClick(async () => { - Logger("Save plugins.", LOG_LEVEL.NOTICE); - await sweepPlugin(true); - Logger("All plugins have been saved.", LOG_LEVEL.NOTICE); - await this.plugin.replicate(true); - }) - ); - updatePluginPane(); - - addScreenElement("60", containerPluginSettings); - - const containerCorruptedDataEl = containerEl.createDiv(); - - containerCorruptedDataEl.createEl("h3", { text: "Corrupted data" }); - - if (Object.keys(this.plugin.localDatabase.corruptedEntries).length > 0) { - let cx = containerCorruptedDataEl.createEl("div", { text: "If you have copy of these items on any device, simply edit once or twice. Or not, delete this. sorry.." }); - for (let k in this.plugin.localDatabase.corruptedEntries) { - let xx = cx.createEl("div", { text: `${k}` }); - - let ba = xx.createEl("button", { text: `Delete this` }, (e) => { - e.addEventListener("click", async () => { - await this.plugin.localDatabase.deleteDBEntry(k); - xx.remove(); - }); - }); - ba.addClass("mod-warning"); - xx.createEl("button", { text: `Restore from file` }, (e) => { - e.addEventListener("click", async () => { - let f = await this.app.vault.getFiles().filter((e) => path2id(e.path) == k); - if (f.length == 0) { - Logger("Not found in vault", LOG_LEVEL.NOTICE); - return; - } - await this.plugin.updateIntoDB(f[0]); - xx.remove(); - }); - }); - xx.addClass("mod-warning"); - } - } else { - let cx = containerCorruptedDataEl.createEl("div", { text: "There's no collupted data." }); - } - applyDisplayEnabled(); - addScreenElement("70", containerCorruptedDataEl); - changeDisplay("0"); - } -} diff --git a/manifest.json b/manifest.json index 108eebb..8d10dc6 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "obsidian-livesync", "name": "Self-hosted LiveSync", - "version": "0.1.26", + "version": "0.1.27", "minAppVersion": "0.9.12", "description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.", "author": "vorotamoroz", diff --git a/package-lock.json b/package-lock.json index 5b3d556..0c6c14e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-livesync", - "version": "0.1.25", + "version": "0.1.27", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "obsidian-livesync", - "version": "0.1.25", + "version": "0.1.27", "license": "MIT", "dependencies": { "diff-match-patch": "^1.0.5", @@ -18,12 +18,204 @@ "@rollup/plugin-typescript": "^8.2.1", "@types/diff-match-patch": "^1.0.32", "@types/pouchdb-browser": "^6.1.3", + "@typescript-eslint/eslint-plugin": "^5.7.0", + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^7.32.0", + "eslint-config-airbnb-base": "^14.2.1", + "eslint-plugin-import": "^2.25.2", "obsidian": "^0.12.0", "rollup": "^2.32.1", "tslib": "^2.2.0", "typescript": "^4.2.4" } }, + "node_modules/@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.0.tgz", + "integrity": "sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.15.7", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", + "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@rollup/plugin-commonjs": { "version": "18.1.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-18.1.0.tgz", @@ -136,6 +328,18 @@ "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", "dev": true }, + "node_modules/@types/json-schema": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true + }, "node_modules/@types/ms": { "version": "0.7.31", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", @@ -245,6 +449,323 @@ "@types/estree": "*" } }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.7.0.tgz", + "integrity": "sha512-8RTGBpNn5a9M628wBPrCbJ+v3YTEOE2qeZb7TDkGKTDXSj36KGRg92SpFFaR/0S3rSXQxM0Og/kV9EyadsYSBg==", + "dev": true, + "dependencies": { + "@typescript-eslint/experimental-utils": "5.7.0", + "@typescript-eslint/scope-manager": "5.7.0", + "debug": "^4.3.2", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.1.8", + "regexpp": "^3.2.0", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/experimental-utils": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.7.0.tgz", + "integrity": "sha512-u57eZ5FbEpzN5kSjmVrSesovWslH2ZyNPnaXQMXWgH57d5+EVHEt76W75vVuI9qKZ5BMDKNfRN+pxcPEjQjb2A==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.7.0", + "@typescript-eslint/types": "5.7.0", + "@typescript-eslint/typescript-estree": "5.7.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/experimental-utils/node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.7.0.tgz", + "integrity": "sha512-m/gWCCcS4jXw6vkrPQ1BjZ1vomP01PArgzvauBqzsoZ3urLbsRChexB8/YV8z9HwE3qlJM35FxfKZ1nfP/4x8g==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.7.0", + "@typescript-eslint/types": "5.7.0", + "@typescript-eslint/typescript-estree": "5.7.0", + "debug": "^4.3.2" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.7.0.tgz", + "integrity": "sha512-7mxR520DGq5F7sSSgM0HSSMJ+TFUymOeFRMfUfGFAVBv8BR+Jv1vHgAouYUvWRZeszVBJlLcc9fDdktxb5kmxA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.7.0", + "@typescript-eslint/visitor-keys": "5.7.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.7.0.tgz", + "integrity": "sha512-5AeYIF5p2kAneIpnLFve8g50VyAjq7udM7ApZZ9JYjdPjkz0LvODfuSHIDUVnIuUoxafoWzpFyU7Sqbxgi79mA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.7.0.tgz", + "integrity": "sha512-aO1Ql+izMrTnPj5aFFlEJkpD4jRqC4Gwhygu2oHK2wfVQpmOPbyDSveJ+r/NQo+PWV43M6uEAeLVbTi09dFLhg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.7.0", + "@typescript-eslint/visitor-keys": "5.7.0", + "debug": "^4.3.2", + "globby": "^11.0.4", + "is-glob": "^4.0.3", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.7.0.tgz", + "integrity": "sha512-hdohahZ4lTFcglZSJ3DGdzxQHBSxsLVqHzkiOmKi7xVAWC4y2c1bIMKmPJSrA4aOEoRUPOKQ87Y/taC7yVHpFg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.7.0", + "eslint-visitor-keys": "^3.0.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-includes": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz", + "integrity": "sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1", + "get-intrinsic": "^1.1.1", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz", + "integrity": "sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -261,6 +782,18 @@ "concat-map": "0.0.1" } }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/builtin-modules": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", @@ -273,6 +806,62 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -285,6 +874,49 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "node_modules/confusing-browser-globals": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, "node_modules/deepmerge": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", @@ -294,17 +926,563 @@ "node": ">=0.10.0" } }, + "node_modules/define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "dependencies": { + "object-keys": "^1.0.12" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/diff-match-patch": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==" }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/es-abstract": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", + "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.4", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.1", + "is-string": "^1.0.7", + "is-weakref": "^1.0.1", + "object-inspect": "^1.11.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-airbnb-base": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz", + "integrity": "sha512-GOrQyDtVEc1Xy20U7vsB2yAoB4nBlfH5HZJeatRXHleO+OS5Ot+MWij4Dpltw4/DyIkqUfqz1epfhVR5XWWQPA==", + "dev": true, + "dependencies": { + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.2" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "eslint": "^5.16.0 || ^6.8.0 || ^7.2.0", + "eslint-plugin-import": "^2.22.1" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", + "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "resolve": "^1.20.0" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.1.tgz", + "integrity": "sha512-fjoetBXQZq2tSTWZ9yWVl2KuFrTZZH3V+9iD1V1RfpDgxzJR+mPd/KZmMiA8gbPqdBzpNiEHOuT7IYEWxrH0zQ==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "find-up": "^2.1.0", + "pkg-dir": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.25.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.25.3.tgz", + "integrity": "sha512-RzAVbby+72IB3iOEL8clzPLzL3wpDrlwjsTBAQXgyp5SeTqqY+0bFubwuo+y/HLhNZcXV4XqTBO4LGsfyHIDXg==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.4", + "array.prototype.flat": "^1.2.5", + "debug": "^2.6.9", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-module-utils": "^2.7.1", + "has": "^1.0.3", + "is-core-module": "^2.8.0", + "is-glob": "^4.0.3", + "minimatch": "^3.0.4", + "object.values": "^1.1.5", + "resolve": "^1.20.0", + "tsconfig-paths": "^3.11.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz", + "integrity": "sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "dev": true, + "dependencies": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", + "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz", + "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", + "dev": true + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -331,6 +1509,42 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "node_modules/get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", @@ -351,6 +1565,53 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", + "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", + "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -363,6 +1624,85 @@ "node": ">= 0.4.0" } }, + "node_modules/has-bigints": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ignore": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", + "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -379,10 +1719,64 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.7.0.tgz", - "integrity": "sha512-ByY+tjCciCr+9nLryBYcSD50EOGWt95c7tIsKTG1J2ixKKXPvF7Ej3AVd+UfDydAJom3biBGDBALaO79ktwgEQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", + "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", "dev": true, "dependencies": { "has": "^1.0.3" @@ -391,12 +1785,93 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", "dev": true }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", + "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-reference": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", @@ -406,6 +1881,172 @@ "@types/estree": "*" } }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", + "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "node_modules/json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", + "dev": true + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/magic-string": { "version": "0.25.7", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", @@ -415,6 +2056,28 @@ "sourcemap-codec": "^1.4.4" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "dependencies": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -427,6 +2090,12 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, "node_modules/moment": { "version": "2.29.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", @@ -436,6 +2105,85 @@ "node": "*" } }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "node_modules/object-inspect": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.1.tgz", + "integrity": "sha512-If7BjFlpkzzBeV1cqgT3OSWT3azyoxDGajR+iGnFBfVV2EWyDyWaZZW2ERDjUaY2QM8i5jI3Sj7mhsM4DDAqWA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz", + "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", + "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/obsidian": { "version": "0.12.17", "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.12.17.tgz", @@ -455,6 +2203,77 @@ "wrappy": "1" } }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -464,12 +2283,30 @@ "node": ">=0.10.0" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/picomatch": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", @@ -482,6 +2319,86 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "dependencies": { + "find-up": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", @@ -495,6 +2412,40 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/rollup": { "version": "2.58.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.58.0.tgz", @@ -510,18 +2461,321 @@ "fsevents": "~2.3.2" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, "node_modules/sourcemap-codec": { "version": "1.4.8", "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", "dev": true }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/table": { + "version": "6.7.5", + "resolved": "https://registry.npmjs.org/table/-/table-6.7.5.tgz", + "integrity": "sha512-LFNeryOqiQHqCVKzhkymKwt6ozeRhlm8IL1mE8rNUurkir4heF6PzMyRgaTa4tlyPTGGgXuvVOF/OLWiH09Lqw==", + "dev": true, + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ajv": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", + "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/table/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz", + "integrity": "sha512-e5adrnOYT6zqVnWqZu7i/BQ3BnhzvGbjEjejFXO20lKIKpwTaupkCPgEfv4GZK1IBciJUEhYs3J3p75FdaTFVg==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + } + }, "node_modules/tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", "dev": true }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typescript": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz", @@ -535,6 +2789,76 @@ "node": ">=4.2.0" } }, + "node_modules/unbox-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", + "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has-bigints": "^1.0.1", + "has-symbols": "^1.0.2", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -545,9 +2869,167 @@ "version": "0.4.2", "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-0.4.2.tgz", "integrity": "sha512-/eyHVRJQCirEkSZ1agRSCwriMhwlyUcFkXD5TPVSLP+IPzjsqMVzZwdoczLp1SoQU0R3dxz1RpIK+4YNQbCVOA==" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true } }, "dependencies": { + "@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true + }, + "@babel/highlight": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.0.tgz", + "integrity": "sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.15.7", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@eslint/eslintrc": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", + "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + } + } + }, + "@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + } + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, "@rollup/plugin-commonjs": { "version": "18.1.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-18.1.0.tgz", @@ -636,6 +3118,18 @@ "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", "dev": true }, + "@types/json-schema": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", + "dev": true + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true + }, "@types/ms": { "version": "0.7.31", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", @@ -745,6 +3239,199 @@ "@types/estree": "*" } }, + "@typescript-eslint/eslint-plugin": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.7.0.tgz", + "integrity": "sha512-8RTGBpNn5a9M628wBPrCbJ+v3YTEOE2qeZb7TDkGKTDXSj36KGRg92SpFFaR/0S3rSXQxM0Og/kV9EyadsYSBg==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "5.7.0", + "@typescript-eslint/scope-manager": "5.7.0", + "debug": "^4.3.2", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.1.8", + "regexpp": "^3.2.0", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "dependencies": { + "@typescript-eslint/experimental-utils": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.7.0.tgz", + "integrity": "sha512-u57eZ5FbEpzN5kSjmVrSesovWslH2ZyNPnaXQMXWgH57d5+EVHEt76W75vVuI9qKZ5BMDKNfRN+pxcPEjQjb2A==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.7.0", + "@typescript-eslint/types": "5.7.0", + "@typescript-eslint/typescript-estree": "5.7.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "dependencies": { + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + } + } + } + }, + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + } + } + }, + "@typescript-eslint/parser": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.7.0.tgz", + "integrity": "sha512-m/gWCCcS4jXw6vkrPQ1BjZ1vomP01PArgzvauBqzsoZ3urLbsRChexB8/YV8z9HwE3qlJM35FxfKZ1nfP/4x8g==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.7.0", + "@typescript-eslint/types": "5.7.0", + "@typescript-eslint/typescript-estree": "5.7.0", + "debug": "^4.3.2" + } + }, + "@typescript-eslint/scope-manager": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.7.0.tgz", + "integrity": "sha512-7mxR520DGq5F7sSSgM0HSSMJ+TFUymOeFRMfUfGFAVBv8BR+Jv1vHgAouYUvWRZeszVBJlLcc9fDdktxb5kmxA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.7.0", + "@typescript-eslint/visitor-keys": "5.7.0" + } + }, + "@typescript-eslint/types": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.7.0.tgz", + "integrity": "sha512-5AeYIF5p2kAneIpnLFve8g50VyAjq7udM7ApZZ9JYjdPjkz0LvODfuSHIDUVnIuUoxafoWzpFyU7Sqbxgi79mA==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.7.0.tgz", + "integrity": "sha512-aO1Ql+izMrTnPj5aFFlEJkpD4jRqC4Gwhygu2oHK2wfVQpmOPbyDSveJ+r/NQo+PWV43M6uEAeLVbTi09dFLhg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.7.0", + "@typescript-eslint/visitor-keys": "5.7.0", + "debug": "^4.3.2", + "globby": "^11.0.4", + "is-glob": "^4.0.3", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.7.0.tgz", + "integrity": "sha512-hdohahZ4lTFcglZSJ3DGdzxQHBSxsLVqHzkiOmKi7xVAWC4y2c1bIMKmPJSrA4aOEoRUPOKQ87Y/taC7yVHpFg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.7.0", + "eslint-visitor-keys": "^3.0.0" + } + }, + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "array-includes": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz", + "integrity": "sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1", + "get-intrinsic": "^1.1.1", + "is-string": "^1.0.7" + } + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "array.prototype.flat": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz", + "integrity": "sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0" + } + }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -761,12 +3448,62 @@ "concat-map": "0.0.1" } }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, "builtin-modules": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", "dev": true }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -779,23 +3516,492 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "confusing-browser-globals": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, "deepmerge": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", "dev": true }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, "diff-match-patch": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==" }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + } + }, + "es-abstract": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", + "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.4", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.1", + "is-string": "^1.0.7", + "is-weakref": "^1.0.1", + "object-inspect": "^1.11.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint": { + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "dev": true, + "requires": { + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + } + } + }, + "eslint-config-airbnb-base": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz", + "integrity": "sha512-GOrQyDtVEc1Xy20U7vsB2yAoB4nBlfH5HZJeatRXHleO+OS5Ot+MWij4Dpltw4/DyIkqUfqz1epfhVR5XWWQPA==", + "dev": true, + "requires": { + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.2" + } + }, + "eslint-import-resolver-node": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", + "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "dev": true, + "requires": { + "debug": "^3.2.7", + "resolve": "^1.20.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "eslint-module-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.1.tgz", + "integrity": "sha512-fjoetBXQZq2tSTWZ9yWVl2KuFrTZZH3V+9iD1V1RfpDgxzJR+mPd/KZmMiA8gbPqdBzpNiEHOuT7IYEWxrH0zQ==", + "dev": true, + "requires": { + "debug": "^3.2.7", + "find-up": "^2.1.0", + "pkg-dir": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "eslint-plugin-import": { + "version": "2.25.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.25.3.tgz", + "integrity": "sha512-RzAVbby+72IB3iOEL8clzPLzL3wpDrlwjsTBAQXgyp5SeTqqY+0bFubwuo+y/HLhNZcXV4XqTBO4LGsfyHIDXg==", + "dev": true, + "requires": { + "array-includes": "^3.1.4", + "array.prototype.flat": "^1.2.5", + "debug": "^2.6.9", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-module-utils": "^2.7.1", + "has": "^1.0.3", + "is-core-module": "^2.8.0", + "is-glob": "^4.0.3", + "minimatch": "^3.0.4", + "object.values": "^1.1.5", + "resolve": "^1.20.0", + "tsconfig-paths": "^3.11.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz", + "integrity": "sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==", + "dev": true + }, + "espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "dev": true, + "requires": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, "estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-glob": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", + "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz", + "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", + "dev": true + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -815,6 +4021,33 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, "glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", @@ -829,6 +4062,38 @@ "path-is-absolute": "^1.0.0" } }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", + "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "globby": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", + "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + } + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -838,6 +4103,55 @@ "function-bind": "^1.1.1" } }, + "has-bigints": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "ignore": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", + "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -854,21 +4168,108 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-callable": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", + "dev": true + }, "is-core-module": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.7.0.tgz", - "integrity": "sha512-ByY+tjCciCr+9nLryBYcSD50EOGWt95c7tIsKTG1J2ixKKXPvF7Ej3AVd+UfDydAJom3biBGDBALaO79ktwgEQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", + "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", "dev": true, "requires": { "has": "^1.0.3" } }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, "is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", "dev": true }, + "is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-number-object": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", + "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, "is-reference": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", @@ -878,6 +4279,133 @@ "@types/estree": "*" } }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-shared-array-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", + "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==", + "dev": true + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, "magic-string": { "version": "0.25.7", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", @@ -887,6 +4415,22 @@ "sourcemap-codec": "^1.4.4" } }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + } + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -896,12 +4440,76 @@ "brace-expansion": "^1.1.7" } }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, "moment": { "version": "2.29.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==", "dev": true }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "object-inspect": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.1.tgz", + "integrity": "sha512-If7BjFlpkzzBeV1cqgT3OSWT3azyoxDGajR+iGnFBfVV2EWyDyWaZZW2ERDjUaY2QM8i5jI3Sj7mhsM4DDAqWA==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "object.entries": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz", + "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, + "object.values": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", + "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, "obsidian": { "version": "0.12.17", "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.12.17.tgz", @@ -921,24 +4529,134 @@ "wrappy": "1" } }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, "path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, "picomatch": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", "dev": true }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "^2.1.0" + } + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, "resolve": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", @@ -949,6 +4667,27 @@ "path-parse": "^1.0.6" } }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, "rollup": { "version": "2.58.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.58.0.tgz", @@ -958,24 +4697,299 @@ "fsevents": "~2.3.2" } }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + }, "sourcemap-codec": { "version": "1.4.8", "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", "dev": true }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "table": { + "version": "6.7.5", + "resolved": "https://registry.npmjs.org/table/-/table-6.7.5.tgz", + "integrity": "sha512-LFNeryOqiQHqCVKzhkymKwt6ozeRhlm8IL1mE8rNUurkir4heF6PzMyRgaTa4tlyPTGGgXuvVOF/OLWiH09Lqw==", + "dev": true, + "requires": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "ajv": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", + "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "tsconfig-paths": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz", + "integrity": "sha512-e5adrnOYT6zqVnWqZu7i/BQ3BnhzvGbjEjejFXO20lKIKpwTaupkCPgEfv4GZK1IBciJUEhYs3J3p75FdaTFVg==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + } + }, "tslib": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", "dev": true }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, "typescript": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz", "integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==", "dev": true }, + "unbox-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", + "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has-bigints": "^1.0.1", + "has-symbols": "^1.0.2", + "which-boxed-primitive": "^1.0.2" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -986,6 +5000,12 @@ "version": "0.4.2", "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-0.4.2.tgz", "integrity": "sha512-/eyHVRJQCirEkSZ1agRSCwriMhwlyUcFkXD5TPVSLP+IPzjsqMVzZwdoczLp1SoQU0R3dxz1RpIK+4YNQbCVOA==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true } } } diff --git a/package.json b/package.json index 82677b8..77649b6 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,12 @@ { "name": "obsidian-livesync", - "version": "0.1.26", + "version": "0.1.27", "description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.", "main": "main.js", "scripts": { "dev": "rollup --config rollup.config.js -w", - "build": "rollup --config rollup.config.js --environment BUILD:production" + "build": "rollup --config rollup.config.js --environment BUILD:production", + "lint": "eslint src" }, "keywords": [], "author": "vorotamoroz", @@ -16,6 +17,11 @@ "@rollup/plugin-typescript": "^8.2.1", "@types/diff-match-patch": "^1.0.32", "@types/pouchdb-browser": "^6.1.3", + "@typescript-eslint/eslint-plugin": "^5.7.0", + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^7.32.0", + "eslint-config-airbnb-base": "^14.2.1", + "eslint-plugin-import": "^2.25.2", "obsidian": "^0.12.0", "rollup": "^2.32.1", "tslib": "^2.2.0", diff --git a/rollup.config.js b/rollup.config.js index 0de2f74..c760aff 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -11,7 +11,7 @@ if you want to view the source visit the plugins github repository `; export default { - input: "main.ts", + input: "./src/main.ts", output: { dir: ".", sourcemap: "inline", diff --git a/src/ConflictResolveModal.ts b/src/ConflictResolveModal.ts new file mode 100644 index 0000000..d858b19 --- /dev/null +++ b/src/ConflictResolveModal.ts @@ -0,0 +1,74 @@ +import { App, Modal } from "obsidian"; +import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT } from "diff-match-patch"; +import { diff_result } from "./types"; +import { escapeStringToHTML } from "./utils"; + +export class ConflictResolveModal extends Modal { + // result: Array<[number, string]>; + result: diff_result; + callback: (remove_rev: string) => Promise; + constructor(app: App, diff: diff_result, callback: (remove_rev: string) => Promise) { + super(app); + this.result = diff; + this.callback = callback; + } + + onOpen() { + const { contentEl } = this; + + contentEl.empty(); + + contentEl.createEl("h2", { text: "This document has conflicted changes." }); + const div = contentEl.createDiv(""); + div.addClass("op-scrollable"); + let diff = ""; + for (const v of this.result.diff) { + const x1 = v[0]; + const x2 = v[1]; + if (x1 == DIFF_DELETE) { + diff += "" + escapeStringToHTML(x2) + ""; + } else if (x1 == DIFF_EQUAL) { + diff += "" + escapeStringToHTML(x2) + ""; + } else if (x1 == DIFF_INSERT) { + diff += "" + escapeStringToHTML(x2) + ""; + } + } + + diff = diff.replace(/\n/g, "
"); + div.innerHTML = diff; + const div2 = contentEl.createDiv(""); + const date1 = new Date(this.result.left.mtime).toLocaleString(); + const date2 = new Date(this.result.right.mtime).toLocaleString(); + div2.innerHTML = ` +A:${date1}
B:${date2}
+ `; + contentEl.createEl("button", { text: "Keep A" }, (e) => { + e.addEventListener("click", async () => { + await this.callback(this.result.right.rev); + this.close(); + }); + }); + contentEl.createEl("button", { text: "Keep B" }, (e) => { + e.addEventListener("click", async () => { + await this.callback(this.result.left.rev); + this.close(); + }); + }); + contentEl.createEl("button", { text: "Concat both" }, (e) => { + e.addEventListener("click", async () => { + await this.callback(null); + this.close(); + }); + }); + contentEl.createEl("button", { text: "Not now" }, (e) => { + e.addEventListener("click", () => { + this.close(); + }); + }); + } + + onClose() { + const { contentEl } = this; + contentEl.empty(); + } +} diff --git a/src/LocalPouchDB.ts b/src/LocalPouchDB.ts new file mode 100644 index 0000000..dd9aac9 --- /dev/null +++ b/src/LocalPouchDB.ts @@ -0,0 +1,1229 @@ +import { Notice } from "obsidian"; +import { PouchDB } from "../pouchdb-browser-webpack/dist/pouchdb-browser.js"; +import xxhash from "xxhash-wasm"; +import { + Entry, + EntryDoc, + EntryDocResponse, + EntryLeaf, + EntryNodeInfo, + NewEntry, + PlainEntry, + LoadedEntry, + ObsidianLiveSyncSettings, + Credential, + EntryMilestoneInfo, + LOG_LEVEL, + LEAF_WAIT_TIMEOUT, + MAX_DOC_SIZE, + MAX_DOC_SIZE_BIN, + NODEINFO_DOCID, + RECENT_MOFIDIED_DOCS_QTY, + VER, + MILSTONE_DOCID, + DatabaseConnectingStatus, +} from "./types"; +import { resolveWithIgnoreKnownError, delay, path2id, runWithLock } from "./utils"; +import { Logger } from "./logger"; +import { checkRemoteVersion, connectRemoteCouchDB } from "./utils_couchdb"; +import { decrypt, encrypt } from "./e2ee"; + +export class LocalPouchDB { + auth: Credential; + dbname: string; + settings: ObsidianLiveSyncSettings; + localDatabase: PouchDB.Database; + nodeid = ""; + isReady = false; + + recentModifiedDocs: string[] = []; + h32: (input: string, seed?: number) => string; + h64: (input: string, seedHigh?: number, seedLow?: number) => string; + h32Raw: (input: Uint8Array, seed?: number) => number; + hashCache: { + [key: string]: string; + } = {}; + hashCacheRev: { + [key: string]: string; + } = {}; + + corruptedEntries: { [key: string]: EntryDoc } = {}; + remoteLocked = false; + remoteLockedAndDeviceNotAccepted = false; + + changeHandler: PouchDB.Core.Changes = null; + syncHandler: PouchDB.Replication.Sync = null; + + leafArrivedCallbacks: { [key: string]: (() => void)[] } = {}; + + syncStatus: DatabaseConnectingStatus = "NOT_CONNECTED"; + docArrived = 0; + docSent = 0; + docSeq = ""; + + cancelHandler | PouchDB.Replication.Sync | PouchDB.Replication.Replication>(handler: T): T { + if (handler != null) { + handler.removeAllListeners(); + handler.cancel(); + handler = null; + } + return null; + } + onunload() { + this.recentModifiedDocs = []; + this.leafArrivedCallbacks; + this.changeHandler = this.cancelHandler(this.changeHandler); + this.syncHandler = this.cancelHandler(this.syncHandler); + this.localDatabase.removeAllListeners(); + } + + constructor(settings: ObsidianLiveSyncSettings, dbname: string) { + this.auth = { + username: "", + password: "", + }; + this.dbname = dbname; + this.settings = settings; + this.cancelHandler = this.cancelHandler.bind(this); + + // this.initializeDatabase(); + } + close() { + Logger("Database closed (by close)"); + this.isReady = false; + this.changeHandler = this.cancelHandler(this.changeHandler); + if (this.localDatabase != null) { + this.localDatabase.close(); + } + } + disposeHashCache() { + this.hashCache = {}; + this.hashCacheRev = {}; + } + + updateRecentModifiedDocs(id: string, rev: string, deleted: boolean) { + const idrev = id + rev; + if (deleted) { + this.recentModifiedDocs = this.recentModifiedDocs.filter((e) => e != idrev); + } else { + this.recentModifiedDocs.push(idrev); + this.recentModifiedDocs = this.recentModifiedDocs.slice(0 - RECENT_MOFIDIED_DOCS_QTY); + } + } + isSelfModified(id: string, rev: string): boolean { + const idrev = id + rev; + return this.recentModifiedDocs.indexOf(idrev) !== -1; + } + + async initializeDatabase() { + await this.prepareHashFunctions(); + if (this.localDatabase != null) this.localDatabase.close(); + this.changeHandler = this.cancelHandler(this.changeHandler); + this.localDatabase = null; + this.localDatabase = new PouchDB(this.dbname + "-livesync", { + auto_compaction: true, + revs_limit: 100, + deterministic_revs: true, + }); + + Logger("Database Info"); + Logger(await this.localDatabase.info(), LOG_LEVEL.VERBOSE); + // initialize local node information. + const nodeinfo: EntryNodeInfo = await resolveWithIgnoreKnownError(this.localDatabase.get(NODEINFO_DOCID), { + _id: NODEINFO_DOCID, + type: "nodeinfo", + nodeid: "", + }); + if (nodeinfo.nodeid == "") { + nodeinfo.nodeid = Math.random().toString(36).slice(-10); + await this.localDatabase.put(nodeinfo); + } + this.localDatabase.on("close", () => { + Logger("Database closed."); + this.isReady = false; + this.localDatabase.removeAllListeners(); + }); + this.nodeid = nodeinfo.nodeid; + + // Traceing the leaf id + const changes = this.localDatabase + .changes({ + since: "now", + live: true, + filter: (doc) => doc.type == "leaf", + }) + .on("change", (e) => { + if (e.deleted) return; + this.leafArrived(e.id); + this.docSeq = `${e.seq}`; + }); + this.changeHandler = changes; + this.isReady = true; + Logger("Database is now ready."); + } + + async prepareHashFunctions() { + if (this.h32 != null) return; + const { h32, h64, h32Raw } = await xxhash(); + this.h32 = h32; + this.h64 = h64; + this.h32Raw = h32Raw; + } + + // leaf waiting + + leafArrived(id: string) { + if (typeof this.leafArrivedCallbacks[id] !== "undefined") { + for (const func of this.leafArrivedCallbacks[id]) { + func(); + } + delete this.leafArrivedCallbacks[id]; + } + } + // wait + waitForLeafReady(id: string): Promise { + return new Promise((res, rej) => { + // Set timeout. + const timer = setTimeout(() => rej(new Error(`Leaf timed out:${id}`)), LEAF_WAIT_TIMEOUT); + if (typeof this.leafArrivedCallbacks[id] == "undefined") { + this.leafArrivedCallbacks[id] = []; + } + this.leafArrivedCallbacks[id].push(() => { + clearTimeout(timer); + res(true); + }); + }); + } + + async getDBLeaf(id: string, waitForReady: boolean): Promise { + await this.waitForGCComplete(); + // when in cache, use that. + if (this.hashCacheRev[id]) { + return this.hashCacheRev[id]; + } + try { + const w = await this.localDatabase.get(id); + if (w.type == "leaf") { + if (id.startsWith("h:+")) { + try { + w.data = await decrypt(w.data, this.settings.passphrase); + } catch (e) { + Logger("The element of the document has been encrypted, but decryption failed.", LOG_LEVEL.NOTICE); + throw e; + } + } + this.hashCache[w.data] = id; + this.hashCacheRev[id] = w.data; + return w.data; + } + throw new Error(`retrive leaf, but it was not leaf.`); + } catch (ex) { + if (ex.status && ex.status == 404 && waitForReady) { + // just leaf is not ready. + // wait for on + if ((await this.waitForLeafReady(id)) === false) { + throw new Error(`time out (waiting leaf)`); + } + try { + // retrive again. + const w = await this.localDatabase.get(id); + if (w.type == "leaf") { + if (id.startsWith("h:+")) { + try { + w.data = await decrypt(w.data, this.settings.passphrase); + } catch (e) { + Logger("The element of the document has been encrypted, but decryption failed.", LOG_LEVEL.NOTICE); + throw e; + } + } + this.hashCache[w.data] = id; + this.hashCacheRev[id] = w.data; + return w.data; + } + throw new Error(`retrive leaf, but it was not leaf.`); + } catch (ex) { + if (ex.status && ex.status == 404) { + throw new Error("leaf is not found"); + } + Logger(`Something went wrong on retriving leaf`); + throw ex; + } + } else { + Logger(`Something went wrong on retriving leaf`); + throw ex; + } + } + } + + async getDBEntryMeta(path: string, opt?: PouchDB.Core.GetOptions): Promise { + await this.waitForGCComplete(); + const id = path2id(path); + try { + let obj: EntryDocResponse = null; + if (opt) { + obj = await this.localDatabase.get(id, opt); + } else { + obj = await this.localDatabase.get(id); + } + + if (obj.type && obj.type == "leaf") { + //do nothing for leaf; + return false; + } + + // retrieve metadata only + if (!obj.type || (obj.type && obj.type == "notes") || obj.type == "newnote" || obj.type == "plain") { + const note = obj as Entry; + let children: string[] = []; + if (obj.type == "newnote" || obj.type == "plain") { + children = obj.children; + } + const doc: LoadedEntry & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta = { + data: "", + _id: note._id, + ctime: note.ctime, + mtime: note.mtime, + size: note.size, + _deleted: obj._deleted, + _rev: obj._rev, + _conflicts: obj._conflicts, + children: children, + datatype: "newnote", + }; + return doc; + } + } catch (ex) { + if (ex.status && ex.status == 404) { + return false; + } + throw ex; + } + return false; + } + async getDBEntry(path: string, opt?: PouchDB.Core.GetOptions, dump = false, waitForReady = true): Promise { + await this.waitForGCComplete(); + const id = path2id(path); + try { + let obj: EntryDocResponse = null; + if (opt) { + obj = await this.localDatabase.get(id, opt); + } else { + obj = await this.localDatabase.get(id); + } + + if (obj.type && obj.type == "leaf") { + //do nothing for leaf; + return false; + } + + //Check it out and fix docs to regular case + if (!obj.type || (obj.type && obj.type == "notes")) { + const note = obj as Entry; + const doc: LoadedEntry & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta = { + data: note.data, + _id: note._id, + ctime: note.ctime, + mtime: note.mtime, + size: note.size, + _deleted: obj._deleted, + _rev: obj._rev, + _conflicts: obj._conflicts, + children: [], + datatype: "newnote", + }; + if (typeof this.corruptedEntries[doc._id] != "undefined") { + delete this.corruptedEntries[doc._id]; + } + if (dump) { + Logger(`Simple doc`); + Logger(doc); + } + + return doc; + // simple note + } + if (obj.type == "newnote" || obj.type == "plain") { + // search childrens + try { + if (dump) { + Logger(`Enhanced doc`); + Logger(obj); + } + let childrens: string[]; + try { + childrens = await Promise.all(obj.children.map((e) => this.getDBLeaf(e, waitForReady))); + if (dump) { + Logger(`childrens:`); + Logger(childrens); + } + } catch (ex) { + Logger(`Something went wrong on reading elements of ${obj._id} from database.`, LOG_LEVEL.NOTICE); + Logger(ex, LOG_LEVEL.VERBOSE); + this.corruptedEntries[obj._id] = obj; + return false; + } + const data = childrens.join(""); + const doc: LoadedEntry & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta = { + data: data, + _id: obj._id, + ctime: obj.ctime, + mtime: obj.mtime, + size: obj.size, + _deleted: obj._deleted, + _rev: obj._rev, + children: obj.children, + datatype: obj.type, + _conflicts: obj._conflicts, + }; + if (dump) { + Logger(`therefore:`); + Logger(doc); + } + if (typeof this.corruptedEntries[doc._id] != "undefined") { + delete this.corruptedEntries[doc._id]; + } + return doc; + } catch (ex) { + if (ex.status && ex.status == 404) { + Logger(`Missing document content!, could not read ${obj._id} from database.`, LOG_LEVEL.NOTICE); + return false; + } + Logger(`Something went wrong on reading ${obj._id} from database.`, LOG_LEVEL.NOTICE); + Logger(ex); + } + } + } catch (ex) { + if (ex.status && ex.status == 404) { + return false; + } + throw ex; + } + return false; + } + async deleteDBEntry(path: string, opt?: PouchDB.Core.GetOptions): Promise { + await this.waitForGCComplete(); + const id = path2id(path); + try { + let obj: EntryDocResponse = null; + if (opt) { + obj = await this.localDatabase.get(id, opt); + } else { + obj = await this.localDatabase.get(id); + } + + if (obj.type && obj.type == "leaf") { + //do nothing for leaf; + return false; + } + //Check it out and fix docs to regular case + if (!obj.type || (obj.type && obj.type == "notes")) { + obj._deleted = true; + const r = await this.localDatabase.put(obj); + this.updateRecentModifiedDocs(r.id, r.rev, true); + if (typeof this.corruptedEntries[obj._id] != "undefined") { + delete this.corruptedEntries[obj._id]; + } + return true; + // simple note + } + if (obj.type == "newnote" || obj.type == "plain") { + obj._deleted = true; + const r = await this.localDatabase.put(obj); + Logger(`entry removed:${obj._id}-${r.rev}`); + this.updateRecentModifiedDocs(r.id, r.rev, true); + if (typeof this.corruptedEntries[obj._id] != "undefined") { + delete this.corruptedEntries[obj._id]; + } + return true; + } else { + return false; + } + } catch (ex) { + if (ex.status && ex.status == 404) { + return false; + } + throw ex; + } + } + async deleteDBEntryPrefix(prefixSrc: string): Promise { + await this.waitForGCComplete(); + // delete database entries by prefix. + // it called from folder deletion. + let c = 0; + let readCount = 0; + const delDocs: string[] = []; + const prefix = path2id(prefixSrc); + do { + const result = await this.localDatabase.allDocs({ include_docs: false, skip: c, limit: 100, conflicts: true }); + readCount = result.rows.length; + if (readCount > 0) { + //there are some result + for (const v of result.rows) { + // let doc = v.doc; + if (v.id.startsWith(prefix) || v.id.startsWith("/" + prefix)) { + delDocs.push(v.id); + // console.log("!" + v.id); + } else { + if (!v.id.startsWith("h:")) { + // console.log("?" + v.id); + } + } + } + } + c += readCount; + } while (readCount != 0); + // items collected. + //bulk docs to delete? + let deleteCount = 0; + let notfound = 0; + for (const v of delDocs) { + try { + const item = await this.localDatabase.get(v); + item._deleted = true; + await this.localDatabase.put(item); + this.updateRecentModifiedDocs(item._id, item._rev, true); + deleteCount++; + } catch (ex) { + if (ex.status && ex.status == 404) { + notfound++; + // NO OP. It should be timing problem. + } else { + throw ex; + } + } + } + Logger(`deleteDBEntryPrefix:deleted ${deleteCount} items, skipped ${notfound}`); + return true; + } + isPlainText(filename: string): boolean { + if (filename.endsWith(".md")) return true; + if (filename.endsWith(".txt")) return true; + if (filename.endsWith(".svg")) return true; + if (filename.endsWith(".html")) return true; + if (filename.endsWith(".csv")) return true; + if (filename.endsWith(".css")) return true; + if (filename.endsWith(".js")) return true; + if (filename.endsWith(".xml")) return true; + + return false; + } + async putDBEntry(note: LoadedEntry) { + await this.waitForGCComplete(); + let leftData = note.data; + const savenNotes = []; + let processed = 0; + let made = 0; + let skiped = 0; + let pieceSize = MAX_DOC_SIZE_BIN; + let plainSplit = false; + let cacheUsed = 0; + const userpasswordHash = this.h32Raw(new TextEncoder().encode(this.settings.passphrase)); + if (this.isPlainText(note._id)) { + pieceSize = MAX_DOC_SIZE; + plainSplit = true; + } + const newLeafs: EntryLeaf[] = []; + do { + // To keep low bandwith and database size, + // Dedup pieces on database. + // from 0.1.10, for best performance. we use markdown delimiters + // 1. \n[^\n]{longLineThreshold}[^\n]*\n -> long sentence shuld break. + // 2. \n\n shold break + // 3. \r\n\r\n should break + // 4. \n# should break. + let cPieceSize = pieceSize; + if (plainSplit) { + let minimumChunkSize = this.settings.minimumChunkSize; + if (minimumChunkSize < 10) minimumChunkSize = 10; + let longLineThreshold = this.settings.longLineThreshold; + if (longLineThreshold < 100) longLineThreshold = 100; + cPieceSize = 0; + // lookup for next splittion . + // we're standing on "\n" + // debugger + do { + const n1 = leftData.indexOf("\n", cPieceSize + 1); + const n2 = leftData.indexOf("\n\n", cPieceSize + 1); + const n3 = leftData.indexOf("\r\n\r\n", cPieceSize + 1); + const n4 = leftData.indexOf("\n#", cPieceSize + 1); + if (n1 == -1 && n2 == -1 && n3 == -1 && n4 == -1) { + cPieceSize = MAX_DOC_SIZE; + break; + } + + if (n1 > longLineThreshold) { + // long sentence is an established piece + cPieceSize = n1; + } else { + // cPieceSize = Math.min.apply([n2, n3, n4].filter((e) => e > 1)); + // ^ heavy. + if (n1 > 0 && cPieceSize < n1) cPieceSize = n1; + if (n2 > 0 && cPieceSize < n2) cPieceSize = n2 + 1; + if (n3 > 0 && cPieceSize < n3) cPieceSize = n3 + 3; + // Choose shorter, empty line and \n# + if (n4 > 0 && cPieceSize > n4) cPieceSize = n4 + 0; + cPieceSize++; + } + } while (cPieceSize < minimumChunkSize); + } + + // piece size determined. + const piece = leftData.substring(0, cPieceSize); + leftData = leftData.substring(cPieceSize); + processed++; + let leafid = ""; + // Get hash of piece. + let hashedPiece = ""; + let hashQ = 0; // if hash collided, **IF**, count it up. + let tryNextHash = false; + let needMake = true; + if (typeof this.hashCache[piece] !== "undefined") { + hashedPiece = ""; + leafid = this.hashCache[piece]; + needMake = false; + skiped++; + cacheUsed++; + } else { + if (this.settings.encrypt) { + // When encryption has been enabled, make hash to be different between each passphrase to avoid inferring password. + hashedPiece = "+" + (this.h32Raw(new TextEncoder().encode(piece)) ^ userpasswordHash).toString(16); + } else { + hashedPiece = this.h32(piece); + } + leafid = "h:" + hashedPiece; + do { + let nleafid = leafid; + try { + nleafid = `${leafid}${hashQ}`; + const pieceData = await this.localDatabase.get(nleafid); + //try decode + if (pieceData._id.startsWith("h:+")) { + try { + pieceData.data = await decrypt(pieceData.data, this.settings.passphrase); + } catch (e) { + Logger("Decode failed !"); + throw e; + } + } + if (pieceData.type == "leaf" && pieceData.data == piece) { + leafid = nleafid; + needMake = false; + tryNextHash = false; + this.hashCache[piece] = leafid; + this.hashCacheRev[leafid] = piece; + } else if (pieceData.type == "leaf") { + Logger("hash:collision!!"); + hashQ++; + tryNextHash = true; + } else { + leafid = nleafid; + tryNextHash = false; + } + } catch (ex) { + if (ex.status && ex.status == 404) { + //not found, we can use it. + leafid = nleafid; + needMake = true; + tryNextHash = false; + } else { + needMake = false; + tryNextHash = false; + throw ex; + } + } + } while (tryNextHash); + if (needMake) { + //have to make + let savePiece = piece; + if (this.settings.encrypt) { + const passphrase = this.settings.passphrase; + savePiece = await encrypt(piece, passphrase); + } + const d: EntryLeaf = { + _id: leafid, + data: savePiece, + type: "leaf", + }; + newLeafs.push(d); + this.hashCache[piece] = leafid; + this.hashCacheRev[leafid] = piece; + made++; + } else { + skiped++; + } + } + savenNotes.push(leafid); + } while (leftData != ""); + let saved = true; + if (newLeafs.length > 0) { + try { + const result = await this.localDatabase.bulkDocs(newLeafs); + for (const item of result) { + if ((item as any).ok) { + this.updateRecentModifiedDocs(item.id, item.rev, false); + Logger(`save ok:id:${item.id} rev:${item.rev}`, LOG_LEVEL.VERBOSE); + } else { + if ((item as any).status && (item as any).status == 409) { + // conflicted, but it would be ok in childrens. + } else { + Logger(`save failed:id:${item.id} rev:${item.rev}`, LOG_LEVEL.NOTICE); + Logger(item); + // this.disposeHashCache(); + saved = false; + } + } + } + } catch (ex) { + Logger("ERROR ON SAVING LEAVES "); + Logger(ex); + saved = false; + } + } + if (saved) { + Logger(`note content saven, pieces:${processed} new:${made}, skip:${skiped}, cache:${cacheUsed}`); + const newDoc: PlainEntry | NewEntry = { + NewNote: true, + children: savenNotes, + _id: note._id, + ctime: note.ctime, + mtime: note.mtime, + size: note.size, + type: plainSplit ? "plain" : "newnote", + }; + // Here for upsert logic, + try { + const old = await this.localDatabase.get(newDoc._id); + if (!old.type || old.type == "notes" || old.type == "newnote" || old.type == "plain") { + // simple use rev for new doc + newDoc._rev = old._rev; + } + } catch (ex) { + if (ex.status && ex.status == 404) { + // NO OP/ + } else { + throw ex; + } + } + const r = await this.localDatabase.put(newDoc, { force: true }); + this.updateRecentModifiedDocs(r.id, r.rev, newDoc._deleted); + if (typeof this.corruptedEntries[note._id] != "undefined") { + delete this.corruptedEntries[note._id]; + } + if (this.settings.checkIntegrityOnSave) { + if (!this.sanCheck(await this.localDatabase.get(r.id))) { + Logger("note save failed!", LOG_LEVEL.NOTICE); + } else { + Logger(`note has been surely saved:${newDoc._id}:${r.rev}`); + } + } else { + Logger(`note saved:${newDoc._id}:${r.rev}`); + } + } else { + Logger(`note coud not saved:${note._id}`); + } + } + + updateInfo: () => void = () => { + console.log("default updinfo"); + }; + // eslint-disable-next-line require-await + async migrate(from: number, to: number): Promise { + Logger(`Database updated from ${from} to ${to}`, LOG_LEVEL.NOTICE); + // no op now, + return true; + } + replicateAllToServer(setting: ObsidianLiveSyncSettings, showingNotice?: boolean) { + return new Promise(async (res, rej) => { + await this.waitForGCComplete(); + this.closeReplication(); + Logger("send all data to server", LOG_LEVEL.NOTICE); + let notice: Notice = null; + if (showingNotice) { + notice = new Notice("Initializing", 0); + } + this.syncStatus = "STARTED"; + this.updateInfo(); + const uri = setting.couchDB_URI + (setting.couchDB_DBNAME == "" ? "" : "/" + setting.couchDB_DBNAME); + const auth: Credential = { + username: setting.couchDB_USER, + password: setting.couchDB_PASSWORD, + }; + const dbret = await connectRemoteCouchDB(uri, auth); + if (typeof dbret === "string") { + Logger(`could not connect to ${uri}:${dbret}`, LOG_LEVEL.NOTICE); + if (notice != null) notice.hide(); + return rej(`could not connect to ${uri}:${dbret}`); + } + + const syncOptionBase: PouchDB.Replication.SyncOptions = { + batch_size: 250, + batches_limit: 40, + }; + + const db = dbret.db; + const totalCount = (await this.localDatabase.info()).doc_count; + //replicate once + const replicate = this.localDatabase.replicate.to(db, syncOptionBase); + replicate + .on("active", () => { + this.syncStatus = "CONNECTED"; + this.updateInfo(); + if (notice) { + notice.setMessage("CONNECTED"); + } + }) + .on("change", (e) => { + // no op. + this.docSent += e.docs.length; + this.updateInfo(); + notice.setMessage(`SENDING:${e.docs_written}/${totalCount}`); + Logger(`replicateAllToServer: sending..:${e.docs.length}`); + }) + .on("complete", (info) => { + this.syncStatus = "COMPLETED"; + this.updateInfo(); + Logger("replicateAllToServer: Completed", LOG_LEVEL.NOTICE); + this.cancelHandler(replicate); + if (notice != null) notice.hide(); + res(true); + }) + .on("error", (e) => { + this.syncStatus = "ERRORED"; + this.updateInfo(); + Logger("replicateAllToServer: Pulling Replication error", LOG_LEVEL.INFO); + Logger(e); + this.cancelHandler(replicate); + if (notice != null) notice.hide(); + rej(e); + }); + }); + } + + async checkReplicationConnectivity(setting: ObsidianLiveSyncSettings, keepAlive: boolean) { + if (!this.isReady) { + Logger("Database is not ready."); + return false; + } + + await this.waitForGCComplete(); + if (setting.versionUpFlash != "") { + new Notice("Open settings and check message, please."); + return false; + } + const uri = setting.couchDB_URI + (setting.couchDB_DBNAME == "" ? "" : "/" + setting.couchDB_DBNAME); + const auth: Credential = { + username: setting.couchDB_USER, + password: setting.couchDB_PASSWORD, + }; + if (this.syncHandler != null) { + Logger("Another replication running."); + return false; + } + const dbret = await connectRemoteCouchDB(uri, auth); + if (typeof dbret === "string") { + Logger(`could not connect to ${uri}:${dbret}`, LOG_LEVEL.NOTICE); + return false; + } + + if (!(await checkRemoteVersion(dbret.db, this.migrate.bind(this), VER))) { + Logger("Remote database is newer or corrupted, make sure to latest version of self-hosted-livesync installed", LOG_LEVEL.NOTICE); + return false; + } + + const defMilestonePoint: EntryMilestoneInfo = { + _id: MILSTONE_DOCID, + type: "milestoneinfo", + created: (new Date() as any) / 1, + locked: false, + accepted_nodes: [this.nodeid], + }; + + const remoteMilestone: EntryMilestoneInfo = await resolveWithIgnoreKnownError(dbret.db.get(MILSTONE_DOCID), defMilestonePoint); + this.remoteLocked = remoteMilestone.locked; + this.remoteLockedAndDeviceNotAccepted = remoteMilestone.locked && remoteMilestone.accepted_nodes.indexOf(this.nodeid) == -1; + + if (remoteMilestone.locked && remoteMilestone.accepted_nodes.indexOf(this.nodeid) == -1) { + Logger("Remote database marked as 'Auto Sync Locked'. And this devide does not marked as resolved device. see settings dialog.", LOG_LEVEL.NOTICE); + return false; + } + if (typeof remoteMilestone._rev == "undefined") { + await dbret.db.put(remoteMilestone); + } + + const syncOptionBase: PouchDB.Replication.SyncOptions = { + batch_size: 250, + batches_limit: 40, + }; + const syncOption: PouchDB.Replication.SyncOptions = keepAlive ? { live: true, retry: true, heartbeat: 30000, ...syncOptionBase } : { ...syncOptionBase }; + + return { db: dbret.db, syncOptionBase, syncOption }; + } + + async openReplication(setting: ObsidianLiveSyncSettings, keepAlive: boolean, showResult: boolean, callback: (e: PouchDB.Core.ExistingDocument[]) => Promise): Promise { + return await runWithLock("replicate", false, () => { + return this._openReplication(setting, keepAlive, showResult, callback); + }); + } + + async _openReplication(setting: ObsidianLiveSyncSettings, keepAlive: boolean, showResult: boolean, callback: (e: PouchDB.Core.ExistingDocument[]) => Promise): Promise { + const ret = await this.checkReplicationConnectivity(setting, keepAlive); + if (ret === false) return false; + let notice: Notice = null; + if (showResult) { + notice = new Notice("Replicating", 0); + } + const { db, syncOptionBase, syncOption } = ret; + //replicate once + this.syncStatus = "STARTED"; + + return new Promise(async (res, rej) => { + let resolved = false; + const _openReplicationSync = () => { + this.syncHandler = this.cancelHandler(this.syncHandler); + this.syncHandler = this.localDatabase.sync(db, syncOption); + this.syncHandler + .on("active", () => { + this.syncStatus = "CONNECTED"; + this.updateInfo(); + Logger("Replication activated"); + }) + .on("change", async (e) => { + try { + if (e.direction == "pull") { + // console.log(`pulled data:${e.change.docs.map((e) => e._id).join(",")}`); + await callback(e.change.docs); + Logger(`replicated ${e.change.docs_read} doc(s)`); + this.docArrived += e.change.docs.length; + } else { + // console.log(`put data:${e.change.docs.map((e) => e._id).join(",")}`); + this.docSent += e.change.docs.length; + } + if (notice != null) { + notice.setMessage(`↑${e.change.docs_written} ↓${e.change.docs_read}`); + } + this.updateInfo(); + } catch (ex) { + Logger("Replication callback error"); + Logger(ex); + } + }) + .on("complete", (e) => { + this.syncStatus = "COMPLETED"; + this.updateInfo(); + Logger("Replication completed", showResult ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO); + if (notice != null) notice.hide(); + if (!keepAlive) { + this.syncHandler = this.cancelHandler(this.syncHandler); + // if keep alive runnning, resolve here, + res(true); + } + }) + .on("denied", (e) => { + this.syncStatus = "ERRORED"; + this.updateInfo(); + this.syncHandler = this.cancelHandler(this.syncHandler); + if (notice != null) notice.hide(); + Logger("Replication denied", LOG_LEVEL.NOTICE); + // Logger(e); + rej(e); + }) + .on("error", (e) => { + this.syncStatus = "ERRORED"; + this.syncHandler = this.cancelHandler(this.syncHandler); + this.updateInfo(); + if (notice != null) notice.hide(); + Logger("Replication error", LOG_LEVEL.NOTICE); + // Logger(e); + rej(e); + }) + .on("paused", (e) => { + this.syncStatus = "PAUSED"; + this.updateInfo(); + if (notice != null) notice.hide(); + Logger("replication paused", LOG_LEVEL.VERBOSE); + if (keepAlive && !resolved) { + // if keep alive runnning, resolve here, + resolved = true; + res(true); + } + // Logger(e); + }); + }; + if (!keepAlive) { + return await _openReplicationSync(); + } + this.syncHandler = this.cancelHandler(this.syncHandler); + Logger("Pull before replicate."); + Logger(await this.localDatabase.info(), LOG_LEVEL.VERBOSE); + Logger(await db.info(), LOG_LEVEL.VERBOSE); + const replicate = this.localDatabase.replicate.from(db, syncOptionBase); + replicate + .on("active", () => { + this.syncStatus = "CONNECTED"; + this.updateInfo(); + Logger("Replication pull activated."); + }) + .on("change", async (e) => { + // when in first run, replication will send us tombstone data + // and in normal cases, all leavs should sent before the entry that contains these item. + // so skip to completed all, we should treat all changes. + try { + await callback(e.docs); + this.docArrived += e.docs.length; + this.updateInfo(); + Logger(`pulled ${e.docs.length} doc(s)`); + if (notice != null) { + notice.setMessage(`Replication pulled:${e.docs_read}`); + } + } catch (ex) { + Logger("Replication callback error"); + Logger(ex); + } + }) + .on("complete", async (info) => { + this.syncStatus = "COMPLETED"; + this.updateInfo(); + this.cancelHandler(replicate); + this.syncHandler = this.cancelHandler(this.syncHandler); + Logger("Replication pull completed."); + await _openReplicationSync(); + }) + .on("denied", (e) => { + this.syncStatus = "ERRORED"; + this.updateInfo(); + if (notice != null) notice.hide(); + Logger("Pulling Replication denied", LOG_LEVEL.NOTICE); + this.cancelHandler(replicate); + this.syncHandler = this.cancelHandler(this.syncHandler); + rej(e); + }) + .on("error", (e) => { + this.syncStatus = "ERRORED"; + this.updateInfo(); + Logger("Pulling Replication error", LOG_LEVEL.INFO); + this.cancelHandler(replicate); + this.syncHandler = this.cancelHandler(this.syncHandler); + if (notice != null) notice.hide(); + // debugger; + Logger(e); + rej(e); + }); + }); + } + + closeReplication() { + this.syncStatus = "CLOSED"; + this.updateInfo(); + this.syncHandler = this.cancelHandler(this.syncHandler); + Logger("Replication closed"); + } + + async resetDatabase() { + await this.waitForGCComplete(); + this.changeHandler = this.cancelHandler(this.changeHandler); + await this.closeReplication(); + Logger("Database closed for reset Database."); + this.isReady = false; + await this.localDatabase.destroy(); + this.localDatabase = null; + await this.initializeDatabase(); + this.disposeHashCache(); + Logger("Local Database Reset", LOG_LEVEL.NOTICE); + } + async tryResetRemoteDatabase(setting: ObsidianLiveSyncSettings) { + await this.closeReplication(); + const uri = setting.couchDB_URI + (setting.couchDB_DBNAME == "" ? "" : "/" + setting.couchDB_DBNAME); + const auth: Credential = { + username: setting.couchDB_USER, + password: setting.couchDB_PASSWORD, + }; + const con = await connectRemoteCouchDB(uri, auth); + if (typeof con == "string") return; + try { + await con.db.destroy(); + Logger("Remote Database Destroyed", LOG_LEVEL.NOTICE); + await this.tryCreateRemoteDatabase(setting); + } catch (ex) { + Logger("something happend on Remote Database Destory", LOG_LEVEL.NOTICE); + } + } + async tryCreateRemoteDatabase(setting: ObsidianLiveSyncSettings) { + await this.closeReplication(); + const uri = setting.couchDB_URI + (setting.couchDB_DBNAME == "" ? "" : "/" + setting.couchDB_DBNAME); + const auth: Credential = { + username: setting.couchDB_USER, + password: setting.couchDB_PASSWORD, + }; + const con2 = await connectRemoteCouchDB(uri, auth); + if (typeof con2 === "string") return; + Logger("Remote Database Created or Connected", LOG_LEVEL.NOTICE); + } + async markRemoteLocked(setting: ObsidianLiveSyncSettings, locked: boolean) { + const uri = setting.couchDB_URI + (setting.couchDB_DBNAME == "" ? "" : "/" + setting.couchDB_DBNAME); + const auth: Credential = { + username: setting.couchDB_USER, + password: setting.couchDB_PASSWORD, + }; + const dbret = await connectRemoteCouchDB(uri, auth); + if (typeof dbret === "string") { + Logger(`could not connect to ${uri}:${dbret}`, LOG_LEVEL.NOTICE); + return; + } + + if (!(await checkRemoteVersion(dbret.db, this.migrate.bind(this), VER))) { + Logger("Remote database is newer or corrupted, make sure to latest version of self-hosted-livesync installed", LOG_LEVEL.NOTICE); + return; + } + const defInitPoint: EntryMilestoneInfo = { + _id: MILSTONE_DOCID, + type: "milestoneinfo", + created: (new Date() as any) / 1, + locked: locked, + accepted_nodes: [this.nodeid], + }; + + const remoteMilestone: EntryMilestoneInfo = await resolveWithIgnoreKnownError(dbret.db.get(MILSTONE_DOCID), defInitPoint); + remoteMilestone.accepted_nodes = [this.nodeid]; + remoteMilestone.locked = locked; + if (locked) { + Logger("Lock remote database to prevent data corruption", LOG_LEVEL.NOTICE); + } else { + Logger("Unlock remote database to prevent data corruption", LOG_LEVEL.NOTICE); + } + await dbret.db.put(remoteMilestone); + } + async markRemoteResolved(setting: ObsidianLiveSyncSettings) { + const uri = setting.couchDB_URI + (setting.couchDB_DBNAME == "" ? "" : "/" + setting.couchDB_DBNAME); + const auth: Credential = { + username: setting.couchDB_USER, + password: setting.couchDB_PASSWORD, + }; + const dbret = await connectRemoteCouchDB(uri, auth); + if (typeof dbret === "string") { + Logger(`could not connect to ${uri}:${dbret}`, LOG_LEVEL.NOTICE); + return; + } + + if (!(await checkRemoteVersion(dbret.db, this.migrate.bind(this), VER))) { + Logger("Remote database is newer or corrupted, make sure to latest version of self-hosted-livesync installed", LOG_LEVEL.NOTICE); + return; + } + const defInitPoint: EntryMilestoneInfo = { + _id: MILSTONE_DOCID, + type: "milestoneinfo", + created: (new Date() as any) / 1, + locked: false, + accepted_nodes: [this.nodeid], + }; + // check local database hash status and remote replicate hash status + const remoteMilestone: EntryMilestoneInfo = await resolveWithIgnoreKnownError(dbret.db.get(MILSTONE_DOCID), defInitPoint); + // remoteMilestone.locked = false; + remoteMilestone.accepted_nodes = Array.from(new Set([...remoteMilestone.accepted_nodes, this.nodeid])); + // this.remoteLocked = false; + Logger("Mark this device as 'resolved'.", LOG_LEVEL.NOTICE); + await dbret.db.put(remoteMilestone); + } + gcRunning = false; + async waitForGCComplete() { + while (this.gcRunning) { + Logger("Waiting for Garbage Collection completed."); + await delay(1000); + } + } + async sanCheck(entry: EntryDoc): Promise { + if (entry.type == "plain" || entry.type == "newnote") { + const children = entry.children; + Logger(`sancheck:checking:${entry._id} : ${children.length}`, LOG_LEVEL.VERBOSE); + try { + const dc = await this.localDatabase.allDocs({ keys: [...children] }); + if (dc.rows.some((e) => "error" in e)) { + this.corruptedEntries[entry._id] = entry; + this.disposeHashCache(); + Logger(`sancheck:corrupted:${entry._id} : ${children.length}`, LOG_LEVEL.VERBOSE); + return false; + } + return true; + } catch (ex) { + Logger(ex); + } + } + return false; + } + async garbageCollect() { + await runWithLock("replicate", true, async () => { + if (this.gcRunning) return; + this.gcRunning = true; + try { + // get all documents of NewEntry2 + // we don't use queries , just use allDocs(); + this.disposeHashCache(); + let c = 0; + let readCount = 0; + let hashPieces: string[] = []; + let usedPieces: string[] = []; + Logger("Collecting Garbage"); + do { + const result = await this.localDatabase.allDocs({ include_docs: true, skip: c, limit: 500, conflicts: true }); + readCount = result.rows.length; + Logger("checked:" + readCount); + if (readCount > 0) { + //there are some result + for (const v of result.rows) { + const doc = v.doc; + if (doc.type == "newnote" || doc.type == "plain") { + // used pieces memo. + usedPieces = Array.from(new Set([...usedPieces, ...doc.children])); + if (doc._conflicts) { + for (const cid of doc._conflicts) { + const p = await this.localDatabase.get(doc._id, { rev: cid }); + if (p.type == "newnote" || p.type == "plain") { + usedPieces = Array.from(new Set([...usedPieces, ...p.children])); + } + } + } + } + if (doc.type == "leaf") { + // all pieces. + hashPieces = Array.from(new Set([...hashPieces, doc._id])); + } + } + } + c += readCount; + } while (readCount != 0); + // items collected. + Logger("Finding unused pieces"); + this.disposeHashCache(); + const garbages = hashPieces.filter((e) => usedPieces.indexOf(e) == -1); + let deleteCount = 0; + Logger("we have to delete:" + garbages.length); + let deleteDoc: EntryDoc[] = []; + for (const v of garbages) { + try { + const item = await this.localDatabase.get(v); + item._deleted = true; + deleteDoc.push(item); + if (deleteDoc.length > 50) { + await this.localDatabase.bulkDocs(deleteDoc); + deleteDoc = []; + Logger("delete:" + deleteCount); + } + deleteCount++; + } catch (ex) { + if (ex.status && ex.status == 404) { + // NO OP. It should be timing problem. + } else { + throw ex; + } + } + } + if (deleteDoc.length > 0) { + await this.localDatabase.bulkDocs(deleteDoc); + } + Logger(`GC:deleted ${deleteCount} items.`); + } finally { + this.gcRunning = false; + } + }); + this.disposeHashCache(); + } +} diff --git a/src/LogDisplayModal.ts b/src/LogDisplayModal.ts new file mode 100644 index 0000000..b222bd4 --- /dev/null +++ b/src/LogDisplayModal.ts @@ -0,0 +1,37 @@ +import { App, Modal } from "obsidian"; +import { escapeStringToHTML } from "./utils"; +import ObsidianLiveSyncPlugin from "./main"; + +export class LogDisplayModal extends Modal { + plugin: ObsidianLiveSyncPlugin; + logEl: HTMLDivElement; + constructor(app: App, plugin: ObsidianLiveSyncPlugin) { + super(app); + this.plugin = plugin; + } + updateLog() { + let msg = ""; + for (const v of this.plugin.logMessage) { + msg += escapeStringToHTML(v) + "
"; + } + this.logEl.innerHTML = msg; + } + onOpen() { + const { contentEl } = this; + + contentEl.empty(); + contentEl.createEl("h2", { text: "Sync Status" }); + const div = contentEl.createDiv(""); + div.addClass("op-scrollable"); + div.addClass("op-pre"); + this.logEl = div; + this.updateLog = this.updateLog.bind(this); + this.plugin.addLogHook = this.updateLog; + this.updateLog(); + } + onClose() { + const { contentEl } = this; + contentEl.empty(); + this.plugin.addLogHook = null; + } +} diff --git a/src/ObsidianLiveSyncSettingTab.ts b/src/ObsidianLiveSyncSettingTab.ts new file mode 100644 index 0000000..a50fdcc --- /dev/null +++ b/src/ObsidianLiveSyncSettingTab.ts @@ -0,0 +1,1093 @@ +import { App, Notice, PluginSettingTab, Setting } from "obsidian"; +import { EntryDoc, LOG_LEVEL } from "./types"; +import { escapeStringToHTML, versionNumberString2Number, path2id, id2path, runWithLock } from "./utils"; +import { Logger } from "./logger"; +import { connectRemoteCouchDB } from "./utils_couchdb"; +import { testCrypt } from "./e2ee"; +import ObsidianLiveSyncPlugin from "./main"; + +export class ObsidianLiveSyncSettingTab extends PluginSettingTab { + plugin: ObsidianLiveSyncPlugin; + + constructor(app: App, plugin: ObsidianLiveSyncPlugin) { + super(app, plugin); + this.plugin = plugin; + } + async testConnection(): Promise { + const db = await connectRemoteCouchDB(this.plugin.settings.couchDB_URI + (this.plugin.settings.couchDB_DBNAME == "" ? "" : "/" + this.plugin.settings.couchDB_DBNAME), { + username: this.plugin.settings.couchDB_USER, + password: this.plugin.settings.couchDB_PASSWORD, + }); + if (typeof db === "string") { + this.plugin.addLog(`could not connect to ${this.plugin.settings.couchDB_URI} : ${this.plugin.settings.couchDB_DBNAME} \n(${db})`, LOG_LEVEL.NOTICE); + return; + } + this.plugin.addLog(`Connected to ${db.info.db_name}`, LOG_LEVEL.NOTICE); + } + display(): void { + const { containerEl } = this; + + containerEl.empty(); + + containerEl.createEl("h2", { text: "Settings for Self-hosted LiveSync." }); + + const w = containerEl.createDiv(""); + const screenElements: { [key: string]: HTMLElement[] } = {}; + const addScreenElement = (key: string, element: HTMLElement) => { + if (!(key in screenElements)) { + screenElements[key] = []; + } + screenElements[key].push(element); + }; + w.addClass("sls-setting-menu"); + w.innerHTML = ` + + + + + + + + + `; + const menutabs = w.querySelectorAll(".sls-setting-label"); + const changeDisplay = (screen: string) => { + for (const k in screenElements) { + if (k == screen) { + screenElements[k].forEach((element) => element.removeClass("setting-collapsed")); + } else { + screenElements[k].forEach((element) => element.addClass("setting-collapsed")); + } + } + }; + menutabs.forEach((element) => { + const e = element.querySelector(".sls-setting-tab"); + if (!e) return; + e.addEventListener("change", (event) => { + menutabs.forEach((element) => element.removeClass("selected")); + changeDisplay((event.currentTarget as HTMLInputElement).value); + element.addClass("selected"); + }); + }); + + const containerRemoteDatabaseEl = containerEl.createDiv(); + containerRemoteDatabaseEl.createEl("h3", { text: "Remote Database configuration" }); + const syncWarn = containerRemoteDatabaseEl.createEl("div", { text: "The remote configuration is locked while any synchronization is enabled." }); + syncWarn.addClass("op-warn"); + syncWarn.addClass("sls-hidden"); + + const isAnySyncEnabled = (): boolean => { + if (this.plugin.settings.liveSync) return true; + if (this.plugin.settings.periodicReplication) return true; + if (this.plugin.settings.syncOnFileOpen) return true; + if (this.plugin.settings.syncOnSave) return true; + if (this.plugin.settings.syncOnStart) return true; + return false; + }; + const applyDisplayEnabled = () => { + if (isAnySyncEnabled()) { + dbsettings.forEach((e) => { + e.setDisabled(true).setTooltip("When any sync is enabled, It cound't be changed."); + }); + syncWarn.removeClass("sls-hidden"); + } else { + dbsettings.forEach((e) => { + e.setDisabled(false).setTooltip(""); + }); + syncWarn.addClass("sls-hidden"); + } + if (this.plugin.settings.liveSync) { + syncNonLive.forEach((e) => { + e.setDisabled(true).setTooltip(""); + }); + syncLive.forEach((e) => { + e.setDisabled(false).setTooltip(""); + }); + } else if (this.plugin.settings.syncOnFileOpen || this.plugin.settings.syncOnSave || this.plugin.settings.syncOnStart || this.plugin.settings.periodicReplication) { + syncNonLive.forEach((e) => { + e.setDisabled(false).setTooltip(""); + }); + syncLive.forEach((e) => { + e.setDisabled(true).setTooltip(""); + }); + } else { + syncNonLive.forEach((e) => { + e.setDisabled(false).setTooltip(""); + }); + syncLive.forEach((e) => { + e.setDisabled(false).setTooltip(""); + }); + } + }; + + const dbsettings: Setting[] = []; + dbsettings.push( + new Setting(containerRemoteDatabaseEl).setName("URI").addText((text) => + text + .setPlaceholder("https://........") + .setValue(this.plugin.settings.couchDB_URI) + .onChange(async (value) => { + this.plugin.settings.couchDB_URI = value; + await this.plugin.saveSettings(); + }) + ), + new Setting(containerRemoteDatabaseEl) + .setName("Username") + .setDesc("username") + .addText((text) => + text + .setPlaceholder("") + .setValue(this.plugin.settings.couchDB_USER) + .onChange(async (value) => { + this.plugin.settings.couchDB_USER = value; + await this.plugin.saveSettings(); + }) + ), + new Setting(containerRemoteDatabaseEl) + .setName("Password") + .setDesc("password") + .addText((text) => { + text.setPlaceholder("") + .setValue(this.plugin.settings.couchDB_PASSWORD) + .onChange(async (value) => { + this.plugin.settings.couchDB_PASSWORD = value; + await this.plugin.saveSettings(); + }); + text.inputEl.setAttribute("type", "password"); + }), + new Setting(containerRemoteDatabaseEl).setName("Database name").addText((text) => + text + .setPlaceholder("") + .setValue(this.plugin.settings.couchDB_DBNAME) + .onChange(async (value) => { + this.plugin.settings.couchDB_DBNAME = value; + await this.plugin.saveSettings(); + }) + ) + ); + + new Setting(containerRemoteDatabaseEl) + .setName("Test Database Connection") + .setDesc("Open database connection. If the remote database is not found and you have the privilege to create a database, the database will be created.") + .addButton((button) => + button + .setButtonText("Test") + .setDisabled(false) + .onClick(async () => { + await this.testConnection(); + }) + ); + + addScreenElement("0", containerRemoteDatabaseEl); + const containerLocalDatabaseEl = containerEl.createDiv(); + containerLocalDatabaseEl.createEl("h3", { text: "Local Database configuration" }); + + new Setting(containerLocalDatabaseEl) + .setName("Batch database update") + .setDesc("Delay all changes, save once before replication or opening another file.") + .addToggle((toggle) => + toggle.setValue(this.plugin.settings.batchSave).onChange(async (value) => { + if (value && this.plugin.settings.liveSync) { + Logger("LiveSync and Batch database update cannot be used at the same time.", LOG_LEVEL.NOTICE); + toggle.setValue(false); + return; + } + this.plugin.settings.batchSave = value; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerLocalDatabaseEl) + .setName("Auto Garbage Collection delay") + .setDesc("(seconds), if you set zero, you have to run manually.") + .addText((text) => { + text.setPlaceholder("") + .setValue(this.plugin.settings.gcDelay + "") + .onChange(async (value) => { + let v = Number(value); + if (isNaN(v) || v > 5000) { + v = 0; + } + this.plugin.settings.gcDelay = v; + await this.plugin.saveSettings(); + }); + text.inputEl.setAttribute("type", "number"); + }); + new Setting(containerLocalDatabaseEl).setName("Manual Garbage Collect").addButton((button) => + button + .setButtonText("Collect now") + .setDisabled(false) + .onClick(async () => { + await this.plugin.garbageCollect(); + }) + ); + new Setting(containerLocalDatabaseEl) + .setName("End to End Encryption") + .setDesc("Encrypting contents on the database.") + .addToggle((toggle) => + toggle.setValue(this.plugin.settings.workingEncrypt).onChange(async (value) => { + this.plugin.settings.workingEncrypt = value; + phasspharase.setDisabled(!value); + await this.plugin.saveSettings(); + }) + ); + const phasspharase = new Setting(containerLocalDatabaseEl) + .setName("Passphrase") + .setDesc("Encrypting passphrase") + .addText((text) => { + text.setPlaceholder("") + .setValue(this.plugin.settings.workingPassphrase) + .onChange(async (value) => { + this.plugin.settings.workingPassphrase = value; + await this.plugin.saveSettings(); + }); + text.inputEl.setAttribute("type", "password"); + }); + phasspharase.setDisabled(!this.plugin.settings.workingEncrypt); + containerLocalDatabaseEl.createEl("div", { + text: "When you change any encryption enabled or passphrase, you have to reset all databases to make sure that the last password is unused and erase encrypted data from anywhere. This operation will not lost your vault if you are fully synced.", + }); + const applyEncryption = async (sendToServer: boolean) => { + if (this.plugin.settings.workingEncrypt && this.plugin.settings.workingPassphrase == "") { + Logger("If you enable encryption, you have to set the passphrase", LOG_LEVEL.NOTICE); + return; + } + if (this.plugin.settings.workingEncrypt && !(await testCrypt())) { + Logger("WARNING! Your device would not support encryption.", LOG_LEVEL.NOTICE); + return; + } + if (!this.plugin.settings.workingEncrypt) { + this.plugin.settings.workingPassphrase = ""; + } + this.plugin.settings.liveSync = false; + this.plugin.settings.periodicReplication = false; + this.plugin.settings.syncOnSave = false; + this.plugin.settings.syncOnStart = false; + this.plugin.settings.syncOnFileOpen = false; + this.plugin.settings.encrypt = this.plugin.settings.workingEncrypt; + this.plugin.settings.passphrase = this.plugin.settings.workingPassphrase; + + await this.plugin.saveSettings(); + await this.plugin.resetLocalDatabase(); + if (sendToServer) { + await this.plugin.initializeDatabase(true); + await this.plugin.markRemoteLocked(); + await this.plugin.tryResetRemoteDatabase(); + await this.plugin.markRemoteLocked(); + await this.plugin.replicateAllToServer(true); + } else { + await this.plugin.markRemoteResolved(); + await this.plugin.replicate(true); + } + }; + new Setting(containerLocalDatabaseEl) + .setName("Apply") + .setDesc("apply encryption settinngs, and re-initialize database") + .addButton((button) => + button + .setButtonText("Apply and send") + .setWarning() + .setDisabled(false) + .setClass("sls-btn-left") + .onClick(async () => { + await applyEncryption(true); + }) + ) + .addButton((button) => + button + .setButtonText("Apply and receive") + .setWarning() + .setDisabled(false) + .setClass("sls-btn-right") + .onClick(async () => { + await applyEncryption(false); + }) + ); + + addScreenElement("10", containerLocalDatabaseEl); + const containerGeneralSettingsEl = containerEl.createDiv(); + containerGeneralSettingsEl.createEl("h3", { text: "General Settings" }); + + new Setting(containerGeneralSettingsEl) + .setName("Do not show low-priority Log") + .setDesc("Reduce log infomations") + .addToggle((toggle) => + toggle.setValue(this.plugin.settings.lessInformationInLog).onChange(async (value) => { + this.plugin.settings.lessInformationInLog = value; + await this.plugin.saveSettings(); + }) + ); + new Setting(containerGeneralSettingsEl) + .setName("Verbose Log") + .setDesc("Show verbose log ") + .addToggle((toggle) => + toggle.setValue(this.plugin.settings.showVerboseLog).onChange(async (value) => { + this.plugin.settings.showVerboseLog = value; + await this.plugin.saveSettings(); + }) + ); + + addScreenElement("20", containerGeneralSettingsEl); + const containerSyncSettingEl = containerEl.createDiv(); + containerSyncSettingEl.createEl("h3", { text: "Sync setting" }); + + if (this.plugin.settings.versionUpFlash != "") { + const c = containerSyncSettingEl.createEl("div", { text: this.plugin.settings.versionUpFlash }); + c.createEl("button", { text: "I got it and updated." }, (e) => { + e.addClass("mod-cta"); + e.addEventListener("click", async () => { + this.plugin.settings.versionUpFlash = ""; + await this.plugin.saveSettings(); + applyDisplayEnabled(); + c.remove(); + }); + }); + c.addClass("op-warn"); + } + + const syncLive: Setting[] = []; + const syncNonLive: Setting[] = []; + syncLive.push( + new Setting(containerSyncSettingEl) + .setName("LiveSync") + .setDesc("Sync realtime") + .addToggle((toggle) => + toggle.setValue(this.plugin.settings.liveSync).onChange(async (value) => { + if (value && this.plugin.settings.batchSave) { + Logger("LiveSync and Batch database update cannot be used at the same time.", LOG_LEVEL.NOTICE); + toggle.setValue(false); + return; + } + + this.plugin.settings.liveSync = value; + // ps.setDisabled(value); + await this.plugin.saveSettings(); + applyDisplayEnabled(); + await this.plugin.realizeSettingSyncMode(); + }) + ) + ); + + syncNonLive.push( + new Setting(containerSyncSettingEl) + .setName("Periodic Sync") + .setDesc("Sync periodically") + .addToggle((toggle) => + toggle.setValue(this.plugin.settings.periodicReplication).onChange(async (value) => { + this.plugin.settings.periodicReplication = value; + await this.plugin.saveSettings(); + applyDisplayEnabled(); + }) + ), + new Setting(containerSyncSettingEl) + .setName("Periodic sync intreval") + .setDesc("Interval (sec)") + .addText((text) => { + text.setPlaceholder("") + .setValue(this.plugin.settings.periodicReplicationInterval + "") + .onChange(async (value) => { + let v = Number(value); + if (isNaN(v) || v > 5000) { + v = 0; + } + this.plugin.settings.periodicReplicationInterval = v; + await this.plugin.saveSettings(); + applyDisplayEnabled(); + }); + text.inputEl.setAttribute("type", "number"); + }), + + new Setting(containerSyncSettingEl) + .setName("Sync on Save") + .setDesc("When you save file, sync automatically") + .addToggle((toggle) => + toggle.setValue(this.plugin.settings.syncOnSave).onChange(async (value) => { + this.plugin.settings.syncOnSave = value; + await this.plugin.saveSettings(); + applyDisplayEnabled(); + }) + ), + new Setting(containerSyncSettingEl) + .setName("Sync on File Open") + .setDesc("When you open file, sync automatically") + .addToggle((toggle) => + toggle.setValue(this.plugin.settings.syncOnFileOpen).onChange(async (value) => { + this.plugin.settings.syncOnFileOpen = value; + await this.plugin.saveSettings(); + applyDisplayEnabled(); + }) + ), + new Setting(containerSyncSettingEl) + .setName("Sync on Start") + .setDesc("Start synchronization on Obsidian started.") + .addToggle((toggle) => + toggle.setValue(this.plugin.settings.syncOnStart).onChange(async (value) => { + this.plugin.settings.syncOnStart = value; + await this.plugin.saveSettings(); + applyDisplayEnabled(); + }) + ) + ); + + new Setting(containerSyncSettingEl) + .setName("Use Trash for deleted files") + .setDesc("Do not delete files that deleted in remote, just move to trash.") + .addToggle((toggle) => + toggle.setValue(this.plugin.settings.trashInsteadDelete).onChange(async (value) => { + this.plugin.settings.trashInsteadDelete = value; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerSyncSettingEl) + .setName("Do not delete empty folder") + .setDesc("Normally, folder is deleted When the folder became empty by replication. enable this, leave it as is") + .addToggle((toggle) => + toggle.setValue(this.plugin.settings.doNotDeleteFolder).onChange(async (value) => { + this.plugin.settings.doNotDeleteFolder = value; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerSyncSettingEl) + .setName("Use newer file if conflicted (beta)") + .setDesc("Resolve conflicts by newer files automatically.") + .addToggle((toggle) => + toggle.setValue(this.plugin.settings.resolveConflictsByNewerFile).onChange(async (value) => { + this.plugin.settings.resolveConflictsByNewerFile = value; + await this.plugin.saveSettings(); + }) + ); + new Setting(containerSyncSettingEl) + .setName("Minimum chunk size") + .setDesc("(letters), minimum chunk size.") + .addText((text) => { + text.setPlaceholder("") + .setValue(this.plugin.settings.minimumChunkSize + "") + .onChange(async (value) => { + let v = Number(value); + if (isNaN(v) || v < 10 || v > 1000) { + v = 10; + } + this.plugin.settings.minimumChunkSize = v; + await this.plugin.saveSettings(); + }); + text.inputEl.setAttribute("type", "number"); + }); + + new Setting(containerSyncSettingEl) + .setName("LongLine Threshold") + .setDesc("(letters), If the line is longer than this, make the line to chunk") + .addText((text) => { + text.setPlaceholder("") + .setValue(this.plugin.settings.longLineThreshold + "") + .onChange(async (value) => { + let v = Number(value); + if (isNaN(v) || v < 10 || v > 1000) { + v = 10; + } + this.plugin.settings.longLineThreshold = v; + await this.plugin.saveSettings(); + }); + text.inputEl.setAttribute("type", "number"); + }); + + addScreenElement("30", containerSyncSettingEl); + const containerMiscellaneousEl = containerEl.createDiv(); + containerMiscellaneousEl.createEl("h3", { text: "Miscellaneous" }); + new Setting(containerMiscellaneousEl) + .setName("Show status inside editor") + .setDesc("") + .addToggle((toggle) => + toggle.setValue(this.plugin.settings.showStatusOnEditor).onChange(async (value) => { + this.plugin.settings.showStatusOnEditor = value; + await this.plugin.saveSettings(); + }) + ); + new Setting(containerMiscellaneousEl) + .setName("Check integrity on saving") + .setDesc("Check database integrity on saving to database") + .addToggle((toggle) => + toggle.setValue(this.plugin.settings.checkIntegrityOnSave).onChange(async (value) => { + this.plugin.settings.checkIntegrityOnSave = value; + await this.plugin.saveSettings(); + }) + ); + let currentPrest = "NONE"; + new Setting(containerMiscellaneousEl) + .setName("Presets") + .setDesc("Apply preset configuration") + .addDropdown((dropdown) => + dropdown + .addOptions({ NONE: "", LIVESYNC: "LiveSync", PERIODIC: "Periodic w/ batch", DISABLE: "Disable all sync" }) + .setValue(currentPrest) + .onChange((value) => (currentPrest = value)) + ) + .addButton((button) => + button + .setButtonText("Apply") + .setDisabled(false) + .setCta() + .onClick(async () => { + if (currentPrest == "") { + Logger("Select any preset.", LOG_LEVEL.NOTICE); + return; + } + this.plugin.settings.batchSave = false; + this.plugin.settings.liveSync = false; + this.plugin.settings.periodicReplication = false; + this.plugin.settings.syncOnSave = false; + this.plugin.settings.syncOnStart = false; + this.plugin.settings.syncOnFileOpen = false; + if (currentPrest == "LIVESYNC") { + this.plugin.settings.liveSync = true; + Logger("Synchronization setting configured as LiveSync.", LOG_LEVEL.NOTICE); + } else if (currentPrest == "PERIODIC") { + this.plugin.settings.batchSave = true; + this.plugin.settings.periodicReplication = false; + this.plugin.settings.syncOnSave = false; + this.plugin.settings.syncOnStart = false; + this.plugin.settings.syncOnFileOpen = false; + Logger("Synchronization setting configured as Periodic sync with batch database update.", LOG_LEVEL.NOTICE); + } else { + Logger("All synchronization disabled.", LOG_LEVEL.NOTICE); + } + this.plugin.saveSettings(); + await this.plugin.realizeSettingSyncMode(); + }) + ); + + addScreenElement("40", containerMiscellaneousEl); + + const containerHatchEl = containerEl.createDiv(); + + containerHatchEl.createEl("h3", { text: "Hatch" }); + + if (this.plugin.localDatabase.remoteLockedAndDeviceNotAccepted) { + const c = containerHatchEl.createEl("div", { + text: "To prevent unwanted vault corruption, the remote database has been locked for synchronization, and this device was not marked as 'resolved'. it caused by some operations like this. re-initialized. Local database initialization should be required. please back your vault up, reset local database, and press 'Mark this device as resolved'. ", + }); + c.createEl("button", { text: "I'm ready, mark this device 'resolved'" }, (e) => { + e.addClass("mod-warning"); + e.addEventListener("click", async () => { + await this.plugin.markRemoteResolved(); + c.remove(); + }); + }); + c.addClass("op-warn"); + } else { + if (this.plugin.localDatabase.remoteLocked) { + const c = containerHatchEl.createEl("div", { + text: "To prevent unwanted vault corruption, the remote database has been locked for synchronization. (This device is marked 'resolved') When all your devices are marked 'resolved', unlock the database.", + }); + c.createEl("button", { text: "I'm ready, unlock the database" }, (e) => { + e.addClass("mod-warning"); + e.addEventListener("click", async () => { + await this.plugin.markRemoteUnlocked(); + c.remove(); + }); + }); + c.addClass("op-warn"); + } + } + const dropHistory = async (sendToServer: boolean) => { + this.plugin.settings.liveSync = false; + this.plugin.settings.periodicReplication = false; + this.plugin.settings.syncOnSave = false; + this.plugin.settings.syncOnStart = false; + this.plugin.settings.syncOnFileOpen = false; + + await this.plugin.saveSettings(); + applyDisplayEnabled(); + await this.plugin.resetLocalDatabase(); + if (sendToServer) { + await this.plugin.initializeDatabase(true); + await this.plugin.markRemoteLocked(); + await this.plugin.tryResetRemoteDatabase(); + await this.plugin.markRemoteLocked(); + await this.plugin.replicateAllToServer(true); + } else { + await this.plugin.markRemoteResolved(); + await this.plugin.replicate(true); + } + }; + new Setting(containerHatchEl) + .setName("Verify and repair all files") + .setDesc("Verify and repair all files and update database without dropping history") + .addButton((button) => + button + .setButtonText("Verify and repair") + .setDisabled(false) + .setWarning() + .onClick(async () => { + const files = this.app.vault.getFiles(); + Logger("Verify and repair all files started", LOG_LEVEL.NOTICE); + const notice = new Notice("", 0); + let i = 0; + for (const file of files) { + i++; + Logger(`Update into ${file.path}`); + notice.setMessage(`${i}/${files.length}\n${file.path}`); + try { + await this.plugin.updateIntoDB(file); + } catch (ex) { + Logger("could not update:"); + Logger(ex); + } + } + notice.hide(); + Logger("done", LOG_LEVEL.NOTICE); + }) + ); + new Setting(containerHatchEl) + .setName("Sanity check") + .setDesc("Verify") + .addButton((button) => + button + .setButtonText("Sanity check") + .setDisabled(false) + .setWarning() + .onClick(async () => { + const notice = new Notice("", 0); + Logger(`Begin sanity check`, LOG_LEVEL.INFO); + notice.setMessage(`Begin sanity check`); + await runWithLock("sancheck", true, async () => { + const db = this.plugin.localDatabase.localDatabase; + const wf = await db.allDocs(); + const filesDatabase = wf.rows.filter((e) => !e.id.startsWith("h:") && !e.id.startsWith("ps:") && e.id != "obsydian_livesync_version").map((e) => e.id); + let count = 0; + for (const id of filesDatabase) { + count++; + notice.setMessage(`${count}/${filesDatabase.length}\n${id2path(id)}`); + const w = await db.get(id); + if (!(await this.plugin.localDatabase.sanCheck(w))) { + Logger(`The file ${id2path(id)} missing child(ren)`, LOG_LEVEL.NOTICE); + } + } + }); + notice.hide(); + Logger(`Done`, LOG_LEVEL.NOTICE); + // Logger("done", LOG_LEVEL.NOTICE); + }) + ); + + new Setting(containerHatchEl) + .setName("Drop History") + .setDesc("Initialize local and remote database, and send all or retrieve all again.") + .addButton((button) => + button + .setButtonText("Drop and send") + .setWarning() + .setDisabled(false) + .setClass("sls-btn-left") + .onClick(async () => { + await dropHistory(true); + }) + ) + .addButton((button) => + button + .setButtonText("Drop and receive") + .setWarning() + .setDisabled(false) + .setClass("sls-btn-right") + .onClick(async () => { + await dropHistory(false); + }) + ); + + new Setting(containerHatchEl) + .setName("Lock remote database") + .setDesc("Lock remote database for synchronize") + .addButton((button) => + button + .setButtonText("Lock") + .setDisabled(false) + .setWarning() + .onClick(async () => { + await this.plugin.markRemoteLocked(); + }) + ); + + new Setting(containerHatchEl) + .setName("Suspend file watching") + .setDesc("if enables it, all file operations are ignored.") + .addToggle((toggle) => + toggle.setValue(this.plugin.settings.suspendFileWatching).onChange(async (value) => { + this.plugin.settings.suspendFileWatching = value; + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerHatchEl) + .setName("Reset remote database") + .setDesc("Reset remote database, this affects only database. If you replicate again, remote database will restored by local database.") + .addButton((button) => + button + .setButtonText("Reset") + .setDisabled(false) + .setWarning() + .onClick(async () => { + await this.plugin.tryResetRemoteDatabase(); + }) + ); + new Setting(containerHatchEl) + .setName("Reset local database") + .setDesc("Reset local database, this affects only database. If you replicate again, local database will restored by remote database.") + .addButton((button) => + button + .setButtonText("Reset") + .setDisabled(false) + .setWarning() + .onClick(async () => { + await this.plugin.resetLocalDatabase(); + }) + ); + new Setting(containerHatchEl) + .setName("Initialize local database again") + .setDesc("WARNING: Reset local database and reconstruct by storage data. It affects local database, but if you replicate remote as is, remote data will be merged or corrupted.") + .addButton((button) => + button + .setButtonText("INITIALIZE") + .setWarning() + .setDisabled(false) + .onClick(async () => { + await this.plugin.resetLocalDatabase(); + await this.plugin.initializeDatabase(); + }) + ); + + addScreenElement("50", containerHatchEl); + // With great respect, thank you TfTHacker! + // refered: https://github.com/TfTHacker/obsidian42-brat/blob/main/src/features/BetaPlugins.ts + const containerPluginSettings = containerEl.createDiv(); + containerPluginSettings.createEl("h3", { text: "Plugins and settings (bleeding edge)" }); + + const updateDisabledOfDeviceAndVaultName = () => { + vaultName.setDisabled(this.plugin.settings.autoSweepPlugins || this.plugin.settings.autoSweepPluginsPeriodic); + vaultName.setTooltip(this.plugin.settings.autoSweepPlugins || this.plugin.settings.autoSweepPluginsPeriodic ? "You could not change when you enabling auto sweep." : ""); + }; + new Setting(containerPluginSettings).setName("Enable plugin synchronization").addToggle((toggle) => + toggle.setValue(this.plugin.settings.usePluginSync).onChange(async (value) => { + this.plugin.settings.usePluginSync = value; + await this.plugin.saveSettings(); + updatePluginPane(); + }) + ); + new Setting(containerPluginSettings).setName("Show own plugins and settings").addToggle((toggle) => + toggle.setValue(this.plugin.settings.showOwnPlugins).onChange(async (value) => { + this.plugin.settings.showOwnPlugins = value; + await this.plugin.saveSettings(); + updatePluginPane(); + }) + ); + + new Setting(containerPluginSettings) + .setName("Sweep plugins automatically") + .setDesc("Sweep plugins before replicating.") + .addToggle((toggle) => + toggle.setValue(this.plugin.settings.autoSweepPlugins).onChange(async (value) => { + this.plugin.settings.autoSweepPlugins = value; + updateDisabledOfDeviceAndVaultName(); + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerPluginSettings) + .setName("Sweep plugins periodically") + .setDesc("Sweep plugins each 1 minutes.") + .addToggle((toggle) => + toggle.setValue(this.plugin.settings.autoSweepPluginsPeriodic).onChange(async (value) => { + this.plugin.settings.autoSweepPluginsPeriodic = value; + updateDisabledOfDeviceAndVaultName(); + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerPluginSettings) + .setName("Notify updates") + .setDesc("Notify when any device has a newer plugin or its setting.") + .addToggle((toggle) => + toggle.setValue(this.plugin.settings.notifyPluginOrSettingUpdated).onChange(async (value) => { + this.plugin.settings.notifyPluginOrSettingUpdated = value; + await this.plugin.saveSettings(); + }) + ); + const vaultName = new Setting(containerPluginSettings) + .setName("Device and Vault name") + .setDesc("") + .addText((text) => { + text.setPlaceholder("desktop-main") + .setValue(this.plugin.settings.deviceAndVaultName) + .onChange(async (value) => { + this.plugin.settings.deviceAndVaultName = value; + await this.plugin.saveSettings(); + }); + // text.inputEl.setAttribute("type", "password"); + }); + + updateDisabledOfDeviceAndVaultName(); + const sweepPlugin = async (showMessage: boolean) => { + if (!this.plugin.settings.usePluginSync) { + return; + } + await this.plugin.sweepPlugin(showMessage); + updatePluginPane(); + }; + const updatePluginPane = async () => { + pluginConfig.innerHTML = "
Retrieving...
"; + const { plugins, allPlugins, thisDevicePlugins } = await this.plugin.getPluginList(); + let html = ` +
+ + `; + for (const vaults in plugins) { + if (!this.plugin.settings.showOwnPlugins && vaults == this.plugin.settings.deviceAndVaultName) continue; + html += ` + + + + `; + for (const v of plugins[vaults]) { + const mtime = v.mtime == 0 ? "-" : new Date(v.mtime).toLocaleString(); + let settingApplyable: boolean | string = "-"; + let settingFleshness = ""; + let isSameVersion = false; + let isSameContents = false; + if (thisDevicePlugins[v.manifest.id]) { + if (thisDevicePlugins[v.manifest.id].manifest.version == v.manifest.version) { + isSameVersion = true; + } + if (thisDevicePlugins[v.manifest.id].styleCss == v.styleCss && thisDevicePlugins[v.manifest.id].mainJs == v.mainJs && thisDevicePlugins[v.manifest.id].manifestJson == v.manifestJson) { + isSameContents = true; + } + } + if (thisDevicePlugins[v.manifest.id] && v.dataJson) { + // have this plugin. + const localSetting = thisDevicePlugins[v.manifest.id].dataJson || null; + + try { + const remoteSetting = v.dataJson; + if (!localSetting) { + settingFleshness = "newer"; + settingApplyable = true; + } else if (localSetting == remoteSetting) { + settingApplyable = "even"; + } else { + if (v.mtime > thisDevicePlugins[v.manifest.id].mtime) { + settingFleshness = "newer"; + } else { + settingFleshness = "older"; + } + settingApplyable = true; + } + } catch (ex) { + settingApplyable = "could not decrypt"; + } + } else if (!v.dataJson) { + settingApplyable = "N/A"; + } + // very ugly way. + const piece = ` + + + + + + + + + + + + `; + html += piece; + } + html += ` + + + +`; + } + html += "
${escapeStringToHTML(vaults)} + + + +
${escapeStringToHTML(v.manifest.name)}${isSameContents ? "even" : ``}
${escapeStringToHTML(mtime)}${settingApplyable === true ? "" : settingApplyable}
"; + pluginConfig.innerHTML = html; + pluginConfig.querySelectorAll(".apply-plugin-data").forEach((e) => + e.addEventListener("click", async (evt) => { + const plugin = allPlugins[e.attributes.getNamedItem("data-key").value]; + Logger(`Updating plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE); + await this.plugin.applyPluginData(plugin); + Logger(`Setting done:${plugin.manifest.name}`, LOG_LEVEL.NOTICE); + await sweepPlugin(true); + }) + ); + pluginConfig.querySelectorAll(".apply-plugin-version").forEach((e) => + e.addEventListener("click", async (evt) => { + const plugin = allPlugins[e.attributes.getNamedItem("data-key").value]; + Logger(`Setting plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE); + await this.plugin.applyPlugin(plugin); + Logger(`Updated plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE); + await sweepPlugin(true); + }) + ); + pluginConfig.querySelectorAll(".sls-plugin-apply-all-newer-plugin").forEach((e) => + e.addEventListener("click", async (evt) => { + Logger("Apply all newer plugins.", LOG_LEVEL.NOTICE); + const vaultname = e.attributes.getNamedItem("data-key").value; + const plugins = Object.values(allPlugins).filter((e) => e.deviceVaultName == vaultname && e.manifest.id != "obsidian-livesync"); + for (const plugin of plugins) { + const currentPlugin = thisDevicePlugins[plugin.manifest.id]; + if (currentPlugin) { + const thisVersion = versionNumberString2Number(plugin.manifest.version); + const currentVersion = versionNumberString2Number(currentPlugin.manifest.version); + if (thisVersion > currentVersion) { + Logger(`Updating plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE); + await this.plugin.applyPlugin(plugin); + Logger(`Updated plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE); + } else { + Logger(`Plugin ${plugin.manifest.name} is not new`); + } + } else { + Logger(`Updating plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE); + await this.plugin.applyPlugin(plugin); + Logger(`Updated plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE); + } + } + await sweepPlugin(true); + Logger("Done", LOG_LEVEL.NOTICE); + }) + ); + pluginConfig.querySelectorAll(".sls-plugin-apply-all-newer-setting").forEach((e) => + e.addEventListener("click", async (evt) => { + Logger("Apply all newer settings.", LOG_LEVEL.NOTICE); + const vaultname = e.attributes.getNamedItem("data-key").value; + const plugins = Object.values(allPlugins).filter((e) => e.deviceVaultName == vaultname && e.manifest.id != "obsidian-livesync"); + for (const plugin of plugins) { + const currentPlugin = thisDevicePlugins[plugin.manifest.id]; + if (currentPlugin) { + const thisVersion = plugin.mtime; + const currentVersion = currentPlugin.mtime; + if (thisVersion > currentVersion) { + Logger(`Setting plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE); + await this.plugin.applyPluginData(plugin); + Logger(`Setting done:${plugin.manifest.name}`, LOG_LEVEL.NOTICE); + } else { + Logger(`Setting ${plugin.manifest.name} is not new`); + } + } else { + Logger(`Setting plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE); + await this.plugin.applyPluginData(plugin); + Logger(`Setting done:${plugin.manifest.name}`, LOG_LEVEL.NOTICE); + } + } + await sweepPlugin(true); + Logger("Done", LOG_LEVEL.NOTICE); + }) + ); + pluginConfig.querySelectorAll(".sls-plugin-delete").forEach((e) => + e.addEventListener("click", async (evt) => { + const db = this.plugin.localDatabase.localDatabase; + const vaultname = e.attributes.getNamedItem("data-key").value; + const oldDocs = await db.allDocs({ startkey: `ps:${vaultname}-`, endkey: `ps:${vaultname}.`, include_docs: true }); + Logger(`Deleting ${vaultname}`, LOG_LEVEL.NOTICE); + const delDocs = oldDocs.rows.map((e) => { + e.doc._deleted = true; + return e.doc; + }); + await db.bulkDocs(delDocs); + Logger(`Deleted ${vaultname}`, LOG_LEVEL.NOTICE); + await this.plugin.replicate(true); + await updatePluginPane(); + }) + ); + }; + + const pluginConfig = containerPluginSettings.createEl("div"); + + new Setting(containerPluginSettings) + .setName("Reload") + .setDesc("Replicate once and reload the list") + .addButton((button) => + button + .setButtonText("Reload") + .setDisabled(false) + .onClick(async () => { + if (!this.plugin.settings.usePluginSync) { + return; + } + await this.plugin.replicate(true); + await updatePluginPane(); + }) + ); + new Setting(containerPluginSettings) + .setName("Save plugins into the database") + .setDesc("") + .addButton((button) => + button + .setButtonText("Save plugins") + .setDisabled(false) + .onClick(async () => { + if (!this.plugin.settings.usePluginSync) { + return; + } + Logger("Save plugins.", LOG_LEVEL.NOTICE); + await sweepPlugin(true); + Logger("All plugins have been saved.", LOG_LEVEL.NOTICE); + await this.plugin.replicate(true); + }) + ); + new Setting(containerPluginSettings) + .setName("Check updates") + .setDesc("") + .addButton((button) => + button + .setButtonText("Check") + .setDisabled(false) + .onClick(async () => { + Logger("Checking plugins.", LOG_LEVEL.NOTICE); + await this.plugin.checkPluginUpdate(); + }) + ); + updatePluginPane(); + + addScreenElement("60", containerPluginSettings); + + const containerCorruptedDataEl = containerEl.createDiv(); + + containerCorruptedDataEl.createEl("h3", { text: "Corrupted data" }); + + if (Object.keys(this.plugin.localDatabase.corruptedEntries).length > 0) { + const cx = containerCorruptedDataEl.createEl("div", { text: "If you have copy of these items on any device, simply edit once or twice. Or not, delete this. sorry.." }); + for (const k in this.plugin.localDatabase.corruptedEntries) { + const xx = cx.createEl("div", { text: `${k}` }); + + const ba = xx.createEl("button", { text: `Delete this` }, (e) => { + e.addEventListener("click", async () => { + await this.plugin.localDatabase.deleteDBEntry(k); + xx.remove(); + }); + }); + ba.addClass("mod-warning"); + xx.createEl("button", { text: `Restore from file` }, (e) => { + e.addEventListener("click", async () => { + const f = await this.app.vault.getFiles().filter((e) => path2id(e.path) == k); + if (f.length == 0) { + Logger("Not found in vault", LOG_LEVEL.NOTICE); + return; + } + await this.plugin.updateIntoDB(f[0]); + xx.remove(); + }); + }); + xx.addClass("mod-warning"); + } + } else { + containerCorruptedDataEl.createEl("div", { text: "There's no collupted data." }); + } + applyDisplayEnabled(); + addScreenElement("70", containerCorruptedDataEl); + changeDisplay("0"); + } +} diff --git a/src/e2ee.ts b/src/e2ee.ts new file mode 100644 index 0000000..bf16474 --- /dev/null +++ b/src/e2ee.ts @@ -0,0 +1,168 @@ +import { Logger } from "./logger"; +import { LOG_LEVEL } from "./types"; + +export type encodedData = [encryptedData: string, iv: string, salt: string]; +export type KeyBuffer = { + index: string; + key: CryptoKey; + salt: Uint8Array; +}; + +const KeyBuffs: KeyBuffer[] = []; +const decKeyBuffs: KeyBuffer[] = []; + +const KEY_RECYCLE_COUNT = 100; +let recycleCount = KEY_RECYCLE_COUNT; + +let semiStaticFieldBuffer: Uint8Array = null; +const nonceBuffer: Uint32Array = new Uint32Array(1); + +export async function getKeyForEncrypt(passphrase: string): Promise<[CryptoKey, Uint8Array]> { + // For performance, the plugin reuses the key KEY_RECYCLE_COUNT times. + const f = KeyBuffs.find((e) => e.index == passphrase); + if (f) { + recycleCount--; + if (recycleCount > 0) { + return [f.key, f.salt]; + } + KeyBuffs.remove(f); + recycleCount = KEY_RECYCLE_COUNT; + } + const xpassphrase = new TextEncoder().encode(passphrase); + const digest = await crypto.subtle.digest({ name: "SHA-256" }, xpassphrase); + const keyMaterial = await crypto.subtle.importKey("raw", digest, { name: "PBKDF2" }, false, ["deriveKey"]); + const salt = crypto.getRandomValues(new Uint8Array(16)); + const key = await crypto.subtle.deriveKey( + { + name: "PBKDF2", + salt, + iterations: 100000, + hash: "SHA-256", + }, + keyMaterial, + { name: "AES-GCM", length: 256 }, + false, + ["encrypt"] + ); + KeyBuffs.push({ + index: passphrase, + key, + salt, + }); + while (KeyBuffs.length > 50) { + KeyBuffs.shift(); + } + return [key, salt]; +} + +export async function getKeyForDecryption(passphrase: string, salt: Uint8Array): Promise<[CryptoKey, Uint8Array]> { + const bufKey = passphrase + uint8ArrayToHexString(salt); + const f = decKeyBuffs.find((e) => e.index == bufKey); + if (f) { + return [f.key, f.salt]; + } + const xpassphrase = new TextEncoder().encode(passphrase); + const digest = await crypto.subtle.digest({ name: "SHA-256" }, xpassphrase); + const keyMaterial = await crypto.subtle.importKey("raw", digest, { name: "PBKDF2" }, false, ["deriveKey"]); + const key = await crypto.subtle.deriveKey( + { + name: "PBKDF2", + salt, + iterations: 100000, + hash: "SHA-256", + }, + keyMaterial, + { name: "AES-GCM", length: 256 }, + false, + ["decrypt"] + ); + decKeyBuffs.push({ + index: bufKey, + key, + salt, + }); + while (decKeyBuffs.length > 50) { + decKeyBuffs.shift(); + } + return [key, salt]; +} + +function getSemiStaticField(reset?: boolean) { + // return fixed field of iv. + if (semiStaticFieldBuffer != null && !reset) { + return semiStaticFieldBuffer; + } + semiStaticFieldBuffer = crypto.getRandomValues(new Uint8Array(12)); + return semiStaticFieldBuffer; +} + +function getNonce() { + // This is nonce, so do not send same thing. + nonceBuffer[0]++; + if (nonceBuffer[0] > 10000) { + // reset semi-static field. + getSemiStaticField(true); + } + return nonceBuffer; +} + +function uint8ArrayToHexString(src: Uint8Array): string { + return Array.from(src) + .map((e: number): string => `00${e.toString(16)}`.slice(-2)) + .join(""); +} +function hexStringToUint8Array(src: string): Uint8Array { + const srcArr = [...src]; + const arr = srcArr.reduce((acc, _, i) => (i % 2 ? acc : [...acc, srcArr.slice(i, i + 2).join("")]), []).map((e) => parseInt(e, 16)); + return Uint8Array.from(arr); +} +export async function encrypt(input: string, passphrase: string) { + const [key, salt] = await getKeyForEncrypt(passphrase); + // Create initial vector with semifixed part and incremental part + // I think it's not good against related-key attacks. + const fixedPart = getSemiStaticField(); + const invocationPart = getNonce(); + const iv = Uint8Array.from([...fixedPart, ...new Uint8Array(invocationPart.buffer)]); + const plainStringified: string = JSON.stringify(input); + const plainStringBuffer: Uint8Array = new TextEncoder().encode(plainStringified); + const encryptedDataArrayBuffer = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, plainStringBuffer); + + const encryptedData = window.btoa(Array.from(new Uint8Array(encryptedDataArrayBuffer), (char) => String.fromCharCode(char)).join("")); + + //return data with iv and salt. + const response: encodedData = [encryptedData, uint8ArrayToHexString(iv), uint8ArrayToHexString(salt)]; + const ret = JSON.stringify(response); + return ret; +} + +export async function decrypt(encryptedResult: string, passphrase: string): Promise { + try { + const [encryptedData, ivString, salt]: encodedData = JSON.parse(encryptedResult); + const [key] = await getKeyForDecryption(passphrase, hexStringToUint8Array(salt)); + const iv = hexStringToUint8Array(ivString); + // decode base 64, it should increase speed and i should with in MAX_DOC_SIZE_BIN, so it won't OOM. + const encryptedDataBin = window.atob(encryptedData); + const encryptedDataArrayBuffer = Uint8Array.from(encryptedDataBin.split(""), (char) => char.charCodeAt(0)); + const plainStringBuffer: ArrayBuffer = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, encryptedDataArrayBuffer); + const plainStringified = new TextDecoder().decode(plainStringBuffer); + const plain = JSON.parse(plainStringified); + return plain; + } catch (ex) { + Logger("Couldn't decode! You should wrong the passphrases", LOG_LEVEL.VERBOSE); + Logger(ex, LOG_LEVEL.VERBOSE); + throw ex; + } +} + +export async function testCrypt() { + const src = "supercalifragilisticexpialidocious"; + const encoded = await encrypt(src, "passwordTest"); + const decrypted = await decrypt(encoded, "passwordTest"); + if (src != decrypted) { + Logger("WARNING! Your device would not support encryption.", LOG_LEVEL.VERBOSE); + return false; + } else { + Logger("CRYPT LOGIC OK", LOG_LEVEL.VERBOSE); + return true; + } +} diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 0000000..4529364 --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,13 @@ +import { LOG_LEVEL } from "./types"; + +// eslint-disable-next-line require-await +export let Logger: (message: any, levlel?: LOG_LEVEL) => Promise = async (message, _) => { + const timestamp = new Date().toLocaleString(); + const messagecontent = typeof message == "string" ? message : message instanceof Error ? `${message.name}:${message.message}` : JSON.stringify(message, null, 2); + const newmessage = timestamp + "->" + messagecontent; + console.log(newmessage); +}; + +export function setLogger(loggerFun: (message: any, levlel?: LOG_LEVEL) => Promise) { + Logger = loggerFun; +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..78cbeba --- /dev/null +++ b/src/main.ts @@ -0,0 +1,1300 @@ +import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest } from "obsidian"; +import { diff_match_patch } from "diff-match-patch"; + +import { EntryDoc, LoadedEntry, ObsidianLiveSyncSettings, diff_check_result, diff_result_leaf, EntryBody, PluginDataEntry, LOG_LEVEL, VER, PERIODIC_PLUGIN_SWEEP, DEFAULT_SETTINGS, PluginList, DevicePluginList } from "./types"; +import { base64ToString, arrayBufferToBase64, base64ToArrayBuffer, isValidPath, versionNumberString2Number, id2path, path2id, runWithLock } from "./utils"; +import { Logger, setLogger } from "./logger"; +import { LocalPouchDB } from "./LocalPouchDB"; +import { LogDisplayModal } from "./LogDisplayModal"; +import { ConflictResolveModal } from "./ConflictResolveModal"; +import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab"; + +export default class ObsidianLiveSyncPlugin extends Plugin { + settings: ObsidianLiveSyncSettings; + localDatabase: LocalPouchDB; + logMessage: string[] = []; + statusBar: HTMLElement; + statusBar2: HTMLElement; + suspended: boolean; + + setInterval(handler: () => any, timeout?: number): number { + const timer = window.setInterval(handler, timeout); + this.registerInterval(timer); + return timer; + } + async onload() { + setLogger(this.addLog.bind(this)); // Logger moved to global. + Logger("loading plugin"); + const lsname = "obsidian-live-sync-ver" + this.app.vault.getName(); + const last_version = localStorage.getItem(lsname); + await this.loadSettings(); + if (!last_version || Number(last_version) < VER) { + this.settings.liveSync = false; + this.settings.syncOnSave = false; + this.settings.syncOnStart = false; + this.settings.syncOnFileOpen = false; + this.settings.periodicReplication = false; + this.settings.versionUpFlash = "I changed specifications incompatiblly, so when you enable sync again, be sure to made version up all nother devides."; + this.saveSettings(); + } + localStorage.setItem(lsname, `${VER}`); + await this.openDatabase(); + + addIcon( + "replicate", + ` + + + + + ` + ); + addIcon( + "view-log", + ` + + + ` + ); + this.addRibbonIcon("replicate", "Replicate", async () => { + await this.replicate(true); + }); + + this.addRibbonIcon("view-log", "Show log", () => { + new LogDisplayModal(this.app, this).open(); + }); + + this.statusBar = this.addStatusBarItem(); + this.statusBar.addClass("syncstatusbar"); + this.refreshStatusText = this.refreshStatusText.bind(this); + + this.statusBar2 = this.addStatusBarItem(); + // this.watchVaultChange = debounce(this.watchVaultChange.bind(this), delay, false); + // this.watchVaultDelete = debounce(this.watchVaultDelete.bind(this), delay, false); + // this.watchVaultRename = debounce(this.watchVaultRename.bind(this), delay, false); + + this.watchVaultChange = this.watchVaultChange.bind(this); + this.watchVaultCreate = this.watchVaultCreate.bind(this); + this.watchVaultDelete = this.watchVaultDelete.bind(this); + this.watchVaultRename = this.watchVaultRename.bind(this); + this.watchWorkspaceOpen = debounce(this.watchWorkspaceOpen.bind(this), 1000, false); + this.watchWindowVisiblity = debounce(this.watchWindowVisiblity.bind(this), 1000, false); + + this.parseReplicationResult = this.parseReplicationResult.bind(this); + + this.periodicSync = this.periodicSync.bind(this); + this.setPeriodicSync = this.setPeriodicSync.bind(this); + + // this.registerWatchEvents(); + this.addSettingTab(new ObsidianLiveSyncSettingTab(this.app, this)); + + this.app.workspace.onLayoutReady(async () => { + try { + await this.initializeDatabase(); + await this.realizeSettingSyncMode(); + this.registerWatchEvents(); + if (this.settings.syncOnStart) { + await this.localDatabase.openReplication(this.settings, false, false, this.parseReplicationResult); + } + } catch (ex) { + Logger("Error while loading Self-hosted LiveSync", LOG_LEVEL.NOTICE); + Logger(ex, LOG_LEVEL.VERBOSE); + } + }); + this.addCommand({ + id: "livesync-replicate", + name: "Replicate now", + callback: () => { + this.replicate(); + }, + }); + this.addCommand({ + id: "livesync-dump", + name: "Dump informations of this doc ", + editorCallback: (editor: Editor, view: MarkdownView) => { + this.localDatabase.disposeHashCache(); + this.localDatabase.getDBEntry(view.file.path, {}, true, false); + }, + }); + this.addCommand({ + id: "livesync-gc", + name: "garbage collect now", + callback: () => { + this.garbageCollect(); + }, + }); + this.addCommand({ + id: "livesync-toggle", + name: "Toggle LiveSync", + callback: async () => { + if (this.settings.liveSync) { + this.settings.liveSync = false; + Logger("LiveSync Disabled.", LOG_LEVEL.NOTICE); + } else { + this.settings.liveSync = true; + Logger("LiveSync Enabled.", LOG_LEVEL.NOTICE); + } + await this.realizeSettingSyncMode(); + this.saveSettings(); + }, + }); + this.addCommand({ + id: "livesync-suspendall", + name: "Toggle All Sync.", + callback: async () => { + if (this.suspended) { + this.suspended = false; + Logger("Self-hosted LiveSync resumed", LOG_LEVEL.NOTICE); + } else { + this.suspended = true; + Logger("Self-hosted LiveSync suspended", LOG_LEVEL.NOTICE); + } + await this.realizeSettingSyncMode(); + this.saveSettings(); + }, + }); + this.triggerRealizeSettingSyncMode = debounce(this.triggerRealizeSettingSyncMode.bind(this), 1000); + this.triggerCheckPluginUpdate = debounce(this.triggerCheckPluginUpdate.bind(this), 3000); + } + onunload() { + this.localDatabase.onunload(); + if (this.gcTimerHandler != null) { + clearTimeout(this.gcTimerHandler); + this.gcTimerHandler = null; + } + this.clearPeriodicSync(); + this.clearPluginSweep(); + this.localDatabase.closeReplication(); + this.localDatabase.close(); + window.removeEventListener("visibilitychange", this.watchWindowVisiblity); + Logger("unloading plugin"); + } + + async openDatabase() { + if (this.localDatabase != null) { + this.localDatabase.close(); + } + const vaultName = this.app.vault.getName(); + Logger("Open Database..."); + this.localDatabase = new LocalPouchDB(this.settings, vaultName); + this.localDatabase.updateInfo = () => { + this.refreshStatusText(); + }; + await this.localDatabase.initializeDatabase(); + } + async garbageCollect() { + await this.localDatabase.garbageCollect(); + } + + async loadSettings() { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + this.settings.workingEncrypt = this.settings.encrypt; + this.settings.workingPassphrase = this.settings.passphrase; + } + + triggerRealizeSettingSyncMode() { + (async () => await this.realizeSettingSyncMode())(); + } + async saveSettings() { + await this.saveData(this.settings); + this.localDatabase.settings = this.settings; + this.triggerRealizeSettingSyncMode(); + } + gcTimerHandler: any = null; + gcHook() { + if (this.settings.gcDelay == 0) return; + const GC_DELAY = this.settings.gcDelay * 1000; // if leaving opening window, try GC, + if (this.gcTimerHandler != null) { + clearTimeout(this.gcTimerHandler); + this.gcTimerHandler = null; + } + this.gcTimerHandler = setTimeout(() => { + this.gcTimerHandler = null; + this.garbageCollect(); + }, GC_DELAY); + } + registerWatchEvents() { + this.registerEvent(this.app.vault.on("modify", this.watchVaultChange)); + this.registerEvent(this.app.vault.on("delete", this.watchVaultDelete)); + this.registerEvent(this.app.vault.on("rename", this.watchVaultRename)); + this.registerEvent(this.app.vault.on("create", this.watchVaultChange)); + this.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen)); + window.addEventListener("visibilitychange", this.watchWindowVisiblity); + } + + watchWindowVisiblity() { + this.watchWindowVisiblityAsync(); + } + async watchWindowVisiblityAsync() { + if (this.settings.suspendFileWatching) return; + // if (this.suspended) return; + const isHidden = document.hidden; + await this.applyBatchChange(); + if (isHidden) { + this.localDatabase.closeReplication(); + this.clearPeriodicSync(); + } else { + // suspend all temporary. + if (this.suspended) return; + if (this.settings.autoSweepPlugins) { + await this.sweepPlugin(false); + } + if (this.settings.liveSync) { + await this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult); + } + if (this.settings.syncOnStart) { + await this.localDatabase.openReplication(this.settings, false, false, this.parseReplicationResult); + } + if (this.settings.periodicReplication) { + this.setPeriodicSync(); + } + } + this.gcHook(); + } + + watchWorkspaceOpen(file: TFile) { + if (this.settings.suspendFileWatching) return; + this.watchWorkspaceOpenAsync(file); + } + async watchWorkspaceOpenAsync(file: TFile) { + await this.applyBatchChange(); + if (file == null) return; + if (this.settings.syncOnFileOpen && !this.suspended) { + await this.replicate(); + } + this.localDatabase.disposeHashCache(); + await this.showIfConflicted(file); + this.gcHook(); + } + watchVaultCreate(file: TFile, ...args: any[]) { + if (this.settings.suspendFileWatching) return; + this.watchVaultChangeAsync(file, ...args); + } + watchVaultChange(file: TAbstractFile, ...args: any[]) { + if (!(file instanceof TFile)) { + return; + } + if (this.settings.suspendFileWatching) return; + // If batchsave is enabled, queue all changes and do nothing. + if (this.settings.batchSave) { + this.batchFileChange = Array.from(new Set([...this.batchFileChange, file.path])); + this.refreshStatusText(); + return; + } + this.watchVaultChangeAsync(file, ...args); + } + async applyBatchChange() { + return await runWithLock("batchSave", false, async () => { + const batchItems = JSON.parse(JSON.stringify(this.batchFileChange)) as string[]; + this.batchFileChange = []; + const files = this.app.vault.getFiles(); + const promises = batchItems.map(async (e) => { + try { + if (await this.app.vault.adapter.exists(normalizePath(e))) { + const f = files.find((f) => f.path == e); + if (f) { + await this.updateIntoDB(f); + Logger(`Batch save:${e}`); + } + } + } catch (ex) { + Logger(`Batch save error:${e}`, LOG_LEVEL.NOTICE); + Logger(ex, LOG_LEVEL.VERBOSE); + } + }); + this.refreshStatusText(); + return await Promise.all(promises); + }); + } + batchFileChange: string[] = []; + async watchVaultChangeAsync(file: TFile, ...args: any[]) { + if (file instanceof TFile) { + await this.updateIntoDB(file); + this.gcHook(); + } + } + watchVaultDelete(file: TAbstractFile) { + // When save is delayed, it should be cancelled. + this.batchFileChange = this.batchFileChange.filter((e) => e == file.path); + if (this.settings.suspendFileWatching) return; + this.watchVaultDeleteAsync(file); + } + async watchVaultDeleteAsync(file: TAbstractFile) { + if (file instanceof TFile) { + await this.deleteFromDB(file); + } else if (file instanceof TFolder) { + await this.deleteFolderOnDB(file); + } + this.gcHook(); + } + GetAllFilesRecursively(file: TAbstractFile): TFile[] { + if (file instanceof TFile) { + return [file]; + } else if (file instanceof TFolder) { + const result: TFile[] = []; + for (const v of file.children) { + result.push(...this.GetAllFilesRecursively(v)); + } + return result; + } else { + Logger(`Filetype error:${file.path}`, LOG_LEVEL.NOTICE); + throw new Error(`Filetype error:${file.path}`); + } + } + watchVaultRename(file: TAbstractFile, oldFile: any) { + if (this.settings.suspendFileWatching) return; + this.watchVaultRenameAsync(file, oldFile); + } + getFilePath(file: TAbstractFile): string { + if (file instanceof TFolder) { + if (file.isRoot()) return ""; + return this.getFilePath(file.parent) + "/" + file.name; + } + if (file instanceof TFile) { + return this.getFilePath(file.parent) + "/" + file.name; + } + + return this.getFilePath(file.parent) + "/" + file.name; + } + async watchVaultRenameAsync(file: TAbstractFile, oldFile: any) { + Logger(`${oldFile} renamed to ${file.path}`, LOG_LEVEL.VERBOSE); + await this.applyBatchChange(); + if (file instanceof TFolder) { + const newFiles = this.GetAllFilesRecursively(file); + // for guard edge cases. this won't happen and each file's event will be raise. + for (const i of newFiles) { + const newFilePath = normalizePath(this.getFilePath(i)); + const newFile = this.app.vault.getAbstractFileByPath(newFilePath); + if (newFile instanceof TFile) { + Logger(`save ${newFile.path} into db`); + await this.updateIntoDB(newFile); + } + } + Logger(`delete below ${oldFile} from db`); + await this.deleteFromDBbyPath(oldFile); + } else if (file instanceof TFile) { + Logger(`file save ${file.path} into db`); + await this.updateIntoDB(file); + Logger(`deleted ${oldFile} into db`); + await this.deleteFromDBbyPath(oldFile); + } + this.gcHook(); + } + addLogHook: () => void = null; + //--> Basic document Functions + notifies: { [key: string]: { notice: Notice; timer: NodeJS.Timeout; count: number } } = {}; + // eslint-disable-next-line require-await + async addLog(message: any, level: LOG_LEVEL = LOG_LEVEL.INFO) { + if (level < LOG_LEVEL.INFO && this.settings && this.settings.lessInformationInLog) { + return; + } + if (this.settings && !this.settings.showVerboseLog && level == LOG_LEVEL.VERBOSE) { + return; + } + const valutName = this.app.vault.getName(); + const timestamp = new Date().toLocaleString(); + const messagecontent = typeof message == "string" ? message : message instanceof Error ? `${message.name}:${message.message}` : JSON.stringify(message, null, 2); + const newmessage = timestamp + "->" + messagecontent; + + this.logMessage = [].concat(this.logMessage).concat([newmessage]).slice(-100); + console.log(valutName + ":" + newmessage); + // if (this.statusBar2 != null) { + // this.statusBar2.setText(newmessage.substring(0, 60)); + // } + + if (level >= LOG_LEVEL.NOTICE) { + if (messagecontent in this.notifies) { + clearTimeout(this.notifies[messagecontent].timer); + this.notifies[messagecontent].count++; + this.notifies[messagecontent].notice.setMessage(`(${this.notifies[messagecontent].count}):${messagecontent}`); + this.notifies[messagecontent].timer = setTimeout(() => { + const notify = this.notifies[messagecontent].notice; + delete this.notifies[messagecontent]; + try { + notify.hide(); + } catch (ex) { + // NO OP + } + }, 5000); + } else { + const notify = new Notice(messagecontent, 0); + this.notifies[messagecontent] = { + count: 0, + notice: notify, + timer: setTimeout(() => { + delete this.notifies[messagecontent]; + notify.hide(); + }, 5000), + }; + } + } + if (this.addLogHook != null) this.addLogHook(); + } + + async ensureDirectory(fullpath: string) { + const pathElements = fullpath.split("/"); + pathElements.pop(); + let c = ""; + for (const v of pathElements) { + c += v; + try { + await this.app.vault.createFolder(c); + } catch (ex) { + // basically skip exceptions. + if (ex.message && ex.message == "Folder already exists.") { + // especialy this message is. + } else { + Logger("Folder Create Error"); + Logger(ex); + } + } + c += "/"; + } + } + + async doc2storage_create(docEntry: EntryBody, force?: boolean) { + const pathSrc = id2path(docEntry._id); + const doc = await this.localDatabase.getDBEntry(pathSrc, { rev: docEntry._rev }); + if (doc === false) return; + const path = id2path(doc._id); + if (doc.datatype == "newnote") { + const bin = base64ToArrayBuffer(doc.data); + if (bin != null) { + if (!isValidPath(path)) { + Logger(`The file that having platform dependent name has been arrived. This file has skipped: ${path}`, LOG_LEVEL.NOTICE); + return; + } + await this.ensureDirectory(path); + try { + const newfile = await this.app.vault.createBinary(normalizePath(path), bin, { ctime: doc.ctime, mtime: doc.mtime }); + Logger("live : write to local (newfile:b) " + path); + await this.app.vault.trigger("create", newfile); + } catch (ex) { + Logger("could not write to local (newfile:bin) " + path, LOG_LEVEL.NOTICE); + Logger(ex, LOG_LEVEL.VERBOSE); + } + } + } else if (doc.datatype == "plain") { + if (!isValidPath(path)) { + Logger(`The file that having platform dependent name has been arrived. This file has skipped: ${path}`, LOG_LEVEL.NOTICE); + return; + } + await this.ensureDirectory(path); + try { + const newfile = await this.app.vault.create(normalizePath(path), doc.data, { ctime: doc.ctime, mtime: doc.mtime }); + Logger("live : write to local (newfile:p) " + path); + await this.app.vault.trigger("create", newfile); + } catch (ex) { + Logger("could not write to local (newfile:plain) " + path, LOG_LEVEL.NOTICE); + Logger(ex, LOG_LEVEL.VERBOSE); + } + } else { + Logger("live : New data imcoming, but we cound't parse that." + doc.datatype, LOG_LEVEL.NOTICE); + } + } + + async deleteVaultItem(file: TFile | TFolder) { + const dir = file.parent; + if (this.settings.trashInsteadDelete) { + await this.app.vault.trash(file, false); + } else { + await this.app.vault.delete(file); + } + Logger(`deleted:${file.path}`); + Logger(`other items:${dir.children.length}`); + if (dir.children.length == 0) { + if (!this.settings.doNotDeleteFolder) { + Logger(`all files deleted by replication, so delete dir`); + await this.deleteVaultItem(dir); + } + } + } + async doc2storate_modify(docEntry: EntryBody, file: TFile, force?: boolean) { + const pathSrc = id2path(docEntry._id); + if (docEntry._deleted) { + //basically pass. + //but if there're no docs left, delete file. + const lastDocs = await this.localDatabase.getDBEntry(pathSrc); + if (lastDocs === false) { + await this.deleteVaultItem(file); + } else { + // it perhaps delete some revisions. + // may be we have to reload this + await this.pullFile(pathSrc, null, true); + Logger(`delete skipped:${lastDocs._id}`); + } + return; + } + const localMtime = ~~(file.stat.mtime / 1000); + const docMtime = ~~(docEntry.mtime / 1000); + if (localMtime < docMtime || force) { + const doc = await this.localDatabase.getDBEntry(pathSrc); + let msg = "livesync : newer local files so write to local:" + file.path; + if (force) msg = "livesync : force write to local:" + file.path; + if (doc === false) return; + const path = id2path(doc._id); + if (doc.datatype == "newnote") { + const bin = base64ToArrayBuffer(doc.data); + if (bin != null) { + if (!isValidPath(path)) { + Logger(`The file that having platform dependent name has been arrived. This file has skipped: ${path}`, LOG_LEVEL.NOTICE); + return; + } + await this.ensureDirectory(path); + try { + await this.app.vault.modifyBinary(file, bin, { ctime: doc.ctime, mtime: doc.mtime }); + Logger(msg); + await this.app.vault.trigger("modify", file); + } catch (ex) { + Logger("could not write to local (modify:bin) " + path, LOG_LEVEL.NOTICE); + } + } + } else if (doc.datatype == "plain") { + if (!isValidPath(path)) { + Logger(`The file that having platform dependent name has been arrived. This file has skipped: ${path}`, LOG_LEVEL.NOTICE); + return; + } + await this.ensureDirectory(path); + try { + await this.app.vault.modify(file, doc.data, { ctime: doc.ctime, mtime: doc.mtime }); + Logger(msg); + await this.app.vault.trigger("modify", file); + } catch (ex) { + Logger("could not write to local (modify:plain) " + path, LOG_LEVEL.NOTICE); + } + } else { + Logger("live : New data imcoming, but we cound't parse that.:" + doc.datatype + "-", LOG_LEVEL.NOTICE); + } + } else if (localMtime > docMtime) { + // newer local file. + // ? + } else { + //Nothing have to op. + //eq.case + } + } + async handleDBChanged(change: EntryBody) { + const allfiles = this.app.vault.getFiles(); + const targetFiles = allfiles.filter((e) => e.path == id2path(change._id)); + if (targetFiles.length == 0) { + if (change._deleted) { + return; + } + const doc = change; + await this.doc2storage_create(doc); + } + if (targetFiles.length == 1) { + const doc = change; + const file = targetFiles[0]; + await this.doc2storate_modify(doc, file); + await this.showIfConflicted(file); + } + } + + periodicSyncHandler: number = null; + //---> Sync + async parseReplicationResult(docs: Array>): Promise { + this.refreshStatusText(); + for (const change of docs) { + if (change._id.startsWith("ps:")) { + if (this.settings.notifyPluginOrSettingUpdated) { + this.triggerCheckPluginUpdate(); + } + continue; + } + if (change._id.startsWith("h:")) { + continue; + } + if (change.type != "leaf" && change.type != "versioninfo" && change.type != "milestoneinfo" && change.type != "nodeinfo") { + Logger("replication change arrived", LOG_LEVEL.VERBOSE); + await this.handleDBChanged(change); + } + if (change.type == "versioninfo") { + if (change.version > VER) { + this.localDatabase.closeReplication(); + Logger(`Remote database updated to incompatible version. update your self-hosted-livesync plugin.`, LOG_LEVEL.NOTICE); + } + } + this.gcHook(); + } + } + triggerCheckPluginUpdate() { + (async () => await this.checkPluginUpdate())(); + } + async checkPluginUpdate() { + if (!this.settings.usePluginSync) return; + await this.sweepPlugin(false); + const { allPlugins, thisDevicePlugins } = await this.getPluginList(); + const arrPlugins = Object.values(allPlugins); + for (const plugin of arrPlugins) { + const currentPlugin = thisDevicePlugins[plugin.manifest.id]; + if (currentPlugin) { + const thisVersion = versionNumberString2Number(plugin.manifest.version); + const currentVersion = versionNumberString2Number(currentPlugin.manifest.version); + if (thisVersion > currentVersion) { + Logger(`the device ${plugin.deviceVaultName} has the newer plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE); + } + if (plugin.mtime > currentPlugin.mtime) { + Logger(`the device ${plugin.deviceVaultName} has the newer settings of the plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE); + } + } else { + Logger(`the device ${plugin.deviceVaultName} has the new plugin:${plugin.manifest.name}`, LOG_LEVEL.NOTICE); + } + } + } + clearPeriodicSync() { + if (this.periodicSyncHandler != null) { + clearInterval(this.periodicSyncHandler); + this.periodicSyncHandler = null; + } + } + setPeriodicSync() { + if (this.settings.periodicReplication && this.settings.periodicReplicationInterval > 0) { + this.clearPeriodicSync(); + this.periodicSyncHandler = this.setInterval(async () => await this.periodicSync(), Math.max(this.settings.periodicReplicationInterval, 30) * 1000); + } + } + async periodicSync() { + await this.replicate(); + } + periodicPluginSweepHandler: number = null; + clearPluginSweep() { + if (this.periodicPluginSweepHandler != null) { + clearInterval(this.periodicPluginSweepHandler); + this.periodicPluginSweepHandler = null; + } + } + setPluginSweep() { + if (this.settings.autoSweepPluginsPeriodic) { + this.clearPluginSweep(); + this.periodicPluginSweepHandler = this.setInterval(async () => await this.periodicPluginSweep(), PERIODIC_PLUGIN_SWEEP * 1000); + } + } + async periodicPluginSweep() { + await this.sweepPlugin(false); + } + async realizeSettingSyncMode() { + this.localDatabase.closeReplication(); + this.clearPeriodicSync(); + this.clearPluginSweep(); + await this.applyBatchChange(); + // disable all sync temporary. + if (this.suspended) return; + if (this.settings.autoSweepPlugins) { + await this.sweepPlugin(false); + } + if (this.settings.liveSync) { + await this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult); + this.refreshStatusText(); + } + this.setPeriodicSync(); + this.setPluginSweep(); + } + lastMessage = ""; + refreshStatusText() { + const sent = this.localDatabase.docSent; + const arrived = this.localDatabase.docArrived; + let w = ""; + switch (this.localDatabase.syncStatus) { + case "CLOSED": + case "COMPLETED": + case "NOT_CONNECTED": + w = "⏹"; + break; + case "STARTED": + w = "🌀"; + break; + case "PAUSED": + w = "💤"; + break; + case "CONNECTED": + w = "⚡"; + break; + case "ERRORED": + w = "⚠"; + break; + default: + w = "?"; + } + this.statusBar.title = this.localDatabase.syncStatus; + let waiting = ""; + if (this.settings.batchSave) { + waiting = " " + this.batchFileChange.map((e) => "🛫").join(""); + waiting = waiting.replace(/🛫{10}/g, "🚀"); + } + const message = `Sync:${w} ↑${sent} ↓${arrived}${waiting}`; + this.setStatusBarText(message); + } + setStatusBarText(message: string) { + if (this.lastMessage != message) { + this.statusBar.setText(message); + if (this.settings.showStatusOnEditor) { + const root = document.documentElement; + root.style.setProperty("--slsmessage", '"' + message + '"'); + } else { + const root = document.documentElement; + root.style.setProperty("--slsmessage", '""'); + } + this.lastMessage = message; + } + } + async replicate(showMessage?: boolean) { + if (this.settings.versionUpFlash != "") { + new Notice("Open settings and check message, please."); + return; + } + await this.applyBatchChange(); + if (this.settings.autoSweepPlugins) { + await this.sweepPlugin(false); + } + await this.localDatabase.openReplication(this.settings, false, showMessage, this.parseReplicationResult); + } + + async initializeDatabase(showingNotice?: boolean) { + await this.openDatabase(); + await this.syncAllFiles(showingNotice); + } + async replicateAllToServer(showingNotice?: boolean) { + if (this.settings.autoSweepPlugins) { + await this.sweepPlugin(showingNotice); + } + return await this.localDatabase.replicateAllToServer(this.settings, showingNotice); + } + async markRemoteLocked() { + return await this.localDatabase.markRemoteLocked(this.settings, true); + } + async markRemoteUnlocked() { + return await this.localDatabase.markRemoteLocked(this.settings, false); + } + async markRemoteResolved() { + return await this.localDatabase.markRemoteResolved(this.settings); + } + async syncAllFiles(showingNotice?: boolean) { + // synchronize all files between database and storage. + let notice: Notice = null; + if (showingNotice) { + notice = new Notice("Initializing", 0); + } + const filesStorage = this.app.vault.getFiles(); + const filesStorageName = filesStorage.map((e) => e.path); + const wf = await this.localDatabase.localDatabase.allDocs(); + const filesDatabase = wf.rows.filter((e) => !e.id.startsWith("h:") && !e.id.startsWith("ps:") && e.id != "obsydian_livesync_version").map((e) => id2path(e.id)); + + const onlyInStorage = filesStorage.filter((e) => filesDatabase.indexOf(e.path) == -1); + const onlyInDatabase = filesDatabase.filter((e) => filesStorageName.indexOf(e) == -1); + + const onlyInStorageNames = onlyInStorage.map((e) => e.path); + + const syncFiles = filesStorage.filter((e) => onlyInStorageNames.indexOf(e.path) == -1); + Logger("Initialize and checking database files"); + Logger("Updating database by new files"); + this.setStatusBarText(`UPDATE DATABASE`); + + const runAll = async (procedurename: string, objects: T[], callback: (arg: T) => Promise) => { + const count = objects.length; + Logger(procedurename); + let i = 0; + // let lastTicks = performance.now() + 2000; + const procs = objects.map(async (e) => { + try { + await callback(e); + i++; + if (i % 25 == 0) { + const notify = `${procedurename} : ${i}/${count}`; + if (notice != null) notice.setMessage(notify); + Logger(notify); + this.setStatusBarText(notify); + } + } catch (ex) { + Logger(`Error while ${procedurename}`, LOG_LEVEL.NOTICE); + Logger(ex); + } + }); + if (!Promise.allSettled) { + await Promise.all( + procs.map((p) => + p + .then((value) => ({ + status: "fulfilled", + value, + })) + .catch((reason) => ({ + status: "rejected", + reason, + })) + ) + ); + } else { + await Promise.allSettled(procs); + } + }; + await runAll("UPDATE DATABASE", onlyInStorage, async (e) => { + Logger(`Update into ${e.path}`); + await this.updateIntoDB(e); + }); + await runAll("UPDATE STORAGE", onlyInDatabase, async (e) => { + Logger(`Pull from db:${e}`); + await this.pullFile(e, filesStorage, false, null, false); + }); + await runAll("CHECK FILE STATUS", syncFiles, async (e) => { + await this.syncFileBetweenDBandStorage(e, filesStorage); + }); + this.setStatusBarText(`NOW TRACKING!`); + Logger("Initialized,NOW TRACKING!"); + if (showingNotice) { + notice.hide(); + Logger("Initialize done!", LOG_LEVEL.NOTICE); + } + } + async deleteFolderOnDB(folder: TFolder) { + Logger(`delete folder:${folder.path}`); + await this.localDatabase.deleteDBEntryPrefix(folder.path + "/"); + for (const v of folder.children) { + const entry = v as TFile & TFolder; + Logger(`->entry:${entry.path}`, LOG_LEVEL.VERBOSE); + if (entry.children) { + Logger(`->is dir`, LOG_LEVEL.VERBOSE); + await this.deleteFolderOnDB(entry); + try { + if (this.settings.trashInsteadDelete) { + await this.app.vault.trash(entry, false); + } else { + await this.app.vault.delete(entry); + } + } catch (ex) { + if (ex.code && ex.code == "ENOENT") { + //NO OP. + } else { + Logger(`error while delete folder:${entry.path}`, LOG_LEVEL.NOTICE); + Logger(ex); + } + } + } else { + Logger(`->is file`, LOG_LEVEL.VERBOSE); + await this.deleteFromDB(entry); + } + } + try { + if (this.settings.trashInsteadDelete) { + await this.app.vault.trash(folder, false); + } else { + await this.app.vault.delete(folder); + } + } catch (ex) { + if (ex.code && ex.code == "ENOENT") { + //NO OP. + } else { + Logger(`error while delete filder:${folder.path}`, LOG_LEVEL.NOTICE); + Logger(ex); + } + } + } + + async renameFolder(folder: TFolder, oldFile: any) { + for (const v of folder.children) { + const entry = v as TFile & TFolder; + if (entry.children) { + await this.deleteFolderOnDB(entry); + if (this.settings.trashInsteadDelete) { + await this.app.vault.trash(entry, false); + } else { + await this.app.vault.delete(entry); + } + } else { + await this.deleteFromDB(entry); + } + } + } + + // --> conflict resolving + async getConflictedDoc(path: string, rev: string): Promise { + try { + const doc = await this.localDatabase.getDBEntry(path, { rev: rev }); + if (doc === false) return false; + let data = doc.data; + if (doc.datatype == "newnote") { + data = base64ToString(doc.data); + } else if (doc.datatype == "plain") { + data = doc.data; + } + return { + ctime: doc.ctime, + mtime: doc.mtime, + rev: rev, + data: data, + }; + } catch (ex) { + if (ex.status && ex.status == 404) { + return false; + } + } + return false; + } + /** + * Getting file conflicted status. + * @param path the file location + * @returns true -> resolved, false -> nothing to do, or check result. + */ + async getConflictedStatus(path: string): Promise { + const test = await this.localDatabase.getDBEntry(path, { conflicts: true }); + if (test === false) return false; + if (test == null) return false; + if (!test._conflicts) return false; + if (test._conflicts.length == 0) return false; + // should be one or more conflicts; + const leftLeaf = await this.getConflictedDoc(path, test._rev); + const rightLeaf = await this.getConflictedDoc(path, test._conflicts[0]); + if (leftLeaf == false) { + // what's going on.. + Logger(`could not get current revisions:${path}`, LOG_LEVEL.NOTICE); + return false; + } + if (rightLeaf == false) { + // Conflicted item could not load, delete this. + await this.localDatabase.deleteDBEntry(path, { rev: test._conflicts[0] }); + await this.pullFile(path, null, true); + Logger(`could not get old revisions, automaticaly used newer one:${path}`, LOG_LEVEL.NOTICE); + return true; + } + // first,check for same contents + if (leftLeaf.data == rightLeaf.data) { + let leaf = leftLeaf; + if (leftLeaf.mtime > rightLeaf.mtime) { + leaf = rightLeaf; + } + await this.localDatabase.deleteDBEntry(path, { rev: leaf.rev }); + await this.pullFile(path, null, true); + Logger(`automaticaly merged:${path}`); + return true; + } + if (this.settings.resolveConflictsByNewerFile) { + const lmtime = ~~(leftLeaf.mtime / 1000); + const rmtime = ~~(rightLeaf.mtime / 1000); + let loser = leftLeaf; + if (lmtime > rmtime) { + loser = rightLeaf; + } + await this.localDatabase.deleteDBEntry(path, { rev: loser.rev }); + await this.pullFile(path, null, true); + Logger(`Automaticaly merged (newerFileResolve) :${path}`, LOG_LEVEL.NOTICE); + return true; + } + // make diff. + const dmp = new diff_match_patch(); + const diff = dmp.diff_main(leftLeaf.data, rightLeaf.data); + dmp.diff_cleanupSemantic(diff); + Logger(`conflict(s) found:${path}`); + return { + left: leftLeaf, + right: rightLeaf, + diff: diff, + }; + } + async showIfConflicted(file: TFile) { + await runWithLock("conflicted", false, async () => { + const conflictCheckResult = await this.getConflictedStatus(file.path); + if (conflictCheckResult === false) return; //nothign to do. + if (conflictCheckResult === true) { + //auto resolved, but need check again; + setTimeout(() => { + this.showIfConflicted(file); + }, 500); + return; + } + //there conflicts, and have to resolve ; + const leaf = this.app.workspace.activeLeaf; + if (leaf) { + new ConflictResolveModal(this.app, conflictCheckResult, async (selected) => { + const testDoc = await this.localDatabase.getDBEntry(file.path, { conflicts: true }); + if (testDoc === false) return; + if (!testDoc._conflicts) { + Logger("something went wrong on merging.", LOG_LEVEL.NOTICE); + return; + } + const toDelete = selected; + if (toDelete == null) { + //concat both, + // write data,and delete both old rev. + const p = conflictCheckResult.diff.map((e) => e[1]).join(""); + await this.app.vault.modify(file, p); + await this.localDatabase.deleteDBEntry(file.path, { rev: conflictCheckResult.left.rev }); + await this.localDatabase.deleteDBEntry(file.path, { rev: conflictCheckResult.right.rev }); + return; + } + if (toDelete == "") { + return; + } + Logger(`resolved conflict:${file.path}`); + await this.localDatabase.deleteDBEntry(file.path, { rev: toDelete }); + await this.pullFile(file.path, null, true); + setTimeout(() => { + //resolved, check again. + this.showIfConflicted(file); + }, 500); + }).open(); + } + }); + } + async pullFile(filename: string, fileList?: TFile[], force?: boolean, rev?: string, waitForReady = true) { + if (!fileList) { + fileList = this.app.vault.getFiles(); + } + const targetFiles = fileList.filter((e) => e.path == id2path(filename)); + if (targetFiles.length == 0) { + //have to create; + const doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady); + if (doc === false) return; + await this.doc2storage_create(doc, force); + } else if (targetFiles.length == 1) { + //normal case + const file = targetFiles[0]; + const doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady); + if (doc === false) return; + await this.doc2storate_modify(doc, file, force); + } else { + Logger(`target files:${filename} is two or more files in your vault`); + //something went wrong.. + } + //when to opened file; + } + async syncFileBetweenDBandStorage(file: TFile, fileList?: TFile[]) { + const doc = await this.localDatabase.getDBEntryMeta(file.path); + if (doc === false) return; + + const storageMtime = ~~(file.stat.mtime / 1000); + const docMtime = ~~(doc.mtime / 1000); + if (storageMtime > docMtime) { + //newer local file. + Logger("STORAGE -> DB :" + file.path); + Logger(`${storageMtime} > ${docMtime}`); + await this.updateIntoDB(file); + } else if (storageMtime < docMtime) { + //newer database file. + Logger("STORAGE <- DB :" + file.path); + Logger(`${storageMtime} < ${docMtime}`); + const docx = await this.localDatabase.getDBEntry(file.path, null, false, false); + if (docx != false) { + await this.doc2storate_modify(docx, file); + } + } else { + // Logger("EVEN :" + file.path, LOG_LEVEL.VERBOSE); + // Logger(`${storageMtime} = ${docMtime}`, LOG_LEVEL.VERBOSE); + //eq.case + } + } + + async updateIntoDB(file: TFile) { + await this.localDatabase.waitForGCComplete(); + let content = ""; + let datatype: "plain" | "newnote" = "newnote"; + if (file.extension != "md") { + const contentBin = await this.app.vault.readBinary(file); + content = await arrayBufferToBase64(contentBin); + datatype = "newnote"; + } else { + content = await this.app.vault.read(file); + datatype = "plain"; + } + const fullpath = path2id(file.path); + const d: LoadedEntry = { + _id: fullpath, + data: content, + ctime: file.stat.ctime, + mtime: file.stat.mtime, + size: file.stat.size, + children: [], + datatype: datatype, + }; + //From here + const old = await this.localDatabase.getDBEntry(fullpath, null, false, false); + if (old !== false) { + const oldData = { data: old.data, deleted: old._deleted }; + const newData = { data: d.data, deleted: d._deleted }; + if (JSON.stringify(oldData) == JSON.stringify(newData)) { + Logger("not changed:" + fullpath + (d._deleted ? " (deleted)" : ""), LOG_LEVEL.VERBOSE); + return; + } + // d._rev = old._rev; + } + await this.localDatabase.putDBEntry(d); + + Logger("put database:" + fullpath + "(" + datatype + ") "); + if (this.settings.syncOnSave && !this.suspended) { + await this.replicate(); + } + } + async deleteFromDB(file: TFile) { + const fullpath = file.path; + Logger(`deleteDB By path:${fullpath}`); + await this.deleteFromDBbyPath(fullpath); + if (this.settings.syncOnSave && !this.suspended) { + await this.replicate(); + } + } + async deleteFromDBbyPath(fullpath: string) { + await this.localDatabase.deleteDBEntry(fullpath); + if (this.settings.syncOnSave && !this.suspended) { + await this.replicate(); + } + } + + async resetLocalDatabase() { + await this.localDatabase.resetDatabase(); + } + async tryResetRemoteDatabase() { + await this.localDatabase.tryResetRemoteDatabase(this.settings); + } + async tryCreateRemoteDatabase() { + await this.localDatabase.tryCreateRemoteDatabase(this.settings); + } + async getPluginList(): Promise<{ plugins: PluginList; allPlugins: DevicePluginList; thisDevicePlugins: DevicePluginList }> { + const db = this.localDatabase.localDatabase; + const docList = await db.allDocs({ startkey: `ps:`, endkey: `ps;`, include_docs: false }); + const oldDocs: PluginDataEntry[] = ((await Promise.all(docList.rows.map(async (e) => await this.localDatabase.getDBEntry(e.id)))).filter((e) => e !== false) as LoadedEntry[]).map((e) => JSON.parse(e.data)); + const plugins: { [key: string]: PluginDataEntry[] } = {}; + const allPlugins: { [key: string]: PluginDataEntry } = {}; + const thisDevicePlugins: { [key: string]: PluginDataEntry } = {}; + for (const v of oldDocs) { + if (typeof plugins[v.deviceVaultName] === "undefined") { + plugins[v.deviceVaultName] = []; + } + plugins[v.deviceVaultName].push(v); + allPlugins[v._id] = v; + if (v.deviceVaultName == this.settings.deviceAndVaultName) { + thisDevicePlugins[v.manifest.id] = v; + } + } + return { plugins, allPlugins, thisDevicePlugins }; + } + async sweepPlugin(showMessage = false) { + console.log(`pluginSync:${this.settings.usePluginSync}`); + if (!this.settings.usePluginSync) return; + await runWithLock("sweepplugin", false, async () => { + const logLevel = showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO; + if (!this.settings.encrypt) { + Logger("You have to encrypt the database to use plugin setting sync.", LOG_LEVEL.NOTICE); + return; + } + if (!this.settings.deviceAndVaultName) { + Logger("You have to set your device and vault name.", LOG_LEVEL.NOTICE); + return; + } + Logger("Sweeping plugins", logLevel); + const db = this.localDatabase.localDatabase; + const oldDocs = await db.allDocs({ startkey: `ps:${this.settings.deviceAndVaultName}-`, endkey: `ps:${this.settings.deviceAndVaultName}.`, include_docs: true }); + Logger("OLD DOCS.", LOG_LEVEL.VERBOSE); + // sweep current plugin. + // @ts-ignore + const pl = this.app.plugins; + const manifests: PluginManifest[] = Object.values(pl.manifests); + for (const m of manifests) { + Logger(`Reading plugin:${m.name}(${m.id})`, LOG_LEVEL.VERBOSE); + const path = normalizePath(m.dir) + "/"; + const adapter = this.app.vault.adapter; + const files = ["manifest.json", "main.js", "styles.css", "data.json"]; + const pluginData: { [key: string]: string } = {}; + for (const file of files) { + const thePath = path + file; + if (await adapter.exists(thePath)) { + pluginData[file] = await adapter.read(thePath); + } + } + let mtime = 0; + if (await adapter.exists(path + "/data.json")) { + mtime = (await adapter.stat(path + "/data.json")).mtime; + } + const p: PluginDataEntry = { + _id: `ps:${this.settings.deviceAndVaultName}-${m.id}`, + dataJson: pluginData["data.json"], + deviceVaultName: this.settings.deviceAndVaultName, + mainJs: pluginData["main.js"], + styleCss: pluginData["styles.css"], + manifest: m, + manifestJson: pluginData["manifest.json"], + mtime: mtime, + type: "plugin", + }; + const d: LoadedEntry = { + _id: p._id, + data: JSON.stringify(p), + ctime: mtime, + mtime: mtime, + size: 0, + children: [], + datatype: "plain", + }; + Logger(`check diff:${m.name}(${m.id})`, LOG_LEVEL.VERBOSE); + await runWithLock("plugin-" + m.id, false, async () => { + const old = await this.localDatabase.getDBEntry(p._id, null, false, false); + if (old !== false) { + const oldData = { data: old.data, deleted: old._deleted }; + const newData = { data: d.data, deleted: d._deleted }; + if (JSON.stringify(oldData) == JSON.stringify(newData)) { + oldDocs.rows = oldDocs.rows.filter((e) => e.id != d._id); + Logger(`Nothing changed:${m.name}`); + return; + } + } + await this.localDatabase.putDBEntry(d); + oldDocs.rows = oldDocs.rows.filter((e) => e.id != d._id); + Logger(`Plugin saved:${m.name}`, logLevel); + }); + //remove saved plugin data. + } + Logger(`Deleting old plugins`, LOG_LEVEL.VERBOSE); + const delDocs = oldDocs.rows.map((e) => { + e.doc._deleted = true; + return e.doc; + }); + await db.bulkDocs(delDocs); + Logger(`Sweep plugin done.`, logLevel); + }); + } + async applyPluginData(plugin: PluginDataEntry) { + await runWithLock("plugin-" + plugin.manifest.id, false, async () => { + const pluginTargetFolderPath = normalizePath(plugin.manifest.dir) + "/"; + const adapter = this.app.vault.adapter; + // @ts-ignore + const stat = this.app.plugins.enabledPlugins[plugin.manifest.id]; + if (stat) { + // @ts-ignore + await this.app.plugins.unloadPlugin(plugin.manifest.id); + Logger(`Unload plugin:${plugin.manifest.id}`, LOG_LEVEL.NOTICE); + } + if (plugin.dataJson) await adapter.write(pluginTargetFolderPath + "data.json", plugin.dataJson); + Logger("wrote:" + pluginTargetFolderPath + "data.json", LOG_LEVEL.NOTICE); + // @ts-ignore + if (stat) { + // @ts-ignore + await this.app.plugins.loadPlugin(plugin.manifest.id); + Logger(`Load plugin:${plugin.manifest.id}`, LOG_LEVEL.NOTICE); + } + }); + } + async applyPlugin(plugin: PluginDataEntry) { + await runWithLock("plugin-" + plugin.manifest.id, false, async () => { + // @ts-ignore + const stat = this.app.plugins.enabledPlugins[plugin.manifest.id]; + if (stat) { + // @ts-ignore + await this.app.plugins.unloadPlugin(plugin.manifest.id); + Logger(`Unload plugin:${plugin.manifest.id}`, LOG_LEVEL.NOTICE); + } + + const pluginTargetFolderPath = normalizePath(plugin.manifest.dir) + "/"; + const adapter = this.app.vault.adapter; + if ((await adapter.exists(pluginTargetFolderPath)) === false) { + await adapter.mkdir(pluginTargetFolderPath); + } + await adapter.write(pluginTargetFolderPath + "main.js", plugin.mainJs); + await adapter.write(pluginTargetFolderPath + "manifest.json", plugin.manifestJson); + if (plugin.styleCss) await adapter.write(pluginTargetFolderPath + "styles.css", plugin.styleCss); + // if (plugin.dataJson) await adapter.write(pluginTargetFolderPath + "data.json", plugin.dataJson); + if (stat) { + // @ts-ignore + await this.app.plugins.loadPlugin(plugin.manifest.id); + Logger(`Load plugin:${plugin.manifest.id}`, LOG_LEVEL.NOTICE); + } + }); + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..4d77c78 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,224 @@ +// docs should be encoded as base64, so 1 char -> 1 bytes +// and cloudant limitation is 1MB , we use 900kb; + +import { PluginManifest } from "obsidian"; + +export const MAX_DOC_SIZE = 1000; // for .md file, but if delimiters exists. use that before. +export const MAX_DOC_SIZE_BIN = 102400; // 100kb +export const VER = 10; + +export const RECENT_MOFIDIED_DOCS_QTY = 30; +export const LEAF_WAIT_TIMEOUT = 90000; // in synchronization, waiting missing leaf time out. +export const LOG_LEVEL = { + VERBOSE: 1, + INFO: 10, + NOTICE: 100, + URGENT: 1000, +} as const; +export type LOG_LEVEL = typeof LOG_LEVEL[keyof typeof LOG_LEVEL]; +export const VERSIONINFO_DOCID = "obsydian_livesync_version"; +export const MILSTONE_DOCID = "_local/obsydian_livesync_milestone"; +export const NODEINFO_DOCID = "_local/obsydian_livesync_nodeinfo"; + +export interface ObsidianLiveSyncSettings { + couchDB_URI: string; + couchDB_USER: string; + couchDB_PASSWORD: string; + couchDB_DBNAME: string; + liveSync: boolean; + syncOnSave: boolean; + syncOnStart: boolean; + syncOnFileOpen: boolean; + savingDelay: number; + lessInformationInLog: boolean; + gcDelay: number; + versionUpFlash: string; + minimumChunkSize: number; + longLineThreshold: number; + showVerboseLog: boolean; + suspendFileWatching: boolean; + trashInsteadDelete: boolean; + periodicReplication: boolean; + periodicReplicationInterval: number; + encrypt: boolean; + passphrase: string; + workingEncrypt: boolean; + workingPassphrase: string; + doNotDeleteFolder: boolean; + resolveConflictsByNewerFile: boolean; + batchSave: boolean; + deviceAndVaultName: string; + usePluginSettings: boolean; + showOwnPlugins: boolean; + showStatusOnEditor: boolean; + usePluginSync: boolean; + autoSweepPlugins: boolean; + autoSweepPluginsPeriodic: boolean; + notifyPluginOrSettingUpdated: boolean; + checkIntegrityOnSave: boolean; +} + +export const DEFAULT_SETTINGS: ObsidianLiveSyncSettings = { + couchDB_URI: "", + couchDB_USER: "", + couchDB_PASSWORD: "", + couchDB_DBNAME: "", + liveSync: false, + syncOnSave: false, + syncOnStart: false, + savingDelay: 200, + lessInformationInLog: false, + gcDelay: 300, + versionUpFlash: "", + minimumChunkSize: 20, + longLineThreshold: 250, + showVerboseLog: false, + suspendFileWatching: false, + trashInsteadDelete: true, + periodicReplication: false, + periodicReplicationInterval: 60, + syncOnFileOpen: false, + encrypt: false, + passphrase: "", + workingEncrypt: false, + workingPassphrase: "", + doNotDeleteFolder: false, + resolveConflictsByNewerFile: false, + batchSave: false, + deviceAndVaultName: "", + usePluginSettings: false, + showOwnPlugins: false, + showStatusOnEditor: false, + usePluginSync: false, + autoSweepPlugins: false, + autoSweepPluginsPeriodic: false, + notifyPluginOrSettingUpdated: false, + checkIntegrityOnSave: false, +}; + +export const PERIODIC_PLUGIN_SWEEP = 60; + +export interface Entry { + _id: string; + data: string; + _rev?: string; + ctime: number; + mtime: number; + size: number; + _deleted?: boolean; + _conflicts?: string[]; + type?: "notes"; +} +export interface NewEntry { + _id: string; + children: string[]; + _rev?: string; + ctime: number; + mtime: number; + size: number; + _deleted?: boolean; + _conflicts?: string[]; + NewNote: true; + type: "newnote"; +} +export interface PlainEntry { + _id: string; + children: string[]; + _rev?: string; + ctime: number; + mtime: number; + size: number; + _deleted?: boolean; + NewNote: true; + _conflicts?: string[]; + type: "plain"; +} +export type LoadedEntry = Entry & { + children: string[]; + datatype: "plain" | "newnote"; +}; + +export interface PluginDataEntry { + _id: string; + deviceVaultName: string; + mtime: number; + manifest: PluginManifest; + mainJs: string; + manifestJson: string; + styleCss?: string; + // it must be encrypted. + dataJson?: string; + _rev?: string; + _deleted?: boolean; + _conflicts?: string[]; + type: "plugin"; +} + +export interface EntryLeaf { + _id: string; + data: string; + _deleted?: boolean; + type: "leaf"; + _rev?: string; +} + +export interface EntryVersionInfo { + _id: typeof VERSIONINFO_DOCID; + _rev?: string; + type: "versioninfo"; + version: number; + _deleted?: boolean; +} + +export interface EntryMilestoneInfo { + _id: typeof MILSTONE_DOCID; + _rev?: string; + type: "milestoneinfo"; + _deleted?: boolean; + created: number; + accepted_nodes: string[]; + locked: boolean; +} + +export interface EntryNodeInfo { + _id: typeof NODEINFO_DOCID; + _rev?: string; + _deleted?: boolean; + type: "nodeinfo"; + nodeid: string; +} + +export type EntryBody = Entry | NewEntry | PlainEntry; +export type EntryDoc = EntryBody | LoadedEntry | EntryLeaf | EntryVersionInfo | EntryMilestoneInfo | EntryNodeInfo; + +export type diff_result_leaf = { + rev: string; + data: string; + ctime: number; + mtime: number; +}; +export type dmp_result = Array<[number, string]>; + +export type diff_result = { + left: diff_result_leaf; + right: diff_result_leaf; + diff: dmp_result; +}; +export type diff_check_result = boolean | diff_result; + +export type Credential = { + username: string; + password: string; +}; + +export type EntryDocResponse = EntryDoc & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta; + +export type DatabaseConnectingStatus = "STARTED" | "NOT_CONNECTED" | "PAUSED" | "CONNECTED" | "COMPLETED" | "CLOSED" | "ERRORED"; + +export interface PluginList { + [key: string]: PluginDataEntry[]; +} + +export interface DevicePluginList { + [key: string]: PluginDataEntry; +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..fc7346d --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,197 @@ +import { normalizePath } from "obsidian"; +import { Logger } from "./logger"; +import { LOG_LEVEL } from "./types"; + +export function arrayBufferToBase64(buffer: ArrayBuffer): Promise { + return new Promise((res) => { + const blob = new Blob([buffer], { type: "application/octet-binary" }); + const reader = new FileReader(); + reader.onload = function (evt) { + const dataurl = evt.target.result.toString(); + res(dataurl.substr(dataurl.indexOf(",") + 1)); + }; + reader.readAsDataURL(blob); + }); +} + +export function base64ToString(base64: string): string { + try { + const binary_string = window.atob(base64); + const len = binary_string.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binary_string.charCodeAt(i); + } + return new TextDecoder().decode(bytes); + } catch (ex) { + return base64; + } +} +export function base64ToArrayBuffer(base64: string): ArrayBuffer { + try { + const binary_string = window.atob(base64); + const len = binary_string.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binary_string.charCodeAt(i); + } + return bytes.buffer; + } catch (ex) { + try { + return new Uint16Array( + [].map.call(base64, function (c: string) { + return c.charCodeAt(0); + }) + ).buffer; + } catch (ex2) { + return null; + } + } +} + +export const escapeStringToHTML = (str: string) => { + if (!str) return ""; + return str.replace(/[<>&"'`]/g, (match) => { + const escape: any = { + "<": "<", + ">": ">", + "&": "&", + '"': """, + "'": "'", + "`": "`", + }; + return escape[match]; + }); +}; + +export function resolveWithIgnoreKnownError(p: Promise, def: T): Promise { + return new Promise((res, rej) => { + p.then(res).catch((ex) => (ex.status && ex.status == 404 ? res(def) : rej(ex))); + }); +} + +export function isValidPath(filename: string): boolean { + // eslint-disable-next-line no-control-regex + const regex = /[\u0000-\u001f]|[\\"':?<>|*]/g; + let x = filename.replace(regex, "_"); + const win = /(\\|\/)(COM\d|LPT\d|CON|PRN|AUX|NUL|CLOCK$)($|\.)/gi; + const sx = (x = x.replace(win, "/_")); + return sx == filename; +} + +export function versionNumberString2Number(version: string): number { + return version // "1.23.45" + .split(".") // 1 23 45 + .reverse() // 45 23 1 + .map((e, i) => ((e as any) / 1) * 1000 ** i) // 45 23000 1000000 + .reduce((prev, current) => prev + current, 0); // 1023045 +} + +export const delay = (ms: number): Promise => { + return new Promise((res) => { + setTimeout(() => { + res(); + }, ms); + }); +}; + +// For backward compatibility, using the path for determining id. +// Only CouchDB nonacceptable ID (that starts with an underscore) has been prefixed with "/". +// The first slash will be deleted when the path is normalized. +export function path2id(filename: string): string { + let x = normalizePath(filename); + if (x.startsWith("_")) x = "/" + x; + return x; +} +export function id2path(filename: string): string { + return normalizePath(filename); +} + +const runningProcs: string[] = []; +const pendingProcs: { [key: string]: (() => Promise)[] } = {}; +function objectToKey(key: any): string { + if (typeof key === "string") return key; + const keys = Object.keys(key).sort((a, b) => a.localeCompare(b)); + return keys.map((e) => e + objectToKey(key[e])).join(":"); +} +// Just run some async/await as like transacion SERIALIZABLE + +export function runWithLock(key: unknown, ignoreWhenRunning: boolean, proc: () => Promise): Promise { + Logger(`Lock:${key}:enter`, LOG_LEVEL.VERBOSE); + const lockKey = typeof key === "string" ? key : objectToKey(key); + const handleNextProcs = () => { + if (typeof pendingProcs[lockKey] === "undefined") { + //simply unlock + runningProcs.remove(lockKey); + Logger(`Lock:${lockKey}:released`, LOG_LEVEL.VERBOSE); + } else { + Logger(`Lock:${lockKey}:left ${pendingProcs[lockKey].length}`, LOG_LEVEL.VERBOSE); + let nextProc = null; + nextProc = pendingProcs[lockKey].shift(); + if (nextProc) { + // left some + nextProc() + .then() + .catch((err) => { + Logger(err); + }) + .finally(() => { + if (pendingProcs && lockKey in pendingProcs && pendingProcs[lockKey].length == 0) { + delete pendingProcs[lockKey]; + } + queueMicrotask(() => { + handleNextProcs(); + }); + }); + } + } + }; + if (runningProcs.contains(lockKey)) { + if (ignoreWhenRunning) { + return null; + } + if (typeof pendingProcs[lockKey] === "undefined") { + pendingProcs[lockKey] = []; + } + let responderRes: (value: T | PromiseLike) => void; + let responderRej: (reason?: unknown) => void; + const responder = new Promise((res, rej) => { + responderRes = res; + responderRej = rej; + //wait for subproc resolved + }); + const subproc = () => + new Promise((res, rej) => { + proc() + .then((v) => { + Logger(`Lock:${key}:processed`, LOG_LEVEL.VERBOSE); + handleNextProcs(); + responderRes(v); + res(); + }) + .catch((reason) => { + Logger(`Lock:${key}:rejected`, LOG_LEVEL.VERBOSE); + handleNextProcs(); + rej(reason); + responderRej(reason); + }); + }); + + pendingProcs[lockKey].push(subproc); + return responder; + } else { + runningProcs.push(lockKey); + Logger(`Lock:${lockKey}:aqquired`, LOG_LEVEL.VERBOSE); + return new Promise((res, rej) => { + proc() + .then((v) => { + handleNextProcs(); + res(v); + }) + .catch((reason) => { + handleNextProcs(); + rej(reason); + }); + }); + } +} diff --git a/src/utils_couchdb.ts b/src/utils_couchdb.ts new file mode 100644 index 0000000..58fe75a --- /dev/null +++ b/src/utils_couchdb.ts @@ -0,0 +1,70 @@ +import { Logger } from "./logger"; +import { LOG_LEVEL, VER, VERSIONINFO_DOCID, EntryVersionInfo, EntryDoc } from "./types"; +import { resolveWithIgnoreKnownError } from "./utils"; +import { PouchDB } from "../pouchdb-browser-webpack/dist/pouchdb-browser.js"; + +export const isValidRemoteCouchDBURI = (uri: string): boolean => { + if (uri.startsWith("https://")) return true; + if (uri.startsWith("http://")) return true; + return false; +}; +export const connectRemoteCouchDB = async (uri: string, auth: { username: string; password: string }): Promise; info: PouchDB.Core.DatabaseInfo }> => { + if (!isValidRemoteCouchDBURI(uri)) return "Remote URI is not valid"; + const db: PouchDB.Database = new PouchDB(uri, { + auth, + }); + try { + const info = await db.info(); + return { db: db, info: info }; + } catch (ex) { + let msg = `${ex.name}:${ex.message}`; + if (ex.name == "TypeError" && ex.message == "Failed to fetch") { + msg += "\n**Note** This error caused by many reasons. The only sure thing is you didn't touch the server.\nTo check details, open inspector."; + } + Logger(ex, LOG_LEVEL.VERBOSE); + return msg; + } +}; +// check the version of remote. +// if remote is higher than current(or specified) version, return false. +export const checkRemoteVersion = async (db: PouchDB.Database, migrate: (from: number, to: number) => Promise, barrier: number = VER): Promise => { + try { + const versionInfo = (await db.get(VERSIONINFO_DOCID)) as EntryVersionInfo; + if (versionInfo.type != "versioninfo") { + return false; + } + + const version = versionInfo.version; + if (version < barrier) { + const versionUpResult = await migrate(version, barrier); + if (versionUpResult) { + await bumpRemoteVersion(db); + return true; + } + } + if (version == barrier) return true; + return false; + } catch (ex) { + if (ex.status && ex.status == 404) { + if (await bumpRemoteVersion(db)) { + return true; + } + return false; + } + throw ex; + } +}; +export const bumpRemoteVersion = async (db: PouchDB.Database, barrier: number = VER): Promise => { + const vi: EntryVersionInfo = { + _id: VERSIONINFO_DOCID, + version: barrier, + type: "versioninfo", + }; + const versionInfo = (await resolveWithIgnoreKnownError(db.get(VERSIONINFO_DOCID), vi)) as EntryVersionInfo; + if (versionInfo.type != "versioninfo") { + return false; + } + vi._rev = versionInfo._rev; + await db.put(vi); + return true; +}; diff --git a/styles.css b/styles.css index 3f2271d..86497ac 100644 --- a/styles.css +++ b/styles.css @@ -136,3 +136,7 @@ div.sls-setting-menu-btn { flex-grow: 0; padding: 6px 10px; } +.sls-plugins-tbl-device-head { + background-color: var(--background-secondary-alt); + color: var(--text-accent); +} diff --git a/tsconfig.json b/tsconfig.json index 27bc9be..04128a6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,9 +9,13 @@ "noImplicitAny": true, "moduleResolution": "node", "importHelpers": true, - "lib": ["dom", "es5", "scripthost", "es2015"] + "noImplicitReturns": true, + "noImplicitThis": true, + "strictFunctionTypes": true, + "alwaysStrict": true, + "lib": ["dom", "es5", "ES6", "ES7", "es2020"] }, - "include": ["**/*.ts"], - "files": ["./main.ts"], + "include": ["./src/*.ts"], + // "files": ["./src/main.ts"], "exclude": ["pouchdb-browser-webpack"] }