diff --git a/.eslintignore b/.eslintignore index 60a1507..9751cf7 100644 --- a/.eslintignore +++ b/.eslintignore @@ -31,5 +31,6 @@ temp-build # generated files package-lock.json +**/assets/sf-pdp/** # #endregion diff --git a/.prettierignore b/.prettierignore index cdae15f..d1d7ed4 100644 --- a/.prettierignore +++ b/.prettierignore @@ -31,5 +31,6 @@ temp-build # generated files package-lock.json +**/assets/sf-pdp/** # #endregion diff --git a/.stylelintignore b/.stylelintignore index 4102db2..e51f808 100644 --- a/.stylelintignore +++ b/.stylelintignore @@ -31,5 +31,6 @@ temp-build # generated files package-lock.json +**/assets/sf-pdp/** # #endregion diff --git a/lib/copy-sf-pdp.ts b/lib/copy-sf-pdp.ts new file mode 100644 index 0000000..69aae2c --- /dev/null +++ b/lib/copy-sf-pdp.ts @@ -0,0 +1,91 @@ +import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'fs'; +import { join, dirname } from 'path'; + +/** Recursively copy a directory and its contents */ +function copyDirectory(src: string, dest: string): void { + // Create destination directory if it doesn't exist + if (!existsSync(dest)) { + mkdirSync(dest, { recursive: true }); + } + + // Get all items in source directory + const items = readdirSync(src); + + items.forEach((item) => { + const srcPath = join(src, item); + const destPath = join(dest, item); + const stat = statSync(srcPath); + + if (stat.isDirectory()) { + // Recursively copy subdirectory + copyDirectory(srcPath, destPath); + } else { + // Copy file, creating parent directories if needed + const destDir = dirname(destPath); + if (!existsSync(destDir)) { + mkdirSync(destDir, { recursive: true }); + } + copyFileSync(srcPath, destPath); + } + }); +} + +/** Copy a single file, creating parent directories if needed */ +function copyFile(src: string, dest: string): void { + const destDir = dirname(dest); + if (!existsSync(destDir)) { + mkdirSync(destDir, { recursive: true }); + } + copyFileSync(src, dest); +} + +// Main script execution +const main = () => { + try { + // Copy sf-pdp dist contents + console.log('Copying sf-pdp dist contents...'); + const sfPdpDistDir = 'src/sf-pdp/dist'; + const sfPdpAssetsDir = 'src/scripture-forge/assets/sf-pdp'; + if (existsSync(sfPdpDistDir)) { + copyDirectory(sfPdpDistDir, sfPdpAssetsDir); + } else { + console.error(`Error: ${sfPdpDistDir} does not exist`); + process.exit(1); + } + + // Copy messages dist contents + console.log('Copying messages dist contents...'); + const messagesDistDir = 'src/messages/dist'; + const messagesNodeModulesDir = 'src/scripture-forge/assets/sf-pdp/node_modules/sf-pdp-messages'; + if (existsSync(messagesDistDir)) { + copyDirectory(messagesDistDir, messagesNodeModulesDir); + } else { + console.error(`Error: ${messagesDistDir} does not exist`); + process.exit(1); + } + + // Copy messages package.json + console.log('Copying messages package.json...'); + const messagesPackageJson = 'src/messages/package.json'; + const destPackageJson = + 'src/scripture-forge/assets/sf-pdp/node_modules/sf-pdp-messages/package.json'; + if (existsSync(messagesPackageJson)) { + copyFile(messagesPackageJson, destPackageJson); + } else { + console.error(`Error: ${messagesPackageJson} does not exist`); + process.exit(1); + } + + console.log('✅ SF-PDP copy operation completed successfully!'); + } catch (error) { + console.error('❌ Error during SF-PDP copy operation:', error); + process.exit(1); + } +}; + +// Run the script if called directly +if (require.main === module) { + main(); +} + +export default main; diff --git a/package-lock.json b/package-lock.json index ff073fa..0850b02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,19 +10,29 @@ "hasInstallScript": true, "license": "MIT", "workspaces": [ - "src/*" + "src/*", + "sf-pdp", + "messages" ], "dependencies": { "@sillsdev/scripture": "^2.0.2", - "platform-bible-utils": "file:../paranext-core/lib/platform-bible-utils" + "partysocket": "^1.1.4", + "platform-bible-utils": "file:../paranext-core/lib/platform-bible-utils", + "quill-delta": "^5.1.0", + "rich-text": "^4.1.0", + "sharedb": "^5.2.2", + "util": "^0.12.5", + "ws": "^8.18.2" }, "devDependencies": { "@swc/core": "1.13.3", "@tailwindcss/typography": "^0.5.16", - "@types/node": "^20.16.11", + "@types/node": "^22.15.34", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", + "@types/sharedb": "^5.1.0", "@types/webpack": "^5.28.5", + "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "autoprefixer": "^10.4.20", @@ -66,7 +76,7 @@ "tsconfig-paths": "^4.2.0", "tsconfig-paths-webpack-plugin": "^4.2.0", "tsx": "^4.19.2", - "typescript": "^5.4.5", + "typescript": "^5.8.3", "webpack": "^5.97.1", "webpack-cli": "^5.1.4", "webpack-merge": "^6.0.1", @@ -219,6 +229,16 @@ "vitest": "^3.2.4" } }, + "messages": { + "name": "sf-pdp-messages", + "version": "0.2.0-alpha.0", + "extraneous": true, + "license": "MIT", + "devDependencies": { + "esbuild": "^0.25.6", + "typescript": "^5.8.3" + } + }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -255,89 +275,20 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/code-frame/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/code-frame/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/code-frame/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/code-frame/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/code-frame/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": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/code-frame/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/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/@babel/compat-data": { "version": "7.23.5", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", @@ -539,20 +490,22 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, + "license": "MIT", "peer": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -568,111 +521,30 @@ } }, "node_modules/@babel/helpers": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz", - "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "@babel/template": "^7.23.9", - "@babel/traverse": "^7.23.9", - "@babel/types": "^7.23.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" }, "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": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "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": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "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": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "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/@babel/parser": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", - "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "dev": true, + "license": "MIT", "peer": true, + "dependencies": { + "@babel/types": "^7.28.0" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -872,27 +744,26 @@ } }, "node_modules/@babel/runtime": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.5.tgz", - "integrity": "sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", "dev": true, - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", - "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.23.9", - "@babel/types": "^7.23.9" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -931,15 +802,15 @@ } }, "node_modules/@babel/types": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", - "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", + "version": "7.28.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz", + "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1070,9 +941,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", - "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", + "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", "cpu": [ "ppc64" ], @@ -1087,9 +958,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", - "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", + "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", "cpu": [ "arm" ], @@ -1104,9 +975,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", - "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", + "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", "cpu": [ "arm64" ], @@ -1121,9 +992,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", - "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", + "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", "cpu": [ "x64" ], @@ -1138,9 +1009,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", - "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", + "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", "cpu": [ "arm64" ], @@ -1155,9 +1026,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", - "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", + "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", "cpu": [ "x64" ], @@ -1172,9 +1043,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", - "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", + "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", "cpu": [ "arm64" ], @@ -1189,9 +1060,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", - "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", + "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", "cpu": [ "x64" ], @@ -1206,9 +1077,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", - "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", + "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", "cpu": [ "arm" ], @@ -1223,9 +1094,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", - "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", + "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", "cpu": [ "arm64" ], @@ -1240,9 +1111,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", - "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", + "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", "cpu": [ "ia32" ], @@ -1257,9 +1128,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", - "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", + "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", "cpu": [ "loong64" ], @@ -1274,9 +1145,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", - "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", + "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", "cpu": [ "mips64el" ], @@ -1291,9 +1162,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", - "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", + "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", "cpu": [ "ppc64" ], @@ -1308,9 +1179,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", - "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", + "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", "cpu": [ "riscv64" ], @@ -1325,9 +1196,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", - "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", + "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", "cpu": [ "s390x" ], @@ -1342,9 +1213,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", - "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", + "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", "cpu": [ "x64" ], @@ -1359,9 +1230,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", - "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", + "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", "cpu": [ "arm64" ], @@ -1376,9 +1247,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", - "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", + "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", "cpu": [ "x64" ], @@ -1393,9 +1264,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", - "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", + "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", "cpu": [ "arm64" ], @@ -1410,9 +1281,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", - "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", + "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", "cpu": [ "x64" ], @@ -1426,10 +1297,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", + "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", - "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", + "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", "cpu": [ "x64" ], @@ -1444,9 +1332,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", - "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", + "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", "cpu": [ "arm64" ], @@ -1461,9 +1349,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", - "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", + "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", "cpu": [ "ia32" ], @@ -1478,9 +1366,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", - "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", + "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", "cpu": [ "x64" ], @@ -2973,13 +2861,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.17.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.17.tgz", - "integrity": "sha512-/WndGO4kIfMicEQLTi/mDANUu/iVUhT7KboZPdEqqHQ4aTS+3qT3U5gIqWDFV+XouorjfgGqvKILJeHhuQgFYg==", + "version": "22.15.34", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.34.tgz", + "integrity": "sha512-8Y6E5WUupYy1Dd0II32BsWAx5MWdcnRd8L84Oys3veg1YrYtNtzgO4CFhiBg6MDSjk7Ay36HYOnU7/tuOzIzcw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.21.0" } }, "node_modules/@types/prop-types": { @@ -3015,6 +2903,13 @@ "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, + "node_modules/@types/sharedb": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/sharedb/-/sharedb-5.1.0.tgz", + "integrity": "sha512-vf/hxO31h+92S/HAVdIIwB/rSIF1vp/8ryjP/QWriJrwqvET5vgtp3hfVz4fGELSE3WKkcaXP1TtHFAui20utg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -3039,6 +2934,16 @@ "webpack": "^5" } }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -3192,10 +3097,11 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -3319,10 +3225,11 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -3487,10 +3394,11 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -4271,6 +4179,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/arraydiff": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/arraydiff/-/arraydiff-0.1.3.tgz", + "integrity": "sha512-t0OgO06uolEcMUvV8+yHc9Pc9pazh8wi/Dtyok/sQwvcr8iFV+P86IfAzK7upUDhI4oavhVREMY7iSWtm38LeA==", + "license": "MIT" + }, "node_modules/ast-metadata-inferer": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/ast-metadata-inferer/-/ast-metadata-inferer-0.8.0.tgz", @@ -4298,8 +4212,7 @@ "node_modules/async": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", - "dev": true + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" }, "node_modules/async-function": { "version": "1.0.0", @@ -4352,7 +4265,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" @@ -4561,10 +4473,11 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4667,7 +4580,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.0", @@ -4686,7 +4598,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4700,7 +4611,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -4742,9 +4652,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001666", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001666.tgz", - "integrity": "sha512-gD14ICmoV5ZZM1OdzPWmpx+q4GyefaK06zi8hmfHV5xe4/2nOQX3+Dw5o+fSqOws2xVwL9j+anOPFwHzdEdV4g==", + "version": "1.0.30001726", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz", + "integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==", "dev": true, "funding": [ { @@ -4759,7 +4669,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chalk": { "version": "4.1.2", @@ -5510,7 +5421,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -5645,7 +5555,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -5809,7 +5718,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5819,7 +5727,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -5862,7 +5769,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -5915,9 +5821,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", - "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", + "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5928,31 +5834,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.0", - "@esbuild/android-arm": "0.25.0", - "@esbuild/android-arm64": "0.25.0", - "@esbuild/android-x64": "0.25.0", - "@esbuild/darwin-arm64": "0.25.0", - "@esbuild/darwin-x64": "0.25.0", - "@esbuild/freebsd-arm64": "0.25.0", - "@esbuild/freebsd-x64": "0.25.0", - "@esbuild/linux-arm": "0.25.0", - "@esbuild/linux-arm64": "0.25.0", - "@esbuild/linux-ia32": "0.25.0", - "@esbuild/linux-loong64": "0.25.0", - "@esbuild/linux-mips64el": "0.25.0", - "@esbuild/linux-ppc64": "0.25.0", - "@esbuild/linux-riscv64": "0.25.0", - "@esbuild/linux-s390x": "0.25.0", - "@esbuild/linux-x64": "0.25.0", - "@esbuild/netbsd-arm64": "0.25.0", - "@esbuild/netbsd-x64": "0.25.0", - "@esbuild/openbsd-arm64": "0.25.0", - "@esbuild/openbsd-x64": "0.25.0", - "@esbuild/sunos-x64": "0.25.0", - "@esbuild/win32-arm64": "0.25.0", - "@esbuild/win32-ia32": "0.25.0", - "@esbuild/win32-x64": "0.25.0" + "@esbuild/aix-ppc64": "0.25.6", + "@esbuild/android-arm": "0.25.6", + "@esbuild/android-arm64": "0.25.6", + "@esbuild/android-x64": "0.25.6", + "@esbuild/darwin-arm64": "0.25.6", + "@esbuild/darwin-x64": "0.25.6", + "@esbuild/freebsd-arm64": "0.25.6", + "@esbuild/freebsd-x64": "0.25.6", + "@esbuild/linux-arm": "0.25.6", + "@esbuild/linux-arm64": "0.25.6", + "@esbuild/linux-ia32": "0.25.6", + "@esbuild/linux-loong64": "0.25.6", + "@esbuild/linux-mips64el": "0.25.6", + "@esbuild/linux-ppc64": "0.25.6", + "@esbuild/linux-riscv64": "0.25.6", + "@esbuild/linux-s390x": "0.25.6", + "@esbuild/linux-x64": "0.25.6", + "@esbuild/netbsd-arm64": "0.25.6", + "@esbuild/netbsd-x64": "0.25.6", + "@esbuild/openbsd-arm64": "0.25.6", + "@esbuild/openbsd-x64": "0.25.6", + "@esbuild/openharmony-arm64": "0.25.6", + "@esbuild/sunos-x64": "0.25.6", + "@esbuild/win32-arm64": "0.25.6", + "@esbuild/win32-ia32": "0.25.6", + "@esbuild/win32-x64": "0.25.6" } }, "node_modules/escalade": { @@ -6741,6 +6648,12 @@ "node": ">=0.10.0" } }, + "node_modules/event-target-polyfill": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/event-target-polyfill/-/event-target-polyfill-0.0.4.tgz", + "integrity": "sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ==", + "license": "MIT" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -6826,14 +6739,12 @@ "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 + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-diff": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==" }, "node_modules/fast-glob": { "version": "3.3.2", @@ -7008,7 +6919,6 @@ "version": "0.3.4", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.4.tgz", "integrity": "sha512-kKaIINnFpzW6ffJNDjjyjrk21BkDx38c0xa/klsT8VzLCaMEefv4ZTacrcVR4DmgTeBra++jMDAfS/tS799YDw==", - "dev": true, "license": "MIT", "dependencies": { "is-callable": "^1.2.7" @@ -7079,7 +6989,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -7137,7 +7046,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -7172,7 +7080,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -7265,10 +7172,11 @@ "dev": true }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -7388,7 +7296,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7444,7 +7351,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0" }, @@ -7472,7 +7378,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7485,7 +7390,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -7500,7 +7404,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -7508,6 +7411,15 @@ "node": ">= 0.4" } }, + "node_modules/hat": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/hat/-/hat-0.0.3.tgz", + "integrity": "sha512-zpImx2GoKXy42fVDSEad2BPKuSQdLcqsCYa48K3zHSzM/ugWuYjLDr8IXxpVuL7uCLHw56eaiLxCRthhOzf5ug==", + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -7662,8 +7574,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/ini": { "version": "1.3.8", @@ -7759,6 +7670,22 @@ "node": ">=10.13.0" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -7861,7 +7788,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7968,7 +7894,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -8066,7 +7991,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -8162,7 +8086,6 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, "license": "MIT", "dependencies": { "which-typed-array": "^1.1.16" @@ -9032,6 +8955,12 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, + "node_modules/json-rpc-2.0": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/json-rpc-2.0/-/json-rpc-2.0-1.7.1.tgz", + "integrity": "sha512-JqZjhjAanbpkXIzFE7u8mE/iFblawwlXtONaCvRqI+pyABVz7B4M1EUNpyVW+dZjqgQ2L5HFmZCmOCgUKm00hg==", + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -9250,6 +9179,12 @@ "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", "dev": true }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, "node_modules/lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", @@ -9268,6 +9203,13 @@ "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", "dev": true }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", @@ -9382,7 +9324,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -10292,6 +10233,12 @@ "node": ">=0.10.0" } }, + "node_modules/ot-json0": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ot-json0/-/ot-json0-1.1.0.tgz", + "integrity": "sha512-wf5fci7GGpMYRDnbbdIFQymvhsbFACMHtxjivQo5KgvAHlxekyfJ9aPsRr6YfFQthQkk4bmsl5yESrZwC/oMYQ==", + "license": "ISC" + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -10389,6 +10336,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/partysocket": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/partysocket/-/partysocket-1.1.4.tgz", + "integrity": "sha512-jXP7PFj2h5/v4UjDS8P7MZy6NJUQ7sspiFyxL4uc/+oKOL+KdtXzHnTV8INPGxBrLTXgalyG3kd12Qm7WrYc3A==", + "license": "ISC", + "dependencies": { + "event-target-polyfill": "^0.0.4" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -10566,7 +10522,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -11037,6 +10992,20 @@ } ] }, + "node_modules/quill-delta": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz", + "integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==", + "license": "MIT", + "dependencies": { + "fast-diff": "^1.3.0", + "lodash.clonedeep": "^4.5.0", + "lodash.isequal": "^4.5.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -11111,10 +11080,11 @@ } }, "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -11179,12 +11149,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", - "dev": true - }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -11224,10 +11188,11 @@ } }, "node_modules/replace-in-file/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -11378,18 +11343,47 @@ "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, + "node_modules/rich-text": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rich-text/-/rich-text-4.1.0.tgz", + "integrity": "sha512-zQg80us6AfopS7s2YPsU+jfLX1RJaOdd6w26Jz4Al1G73AMzqDLTIZSnr0I7HTdCIU1gcGSMDlhq2v4IfoKfbg==", + "license": "MIT", "dependencies": { - "glob": "^7.1.3" + "quill-delta": "^4.2.1" }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { + "engines": { + "node": ">=0.10" + } + }, + "node_modules/rich-text/node_modules/fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "license": "Apache-2.0" + }, + "node_modules/rich-text/node_modules/quill-delta": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-4.2.2.tgz", + "integrity": "sha512-qjbn82b/yJzOjstBgkhtBjN2TNK+ZHP/BgUQO+j6bRhWQQdmj2lH6hXG7+nwwLF41Xgn//7/83lxs9n2BkTtTg==", + "license": "MIT", + "dependencies": { + "fast-diff": "1.2.0", + "lodash.clonedeep": "^4.5.0", + "lodash.isequal": "^4.5.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" } }, @@ -11516,7 +11510,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -11690,7 +11683,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -11733,6 +11725,14 @@ "node": ">= 0.4" } }, + "node_modules/sf-pdp": { + "resolved": "src/sf-pdp", + "link": true + }, + "node_modules/sf-pdp-messages": { + "resolved": "src/messages", + "link": true + }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -11745,6 +11745,19 @@ "node": ">=8" } }, + "node_modules/sharedb": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/sharedb/-/sharedb-5.2.2.tgz", + "integrity": "sha512-F9zf+OsQZLM13Vu1Jw6om4MB1jgPqCGdIR92qkPaeiEnimR9Ig3j7dVsvXEbJnkTrljlkb2aMDNRVEU/dlGGtg==", + "license": "MIT", + "dependencies": { + "arraydiff": "^0.1.3", + "async": "^3.2.4", + "fast-deep-equal": "^3.1.3", + "hat": "0.0.3", + "ot-json0": "^1.1.0" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -13041,16 +13054,6 @@ "dev": true, "peer": true }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -13332,10 +13335,11 @@ } }, "node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13364,10 +13368,11 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" }, "node_modules/unicorn-magic": { "version": "0.1.0", @@ -13433,6 +13438,19 @@ "punycode": "^2.1.0" } }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -13735,7 +13753,6 @@ "version": "1.1.18", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", - "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", @@ -13879,6 +13896,27 @@ "dev": true, "peer": true }, + "node_modules/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -14135,21 +14173,70 @@ "url": "https://github.com/sponsors/isaacs" } }, + "sf-pdp": { + "version": "0.2.0-alpha.0", + "extraneous": true, + "license": "MIT", + "dependencies": { + "json-rpc-2.0": "^1.7.0", + "partysocket": "^1.1.4", + "quill-delta": "^5.1.0", + "rich-text": "^4.1.0", + "sf-pdp-messages": "file:../messages", + "sharedb": "^5.2.2", + "ws": "^8.18.2" + }, + "devDependencies": { + "@types/node": "^22.15.34", + "@types/ws": "^8.18.1", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "cross-env": "^7.0.3", + "eslint": "^8.57.1", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-erb": "^4.1.0", + "eslint-import-resolver-typescript": "^3.8.3", + "eslint-plugin-compat": "^4.2.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-no-null": "^1.0.2", + "eslint-plugin-no-type-assertion": "^1.3.0", + "eslint-plugin-promise": "^6.6.0", + "eslint-plugin-react": "^7.37.4", + "eslint-plugin-react-hooks": "^4.6.2", + "ts-node": "^10.9.2", + "typescript": "^5.8.3" + } + }, + "src/messages": { + "name": "sf-pdp-messages", + "version": "0.3.0-alpha.0", + "license": "MIT", + "devDependencies": { + "esbuild": "^0.25.6", + "typescript": "^5.8.3" + } + }, + "src/paranext-core/lib/platform-bible-utils": { + "extraneous": true + }, "src/scripture-forge": { "version": "0.3.0-alpha.0", "license": "MIT", "dependencies": { "@sillsdev/scripture": "^2.0.2", - "platform-bible-utils": "file:../../../paranext-core/lib/platform-bible-utils" + "platform-bible-utils": "file:../../../paranext-core/lib/platform-bible-utils", + "sf-pdp-messages": "file:../messages" }, "devDependencies": { "@biblionexus-foundation/scripture-utilities": "~0.0.7", "@swc/core": "1.13.3", "@tailwindcss/typography": "^0.5.16", - "@types/node": "^20.17.10", + "@types/node": "22.15.34", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", "@types/webpack": "^5.28.5", + "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "autoprefixer": "^10.4.20", @@ -14190,7 +14277,7 @@ "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", "tsconfig-paths-webpack-plugin": "^4.2.0", - "typescript": "^5.4.5", + "typescript": "^5.8.3", "webpack": "^5.97.1", "webpack-cli": "^5.1.4", "webpack-merge": "^6.0.1", @@ -14200,6 +14287,58 @@ "react": ">=18.3.1", "react-dom": ">=18.3.1" } + }, + "src/scripture-forge/node_modules/@types/node": { + "version": "24.0.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.8.tgz", + "integrity": "sha512-WytNrFSgWO/esSH9NbpWUfTMGQwCGIKfCmNlmFDNiI5gGhgMmEA+V1AEvKLeBNvvtBnailJtkrEa2OIISwrVAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "src/scripture-forge/node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true, + "license": "MIT" + }, + "src/sf-pdp": { + "version": "0.3.0-alpha.0", + "license": "MIT", + "dependencies": { + "json-rpc-2.0": "^1.7.0", + "partysocket": "^1.1.4", + "quill-delta": "^5.1.0", + "rich-text": "^4.1.0", + "sf-pdp-messages": "file:../messages", + "sharedb": "^5.2.2", + "ws": "^8.18.2" + }, + "devDependencies": { + "@types/node": "^22.15.34", + "@types/ws": "^8.18.1", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "cross-env": "^7.0.3", + "esbuild": "^0.25.6", + "eslint": "^8.57.1", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-erb": "^4.1.0", + "eslint-import-resolver-typescript": "^3.8.3", + "eslint-plugin-compat": "^4.2.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-no-null": "^1.0.2", + "eslint-plugin-no-type-assertion": "^1.3.0", + "eslint-plugin-promise": "^6.6.0", + "eslint-plugin-react": "^7.37.4", + "eslint-plugin-react-hooks": "^4.6.2", + "ts-node": "^10.9.2", + "typescript": "^5.8.3" + } } }, "dependencies": { @@ -14227,71 +14366,14 @@ } }, "@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "requires": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" - }, - "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": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "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": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "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" - } - } + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" } }, "@babel/compat-data": { @@ -14450,16 +14532,16 @@ } }, "@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "peer": true }, "@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true }, "@babel/helper-validator-option": { @@ -14470,93 +14552,26 @@ "peer": true }, "@babel/helpers": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz", - "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", "dev": true, "peer": true, "requires": { - "@babel/template": "^7.23.9", - "@babel/traverse": "^7.23.9", - "@babel/types": "^7.23.9" + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" } }, - "@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "@babel/parser": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "dev": true, + "peer": true, "requires": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "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": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "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": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "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" - } - } + "@babel/types": "^7.28.0" } }, - "@babel/parser": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", - "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", - "dev": true, - "peer": true - }, "@babel/plugin-syntax-async-generators": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", @@ -14698,24 +14713,21 @@ } }, "@babel/runtime": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.5.tgz", - "integrity": "sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.14.0" - } + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "dev": true }, "@babel/template": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", - "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, "peer": true, "requires": { - "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.23.9", - "@babel/types": "^7.23.9" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" } }, "@babel/traverse": { @@ -14747,15 +14759,14 @@ } }, "@babel/types": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", - "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", + "version": "7.28.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz", + "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", "dev": true, "peer": true, "requires": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" } }, "@bcoe/v8-coverage": { @@ -14828,177 +14839,184 @@ "dev": true }, "@esbuild/aix-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", - "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", + "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", "dev": true, "optional": true }, "@esbuild/android-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", - "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", + "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", "dev": true, "optional": true }, "@esbuild/android-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", - "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", + "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", "dev": true, "optional": true }, "@esbuild/android-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", - "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", + "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", "dev": true, "optional": true }, "@esbuild/darwin-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", - "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", + "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", "dev": true, "optional": true }, "@esbuild/darwin-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", - "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", + "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", "dev": true, "optional": true }, "@esbuild/freebsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", - "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", + "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", "dev": true, "optional": true }, "@esbuild/freebsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", - "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", + "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", "dev": true, "optional": true }, "@esbuild/linux-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", - "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", + "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", "dev": true, "optional": true }, "@esbuild/linux-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", - "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", + "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", "dev": true, "optional": true }, "@esbuild/linux-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", - "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", + "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", "dev": true, "optional": true }, "@esbuild/linux-loong64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", - "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", + "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", "dev": true, "optional": true }, "@esbuild/linux-mips64el": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", - "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", + "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", "dev": true, "optional": true }, "@esbuild/linux-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", - "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", + "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", "dev": true, "optional": true }, "@esbuild/linux-riscv64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", - "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", + "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", "dev": true, "optional": true }, "@esbuild/linux-s390x": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", - "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", + "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", "dev": true, "optional": true }, "@esbuild/linux-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", - "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", + "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", "dev": true, "optional": true }, "@esbuild/netbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", - "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", + "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", "dev": true, "optional": true }, "@esbuild/netbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", - "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", + "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", "dev": true, "optional": true }, "@esbuild/openbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", - "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", + "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", "dev": true, "optional": true }, "@esbuild/openbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", - "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", + "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", + "dev": true, + "optional": true + }, + "@esbuild/openharmony-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", + "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", "dev": true, "optional": true }, "@esbuild/sunos-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", - "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", + "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", "dev": true, "optional": true }, "@esbuild/win32-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", - "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", + "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", "dev": true, "optional": true }, "@esbuild/win32-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", - "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", + "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", "dev": true, "optional": true }, "@esbuild/win32-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", - "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", + "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", "dev": true, "optional": true }, @@ -16004,12 +16022,12 @@ "dev": true }, "@types/node": { - "version": "20.17.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.17.tgz", - "integrity": "sha512-/WndGO4kIfMicEQLTi/mDANUu/iVUhT7KboZPdEqqHQ4aTS+3qT3U5gIqWDFV+XouorjfgGqvKILJeHhuQgFYg==", + "version": "22.15.34", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.34.tgz", + "integrity": "sha512-8Y6E5WUupYy1Dd0II32BsWAx5MWdcnRd8L84Oys3veg1YrYtNtzgO4CFhiBg6MDSjk7Ay36HYOnU7/tuOzIzcw==", "dev": true, "requires": { - "undici-types": "~6.19.2" + "undici-types": "~6.21.0" } }, "@types/prop-types": { @@ -16041,6 +16059,12 @@ "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, + "@types/sharedb": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/sharedb/-/sharedb-5.1.0.tgz", + "integrity": "sha512-vf/hxO31h+92S/HAVdIIwB/rSIF1vp/8ryjP/QWriJrwqvET5vgtp3hfVz4fGELSE3WKkcaXP1TtHFAui20utg==", + "dev": true + }, "@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -16065,6 +16089,15 @@ "webpack": "^5" } }, + "@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -16159,9 +16192,9 @@ } }, "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -16234,9 +16267,9 @@ } }, "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -16333,9 +16366,9 @@ } }, "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -16918,6 +16951,11 @@ "is-array-buffer": "^3.0.4" } }, + "arraydiff": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/arraydiff/-/arraydiff-0.1.3.tgz", + "integrity": "sha512-t0OgO06uolEcMUvV8+yHc9Pc9pazh8wi/Dtyok/sQwvcr8iFV+P86IfAzK7upUDhI4oavhVREMY7iSWtm38LeA==" + }, "ast-metadata-inferer": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/ast-metadata-inferer/-/ast-metadata-inferer-0.8.0.tgz", @@ -16942,8 +16980,7 @@ "async": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", - "dev": true + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" }, "async-function": { "version": "1.0.0", @@ -16969,7 +17006,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, "requires": { "possible-typed-array-names": "^1.0.0" } @@ -17123,9 +17159,9 @@ } }, "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "requires": { "balanced-match": "^1.0.0", @@ -17189,7 +17225,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, "requires": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", @@ -17201,7 +17236,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", - "dev": true, "requires": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -17211,7 +17245,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", - "dev": true, "requires": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" @@ -17237,9 +17270,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001666", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001666.tgz", - "integrity": "sha512-gD14ICmoV5ZZM1OdzPWmpx+q4GyefaK06zi8hmfHV5xe4/2nOQX3+Dw5o+fSqOws2xVwL9j+anOPFwHzdEdV4g==", + "version": "1.0.30001726", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz", + "integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==", "dev": true }, "chalk": { @@ -17753,7 +17786,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, "requires": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -17847,7 +17879,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "requires": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -17981,14 +18012,12 @@ "es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" }, "es-errors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" }, "es-iterator-helpers": { "version": "1.2.1", @@ -18024,7 +18053,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "requires": { "es-errors": "^1.3.0" } @@ -18062,36 +18090,37 @@ } }, "esbuild": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", - "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", - "dev": true, - "requires": { - "@esbuild/aix-ppc64": "0.25.0", - "@esbuild/android-arm": "0.25.0", - "@esbuild/android-arm64": "0.25.0", - "@esbuild/android-x64": "0.25.0", - "@esbuild/darwin-arm64": "0.25.0", - "@esbuild/darwin-x64": "0.25.0", - "@esbuild/freebsd-arm64": "0.25.0", - "@esbuild/freebsd-x64": "0.25.0", - "@esbuild/linux-arm": "0.25.0", - "@esbuild/linux-arm64": "0.25.0", - "@esbuild/linux-ia32": "0.25.0", - "@esbuild/linux-loong64": "0.25.0", - "@esbuild/linux-mips64el": "0.25.0", - "@esbuild/linux-ppc64": "0.25.0", - "@esbuild/linux-riscv64": "0.25.0", - "@esbuild/linux-s390x": "0.25.0", - "@esbuild/linux-x64": "0.25.0", - "@esbuild/netbsd-arm64": "0.25.0", - "@esbuild/netbsd-x64": "0.25.0", - "@esbuild/openbsd-arm64": "0.25.0", - "@esbuild/openbsd-x64": "0.25.0", - "@esbuild/sunos-x64": "0.25.0", - "@esbuild/win32-arm64": "0.25.0", - "@esbuild/win32-ia32": "0.25.0", - "@esbuild/win32-x64": "0.25.0" + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", + "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", + "dev": true, + "requires": { + "@esbuild/aix-ppc64": "0.25.6", + "@esbuild/android-arm": "0.25.6", + "@esbuild/android-arm64": "0.25.6", + "@esbuild/android-x64": "0.25.6", + "@esbuild/darwin-arm64": "0.25.6", + "@esbuild/darwin-x64": "0.25.6", + "@esbuild/freebsd-arm64": "0.25.6", + "@esbuild/freebsd-x64": "0.25.6", + "@esbuild/linux-arm": "0.25.6", + "@esbuild/linux-arm64": "0.25.6", + "@esbuild/linux-ia32": "0.25.6", + "@esbuild/linux-loong64": "0.25.6", + "@esbuild/linux-mips64el": "0.25.6", + "@esbuild/linux-ppc64": "0.25.6", + "@esbuild/linux-riscv64": "0.25.6", + "@esbuild/linux-s390x": "0.25.6", + "@esbuild/linux-x64": "0.25.6", + "@esbuild/netbsd-arm64": "0.25.6", + "@esbuild/netbsd-x64": "0.25.6", + "@esbuild/openbsd-arm64": "0.25.6", + "@esbuild/openbsd-x64": "0.25.6", + "@esbuild/openharmony-arm64": "0.25.6", + "@esbuild/sunos-x64": "0.25.6", + "@esbuild/win32-arm64": "0.25.6", + "@esbuild/win32-ia32": "0.25.6", + "@esbuild/win32-x64": "0.25.6" } }, "escalade": { @@ -18643,6 +18672,11 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "event-target-polyfill": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/event-target-polyfill/-/event-target-polyfill-0.0.4.tgz", + "integrity": "sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ==" + }, "events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -18711,14 +18745,12 @@ "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 + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-diff": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==" }, "fast-glob": { "version": "3.3.2", @@ -18859,7 +18891,6 @@ "version": "0.3.4", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.4.tgz", "integrity": "sha512-kKaIINnFpzW6ffJNDjjyjrk21BkDx38c0xa/klsT8VzLCaMEefv4ZTacrcVR4DmgTeBra++jMDAfS/tS799YDw==", - "dev": true, "requires": { "is-callable": "^1.2.7" } @@ -18902,8 +18933,7 @@ "function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "function.prototype.name": { "version": "1.1.8", @@ -18942,7 +18972,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", - "dev": true, "requires": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", @@ -18967,7 +18996,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "requires": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -19015,9 +19043,9 @@ }, "dependencies": { "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -19122,8 +19150,7 @@ "gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" }, "graceful-fs": { "version": "4.2.11", @@ -19159,7 +19186,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, "requires": { "es-define-property": "^1.0.0" } @@ -19176,14 +19202,12 @@ "has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" }, "has-tostringtag": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "requires": { "has-symbols": "^1.0.3" } @@ -19192,11 +19216,15 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "requires": { "function-bind": "^1.1.2" } }, + "hat": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/hat/-/hat-0.0.3.tgz", + "integrity": "sha512-zpImx2GoKXy42fVDSEad2BPKuSQdLcqsCYa48K3zHSzM/ugWuYjLDr8IXxpVuL7uCLHw56eaiLxCRthhOzf5ug==" + }, "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -19296,8 +19324,7 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { "version": "1.3.8", @@ -19375,6 +19402,15 @@ "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", "dev": true }, + "is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "requires": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + } + }, "is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -19445,8 +19481,7 @@ "is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" }, "is-core-module": { "version": "2.15.1", @@ -19510,7 +19545,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "dev": true, "requires": { "call-bound": "^1.0.3", "get-proto": "^1.0.0", @@ -19571,7 +19605,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, "requires": { "call-bound": "^1.0.2", "gopd": "^1.2.0", @@ -19626,7 +19659,6 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, "requires": { "which-typed-array": "^1.1.16" } @@ -20287,6 +20319,11 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, + "json-rpc-2.0": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/json-rpc-2.0/-/json-rpc-2.0-1.7.1.tgz", + "integrity": "sha512-JqZjhjAanbpkXIzFE7u8mE/iFblawwlXtONaCvRqI+pyABVz7B4M1EUNpyVW+dZjqgQ2L5HFmZCmOCgUKm00hg==" + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -20463,6 +20500,11 @@ "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", "dev": true }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, "lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", @@ -20481,6 +20523,11 @@ "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", "dev": true }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, "lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", @@ -20575,8 +20622,7 @@ "math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" }, "mathml-tag-names": { "version": "2.1.3", @@ -21143,6 +21189,11 @@ "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", "dev": true }, + "ot-json0": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ot-json0/-/ot-json0-1.1.0.tgz", + "integrity": "sha512-wf5fci7GGpMYRDnbbdIFQymvhsbFACMHtxjivQo5KgvAHlxekyfJ9aPsRr6YfFQthQkk4bmsl5yESrZwC/oMYQ==" + }, "own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -21214,6 +21265,14 @@ "lines-and-columns": "^1.1.6" } }, + "partysocket": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/partysocket/-/partysocket-1.1.4.tgz", + "integrity": "sha512-jXP7PFj2h5/v4UjDS8P7MZy6NJUQ7sspiFyxL4uc/+oKOL+KdtXzHnTV8INPGxBrLTXgalyG3kd12Qm7WrYc3A==", + "requires": { + "event-target-polyfill": "^0.0.4" + } + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -21455,8 +21514,7 @@ "possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", - "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", - "dev": true + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==" }, "postcss": { "version": "8.5.3", @@ -21705,6 +21763,16 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, + "quill-delta": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz", + "integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==", + "requires": { + "fast-diff": "^1.3.0", + "lodash.clonedeep": "^4.5.0", + "lodash.isequal": "^4.5.0" + } + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -21770,9 +21838,9 @@ }, "dependencies": { "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -21820,12 +21888,6 @@ "which-builtin-type": "^1.2.1" } }, - "regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", - "dev": true - }, "regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -21852,9 +21914,9 @@ }, "dependencies": { "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -21967,6 +22029,31 @@ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true }, + "rich-text": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rich-text/-/rich-text-4.1.0.tgz", + "integrity": "sha512-zQg80us6AfopS7s2YPsU+jfLX1RJaOdd6w26Jz4Al1G73AMzqDLTIZSnr0I7HTdCIU1gcGSMDlhq2v4IfoKfbg==", + "requires": { + "quill-delta": "^4.2.1" + }, + "dependencies": { + "fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==" + }, + "quill-delta": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-4.2.2.tgz", + "integrity": "sha512-qjbn82b/yJzOjstBgkhtBjN2TNK+ZHP/BgUQO+j6bRhWQQdmj2lH6hXG7+nwwLF41Xgn//7/83lxs9n2BkTtTg==", + "requires": { + "fast-diff": "1.2.0", + "lodash.clonedeep": "^4.5.0", + "lodash.isequal": "^4.5.0" + } + } + } + }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -22049,7 +22136,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, "requires": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -22140,10 +22226,11 @@ "@sillsdev/scripture": "^2.0.2", "@swc/core": "1.13.3", "@tailwindcss/typography": "^0.5.16", - "@types/node": "^20.17.10", + "@types/node": "22.15.34", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", "@types/webpack": "^5.28.5", + "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "autoprefixer": "^10.4.20", @@ -22175,6 +22262,7 @@ "prettier-plugin-jsdoc": "^1.3.2", "sass": "^1.85.0", "sass-loader": "^16.0.5", + "sf-pdp-messages": "file:../messages", "stylelint": "^16.11.0", "stylelint-config-recommended": "^14.0.1", "stylelint-config-sass-guidelines": "^12.1.0", @@ -22185,11 +22273,27 @@ "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", "tsconfig-paths-webpack-plugin": "^4.2.0", - "typescript": "^5.4.5", + "typescript": "^5.8.3", "webpack": "^5.97.1", "webpack-cli": "^5.1.4", "webpack-merge": "^6.0.1", "zip-build": "^1.8.0" + }, + "dependencies": { + "@types/node": { + "version": "https://registry.npmjs.org/@types/node/-/node-24.0.8.tgz", + "integrity": "sha512-WytNrFSgWO/esSH9NbpWUfTMGQwCGIKfCmNlmFDNiI5gGhgMmEA+V1AEvKLeBNvvtBnailJtkrEa2OIISwrVAA==", + "dev": true, + "requires": { + "undici-types": "~7.8.0" + } + }, + "undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true + } } }, "semver": { @@ -22211,7 +22315,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, "requires": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -22244,6 +22347,45 @@ "es-object-atoms": "^1.0.0" } }, + "sf-pdp": { + "version": "file:src/sf-pdp", + "requires": { + "@types/node": "^22.15.34", + "@types/ws": "^8.18.1", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "cross-env": "^7.0.3", + "esbuild": "^0.25.6", + "eslint": "^8.57.1", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-erb": "^4.1.0", + "eslint-import-resolver-typescript": "^3.8.3", + "eslint-plugin-compat": "^4.2.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-no-null": "^1.0.2", + "eslint-plugin-no-type-assertion": "^1.3.0", + "eslint-plugin-promise": "^6.6.0", + "eslint-plugin-react": "^7.37.4", + "eslint-plugin-react-hooks": "^4.6.2", + "json-rpc-2.0": "^1.7.0", + "partysocket": "^1.1.4", + "quill-delta": "^5.1.0", + "rich-text": "^4.1.0", + "sf-pdp-messages": "file:../messages", + "sharedb": "^5.2.2", + "ts-node": "^10.9.2", + "typescript": "^5.8.3", + "ws": "^8.18.2" + } + }, + "sf-pdp-messages": { + "version": "file:src/messages", + "requires": { + "esbuild": "^0.25.6", + "typescript": "^5.8.3" + } + }, "shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -22253,6 +22395,18 @@ "kind-of": "^6.0.2" } }, + "sharedb": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/sharedb/-/sharedb-5.2.2.tgz", + "integrity": "sha512-F9zf+OsQZLM13Vu1Jw6om4MB1jgPqCGdIR92qkPaeiEnimR9Ig3j7dVsvXEbJnkTrljlkb2aMDNRVEU/dlGGtg==", + "requires": { + "arraydiff": "^0.1.3", + "async": "^3.2.4", + "fast-deep-equal": "^3.1.3", + "hat": "0.0.3", + "ot-json0": "^1.1.0" + } + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -23175,13 +23329,6 @@ "dev": true, "peer": true }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "peer": true - }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -23372,9 +23519,9 @@ } }, "typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true }, "unbox-primitive": { @@ -23390,9 +23537,9 @@ } }, "undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true }, "unicorn-magic": { @@ -23429,6 +23576,18 @@ "punycode": "^2.1.0" } }, + "util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "requires": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -23642,7 +23801,6 @@ "version": "1.1.18", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", - "dev": true, "requires": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", @@ -23748,6 +23906,12 @@ } } }, + "ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "requires": {} + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 9729733..8507bdd 100644 --- a/package.json +++ b/package.json @@ -5,29 +5,36 @@ "author": "SIL", "license": "MIT", "scripts": { - "build:web-view": "webpack --config ./webpack/webpack.config.web-view.ts", + "build": "npm run build:messages && npm run bundle:sf-pdp && webpack", "build:main": "webpack --config ./webpack/webpack.config.main.ts", - "build": "webpack", - "watch": "npm run build -- --watch", - "build:production": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=false webpack", - "watch:production": "npm run build:production -- --watch", + "build:messages": "cd src/messages && npm run build", + "build:messages:production": "cd src/messages && npm run build:production", + "build:production": "npm run build:messages:production && npm run bundle:sf-pdp:production && cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=false webpack", + "build:sf-pdp": "cd src/sf-pdp && npm run build", + "build:sf-pdp:production": "cd src/sf-pdp && npm run build:production", + "build:web-view": "webpack --config ./webpack/webpack.config.web-view.ts", + "bump-versions": "tsx ./lib/bump-versions.ts", + "bundle:sf-pdp": "npm run build:sf-pdp && npm run copy:sf-pdp", + "bundle:sf-pdp:production": "npm run build:sf-pdp:production && npm run copy:sf-pdp", + "copy:sf-pdp": "tsx ./lib/copy-sf-pdp.ts", + "create-extension": "tsx ./lib/create-extension.ts", "format": "prettier --write .", "format:check": "prettier --check .", - "zip": "tsx ./lib/zip-extensions.ts", - "package": "npm run build:production && npm run zip", - "package:debug": "cross-env DEBUG_PROD=true npm run package", - "start:core": "cd ../paranext-core && npm run start", - "start": "cross-env MAIN_ARGS=\"--extensionDirs $INIT_CWD/dist\" concurrently \"npm:watch\" \"npm:start:core\"", - "start:production": "cross-env MAIN_ARGS=\"--extensionDirs $INIT_CWD/dist\" concurrently \"npm:watch:production\" \"npm:start:core\"", "lint": "npm run lint:scripts && npm run lint:styles", "lint:scripts": "cross-env NODE_ENV=development eslint --ext .cjs,.js,.jsx,.ts,.tsx --cache .", "lint:styles": "stylelint **/*.{css,scss} --allow-empty-input", "lint-fix": "npm run lint-fix:scripts && npm run lint:styles -- --fix", "lint-fix:scripts": "npm run format && npm run lint:scripts", + "package": "npm run build:production && npm run zip", + "package:debug": "cross-env DEBUG_PROD=true npm run package", "postinstall": "tsx ./lib/add-remotes.ts", - "create-extension": "tsx ./lib/create-extension.ts", + "start": "cross-env MAIN_ARGS=\"--extensionDirs $INIT_CWD/dist\" concurrently \"npm:watch\" \"npm:start:core\"", + "start:core": "cd ../paranext-core && npm run start", + "start:production": "cross-env MAIN_ARGS=\"--extensionDirs $INIT_CWD/dist\" concurrently \"npm:watch:production\" \"npm:start:core\"", "update-from-templates": "tsx ./lib/update-from-templates.ts", - "bump-versions": "tsx ./lib/bump-versions.ts" + "watch": "npm run build -- --watch", + "watch:production": "npm run build:production -- --watch", + "zip": "tsx ./lib/zip-extensions.ts" }, "browserslist": [], "peerDependencies": { @@ -36,15 +43,23 @@ }, "dependencies": { "@sillsdev/scripture": "^2.0.2", - "platform-bible-utils": "file:../paranext-core/lib/platform-bible-utils" + "partysocket": "^1.1.4", + "platform-bible-utils": "file:../paranext-core/lib/platform-bible-utils", + "quill-delta": "^5.1.0", + "rich-text": "^4.1.0", + "sharedb": "^5.2.2", + "util": "^0.12.5", + "ws": "^8.18.2" }, "devDependencies": { "@swc/core": "1.13.3", "@tailwindcss/typography": "^0.5.16", - "@types/node": "^20.16.11", + "@types/node": "^22.15.34", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", + "@types/sharedb": "^5.1.0", "@types/webpack": "^5.28.5", + "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "autoprefixer": "^10.4.20", @@ -88,7 +103,7 @@ "tsconfig-paths": "^4.2.0", "tsconfig-paths-webpack-plugin": "^4.2.0", "tsx": "^4.19.2", - "typescript": "^5.4.5", + "typescript": "^5.8.3", "webpack": "^5.97.1", "webpack-cli": "^5.1.4", "webpack-merge": "^6.0.1", diff --git a/src/messages/error-message.ts b/src/messages/error-message.ts new file mode 100644 index 0000000..314f445 --- /dev/null +++ b/src/messages/error-message.ts @@ -0,0 +1,28 @@ +import { SfPdpIpcMessage } from './sf-pdp-ipc-message'; + +export type ErrorMessage = SfPdpIpcMessage & { + type: 'error'; + error: string; + stack?: string; +}; + +export function isErrorMessage(message: SfPdpIpcMessage): message is ErrorMessage { + return message.type === 'error'; +} + +export function createErrorMessage( + id: number, + error: string | Error, + stack?: string, +): ErrorMessage { + const errorString = error instanceof Error ? error.message : error; + const errorStack = stack || (error instanceof Error ? error.stack : undefined); + + return { + id, + type: 'error', + timestamp: Date.now(), + error: errorString, + stack: errorStack, + }; +} diff --git a/src/messages/get-projects-message.ts b/src/messages/get-projects-message.ts new file mode 100644 index 0000000..5b91f90 --- /dev/null +++ b/src/messages/get-projects-message.ts @@ -0,0 +1,17 @@ +import { SfPdpIpcMessage } from './sf-pdp-ipc-message'; + +export type GetProjectsMessage = SfPdpIpcMessage & { + type: 'getProjects'; +}; + +export function isGetProjectsMessage(message: SfPdpIpcMessage): message is GetProjectsMessage { + return message.type === 'getProjects'; +} + +export function createGetProjectsMessage(id: number): GetProjectsMessage { + return { + id, + type: 'getProjects', + timestamp: Date.now(), + }; +} diff --git a/src/messages/index.ts b/src/messages/index.ts new file mode 100644 index 0000000..5d34aba --- /dev/null +++ b/src/messages/index.ts @@ -0,0 +1,37 @@ +import { ErrorMessage } from './error-message'; +import { GetProjectsMessage } from './get-projects-message'; +import { InitializeMessage } from './initialize-message'; +import { PingMessage } from './ping-message'; +import { PongMessage } from './pong-message'; +import { ProjectResultsMessage } from './project-results-message'; +import { ShutdownMessage } from './shutdown-message'; + +export { type ErrorMessage, createErrorMessage, isErrorMessage } from './error-message'; +export { + type GetProjectsMessage, + createGetProjectsMessage, + isGetProjectsMessage, +} from './get-projects-message'; +export { + type InitializeMessage, + createInitializeMessage, + isInitializeMessage, +} from './initialize-message'; +export { type PingMessage, createPingMessage, isPingMessage } from './ping-message'; +export { type PongMessage, createPongMessage, isPongMessage } from './pong-message'; +export { + type ProjectResultsMessage, + createProjectResultsMessage, + isProjectResultsMessage, +} from './project-results-message'; +export { type SfPdpIpcMessage, SfPdpIpcMessageTypes, isStaleMessage } from './sf-pdp-ipc-message'; +export { type ShutdownMessage, createShutdownMessage, isShutdownMessage } from './shutdown-message'; + +export type SfPdpMessage = + | ErrorMessage + | GetProjectsMessage + | InitializeMessage + | PingMessage + | PongMessage + | ProjectResultsMessage + | ShutdownMessage; diff --git a/src/messages/initialize-message.ts b/src/messages/initialize-message.ts new file mode 100644 index 0000000..cd37bfb --- /dev/null +++ b/src/messages/initialize-message.ts @@ -0,0 +1,35 @@ +import { SfPdpIpcMessage } from './sf-pdp-ipc-message'; + +export type InitializeMessage = SfPdpIpcMessage & { + type: 'initialize'; + /** OAuth token required for connecting to the SF WebSocket */ + authToken: string; + /** Base URL for the SF HTTP connection */ + httpBaseUrl: string; + /** Base URL for the SF WebSocket connection */ + wssBaseUrl: string; + /** Port for the PAPI WebSocket connection */ + papiPort: number; +}; + +export function isInitializeMessage(message: SfPdpIpcMessage): message is InitializeMessage { + return message.type === 'initialize'; +} + +export function createInitializeMessage( + id: number, + authToken: string, + httpBaseUrl: string, + wssBaseUrl: string, + papiPort: number, +): InitializeMessage { + return { + id, + type: 'initialize', + timestamp: Date.now(), + authToken, + httpBaseUrl, + wssBaseUrl, + papiPort, + }; +} diff --git a/src/messages/package.json b/src/messages/package.json new file mode 100644 index 0000000..07e77e3 --- /dev/null +++ b/src/messages/package.json @@ -0,0 +1,19 @@ +{ + "name": "sf-pdp-messages", + "private": true, + "version": "0.3.0-alpha.0", + "main": "index.js", + "types": "index.d.ts", + "author": "SIL", + "license": "MIT", + "scripts": { + "build": "npm run build:types && npm run build:functions", + "build:functions": "esbuild index.ts --bundle --minify --platform=node --outfile=dist/index.js", + "build:production": "cross-env NODE_ENV=production npm run build", + "build:types": "tsc --emitDeclarationOnly" + }, + "devDependencies": { + "esbuild": "^0.25.6", + "typescript": "^5.8.3" + } +} diff --git a/src/messages/ping-message.ts b/src/messages/ping-message.ts new file mode 100644 index 0000000..ea7003e --- /dev/null +++ b/src/messages/ping-message.ts @@ -0,0 +1,19 @@ +import { SfPdpIpcMessage } from './sf-pdp-ipc-message'; + +export type PingMessage = SfPdpIpcMessage & { + type: 'ping'; + uptimeSec: number; +}; + +export function isPingMessage(message: SfPdpIpcMessage): message is PingMessage { + return message.type === 'ping'; +} + +export function createPingMessage(id: number): PingMessage { + return { + id, + type: 'ping', + timestamp: Date.now(), + uptimeSec: process.uptime(), + }; +} diff --git a/src/messages/pong-message.ts b/src/messages/pong-message.ts new file mode 100644 index 0000000..4c7aea7 --- /dev/null +++ b/src/messages/pong-message.ts @@ -0,0 +1,17 @@ +import { SfPdpIpcMessage } from './sf-pdp-ipc-message'; + +export type PongMessage = SfPdpIpcMessage & { + type: 'pong'; +}; + +export function isPongMessage(message: SfPdpIpcMessage): message is PongMessage { + return message.type === 'pong'; +} + +export function createPongMessage(id: number): PongMessage { + return { + id, + type: 'pong', + timestamp: Date.now(), + }; +} diff --git a/src/messages/project-results-message.ts b/src/messages/project-results-message.ts new file mode 100644 index 0000000..31b26e0 --- /dev/null +++ b/src/messages/project-results-message.ts @@ -0,0 +1,24 @@ +import { SfPdpIpcMessage } from './sf-pdp-ipc-message'; + +export type ProjectResultsMessage = SfPdpIpcMessage & { + type: 'projectResults'; + projectIds: string[]; +}; + +export function isProjectResultsMessage( + message: SfPdpIpcMessage, +): message is ProjectResultsMessage { + return message.type === 'projectResults'; +} + +export function createProjectResultsMessage( + id: number, + projectIds: string[], +): ProjectResultsMessage { + return { + id, + type: 'projectResults', + timestamp: Date.now(), + projectIds, + }; +} diff --git a/src/messages/sf-pdp-ipc-message.ts b/src/messages/sf-pdp-ipc-message.ts new file mode 100644 index 0000000..e756b12 --- /dev/null +++ b/src/messages/sf-pdp-ipc-message.ts @@ -0,0 +1,21 @@ +export const SfPdpIpcMessageTypes = [ + 'authToken', + 'error', + 'getProjects', + 'initialize', + 'ping', + 'pong', + 'projectResults', + 'shutdown', +] as const; + +// Define the type for IPC messages used in the Scripture Forge Project Data Provider (SF-PDP) +export type SfPdpIpcMessage = { + id: number; + type: (typeof SfPdpIpcMessageTypes)[number]; + timestamp: number; +}; + +export function isStaleMessage(message: SfPdpIpcMessage, timeoutMs: number = 5000): boolean { + return Date.now() - message.timestamp > timeoutMs; +} diff --git a/src/messages/shutdown-message.ts b/src/messages/shutdown-message.ts new file mode 100644 index 0000000..5a1c035 --- /dev/null +++ b/src/messages/shutdown-message.ts @@ -0,0 +1,19 @@ +import { SfPdpIpcMessage } from './sf-pdp-ipc-message'; + +export type ShutdownMessage = SfPdpIpcMessage & { + type: 'shutdown'; + reason?: string; +}; + +export function isShutdownMessage(message: SfPdpIpcMessage): message is ShutdownMessage { + return message.type === 'shutdown'; +} + +export function createShutdownMessage(id: number, reason?: string): ShutdownMessage { + return { + id, + type: 'shutdown', + timestamp: Date.now(), + reason, + }; +} diff --git a/src/messages/tsconfig.json b/src/messages/tsconfig.json new file mode 100644 index 0000000..9a28f75 --- /dev/null +++ b/src/messages/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "outDir": "./dist", + "rootDir": ".", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": false, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": false, + "sourceMap": false, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "types": ["node"] + }, + "include": ["./**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/src/scripture-forge/.gitignore b/src/scripture-forge/.gitignore index cb00ea2..5f0b238 100644 --- a/src/scripture-forge/.gitignore +++ b/src/scripture-forge/.gitignore @@ -29,5 +29,6 @@ dist-ssr # Temporary intermediate build files temp-build +assets/sf-pdp # #endregion diff --git a/src/scripture-forge/manifest.json b/src/scripture-forge/manifest.json index 07e59fd..5010474 100644 --- a/src/scripture-forge/manifest.json +++ b/src/scripture-forge/manifest.json @@ -7,7 +7,7 @@ "license": "MIT", "main": "src/main.ts", "extensionDependencies": {}, - "elevatedPrivileges": ["handleUri"], + "elevatedPrivileges": ["createProcess", "handleUri"], "types": "src/types/scripture-forge.d.ts", "menus": "contributions/menus.json", "settings": "contributions/settings.json", diff --git a/src/scripture-forge/package.json b/src/scripture-forge/package.json index 745e0d3..c2ba4bd 100644 --- a/src/scripture-forge/package.json +++ b/src/scripture-forge/package.json @@ -35,16 +35,18 @@ }, "dependencies": { "@sillsdev/scripture": "^2.0.2", - "platform-bible-utils": "file:../../../paranext-core/lib/platform-bible-utils" + "platform-bible-utils": "file:../../../paranext-core/lib/platform-bible-utils", + "sf-pdp-messages": "file:../messages" }, "devDependencies": { "@biblionexus-foundation/scripture-utilities": "~0.0.7", "@swc/core": "1.13.3", "@tailwindcss/typography": "^0.5.16", - "@types/node": "^20.17.10", + "@types/node": "22.15.34", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", "@types/webpack": "^5.28.5", + "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "autoprefixer": "^10.4.20", @@ -85,7 +87,7 @@ "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", "tsconfig-paths-webpack-plugin": "^4.2.0", - "typescript": "^5.4.5", + "typescript": "^5.8.3", "webpack": "^5.97.1", "webpack-cli": "^5.1.4", "webpack-merge": "^6.0.1", diff --git a/src/scripture-forge/src/auth/scripture-forge-authentication-provider.model.ts b/src/scripture-forge/src/auth/scripture-forge-authentication-provider.model.ts index 46074c2..d424dab 100644 --- a/src/scripture-forge/src/auth/scripture-forge-authentication-provider.model.ts +++ b/src/scripture-forge/src/auth/scripture-forge-authentication-provider.model.ts @@ -74,6 +74,9 @@ export const AUTH_PATH = '/callback/auth0'; /** Error message thrown when not logged in when trying to get access token */ export const NOT_LOGGED_IN_ERROR_MESSAGE = 'Not logged in'; +/** Special redirect URI for development mode when our custom protocol can't be registered. */ +export const DEV_REDIRECT_URI = `http://localhost:5000${AUTH_PATH}`; + /** Necessary auth scopes for accessing Slingshot drafts */ const SCOPES = ['openid', 'profile', 'email', 'sf_data', 'offline_access'].join(' '); /** Auth audience for Scripture Forge...? */ @@ -164,6 +167,11 @@ export default class ScriptureForgeAuthenticationProvider implements Dispose { /** Whether we have retrieved the tokens from storage for the first time */ #hasRetrievedAuthTokensFromStorage = false; + /** Rate limiting settings for fetch requests */ + #rateLimitIntervalMs = 1000; // 1 second by default + /** Map to store pending requests for the same URL to avoid duplicate fetches */ + #requestsByUrl = new Map>(); + /** * @param redirectUri The full uri including path to which the authentication site will redirect * when finished authenticating. If `undefined`, will not be able to log in @@ -199,9 +207,40 @@ export default class ScriptureForgeAuthenticationProvider implements Dispose { this.#authTokensMutex.cancel(); this.#authTokens = undefined; this.#hasRetrievedAuthTokensFromStorage = false; + this.#requestsByUrl.clear(); this.emitSessionChangeEvent(undefined); } + /** Creates a cache key for rate limiting based on URL and relevant request options */ + static #createRequestCacheKey(url: string, options: RequestInit): string { + // Include method and body in the cache key as they affect the response + const method = options.method || 'GET'; + const bodyKey = options.body + ? crypto.createHash('md5').update(options.body.toString()).digest('hex').substring(0, 8) + : ''; + return `${method}:${url}${bodyKey ? `:${bodyKey}` : ''}`; + } + + /** + * Configure the rate limiting interval for fetchWithAuthorization requests + * + * @param intervalMs Minimum time in milliseconds between requests to the same URL. Default is + * 1000ms (1 second) + */ + setRateLimitInterval(intervalMs: number): void { + this.#rateLimitIntervalMs = intervalMs; + } + + /** Get the appropriate redirect URI for the current environment */ + getRedirectUri(): string { + // Only packaged Electron applications properly register protocol handlers on macOS and Linux + // https://www.electronjs.org/docs/latest/tutorial/launch-app-from-url-in-another-app#packaging + // Note that this line can only be uncommented if the OAuth server/application used supports localhost redirect URIs + // if (!globalThis.isPackaged) return DEV_REDIRECT_URI; + if (!this.redirectUri) throw new Error('OAuth redirect URI is not set'); + return this.redirectUri; + } + /** * Receives url redirect to extension's url at {@link AUTH_PATH}. Accepts authorization code and * signals to complete the login process @@ -243,7 +282,7 @@ export default class ScriptureForgeAuthenticationProvider implements Dispose { */ async isLoggedIn(): Promise { try { - if (await this.#getAccessToken()) return true; + if (await this.getAccessToken()) return true; } catch (e) { logger.debug(`Error trying to get access token to check if logged in: ${getErrorMessage(e)}`); } @@ -262,8 +301,6 @@ export default class ScriptureForgeAuthenticationProvider implements Dispose { * was already logged in */ async login(): Promise { - if (!this.redirectUri) throw new Error('Cannot log in without a redirect uri'); - // If already logged in, don't need to log in again if (await this.isLoggedIn()) return false; @@ -286,7 +323,7 @@ export default class ScriptureForgeAuthenticationProvider implements Dispose { const authorizeParamsObject: AuthorizeRequestUrlParams = { response_type: 'code', client_id: this.#serverConfiguration.auth.clientId, - redirect_uri: this.redirectUri, + redirect_uri: this.getRedirectUri(), scope: SCOPES, state: this.#authorizeRequestInfo.state, prompt: 'login', @@ -349,7 +386,8 @@ export default class ScriptureForgeAuthenticationProvider implements Dispose { /** * Runs [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch) with Scripture * Forge authorization. Attempts to refresh the access token if needed. Logs out automatically if - * unauthorized. + * unauthorized. Includes rate limiting to prevent too many requests to the same URL within a + * short time window. * * Throws if something went wrong with setting up and running the fetch, like no internet, not * logged in, or access token is expired and refresh token doesn't work so we don't even have an @@ -359,8 +397,115 @@ export default class ScriptureForgeAuthenticationProvider implements Dispose { const fullUrl = url.startsWith(this.#serverConfiguration.scriptureForge.domain) ? url : `${this.#serverConfiguration.scriptureForge.domain}${url.startsWith('/') ? '' : '/'}${url}`; - logger.debug(`SF Auth provider fetching with authorization: ${fullUrl}`); - const accessToken = await this.#getAccessToken(); + + // Create a cache key that includes URL and relevant request options that might affect the response + const cacheKey = ScriptureForgeAuthenticationProvider.#createRequestCacheKey(fullUrl, options); + + // Check if there's already a pending request for this exact URL and options + const existingRequest = this.#requestsByUrl.get(cacheKey); + if (existingRequest) { + const response = await existingRequest; + return response.clone(); + } + + // Create and store the promise for this request + logger.debug(`SF Auth provider fetching: ${fullUrl}`); + const requestPromise = this.#performFetchWithAuthorization(fullUrl, options); + this.#requestsByUrl.set(cacheKey, requestPromise); + + try { + const response = await requestPromise; + return response.clone(); + } finally { + // Make sure the requests expire after the rate limit interval + setTimeout(() => this.#requestsByUrl.delete(cacheKey), this.#rateLimitIntervalMs); + } + } + + /** + * Gets the current access token for the logged-in user. Retrieves a new access token if the + * current one expires. Throws if not logged in, access token is expired and refresh token doesn't + * work, or failed to get access token in some other way + */ + async getAccessToken(shouldAcquireLock = true): Promise { + const getAccessTokenInternal = async () => { + if (!this.#authTokens && !this.#hasRetrievedAuthTokensFromStorage) { + if (this.storageManager) { + const authTokensJSON = await this.storageManager.get( + getAuthTokensStorageKey(this.serverConfiguration), + ); + if (isString(authTokensJSON)) { + try { + this.#authTokens = JSON.parse(authTokensJSON); + } catch (e) { + logger.warn(`Error parsing auth tokens from storage: ${getErrorMessage(e)}`); + } + } + } + this.#hasRetrievedAuthTokensFromStorage = true; + } + if (!this.#authTokens) throw new Error(NOT_LOGGED_IN_ERROR_MESSAGE); + + if (!this.#authTokens.didExpire && this.#authTokens.accessTokenExpireTime > Date.now()) + return this.#authTokens.accessToken; + + // Access token is expired. Try exchanging refresh token for access token + const { refreshToken } = this.#authTokens; + // Remove auth tokens but don't send an update yet as we don't know what update to send + this.#authTokens = undefined; + + if (!refreshToken) { + // If the access token is expired and there isn't a refresh token, finalize the removal + // of the auth tokens and send an update + await this.#setAuthTokens(undefined); + throw new Error('No refresh token; not logged in'); + } + + let tokenResponse: ResponseTokenSet; + try { + const tokenSetOrStatusCode = await this.#requestAccessTokenUsingRefreshToken(refreshToken); + + if (tokenSetOrStatusCode === StatusCodes.UNAUTHORIZED) { + // Not authorized to get new access token, so log out + try { + await this.logout(true); + } catch (err) { + logger.warn( + `Failed to log out after ${tokenSetOrStatusCode} refreshing access token. ${getErrorMessage(err)}`, + ); + } + } + + if (typeof tokenSetOrStatusCode === 'number') + throw new Error(`Refresh request responded with error code ${tokenSetOrStatusCode}`); + + tokenResponse = tokenSetOrStatusCode; + } catch (e) { + throw new Error(`Error refreshing access token: ${getErrorMessage(e)}`); + } + + await this.#setAuthTokens(tokenResponse); + + // We just set this, so bang is fine as it is definitely defined + // eslint-disable-next-line no-type-assertion/no-type-assertion + return this.#authTokens!.accessToken; + }; + + if (shouldAcquireLock) return this.#authTokensMutex.runExclusive(getAccessTokenInternal); + return getAccessTokenInternal(); + } + + async dispose() { + this.#authorizeRequestInfo?.authorizationCodeAsyncVar.rejectWithReason( + 'Login canceled because Scripture Forge authentication provider is disposing', + ); + + return true; + } + + /** Performs the actual fetch with authorization logic, separated from rate limiting logic */ + async #performFetchWithAuthorization(fullUrl: string, options: RequestInit): Promise { + const accessToken = await this.getAccessToken(); const fullOptions = { ...options, headers: { ...options.headers, Authorization: `Bearer ${accessToken}` }, @@ -371,7 +516,7 @@ export default class ScriptureForgeAuthenticationProvider implements Dispose { /** Special JSON-based response contents from failed fetch */ let error: { error: string }; try { - error = await response.json(); + error = await response.clone().json(); } catch (e) { logger.debug( `Error parsing ${response.status} ${response.statusText} error response from ${fullUrl}. This probably means it was not a typical OAuth unauthorized error response indicating our access token expired. Logging out and returning the response instead of trying to retrieve a new access token. ${getErrorMessage(e)}`, @@ -410,7 +555,7 @@ export default class ScriptureForgeAuthenticationProvider implements Dispose { try { newAccessToken = await this.#authTokensMutex.runExclusive(async () => { if (this.#authTokens) this.#authTokens.didExpire = true; - return this.#getAccessToken(false); + return this.getAccessToken(false); }); } catch (e) { throw new Error( @@ -444,14 +589,6 @@ export default class ScriptureForgeAuthenticationProvider implements Dispose { } } - async dispose() { - this.#authorizeRequestInfo?.authorizationCodeAsyncVar.rejectWithReason( - 'Login canceled because Scripture Forge authentication provider is disposing', - ); - - return true; - } - async #setAuthTokens(tokenSet: ResponseTokenSet | undefined): Promise { this.#authTokens = tokenSet ? { @@ -473,79 +610,6 @@ export default class ScriptureForgeAuthenticationProvider implements Dispose { this.emitSessionChangeEvent(undefined); } - /** - * Gets the current access token for the logged-in user. Retrieves a new access token if the - * current one expires. Throws if not logged in, access token is expired and refresh token doesn't - * work, or failed to get access token in some other way - */ - async #getAccessToken(shouldAcquireLock = true): Promise { - const getAccessTokenInternal = async () => { - if (!this.#authTokens && !this.#hasRetrievedAuthTokensFromStorage) { - if (this.storageManager) { - const authTokensJSON = await this.storageManager.get( - getAuthTokensStorageKey(this.serverConfiguration), - ); - if (isString(authTokensJSON)) { - try { - this.#authTokens = JSON.parse(authTokensJSON); - } catch (e) { - logger.warn(`Error parsing auth tokens from storage: ${getErrorMessage(e)}`); - } - } - } - this.#hasRetrievedAuthTokensFromStorage = true; - } - if (!this.#authTokens) throw new Error(NOT_LOGGED_IN_ERROR_MESSAGE); - - if (!this.#authTokens.didExpire && this.#authTokens.accessTokenExpireTime > Date.now()) - return this.#authTokens.accessToken; - - // Access token is expired. Try exchanging refresh token for access token - const { refreshToken } = this.#authTokens; - // Remove auth tokens but don't send an update yet as we don't know what update to send - this.#authTokens = undefined; - - if (!refreshToken) { - // If the access token is expired and there isn't a refresh token, finalize the removal - // of the auth tokens and send an update - await this.#setAuthTokens(undefined); - throw new Error('No refresh token; not logged in'); - } - - let tokenResponse: ResponseTokenSet; - try { - const tokenSetOrStatusCode = await this.#requestAccessTokenUsingRefreshToken(refreshToken); - - if (tokenSetOrStatusCode === StatusCodes.UNAUTHORIZED) { - // Not authorized to get new access token, so log out - try { - await this.logout(true); - } catch (err) { - logger.warn( - `Failed to log out after ${tokenSetOrStatusCode} refreshing access token. ${getErrorMessage(err)}`, - ); - } - } - - if (typeof tokenSetOrStatusCode === 'number') - throw new Error(`Refresh request responded with error code ${tokenSetOrStatusCode}`); - - tokenResponse = tokenSetOrStatusCode; - } catch (e) { - throw new Error(`Error refreshing access token: ${getErrorMessage(e)}`); - } - - await this.#setAuthTokens(tokenResponse); - - // We just set this, so bang is fine as it is definitely defined - // eslint-disable-next-line no-type-assertion/no-type-assertion - return this.#authTokens!.accessToken; - }; - - if (shouldAcquireLock) return this.#authTokensMutex.runExclusive(getAccessTokenInternal); - return getAccessTokenInternal(); - } - // #region OAuth requests // TODO: consider factoring out into another file after seeing how the error handling integrates @@ -558,14 +622,12 @@ export default class ScriptureForgeAuthenticationProvider implements Dispose { async #requestAccessTokenUsingAuthorizationCode( authorizationCode: string, ): Promise { - if (!this.redirectUri) - throw new Error('Cannot request authorization code grant without a redirect uri'); if (!this.#authorizeRequestInfo) throw new Error('Authorization request info missing'); const authorizationCodeTokenRequestParamsObject: AuthorizationCodeTokenRequestBody = { grant_type: 'authorization_code', client_id: this.#serverConfiguration.auth.clientId, - redirect_uri: this.redirectUri, + redirect_uri: this.getRedirectUri(), code: authorizationCode, code_verifier: this.#authorizeRequestInfo.codeVerifier, }; diff --git a/src/scripture-forge/src/auth/server-configuration.model.ts b/src/scripture-forge/src/auth/server-configuration.model.ts index 64b0d12..0aa4ac4 100644 --- a/src/scripture-forge/src/auth/server-configuration.model.ts +++ b/src/scripture-forge/src/auth/server-configuration.model.ts @@ -18,6 +18,7 @@ const SERVER_CONFIGURATIONS: { dev: { scriptureForge: { domain: 'localhost', + webSocket: 'ws://localhost', }, auth: { domain: 'https://sil-appbuilder.auth0.com', @@ -27,15 +28,20 @@ const SERVER_CONFIGURATIONS: { qa: { scriptureForge: { domain: 'https://qa.scriptureforge.org', + webSocket: 'wss://qa.scriptureforge.org/realtime-api', }, auth: { domain: 'https://dev-sillsdev.auth0.com', clientId: '4eHLjo40mAEGFU6zUxdYjnpnC1K1Ydnj', + // This clientId was setup within Auth0 as a separate application with more redirect URL options + // However, it was not setup in https://github.com/sillsdev/auth0-configs, so it will disappear the next time a config is pushed + // clientId: 'keZORmin26og8b8cNvcdLTTu9GRUu5VA', }, }, live: { scriptureForge: { domain: 'https://scriptureforge.org', + webSocket: 'wss://scriptureforge.org/ws', }, auth: { domain: 'https://login.languagetechnology.org', diff --git a/src/scripture-forge/src/home/home.web-view.tsx b/src/scripture-forge/src/home/home.web-view.tsx index 3374a93..0767918 100644 --- a/src/scripture-forge/src/home/home.web-view.tsx +++ b/src/scripture-forge/src/home/home.web-view.tsx @@ -31,6 +31,7 @@ import { formatReplacementStringToArray, getErrorMessage, isLocalizeKey, + isPlatformError, isString, LocalizeKey, } from 'platform-bible-utils'; @@ -98,8 +99,17 @@ globalThis.webViewComponent = function ScriptureForgeHome({ const [localizedStrings] = useLocalizedStrings(localizedStringKeys); const [serverConfigurationCondensed] = useSetting('scriptureForge.serverConfiguration', 'live'); + if (isPlatformError(serverConfigurationCondensed)) { + logger.error( + `Failed to get server configuration, defaulting to "live": ${serverConfigurationCondensed.message}`, + serverConfigurationCondensed, + ); + } const serverConfiguration = useMemo( - () => expandServerConfiguration(serverConfigurationCondensed), + () => + expandServerConfiguration( + isPlatformError(serverConfigurationCondensed) ? 'live' : serverConfigurationCondensed, + ), [serverConfigurationCondensed], ); diff --git a/src/scripture-forge/src/main.ts b/src/scripture-forge/src/main.ts index 715c9d4..c7dda1c 100644 --- a/src/scripture-forge/src/main.ts +++ b/src/scripture-forge/src/main.ts @@ -1,6 +1,12 @@ import papi, { logger } from '@papi/backend'; import { ExecutionActivationContext, IWebViewProvider } from '@papi/core'; -import { formatReplacementString, isPlatformError, isString } from 'platform-bible-utils'; +import { + formatReplacementString, + getErrorMessage, + isPlatformError, + isString, +} from 'platform-bible-utils'; +import { ChildProcess } from 'child_process'; import ScriptureForgeAuthenticationProvider, { AUTH_PATH, } from './auth/scripture-forge-authentication-provider.model'; @@ -11,12 +17,16 @@ import { SERVER_CONFIGURATION_PRESET_NAMES } from './auth/server-configuration.m import ScriptureForgeApi from './projects/scripture-forge-api.model'; import SlingshotProjectDataProviderEngineFactory from './projects/slingshot-project-data-provider-engine-factory.model'; import { SLINGSHOT_PROJECT_INTERFACES } from './projects/slingshot-project-data-provider-engine.model'; +import { initializeChildProcess } from './projects/child-process-handler'; type IWebViewProviderWithType = IWebViewProvider & { webViewType: string }; const SCRIPTURE_FORGE_HOME_WEB_VIEW_TYPE = 'scriptureForge.home'; const SCRIPTURE_FORGE_SLINGSHOT_PDPF_ID = 'scriptureForge.slingshotPdpf'; +let childProcess: ChildProcess | undefined; +const realTimeCollaborativeEditingEnabled = process.env.SF_RCE_ENABLED === 'true'; + /** Simple web view provider that provides Scripture Forge Home web views when papi requests them */ const homeWebViewProvider: IWebViewProviderWithType = { webViewType: SCRIPTURE_FORGE_HOME_WEB_VIEW_TYPE, @@ -39,6 +49,9 @@ const homeWebViewProvider: IWebViewProviderWithType = { export async function activate(context: ExecutionActivationContext) { logger.debug('Scripture Forge Extension is activating!'); + if (realTimeCollaborativeEditingEnabled && !context.elevatedPrivileges.createProcess) + throw new Error('Scripture Forge extension requires createProcess elevated privilege'); + const homeWebViewProviderPromise = papi.webViewProviders.registerWebViewProvider( homeWebViewProvider.webViewType, homeWebViewProvider, @@ -156,7 +169,9 @@ export async function activate(context: ExecutionActivationContext) { 'scriptureForge.serverConfiguration', (newServerConfiguration) => { if (isPlatformError(newServerConfiguration)) { - logger.warn(`Error on getting new SF server configuration: ${newServerConfiguration}`); + logger.warn( + `Error on getting new SF server configuration: ${getErrorMessage(newServerConfiguration)}`, + ); return; } @@ -225,6 +240,17 @@ export async function activate(context: ExecutionActivationContext) { }, ); + if (realTimeCollaborativeEditingEnabled) { + // Verified createProcess exists earlier + // eslint-disable-next-line no-type-assertion/no-type-assertion + childProcess = context.elevatedPrivileges.createProcess!.fork( + context.executionToken, + 'assets/sf-pdp/index.js', + ); + + initializeChildProcess(childProcess, authenticationProvider, scriptureForgeApi); + } + // Await registration promises at the end so we don't hold everything else up context.registrations.add( await homeWebViewProviderPromise, diff --git a/src/scripture-forge/src/projects/child-process-handler.ts b/src/scripture-forge/src/projects/child-process-handler.ts new file mode 100644 index 0000000..ba1e5ac --- /dev/null +++ b/src/scripture-forge/src/projects/child-process-handler.ts @@ -0,0 +1,117 @@ +import { logger, settings } from '@papi/backend'; +import { ChildProcess } from 'child_process'; +import { + createInitializeMessage, + createPingMessage, + createPongMessage, + createProjectResultsMessage, + type SfPdpMessage, +} from 'sf-pdp-messages'; +import { expandServerConfiguration } from '../auth/server-configuration.model'; +import ScriptureForgeAuthenticationProvider from '../auth/scripture-forge-authentication-provider.model'; +import ScriptureForgeApi from './scripture-forge-api.model'; + +// PAPI websocket port +// TODO: It would be good to get this from some config instead of hardcoding it +const WEBSOCKET_PORT = 8876; + +let childProcess: ChildProcess | undefined; +let sfAuthProvider: ScriptureForgeAuthenticationProvider | undefined; +let sfApi: ScriptureForgeApi | undefined; +let nextMessageId = 0; + +/** + * Sets up the Scripture Forge Project Data Provider (SF PDP) child process to pass messages back + * and forth. + * + * @param sfPdpProcess - The child process instance representing the SF PDP. + * @param authProvider - The authentication provider for Scripture Forge. + * @param scriptureForgeApi - The API instance for interacting with Scripture Forge. + */ +export function initializeChildProcess( + sfPdpProcess: ChildProcess, + authProvider: ScriptureForgeAuthenticationProvider, + scriptureForgeApi: ScriptureForgeApi, +): void { + if (childProcess) throw new Error('Child process is already set'); + childProcess = sfPdpProcess; + sfAuthProvider = authProvider; + sfApi = scriptureForgeApi; + + childProcess.on('exit', (code) => { + if (code === 0) { + logger.info('SF PDP exited gracefully'); + } else { + logger.error(`SF PDP exited with code ${code}`); + } + }); + + childProcess.on('message', (message: SfPdpMessage) => { + handleMessage(message); + }); + + childProcess.send(createPingMessage(getNextMessageId())); +} + +function getNextMessageId(): number { + nextMessageId += 1; + return nextMessageId; +} + +async function handleMessage(message: SfPdpMessage): Promise { + logger.verbose('Received message:', JSON.stringify(message)); + + switch (message.type) { + case 'getProjects': + await sendProjectResultsMessage(); + return; + case 'ping': + childProcess?.send(createPongMessage(getNextMessageId())); + return; + case 'pong': + await sendInitializeMessage(); + return; + default: + logger.warn('Unexpected SF PDP message:', JSON.stringify(message)); + } +} + +async function sendProjectResultsMessage(): Promise { + const projectIds: string[] = []; + const projects = await sfApi?.getProjects(); + logger.debug(`Received SF projects from API: ${JSON.stringify(projects)}`); + if (Array.isArray(projects)) { + projects.forEach((project) => { + if (project.projectId) projectIds.push(project.projectId); + }); + } + childProcess?.send(createProjectResultsMessage(getNextMessageId(), projectIds)); +} + +async function sendInitializeMessage(): Promise { + logger.debug('Sending initialize message to SF PDP'); + if (sfAuthProvider === undefined) + throw new Error('ScriptureForgeAuthenticationProvider is not set'); + + const serverConfig = expandServerConfiguration( + await settings.get('scriptureForge.serverConfiguration'), + ); + + // To have an auth token we have to be logged in + if (!(await sfAuthProvider.isLoggedIn())) { + logger.debug('ScriptureForgeAuthenticationProvider is not logged in, attempting to log in'); + if (!(await sfAuthProvider.login())) throw new Error('Failed to log in to Scripture Forge'); + } else logger.debug('ScriptureForgeAuthenticationProvider is already logged in'); + + const authToken = await sfAuthProvider.getAccessToken(); + if (!authToken) throw new Error('No auth token available for SF PDP'); + childProcess?.send( + createInitializeMessage( + getNextMessageId(), + authToken, + serverConfig.scriptureForge.domain, + serverConfig.scriptureForge.webSocket, + WEBSOCKET_PORT, + ), + ); +} diff --git a/src/scripture-forge/src/projects/slingshot-project-data-provider-engine-factory.model.ts b/src/scripture-forge/src/projects/slingshot-project-data-provider-engine-factory.model.ts index 96c00fa..cda3284 100644 --- a/src/scripture-forge/src/projects/slingshot-project-data-provider-engine-factory.model.ts +++ b/src/scripture-forge/src/projects/slingshot-project-data-provider-engine-factory.model.ts @@ -5,6 +5,7 @@ import { } from '@papi/core'; import { logger } from '@papi/backend'; import { Dispose, getErrorMessage, Mutex, PlatformEvent, Unsubscriber } from 'platform-bible-utils'; +import { StatusCodes } from 'http-status-codes'; import SlingshotProjectDataProviderEngine, { SLINGSHOT_PROJECT_INTERFACES, } from './slingshot-project-data-provider-engine.model'; @@ -119,9 +120,10 @@ export default class SlingshotProjectDataProviderEngineFactory } if (typeof projectsInfo === 'number') { - logger.warn( - `Slingshot PDPEF received error while getting available projects: ${projectsInfo}`, - ); + if (projectsInfo !== StatusCodes.NO_CONTENT) + logger.warn( + `Slingshot PDPEF received error while getting available projects: ${projectsInfo}`, + ); return []; } diff --git a/src/scripture-forge/src/types/scripture-forge.d.ts b/src/scripture-forge/src/types/scripture-forge.d.ts index e598795..909ff70 100644 --- a/src/scripture-forge/src/types/scripture-forge.d.ts +++ b/src/scripture-forge/src/types/scripture-forge.d.ts @@ -1,4 +1,6 @@ declare module 'scripture-forge' { + import { SerializedVerseRef } from '@sillsdev/scripture'; + import { Op } from 'quill-delta'; import { DataProviderDataType, DataProviderSubscriberOptions, @@ -15,6 +17,7 @@ declare module 'scripture-forge' { /** Settings related to connecting to the Scripture Forge API */ scriptureForge: { domain: string; + webSocket: string; }; /** Settings related to authenticating with the authentication server */ auth: { @@ -165,10 +168,232 @@ declare module 'scripture-forge' { options?: DataProviderSubscriberOptions, ): Promise; }; + + export type DeltaOperation = Op; + + export type ChapterDeltaOperationsDataTypes = { + ChapterDeltaOperations: DataProviderDataType< + SerializedVerseRef, + DeltaOperation[], + DeltaOperation[] + >; + }; + + export type IChapterDeltaOperationsDataProvider = + IProjectDataProvider; + + export type ScriptureForgeProjectInterfaceDataTypes = { + Project: DataProviderDataType; + }; + + export type IScriptureForgeProjectDataProvider = + IProjectDataProvider; + + // #region Project Document Types + + /** + * Information about a Scripture Forge project, including its configuration and permissions. + * + * Largely a copy of "SFProject" in "sf-project.ts" in the web-xforge repo. + */ + export type ScriptureForgeProjectDocument = { + name: string; + rolePermissions: { [role: string]: string[] }; + userRoles: { [userRef: string]: string }; + userPermissions: { [userRef: string]: string[] }; + /** Whether the project has its capability to synchronize project data turned off. */ + syncDisabled?: boolean; + paratextId: string; + shortName: string; + writingSystem: WritingSystem; + isRightToLeft?: boolean; + translateConfig: TranslateConfig; + checkingConfig: CheckingConfig; + resourceConfig?: ResourceConfig; + texts: TextInfo[]; + noteTags?: NoteTag[]; + sync: Sync; + editable: boolean; + defaultFontSize?: number; + defaultFont?: string; + maxGeneratedUsersPerShareKey?: number; + biblicalTermsConfig: BiblicalTermsConfig; + copyrightBanner?: string; + copyrightNotice?: string; + paratextUsers: ParatextUserProfile[]; + }; + + export type BaseProject = { + paratextId: string; + shortName: string; + }; + + export type BiblicalTermsConfig = { + biblicalTermsEnabled: boolean; + errorMessage?: string; + hasRenderings: boolean; + }; + + export type Chapter = { + number: number; + lastVerse: number; + isValid: boolean; + permissions: { [userRef: string]: string }; + hasAudio?: boolean; + hasDraft?: boolean; + draftApplied?: boolean; + }; + + export enum CheckingAnswerExport { + All = 'all', + MarkedForExport = 'marked_for_export', + None = 'none', + } + + export type CheckingConfig = { + checkingEnabled: boolean; + usersSeeEachOthersResponses: boolean; + answerExportMethod: CheckingAnswerExport; + noteTagId?: number; + hideCommunityCheckingText?: boolean; + }; + + export type DraftConfig = { + additionalTrainingData: boolean; + additionalTrainingSourceEnabled: boolean; + additionalTrainingSource?: TranslateSource; + alternateSourceEnabled: boolean; + alternateSource?: TranslateSource; + alternateTrainingSourceEnabled: boolean; + alternateTrainingSource?: TranslateSource; + lastSelectedTrainingBooks: number[]; + lastSelectedTrainingDataFiles: string[]; + lastSelectedTrainingScriptureRange?: string; + lastSelectedTrainingScriptureRanges?: ProjectScriptureRange[]; + lastSelectedTranslationBooks: number[]; + lastSelectedTranslationScriptureRange?: string; + lastSelectedTranslationScriptureRanges?: ProjectScriptureRange[]; + servalConfig?: string; + usfmConfig?: DraftUsfmConfig; + }; + + export type DraftUsfmConfig = { + paragraphFormat: ParagraphBreakFormat; + }; + + export type NoteTag = { + tagId: number; + name: string; + icon: string; + creatorResolve: boolean; + }; + + export enum ParagraphBreakFormat { + BestGuess = 'best_guess', + Remove = 'remove', + MoveToEnd = 'move_to_end', + } + + export type ParatextUserProfile = { + username: string; + opaqueUserId: string; + sfUserId?: string; + }; + + /** A per-project scripture range. */ + export type ProjectScriptureRange = { + projectId: string; + scriptureRange: string; + }; + + export enum ProjectType { + Standard = 'Standard', + Resource = 'Resource', + BackTranslation = 'BackTranslation', + Daughter = 'Daughter', + Transliteration = 'Transliteration', + TransliterationManual = 'TransliterationManual', + TransliterationWithEncoder = 'TransliterationWithEncoder', + StudyBible = 'StudyBible', + ConsultantNotes = 'ConsultantNotes', + GlobalConsultantNotes = 'GlobalConsultantNotes', + GlobalAnthropologyNotes = 'GlobalAnthropologyNotes', + StudyBibleAdditions = 'StudyBibleAdditions', + Auxiliary = 'Auxiliary', + AuxiliaryResource = 'AuxiliaryResource', + MarbleResource = 'MarbleResource', + Xml = 'Xml', + XmlResource = 'XmlResource', + XmlDictionary = 'XmlDictionary', + SourceLanguage = 'SourceLanguage', + Dictionary = 'Dictionary', + EnhancedResource = 'EnhancedResource', + } + + export type ResourceConfig = { + createdTimestamp: Date; + manifestChecksum: string; + permissionsChecksum: string; + revision: number; + }; + + export type Sync = { + queuedCount: number; + lastSyncSuccessful?: boolean; + dateLastSuccessfulSync?: string; + syncedToRepositoryVersion?: string; + /** + * Indicates if PT project data from the last send/receive operation was incorporated into the + * SF project docs + */ + dataInSync?: boolean; + lastSyncErrorCode?: number; + }; + + export type TextInfo = { + bookNum: number; + hasSource: boolean; + chapters: Chapter[]; + permissions: { [userRef: string]: string }; + }; + + export type TranslateConfig = { + translationSuggestionsEnabled: boolean; + source?: TranslateSource; + defaultNoteTagId?: number; + preTranslate: boolean; + draftConfig: DraftConfig; + projectType?: ProjectType; + baseProject?: BaseProject; + }; + + export enum TranslateShareLevel { + Anyone = 'anyone', + Specific = 'specific', + } + + export type TranslateSource = { + paratextId: string; + projectRef: string; + name: string; + shortName: string; + writingSystem: WritingSystem; + isRightToLeft?: boolean; + }; + + export type WritingSystem = { + script?: string; + region?: string; + tag: string; + }; + + // #endregion } declare module 'papi-shared-types' { import { + IChapterDeltaOperationsDataProvider, + IScriptureForgeProjectDataProvider, ISlingshotDraftInfoProjectDataProvider, ServerConfiguration, ServerConfigurationPresetNames, @@ -227,6 +452,8 @@ declare module 'papi-shared-types' { } export interface ProjectDataProviderInterfaces { + 'scriptureForge.chapterDeltaOperations': IChapterDeltaOperationsDataProvider; + 'scriptureForge.scriptureForgeProject': IScriptureForgeProjectDataProvider; 'scriptureForge.slingshotDraftInfo': ISlingshotDraftInfoProjectDataProvider; } } diff --git a/src/sf-pdp/.eslintignore b/src/sf-pdp/.eslintignore new file mode 100644 index 0000000..60a1507 --- /dev/null +++ b/src/sf-pdp/.eslintignore @@ -0,0 +1,35 @@ +# #region shared with https://github.com/paranext/paranext-extension-template/blob/main/.eslintignore + +# Please keep this file in sync with .prettierignore and .stylelintignore + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Dependency directory +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git +node_modules + +# OSX +.DS_Store + +.idea +npm-debug.log.* +*.css.d.ts +*.sass.d.ts +*.scss.d.ts + +# Built files +dist +release +temp-build + +# generated files +package-lock.json + +# #endregion diff --git a/src/sf-pdp/.eslintrc.js b/src/sf-pdp/.eslintrc.js new file mode 100644 index 0000000..da466ad --- /dev/null +++ b/src/sf-pdp/.eslintrc.js @@ -0,0 +1,168 @@ +// #region shared with https://github.com/paranext/paranext-multi-extension-template/blob/main/.eslintrc.cjs + +module.exports = { + extends: [ + // https://github.com/electron-react-boilerplate/eslint-config-erb/blob/main/index.js + // airbnb rules are embedded in erb https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb + 'erb', + // https://github.com/import-js/eslint-plugin-import?tab=readme-ov-file#typescript + 'plugin:import/recommended', + 'plugin:import/typescript', + // Make sure this is last so it gets the chance to override other configs. + // See https://github.com/prettier/eslint-config-prettier and https://github.com/prettier/eslint-plugin-prettier + 'plugin:prettier/recommended', + ], + + rules: { + // Some rules in this following shared region are not applied since they are overridden in subsequent regions + // #region shared with https://github.com/paranext/paranext-core/blob/main/.eslintrc.js except certain overrides + + // #region ERB rules + + // Use `noImplicitReturns` instead. See https://typescript-eslint.io/rules/consistent-return/. + 'consistent-return': 'off', + 'import/default': 'off', + 'import/extensions': 'off', + // A temporary hack related to IDE not resolving correct package.json + 'import/no-extraneous-dependencies': 'off', + 'import/no-import-module-exports': 'off', + 'import/no-unresolved': 'error', + 'import/prefer-default-export': 'off', + 'react/jsx-filename-extension': 'off', + 'react/react-in-jsx-scope': 'off', + + // #endregion + + // #region Platform.Bible rules + + // Rules in each section are generally in alphabetical order. However, several + // `@typescript-eslint` rules require disabling the equivalent ESLint rule. So in these cases + // each ESLint rule is turned off immediately above the corresponding `@typescript-eslint` rule. + 'class-methods-use-this': 'off', + '@typescript-eslint/class-methods-use-this': [ + 'error', + { ignoreOverrideMethods: true, ignoreClassesThatImplementAnInterface: false }, + ], + '@typescript-eslint/explicit-member-accessibility': ['error', { accessibility: 'no-public' }], + 'lines-between-class-members': 'off', + '@typescript-eslint/lines-between-class-members': [ + 'error', + 'always', + { exceptAfterSingleLine: true, exceptAfterOverload: true }, + ], + '@typescript-eslint/member-ordering': 'error', + 'no-empty-function': 'off', + '@typescript-eslint/no-empty-function': [ + 'error', + { + allow: ['arrowFunctions', 'functions', 'methods'], + }, + ], + '@typescript-eslint/no-explicit-any': 'error', + 'no-redeclare': 'off', + '@typescript-eslint/no-redeclare': 'error', + 'no-restricted-imports': [ + 'error', + { + patterns: [ + { + group: ['shared/*', 'renderer/*', 'extension-host/*', 'node/*', 'client/*', 'main/*'], + message: `Importing from this path is not allowed. Try importing from @papi/core. Imports from paths like 'shared', 'renderer', 'node', 'client' and 'main' are not allowed to prevent unnecessary import break.`, + }, + ], + }, + ], + 'no-shadow': 'off', + '@typescript-eslint/no-shadow': 'error', + 'no-use-before-define': 'off', + '@typescript-eslint/no-use-before-define': [ + 'error', + { functions: false, allowNamedExports: true, typedefs: false, ignoreTypeReferences: true }, + ], + '@typescript-eslint/no-unnecessary-type-assertion': 'error', + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': 'error', + 'no-useless-constructor': 'off', + '@typescript-eslint/no-useless-constructor': 'error', + 'comma-dangle': ['error', 'always-multiline'], + 'import/no-anonymous-default-export': ['error', { allowCallExpression: false }], + indent: 'off', + 'jsx-a11y/label-has-associated-control': [ + 'error', + { + assert: 'either', + }, + ], + // Should use our logger anytime you want logs that persist. Otherwise use console only in testing + 'no-console': 'warn', + 'no-null/no-null': 2, + 'no-plusplus': ['error', { allowForLoopAfterthoughts: true }], + 'no-type-assertion/no-type-assertion': 'error', + 'prettier/prettier': ['warn', { tabWidth: 2, trailingComma: 'all' }], + 'react/jsx-indent-props': ['warn', 2], + 'react/jsx-props-no-spreading': ['error', { custom: 'ignore' }], + 'react/require-default-props': 'off', + + // #endregion + + // #endregion + + // #region Overrides to rules from paranext-core + + 'import/no-unresolved': ['error', { ignore: ['@papi'] }], + + // #endregion + }, + globals: { + globalThis: 'readonly', + }, + overrides: [ + { + // Allow this file to have overrides to rules from paranext-core + files: ['.eslintrc.*js'], + rules: { + 'no-dupe-keys': 'off', + }, + }, + { + files: ['*.js'], + rules: { + strict: 'off', + }, + }, + { + files: ['./lib/*', './webpack/*'], + rules: { + // These files are scripts not running in Platform.Bible, so they can't use the logger + 'no-console': 'off', + }, + }, + { + files: ['*.d.ts'], + rules: { + // Allow .d.ts files to self import so they can refer to their types in `papi-shared-types` + 'import/no-self-import': 'off', + }, + }, + ], + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + project: './tsconfig.lint.json', + tsconfigRootDir: __dirname, + createDefaultProgram: true, + }, + plugins: ['@typescript-eslint', 'no-type-assertion', 'no-null'], + settings: { + 'import/resolver': { + typescript: { + alwaysTryTypes: true, + }, + }, + 'import/parsers': { + '@typescript-eslint/parser': ['.ts', '.tsx'], + }, + }, +}; + +// #endregion diff --git a/src/sf-pdp/README.md b/src/sf-pdp/README.md new file mode 100644 index 0000000..0958bea --- /dev/null +++ b/src/sf-pdp/README.md @@ -0,0 +1,71 @@ +# SF-PDP (Scripture Forge Project Data Provider) + +This is a standalone Node.js process that can be forked from the main scripture-forge extension to handle project data processing tasks in isolation. + +## Purpose + +The SF-PDP process allows the scripture-forge extension to: + +- Offload heavy data processing tasks +- Maintain responsiveness in the main extension process +- Isolate potentially unstable operations +- Scale processing workloads + +## Building + +```bash +# Build the process +npm run build + +# Build for production +npm run build:production +``` + +## Running + +```bash +# Run the built process +npm start +``` + +## Integration + +The built process artifacts are copied to the scripture-forge extension's assets directory during the main build process. The extension can then fork this process using Node.js's `child_process.fork()` method. + +## API + +The process communicates with the parent via IPC messages. Message TS types are exported from `../messages/index.ts`. + +### Message Types + +- `ping`: Initialize (and could be used as a health check) +- `pong`: Respond to a ping message +- `getProjects`: Request to get a list of available projects (sent from the SF PDP process to the extension host since the extension host talks with the REST API while the SF PDP process talks with the websocket) +- `projectResults`: Response to a getProjects message +- `error`: Send information about an error that occurred +- `shutdown`: Graceful shutdown (sent by the extension host to the SF PDP process) + +### Example Usage + +```typescript +import { fork } from 'child_process'; +import * as path from 'path'; + +const sfPdpPath = path.join(__dirname, 'assets', 'sf-pdp', 'index.js'); +const child = fork(sfPdpPath); + +child.on('exit', (code) => { + if (code === 0) { + logger.info('SF PDP exited gracefully'); + } else { + logger.error(`SF PDP exited with code ${code}`); + } +}); + +child.on('message', (message: SfPdpMessage) => { + handleMessage(message); +}); + +// Initialize with a ping message +child.send(createPingMessage(getNextMessageId())); +``` diff --git a/src/sf-pdp/esbuild.config.js b/src/sf-pdp/esbuild.config.js new file mode 100644 index 0000000..4ce507b --- /dev/null +++ b/src/sf-pdp/esbuild.config.js @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ +const esbuild = require('esbuild'); + +const isProduction = process.env.NODE_ENV === 'production'; + +const config = { + entryPoints: ['src/index.ts'], + bundle: true, + outfile: 'dist/index.js', + platform: 'node', + target: 'node22', + format: 'cjs', + minify: isProduction, + treeShaking: true, + sourcemap: true, + // External packages that should not be bundled (if any) + external: [], + // Define environment variables + define: { + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), + }, + // Additional options for production builds + ...(isProduction && { + drop: ['debugger'], + keepNames: false, + }), +}; + +// Build function +async function build() { + try { + await esbuild.build(config); + console.log('✅ Build completed successfully'); + } catch (error) { + console.error('❌ Build failed:', error); + process.exit(1); + } +} + +// Run build if this file is executed directly +if (require.main === module) { + build(); +} + +module.exports = { config, build }; diff --git a/src/sf-pdp/package.json b/src/sf-pdp/package.json new file mode 100644 index 0000000..565259c --- /dev/null +++ b/src/sf-pdp/package.json @@ -0,0 +1,47 @@ +{ + "name": "sf-pdp", + "private": true, + "version": "0.3.0-alpha.0", + "main": "dist/index.js", + "author": "SIL", + "license": "MIT", + "scripts": { + "build": "node esbuild.config.js", + "build:production": "cross-env NODE_ENV=production npm run build", + "lint": "eslint --ext .cjs,.js,.jsx,.ts,.tsx --cache ." + }, + "dependencies": { + "json-rpc-2.0": "^1.7.0", + "partysocket": "^1.1.4", + "quill-delta": "^5.1.0", + "rich-text": "^4.1.0", + "sf-pdp-messages": "file:../messages", + "sharedb": "^5.2.2", + "ws": "^8.18.2" + }, + "devDependencies": { + "@types/node": "^22.15.34", + "@types/ws": "^8.18.1", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "cross-env": "^7.0.3", + "esbuild": "^0.25.6", + "eslint": "^8.57.1", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-erb": "^4.1.0", + "eslint-import-resolver-typescript": "^3.8.3", + "eslint-plugin-compat": "^4.2.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-no-null": "^1.0.2", + "eslint-plugin-no-type-assertion": "^1.3.0", + "eslint-plugin-promise": "^6.6.0", + "eslint-plugin-react": "^7.37.4", + "eslint-plugin-react-hooks": "^4.6.2", + "ts-node": "^10.9.2", + "typescript": "^5.8.3" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/src/sf-pdp/src/@types/rich-text/index.d.ts b/src/sf-pdp/src/@types/rich-text/index.d.ts new file mode 100644 index 0000000..3aae189 --- /dev/null +++ b/src/sf-pdp/src/@types/rich-text/index.d.ts @@ -0,0 +1,8 @@ +declare module 'rich-text' { + import Delta from 'quill-delta'; + import { type Type } from 'sharedb/lib/sharedb'; + + export const type: Type; + + export { Delta }; +} diff --git a/src/sf-pdp/src/index.ts b/src/sf-pdp/src/index.ts new file mode 100644 index 0000000..0719228 --- /dev/null +++ b/src/sf-pdp/src/index.ts @@ -0,0 +1,203 @@ +import { + createErrorMessage, + createGetProjectsMessage, + createPingMessage, + createPongMessage, + InitializeMessage, + PingMessage, + PongMessage, + ProjectResultsMessage, + SfPdpMessage, + ShutdownMessage, +} from 'sf-pdp-messages'; +import { AsyncVariable, getErrorMessage } from 'platform-bible-utils'; +import { RpcClient } from './papi-websocket/rpc-client'; +import { SCRIPTURE_FORGE_BACK_END_CONNECTION } from './sf-backend/scripture-forge-back-end-connection'; +import * as log from './log'; +import { registerNetworkObject } from './papi-websocket/network-object'; +import { scriptureForgePdpFactory, setGetProjectsMessage, setRpcClient } from './pdp/pdp-factory'; +import { setOrigin } from './sf-backend/custom-origin-websocket'; + +/** + * SF-PDP (Scripture Forge Project Data Provider) Process + * + * This is a standalone Node.js process that can be forked from the main scripture-forge extension. + * It handles project data processing tasks in isolation from the extension host process. + */ + +export type SfPdpConfig = { + httpBaseUrl?: string; + wssBaseUrl?: string; + papiPort?: number; +}; + +export class SfPdpProcess { + private readonly config: SfPdpConfig = {}; + private nextMsgId: number = 0; + private receivedPing = false; + private receivedPong = false; + private rpcClient: RpcClient | undefined; + private currentAuthToken: string | undefined; + private pendingProjectResults: AsyncVariable | undefined; + + private get nextMessageId(): number { + const id = this.nextMsgId; + this.nextMsgId += 1; + return id; + } + + start(): void { + log.info('Starting SF-PDP process', { pid: process.pid }); + + if (!process.send) this.exitProcess('process is not forked, cannot continue', 100); + + process.on('message', (message: SfPdpMessage) => { + try { + this.handleMessage(message); + } catch (error) { + this.exitProcess(`error handling IPC message: ${getErrorMessage(error)}`, 101); + } + }); + + process.on('SIGINT', () => this.exitProcess('received SIGINT', 0)); + process.on('SIGTERM', () => this.exitProcess('received SIGTERM', 0)); + + this.sendMessage(createPingMessage(this.nextMessageId)); + } + + async getProjects(): Promise { + if (this.pendingProjectResults) return this.pendingProjectResults.promise; + this.pendingProjectResults = new AsyncVariable('SF PDP getProjects'); + this.sendMessage(createGetProjectsMessage(this.nextMessageId)); + return this.pendingProjectResults.promise; + } + + // eslint-disable-next-line @typescript-eslint/class-methods-use-this + private exitProcess(message: string, code: number): void { + if (code === 0) log.info(`Exiting SF-PDP process gracefully: ${message}`); + else log.error(`Exiting SF-PDP process with code ${code}: ${message}`); + log.flush(); + process.exit(code); + } + + private sendMessage(message: SfPdpMessage): void { + if (!this.receivedPing || !this.receivedPong) { + switch (message.type) { + case 'error': + case 'ping': + case 'pong': + break; + default: + log.warn(`Cannot send a message before trading ping/pongs: ${JSON.stringify(message)}`); + return; + } + } + + if (!process.send) throw new Error('Process is not forked, cannot send message'); + process.send(message); + } + + private handleMessage(message: SfPdpMessage): void { + log.verbose('Received message:', JSON.stringify(message)); + + switch (message.type) { + case 'initialize': + this.handleInitialize(message); + return; + case 'ping': + this.handlePing(message); + return; + case 'pong': + this.handlePong(message); + return; + case 'projectResults': + this.handleProjectResults(message); + return; + case 'shutdown': + this.handleShutdown(message); + return; + default: + log.warn('Unexpected message:', JSON.stringify(message)); + } + } + + private handleInitialize(message: InitializeMessage): void { + if (this.config.papiPort !== undefined || this.rpcClient) { + log.warn('Already initialized'); + this.sendMessage(createErrorMessage(this.nextMessageId, 'Already initialized')); + return; + } + + this.currentAuthToken = message.authToken; + this.config.httpBaseUrl = message.httpBaseUrl; + this.config.wssBaseUrl = message.wssBaseUrl; + this.config.papiPort = message.papiPort; + + setOrigin(this.config.httpBaseUrl); + (async () => { + try { + if (!this.config.papiPort) throw new Error('PAPI port is not configured'); + this.rpcClient = new RpcClient(this.config.papiPort); + // TODO: Insert a proper event handler + if (!(await this.rpcClient.connect(() => {}))) + throw new Error('Failed to connect RPC client to the PAPI web socket'); + } catch (error) { + this.exitProcess(`Error initializing PAPI connection: ${getErrorMessage(error)}`, 102); + } + + try { + if (!this.config.wssBaseUrl) throw new Error('WSS base URL is not configured'); + if (!this.currentAuthToken) throw new Error('Current auth token is not set'); + await SCRIPTURE_FORGE_BACK_END_CONNECTION.connect( + this.config.wssBaseUrl, + this.currentAuthToken, + ); + } catch (error) { + this.exitProcess(`Error connecting to SF backend: ${getErrorMessage(error)}`, 103); + } + + // Wait to register the PDP factory until after connecting to the SF backend + try { + if (!this.rpcClient) throw new Error('RPC client is not initialized'); + setRpcClient(this.rpcClient); + setGetProjectsMessage(this.getProjects.bind(this)); + await registerNetworkObject(this.rpcClient, scriptureForgePdpFactory); + } catch (error) { + this.exitProcess(`Error registering PDP factory: ${getErrorMessage(error)}`, 104); + } + })(); + } + + private handlePing(message: PingMessage): void { + this.receivedPing = true; + log.info('Received ping:', JSON.stringify(message)); + this.sendMessage(createPongMessage(message.id)); + } + + private handlePong(message: PongMessage): void { + this.receivedPong = true; + log.info('Received pong:', JSON.stringify(message)); + } + + private handleProjectResults(message: ProjectResultsMessage): void { + log.info('Received project results:', JSON.stringify(message)); + + if (this.pendingProjectResults) { + this.pendingProjectResults.resolveToValue(message, true); + this.pendingProjectResults = undefined; + } else { + log.warn('Received project results but no async variable found'); + } + } + + private handleShutdown(message: ShutdownMessage): void { + const reason = message.reason || 'no reason provided'; + this.exitProcess(`received shutdown message (${reason})`, 0); + } +} + +// If this file is being run directly (not required as a module) +if (require.main === module) { + const process = new SfPdpProcess(); + process.start(); +} diff --git a/src/sf-pdp/src/log.ts b/src/sf-pdp/src/log.ts new file mode 100644 index 0000000..3b67fd0 --- /dev/null +++ b/src/sf-pdp/src/log.ts @@ -0,0 +1,47 @@ +const logLevels = ['error', 'warn', 'info', 'debug', 'verbose'] as const; +const logLevelError = logLevels.indexOf('error'); +const logLevelWarn = logLevels.indexOf('warn'); +const logLevelInfo = logLevels.indexOf('info'); +const logLevelDebug = logLevels.indexOf('debug'); +const logLevelVerbose = logLevels.indexOf('verbose'); +let currentLogLevel: number = logLevels.indexOf('info'); + +export function setLoggingLevel(level: (typeof logLevels)[number]): void { + currentLogLevel = logLevels.indexOf(level); +} + +// Return a string of the format "[YYYY-MM-dd HH:mm:ss.SSS] [] [sfdp]" +function logLineTemplate(level: (typeof logLevels)[number]): string { + return `[${new Date().toISOString().replace('T', ' ').replace('Z', '')}] [${level}] [sfdp]`; +} + +// This file's purpose is to channel logging through the console in a consistent format. +/* eslint-disable no-console */ + +export function error(...args: unknown[]): void { + if (currentLogLevel >= logLevelError) console.error(logLineTemplate('error'), ...args); +} + +export function warn(...args: unknown[]): void { + if (currentLogLevel >= logLevelWarn) console.warn(logLineTemplate('warn'), ...args); +} + +export function info(...args: unknown[]): void { + if (currentLogLevel >= logLevelInfo) console.info(logLineTemplate('info'), ...args); +} + +export function debug(...args: unknown[]): void { + if (currentLogLevel >= logLevelDebug) console.debug(logLineTemplate('debug'), ...args); +} + +export function verbose(...args: unknown[]): void { + if (currentLogLevel >= logLevelVerbose) console.log(logLineTemplate('verbose'), ...args); +} + +export function flush(): void { + process.stdout.write('', () => { + process.stderr.write('', () => { + // Flushing complete + }); + }); +} diff --git a/src/sf-pdp/src/papi-websocket/network-object.ts b/src/sf-pdp/src/papi-websocket/network-object.ts new file mode 100644 index 0000000..9582900 --- /dev/null +++ b/src/sf-pdp/src/papi-websocket/network-object.ts @@ -0,0 +1,62 @@ +import { RpcClient } from './rpc-client'; +import { InternalRequestHandler } from './rpc.model'; +import { SingleMethodDocumentation } from './openrpc.model'; +import * as logger from '../log'; + +export const NetworkObjectTypes = { + DATA_PROVIDER: 'dataProvider', + OBJECT: 'object', + PROJECT_DATA_PROVIDER_FACTORY: 'pdpFactory', + PROJECT_DATA_PROVIDER: 'pdp', +} as const; + +export type NetworkObjectType = (typeof NetworkObjectTypes)[keyof typeof NetworkObjectTypes]; + +export type NetworkObjectRegistrationData = { + objectId: string; + objectType: NetworkObjectType; + functions: Record; + /** + * Optional object containing properties that describe this network object. The properties + * associated with this network object depend on the `objectType`. + */ + attributes?: Record; + documentation?: Record; +}; + +export async function registerNetworkObject( + rpcClient: RpcClient, + registrationData: NetworkObjectRegistrationData, +): Promise { + const { objectId, objectType, functions, attributes, documentation } = registrationData; + const prefixedObjectId = objectId.startsWith('object:') ? objectId : `object:${objectId}`; + + // Make sure the network object identity function gets registered + functions[''] = () => true; + + const functionNames = Object.keys(functions); + if (functionNames.length === 0) + throw new Error(`Cannot register network object '${objectId}' with no functions.`); + + const registrationPromises = Object.entries(functions).map(([fnName, handler]) => { + const nameToRegister = fnName === '' ? prefixedObjectId : `${prefixedObjectId}.${fnName}`; + const docs = documentation && documentation[fnName] ? documentation[fnName] : undefined; + return rpcClient.registerMethod(nameToRegister, handler, docs); + }); + + const responses: boolean[] = await Promise.all(registrationPromises); + if (responses.some((response) => !response)) + throw new Error(`Failed to register all functions for network object '${objectId}'.`); + else + logger.info( + `Registered network object '${objectId}' of type '${objectType}' with functions: ${functionNames.join(', ')}`, + ); + + const registrationMessage = { + id: objectId, + objectType, + functionNames, + attributes, + }; + rpcClient.emitEventOnNetwork('object:onDidCreateNetworkObject', registrationMessage); +} diff --git a/src/sf-pdp/src/papi-websocket/openrpc.model.ts b/src/sf-pdp/src/papi-websocket/openrpc.model.ts new file mode 100644 index 0000000..3ecec5c --- /dev/null +++ b/src/sf-pdp/src/papi-websocket/openrpc.model.ts @@ -0,0 +1,168 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { JSONSchema7 } from 'json-schema'; + +// #region OpenRPC types translated from JSON Schema to TypeScript + +/** + * Describes APIs available to call using JSON-RPC 2.0 + * + * See https://github.com/open-rpc/meta-schema/releases - Release 1.14.2 aligns with OpenRPC 1.2.6. + * https://github.com/open-rpc/meta-schema/releases/download/1.14.2/open-rpc-meta-schema.json + * + * We don't want to go past 1.2.6 because https://playground.open-rpc.org/ doesn't support anything + * past 1.2.6 for now. See https://github.com/open-rpc/playground/issues/606. + * + * Note that the types from https://www.npmjs.com/package/@open-rpc/meta-schema/v/1.14.2 are not + * very good. For example, all the properties of `Components` are of type `any` instead of the + * specific types they should be, and they redefine types for JSON Schema. So we're using our own + * types here instead. + */ +export type OpenRpc = { + openrpc: string; + info: Info; + servers?: Server[]; + methods: Method[]; + components?: Components; + externalDocs?: ExternalDocumentation; +}; + +export type Components = { + schemas?: { [key: string]: Schema }; + contentDescriptors?: { [key: string]: ContentDescriptor }; + examples?: { [key: string]: Example }; + links?: { [key: string]: Link }; + errors?: { [key: string]: Error }; + tags?: { [key: string]: Tag }; +}; + +export type ComponentsReference = `#/components/${string}`; + +export type Contact = { + name?: string; + email?: string; + url?: string; +}; + +export type ContentDescriptor = { + name: string; + schema: Schema; + required?: boolean; + summary?: string; + description?: string; + deprecated?: boolean; +}; + +export type Error = { + code: number; + message: string; + data?: any; +}; + +export type Example = { + name: string; + value: any; + summary?: string; + description?: string; +}; + +export type ExamplePairingObject = { + name: string; + params: (Example | Reference)[]; + result: Example | Reference; + description?: string; +}; + +export type ExternalDocumentation = { + url: string; + description?: string; +}; + +export type Info = { + title: string; + version: string; + description?: string; + termsOfService?: string; + contact?: Contact; + license?: License; +}; + +export type License = { + name: string; + url?: string; +}; + +export type Link = { + name?: string; + summary?: string; + description?: string; + method?: string; + params?: { [key: string]: any }; + server?: Server; +}; + +export type Method = { + /** The canonical name for the method. The name MUST be unique within the methods array. */ + name: string; + params: (ContentDescriptor | Reference)[]; + result: ContentDescriptor | Reference; + /** A short summary of what the method does. */ + summary?: string; + /** + * A verbose explanation of the method behavior. GitHub Flavored Markdown syntax MAY be used for + * rich text representation. + */ + description?: string; + deprecated?: boolean; + servers?: Server[]; + tags?: (Tag | Reference)[]; + /** Format the server expects the params. Defaults to 'either'. */ + paramStructure?: 'by-name' | 'by-position' | 'either'; + errors?: (Error | Reference)[]; + links?: (Link | Reference)[]; + examples?: (ExamplePairingObject | Reference)[]; + externalDocs?: ExternalDocumentation; +}; + +export type Reference = { + $ref: ComponentsReference; +}; + +export type Server = { + url: string; + name?: string; + description?: string; + summary?: string; + variables?: { [key: string]: ServerVariable }; +}; + +export type ServerVariable = { + default: string; + description?: string; + enum?: string[]; +}; + +export type Schema = JSONSchema7; + +export type Tag = { + name: string; + description?: string; + externalDocs?: ExternalDocumentation; +}; + +// #endregion + +export type MethodDocumentationWithoutName = Omit; + +/** Documentation about a single method */ +export type SingleMethodDocumentation = { + method: MethodDocumentationWithoutName; + components?: Components; +}; + +/** Documentation about all methods on a network object */ +export type NetworkObjectDocumentation = { + summary?: string; + description?: string; + methods?: Method[]; + components?: Components; +}; diff --git a/src/sf-pdp/src/papi-websocket/rpc-client.ts b/src/sf-pdp/src/papi-websocket/rpc-client.ts new file mode 100644 index 0000000..d79d3a8 --- /dev/null +++ b/src/sf-pdp/src/papi-websocket/rpc-client.ts @@ -0,0 +1,228 @@ +import { + JSONRPCClient, + JSONRPCErrorCode, + JSONRPCRequest, + JSONRPCResponse, + JSONRPCServer, + JSONRPCServerAndClient, + JSONRPCServerMiddlewareNext, +} from 'json-rpc-2.0'; +import { AsyncVariable, getErrorMessage, Mutex, MutexMap } from 'platform-bible-utils'; +import * as logger from '../log'; +import { IRpcMethodRegistrar } from './rpc.interface'; +import { + ConnectionStatus, + createErrorResponse, + createRequest, + deserializeMessage, + EventHandler, + InternalRequestHandler, + REGISTER_METHOD, + RequestParams, + sendPayloadToWebSocket, + UNREGISTER_METHOD, +} from './rpc.model'; +import { bindClassMethods, SerializedRequestType } from './util'; +import { SingleMethodDocumentation } from './openrpc.model'; + +/** + * Manages the JSON-RPC protocol on the client end of a websocket that connects to main + * + * Created by any process that connects to the websocket server owned by main + */ +export class RpcClient implements IRpcMethodRegistrar { + connectionStatus: ConnectionStatus = ConnectionStatus.Disconnected; + private ws: WebSocket | undefined; + private webSocketPort: number; + private requestId: number = 1; + /** Refers to the current process that created this object (i.e., not main) */ + private readonly jsonRpcServer: JSONRPCServer; + /** Refers to main */ + private readonly jsonRpcClient: JSONRPCClient; + private readonly jsonRpcClientServer: JSONRPCServerAndClient; + private readonly connectionMutex: Mutex = new Mutex(); + private readonly registrationMutexMap: MutexMap = new MutexMap(); + private readonly connectionComplete = new AsyncVariable('websocket connected'); + + constructor(webSocketPort: number) { + bindClassMethods.call(this); + this.webSocketPort = webSocketPort; + this.jsonRpcServer = new JSONRPCServer(); + this.jsonRpcClient = new JSONRPCClient( + (payload) => sendPayloadToWebSocket(this.ws, payload), + this.createNextRequestId, + ); + this.jsonRpcClientServer = new JSONRPCServerAndClient(this.jsonRpcServer, this.jsonRpcClient, { + errorListener: RpcClient.handleError, + }); + } + + private static handleError(message: string, data: unknown): void { + logger.error(`${message}: ${typeof data === 'string' ? data : JSON.stringify(data)}`); + } + + private static onError(ev: Event): void { + RpcClient.handleError('Client websocket error event occurred', ev); + } + + async connect(localEventHandler: EventHandler): Promise { + return this.connectionMutex.runExclusive(async () => { + if (this.connectionStatus === ConnectionStatus.Connected) return true; + if (this.ws) { + logger.warn('Client connect() called when websocket exists but not connected'); + return false; + } + + // Locally process incoming events from other parts of the network + this.jsonRpcServer.applyMiddleware( + async (next: JSONRPCServerMiddlewareNext, request: JSONRPCRequest) => { + if (!request.id) { + const eventData = request.params; + if (!Array.isArray(eventData) || eventData.length !== 1) + throw new Error(`event data for ${request.method} not wrapped in array`); + localEventHandler(request.method, eventData[0]); + } + return next(request); + }, + ); + + try { + this.connectionStatus = ConnectionStatus.Connecting; + this.ws = new WebSocket(`ws://localhost:${this.webSocketPort}`); + this.addEventListenersToWebSocket(); + + // Wait for the socket to finish connecting before continuing (0 means connecting) + if (this.ws.readyState !== 0) this.connectionComplete.resolveToValue(); + await this.connectionComplete.promise; + + this.connectionStatus = ConnectionStatus.Connected; + logger.info(`Websocket connected to ${this.ws.url}`); + } catch (error) { + RpcClient.handleError(`RPC client connection error: ${getErrorMessage(error)}`, this.ws); + this.removeEventListenersFromWebSocket(); + this.connectionStatus = ConnectionStatus.Disconnected; + this.ws = undefined; + return false; + } + + return true; + }); + } + + async disconnect(): Promise { + return this.connectionMutex.runExclusive(async () => { + if (this.connectionStatus === ConnectionStatus.Disconnected) return; + if (this.connectionStatus === ConnectionStatus.Connecting) { + logger.warn(`Cannot disconnect client websocket while connecting`); + return; + } + if (!this.ws) { + logger.warn(`Client connected but websocket is not set`); + return; + } + logger.info(`Websocket disconnecting from ${this.ws.url}`); + this.ws.close(); + }); + } + + async request( + requestType: SerializedRequestType, + requestParams: RequestParams, + ): Promise { + const newRequest = createRequest(requestType, requestParams, this.createNextRequestId()); + // Need to use null since it's part of the API + // eslint-disable-next-line no-null/no-null + let response: JSONRPCResponse | null = null; + const isLocal = this.jsonRpcServer.hasMethod(requestType); + if (isLocal) response = await this.jsonRpcServer.receive(newRequest); + else response = await this.jsonRpcClient.requestAdvanced(newRequest); + if (response) return response; + return createErrorResponse( + `No response from ${isLocal ? 'local' : 'remote'} RPC server`, + JSONRPCErrorCode.InternalError, + ); + } + + // Outgoing events from this client to the rest of the network + emitEventOnNetwork(eventType: string, event: T): void { + this.jsonRpcClient.notify(eventType, [event]); + } + + async registerMethod( + methodName: string, + method: InternalRequestHandler, + methodDocs?: SingleMethodDocumentation, + ): Promise { + if (this.jsonRpcServer.hasMethod(methodName)) { + logger.warn(`RPC method ${methodName} already registered`); + return false; + } + const mutex = this.registrationMutexMap.get(methodName); + return mutex.runExclusive(async () => { + if (this.jsonRpcServer.hasMethod(methodName)) { + logger.warn(`RPC method ${methodName} already registered`); + return false; + } + const success = await this.jsonRpcClient.request(REGISTER_METHOD, [methodName, methodDocs]); + if (success) + this.jsonRpcServer.addMethod(methodName, (params: RequestParams) => method(...params)); + return success; + }); + } + + async unregisterMethod(methodName: string): Promise { + if (!this.jsonRpcServer.hasMethod(methodName)) return false; + const mutex = this.registrationMutexMap.get(methodName); + return mutex.runExclusive(async () => { + if (!this.jsonRpcServer.hasMethod(methodName)) return false; + const successful = await this.jsonRpcClient.request(UNREGISTER_METHOD, [methodName]); + if (successful) this.jsonRpcServer.removeMethod(methodName); + return successful; + }); + } + + private createNextRequestId(): number { + const retVal = this.requestId; + this.requestId += 1; + return retVal; + } + + private addEventListenersToWebSocket() { + if (this.ws) { + this.ws.addEventListener('close', this.onWebSocketClose); + this.ws.addEventListener('error', RpcClient.onError); + this.ws.addEventListener('message', this.onMessageReceivedByWebSocket); + this.ws.addEventListener('open', this.onWebSocketOpen); + } + } + + private removeEventListenersFromWebSocket() { + if (this.ws) { + this.ws.removeEventListener('close', this.onWebSocketClose); + this.ws.removeEventListener('error', RpcClient.onError); + this.ws.removeEventListener('message', this.onMessageReceivedByWebSocket); + this.ws.removeEventListener('open', this.onWebSocketOpen); + this.ws = undefined; + } + } + + private onWebSocketOpen(): void { + this.connectionComplete.resolveToValue(); + } + + private onWebSocketClose(): void { + this.jsonRpcClientServer.rejectAllPendingRequests('The web socket has closed'); + this.removeEventListenersFromWebSocket(); + this.connectionStatus = ConnectionStatus.Disconnected; + } + + private async onMessageReceivedByWebSocket(ev: MessageEvent) { + try { + await this.jsonRpcClientServer.receiveAndSend(deserializeMessage(ev.data)); + } catch (error) { + RpcClient.handleError(`Error processing message "${JSON.stringify(ev.data)}"`, error); + } + } +} + +export default RpcClient; diff --git a/src/sf-pdp/src/papi-websocket/rpc.interface.ts b/src/sf-pdp/src/papi-websocket/rpc.interface.ts new file mode 100644 index 0000000..22bf7a7 --- /dev/null +++ b/src/sf-pdp/src/papi-websocket/rpc.interface.ts @@ -0,0 +1,85 @@ +import { JSONRPCResponse } from 'json-rpc-2.0'; +import { SerializedRequestType } from './util'; +import { SingleMethodDocumentation } from './openrpc.model'; +import { ConnectionStatus, EventHandler, InternalRequestHandler, RequestParams } from './rpc.model'; + +/** + * Defines how to support sending requests on the network and emitting events on the network + * + * NOTE: In JSONRPC jargon, a "request" is made to a "method". In our code we talk about "request + * types", but JSONRPC doesn't have the notion of a "request type". However, a "request type" is + * really just the name of a method in JSONRPC. So "method names" and "request types" are treated as + * the same thing. Similarly, what we call a "request handler" is the same thing as a "method" that + * has been registered with a JSONRPC server. + */ +export interface IRpcHandler { + /** + * Whether this connector is setting up or has finished setting up its connection and is ready to + * communicate on the network + */ + connectionStatus: ConnectionStatus; + /** + * Sets up the RPC handler by populating connector info, setting up event handlers, and doing one + * of the following: + * + * - On clients: connecting to the server + * - On servers: opening an endpoint for clients to connect + * + * @param localEventHandler Function that handles events from the server by accepting an eventType + * and an event and emitting the event locally. Used when receiving an event over the network. + * @returns Promise that resolves when finished connecting + */ + connect: (localEventHandler: EventHandler) => Promise; + /** + * Disconnects from the connection: + * + * - On clients: disconnects from the server + * - On servers: disconnects from all clients and closes its connection endpoint + */ + disconnect: () => Promise; + /** + * Send a request and resolve after receiving a response + * + * @param requestType Type of request (or "method" in JSONRPC jargon) to call + * @param requestParams Parameters associated with this request + * @returns Promise that resolves to a JSONRPCSuccessResponse or JSONRPCErrorResponse message + */ + request: ( + requestType: SerializedRequestType, + requestParams: RequestParams, + ) => Promise; + /** + * Sends an event to other processes. Does NOT run the local event subscriptions as they should be + * run by NetworkEventEmitter after sending on network. + * + * @param eventType Unique network event type for coordinating between processes + * @param event Event data to emit on the network + */ + emitEventOnNetwork: EventHandler; +} + +/** + * Represents anything that handles the RPC protocol and allows callers to register methods that can + * be called remotely over the network. + * + * NOTE: In JSONRPC jargon, a "request" is made to a "method". In our code we talk about "request + * types", but JSONRPC doesn't have the notion of a "request type". However, a "request type" is + * really just the name of a method in JSONRPC. So "method names" and "request types" are treated as + * the same thing. Similarly, what we call a "request handler" is the same thing as a "method" that + * has been registered with a JSONRPC server. + */ +export interface IRpcMethodRegistrar extends IRpcHandler { + /** Register a method that will be called if an RPC request is made */ + registerMethod: ( + methodName: string, + method: InternalRequestHandler, + methodDocs?: SingleMethodDocumentation, + ) => Promise; + /** Unregister a method so it is no longer available to RPC requests */ + unregisterMethod: (methodName: string) => Promise; +} + +export type RegisteredRpcMethodDetails = { + handler: IRpcHandler; + methodDocs?: SingleMethodDocumentation; +}; diff --git a/src/sf-pdp/src/papi-websocket/rpc.model.ts b/src/sf-pdp/src/papi-websocket/rpc.model.ts new file mode 100644 index 0000000..4ef55fa --- /dev/null +++ b/src/sf-pdp/src/papi-websocket/rpc.model.ts @@ -0,0 +1,219 @@ +import { + JSONRPC, + JSONRPCErrorCode, + JSONRPCErrorResponse, + JSONRPCRequest, + JSONRPCResponse, + JSONRPCSuccessResponse, +} from 'json-rpc-2.0'; +import { deserialize, serialize, wait } from 'platform-bible-utils'; +import { SerializedRequestType } from './util'; +import * as logger from '../log'; + +/** How many times to try sending a request before giving up if the request is not yet registered */ +const MAX_REQUEST_ATTEMPTS = 10; +/** How long in ms to wait between request attempts if the request is not yet registered */ +const REQUEST_ATTEMPT_WAIT_TIME_MS = 1000; + +/** + * Whether an RPC object is setting up or has finished setting up its connection and is ready to + * communicate on the network + */ +export enum ConnectionStatus { + /** Not connected to the network */ + Disconnected, + /** Attempting to connect to the network */ + Connecting, + /** Finished setting up its connection */ + Connected, +} + +/** Parameters provided to an RPC request message */ +// Align with types from the JSON RPC package +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type RequestParams = Array; + +/** + * Function to call internally when a request is received. The return value is sent back as the + * response to the request. If the request was received over the network, the response will be + * packaged into a JSONRPCSuccessResponse message. + */ +// Align with types from the JSON RPC package +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type InternalRequestHandler = (...requestParams: RequestParams) => any; + +/** Function that processes an event received locally or over the network */ +export type EventHandler = (eventType: string, event: T) => void; + +/** + * ID of an individual request. It must be unique between an RPC client and server for a single + * connection. Once a connection has closed and reopens, IDs can be reused. + */ +export type RequestId = number | string; + +/** + * Create a JSONRPCRequest message + * + * @param requestType Indicates what to do with the request + * @param requestParams Parameters to pass along when the request is processed + * @param requestId Unique ID for this connection of this request + * @returns JSONRPCRequest message that can be serialized and sent over a connection + */ +export function createRequest( + requestType: SerializedRequestType, + requestParams: RequestParams, + requestId: RequestId, +): JSONRPCRequest { + return { jsonrpc: JSONRPC, id: requestId, method: requestType, params: requestParams }; +} + +/** + * Create a JSONRPCSuccessResponse message + * + * @param contents Data to return to the requester when the request succeeds + * @param requestId ID of the request that this response is intended to address. If no ID was + * provided, don't pass a value to this parameter. + * @returns JSONRPCSuccessResponse message that can be serialized and sent over a connection + */ +export function createSuccessResponse( + contents: T, + requestId: RequestId = 0, +): JSONRPCSuccessResponse { + return { jsonrpc: JSONRPC, id: requestId, result: contents }; +} + +/** + * Create a JSONRPCErrorResponse message + * + * @param errorMessage Text to provide to the requester about why this request failed + * @param errorCode JSONRPCErrorCode value that best aligns with the purpose of the failure + * @param requestId ID of the request that this response is intended to address. If no ID was + * provided, don't pass a value to this parameter. + * @returns JSONRPCErrorResponse message that can be serialized and sent over a connection + */ +export function createErrorResponse( + errorMessage: string, + errorCode: JSONRPCErrorCode = JSONRPCErrorCode.InternalError, + requestId: RequestId = 0, +): JSONRPCErrorResponse { + return { jsonrpc: JSONRPC, id: requestId, error: { code: errorCode, message: errorMessage } }; +} + +/** Serialize a payload, if needed, and send it over the provided WebSocket */ +export function sendPayloadToWebSocket(ws: WebSocket | undefined, payload: unknown): void { + if (!ws) throw new Error(`Tried to send payload while not connected`); + if ( + typeof payload === 'string' || + payload instanceof ArrayBuffer || + payload instanceof Blob || + ArrayBuffer.isView(payload) + ) { + ws.send(payload); + } else { + ws.send(serialize(payload)); + } +} + +/** + * Deserialize a payload from the network and return it as a JSONRPC message or array of messages. + * Note that all `null` values from the payload will be converted into `undefined` values except for + * `result` values in JSONRPCSuccessResponse messages. A `null` value as the response to a request + * must not be converted to `undefined` per the JSONRPC protocol. + * + * After a request has been processed by the protocol stack, call `fixupResponse` to restore + * `undefined` responses. + */ +export function deserializeMessage( + payload: string, +): JSONRPCRequest | JSONRPCResponse | Array { + const message = deserialize(payload); + const messageType = typeof message; + if (messageType !== 'object') return message; + if (Array.isArray(message)) { + message.forEach((msg) => { + // Required by the protocol since we convert "undefined" to "null" in "deserialize" + // eslint-disable-next-line no-null/no-null + if (typeof msg === 'object' && 'result' in msg && msg.result === undefined) msg.result = null; + }); + } else if ('result' in message && message.result === undefined) + // Required by the protocol since we convert "undefined" to "null" in "deserialize" + // eslint-disable-next-line no-null/no-null + message.result = null; + + return message; +} + +/** + * Convert `null` results back to `undefined` once we're out of the protocol stack. + * + * This works in tandem with `deserializeMessage` to properly handle `null` values in JSONRPC + * messages. + */ +export function fixupResponse(response: JSONRPCResponse): JSONRPCResponse { + // Convert "null" back to "undefined" before it flows back out to callers + // eslint-disable-next-line no-null/no-null + if ('result' in response && response.result === null) response.result = undefined; + return response; +} + +/** + * Runs the request callback and retries a number of times if `requestCallback` resolves to a method + * not found error + * + * @param requestCallback Function to run to send a JSON-RPC request. Should return a JSONRPC error + * with code {@link JSONRPCErrorCode.MethodNotFound} if it fails to find the method + * @param name Name of the handler running this request for logging purposes + * @param requestType Type of request for logging purposes + * @returns The response from the request including the method not found error if it times out + */ +export async function requestWithRetry( + requestCallback: () => Promise, + name: string, + requestType: string, +): Promise { + // https://github.com/paranext/paranext-core/issues/51 + // If the request type doesn't have a registered handler yet, retry a few times to help with race + // conditions. This approach is hacky but works well enough for now. + for (let attemptsRemaining = MAX_REQUEST_ATTEMPTS; attemptsRemaining > 0; attemptsRemaining--) { + // Intentionally awaiting inside for loop so we attempt once at a time + // eslint-disable-next-line no-await-in-loop + const response = await requestCallback(); + + if (!response.error || response.error.code !== JSONRPCErrorCode.MethodNotFound) return response; + + logger.debug( + `RPC handler ${name} could not find a request handler for requestType ${requestType} on attempt ${MAX_REQUEST_ATTEMPTS - attemptsRemaining + 1} of ${MAX_REQUEST_ATTEMPTS}. ${attemptsRemaining === 1 ? 'Giving up.' : 'Retrying...'}`, + ); + + // No need to wait again after the last attempt fails. Return the error response + if (attemptsRemaining === 1) return response; + + // Intentionally awaiting inside for loop so we wait a bit before retrying + // eslint-disable-next-line no-await-in-loop + await wait(REQUEST_ATTEMPT_WAIT_TIME_MS); + } + throw new Error( + `RPC handler ${name} did not return a response after retrying to find request handler for requestType ${requestType}. This should never happen. Please investigate`, + ); +} + +/** + * Register a method on the network so that requests of the given type are routed to your request + * handler. + */ +export const REGISTER_METHOD = 'network:registerMethod'; + +/** + * Unregister a method on the network so that requests of the given type are no longer routed to + * your request handler. + */ +export const UNREGISTER_METHOD = 'network:unregisterMethod'; + +/** + * Get all methods that are currently registered on the network. Required to be 'rpc.discover' by + * the OpenRPC specification. + */ +export const GET_METHODS = 'rpc.discover'; + +/** Prefix on requests that indicates that the request is a command */ +export const CATEGORY_COMMAND = 'command'; diff --git a/src/sf-pdp/src/papi-websocket/util.ts b/src/sf-pdp/src/papi-websocket/util.ts new file mode 100644 index 0000000..f84dab4 --- /dev/null +++ b/src/sf-pdp/src/papi-websocket/util.ts @@ -0,0 +1,69 @@ +import { indexOf, stringLength, substring } from 'platform-bible-utils'; + +// #region Serialization and deserialization functions + +/** Separator between parts of a serialized request */ +const REQUEST_TYPE_SEPARATOR = ':'; + +/** Information about a request that tells us what to do with it */ +export type RequestType = { + /** The general category of request */ + category: string; + /** Specific identifier for this type of request */ + directive: string; +}; + +/** + * String version of a request type that tells us what to do with a request. + * + * Consists of two strings concatenated by a colon + */ +export type SerializedRequestType = `${string}${typeof REQUEST_TYPE_SEPARATOR}${string}`; + +/** + * Create a request message requestType string from a category and a directive + * + * @param category The general category of request + * @param directive Specific identifier for this type of request + * @returns Full requestType for use in network calls + */ +export function serializeRequestType(category: string, directive: string): SerializedRequestType { + if (!category) throw new Error('serializeRequestType: "category" is not defined or empty.'); + if (!directive) throw new Error('serializeRequestType: "directive" is not defined or empty.'); + + return `${category}${REQUEST_TYPE_SEPARATOR}${directive}`; +} + +/** Split a request message requestType string into its parts */ +export function deserializeRequestType(requestType: SerializedRequestType): RequestType { + if (!requestType) throw new Error('deserializeRequestType: must be a non-empty string'); + + const colonIndex = indexOf(requestType, REQUEST_TYPE_SEPARATOR); + if (colonIndex <= 0 || colonIndex >= stringLength(requestType) - 1) + throw new Error( + `deserializeRequestType: Must have two parts divided by a ${REQUEST_TYPE_SEPARATOR} (${requestType})`, + ); + const category = substring(requestType, 0, colonIndex); + const directive = substring(requestType, colonIndex + 1); + return { category, directive }; +} + +// #endregion + +/** + * Allow an object to bind all its class-defined functions to itself to ensure all references to + * "this" in its functions refer to the object rather than the caller of the function. For example, + * if a function on the class is provided to a callback, if "this" isn't bound to the object then + * "this" will refer to the entity running the callback. + */ +export function bindClassMethods(this: T): void { + const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(this)); + methods.forEach((method) => { + // Allow indexing to work for this object + // eslint-disable-next-line @typescript-eslint/no-explicit-any, no-type-assertion/no-type-assertion + const thisAsAny = this as any; + if (typeof thisAsAny[method] === 'function') { + thisAsAny[method] = thisAsAny[method].bind(this); + } + }); +} diff --git a/src/sf-pdp/src/pdp/pdp-factory.ts b/src/sf-pdp/src/pdp/pdp-factory.ts new file mode 100644 index 0000000..3868271 --- /dev/null +++ b/src/sf-pdp/src/pdp/pdp-factory.ts @@ -0,0 +1,87 @@ +import { ProjectResultsMessage } from 'sf-pdp-messages'; +import { + NetworkObjectRegistrationData, + NetworkObjectTypes, + registerNetworkObject, +} from '../papi-websocket/network-object'; +import { RpcClient } from '../papi-websocket/rpc-client'; +import { ScriptureForgeProjectDataProvider } from './pdp'; +import * as logger from '../log'; + +const SCRIPTURE_FORGE_PROJECT_INTERFACES = [ + 'platform.base', + 'scriptureForge.chapterDeltaOperations', + 'scriptureForge.scriptureForgeProject', +] as const; + +type ProjectMetadataWithoutFactoryInfo = { + /** ID of a project (must be unique and case insensitive) */ + id: string; + projectInterfaces: typeof SCRIPTURE_FORGE_PROJECT_INTERFACES; +}; + +let rpcClient: RpcClient | undefined; +export function setRpcClient(client: RpcClient): void { + rpcClient = client; +} + +let getProjectsMessage: (() => Promise) | undefined; +export function setGetProjectsMessage( + projectsMessageRetriever: () => Promise, +): void { + getProjectsMessage = projectsMessageRetriever; +} + +const knownPdps = new Map(); + +async function getAvailableProjects(): Promise { + if (!getProjectsMessage) + throw new Error('getProjectsMessage is not set. Please set it before calling this function.'); + const retVal: ProjectMetadataWithoutFactoryInfo[] = []; + const projectsMessage = await getProjectsMessage(); + projectsMessage.projectIds.forEach((projectId) => { + retVal.push({ + id: projectId, + projectInterfaces: SCRIPTURE_FORGE_PROJECT_INTERFACES, + }); + }); + logger.debug(`Available SF projects: ${JSON.stringify(projectsMessage.projectIds)}`); + return retVal; +} + +const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; +function generateRandomLetters(length: number): string { + let result = ''; + for (let i = 0; i < length; i++) { + const randomIndex = Math.floor(Math.random() * characters.length); + result += characters[randomIndex]; + } + return result; +} + +async function getProjectDataProviderId(projectId: string): Promise { + const existingPdp = knownPdps.get(projectId); + if (existingPdp) return existingPdp.pdpId; + + if (!rpcClient) throw new Error('Internal error - RPC client is not set.'); + const pdpId = `SF0${generateRandomLetters(20)}-pdp-data`; + const projectPdp = new ScriptureForgeProjectDataProvider(projectId, rpcClient, pdpId); + await registerNetworkObject(rpcClient, projectPdp.networkObjectRegistrationData); + + knownPdps.set(projectId, projectPdp); + return pdpId; +} + +const pdpFactoryFunctions = { + getAvailableProjects, + getProjectDataProviderId, +}; + +export const scriptureForgePdpFactory: NetworkObjectRegistrationData = { + objectId: 'platform.scriptureForgeProjects-pdpf', + objectType: NetworkObjectTypes.PROJECT_DATA_PROVIDER_FACTORY, + functions: pdpFactoryFunctions, + attributes: { + projectInterfaces: SCRIPTURE_FORGE_PROJECT_INTERFACES, + }, +}; diff --git a/src/sf-pdp/src/pdp/pdp.ts b/src/sf-pdp/src/pdp/pdp.ts new file mode 100644 index 0000000..5dcbfcf --- /dev/null +++ b/src/sf-pdp/src/pdp/pdp.ts @@ -0,0 +1,59 @@ +import { DeltaOperation, ScriptureForgeProjectDocument } from 'scripture-forge'; +import { SerializedVerseRef } from '@sillsdev/scripture'; +import { SCRIPTURE_FORGE_BACK_END_CONNECTION } from '../sf-backend/scripture-forge-back-end-connection'; +import { bindClassMethods } from '../papi-websocket/util'; +import { Delta } from '../sf-backend/rce-utils'; +import { RpcClient } from '../papi-websocket/rpc-client'; +import { + NetworkObjectRegistrationData, + NetworkObjectTypes, +} from '../papi-websocket/network-object'; + +export class ScriptureForgeProjectDataProvider { + private readonly projectId: string; + private readonly rpcClient: RpcClient; + private readonly dataProviderId: string; + private readonly dataUpdateEventName: string; + + constructor(projectId: string, rpcClient: RpcClient, dataProviderId: string) { + bindClassMethods.call(this); + this.projectId = projectId; + this.rpcClient = rpcClient; + this.dataProviderId = dataProviderId; + this.dataUpdateEventName = `${this.dataProviderId}:onDidUpdate`; + } + + get pdpId(): string { + return this.dataProviderId; + } + + get networkObjectRegistrationData(): NetworkObjectRegistrationData { + return { + objectId: this.dataProviderId, + objectType: NetworkObjectTypes.PROJECT_DATA_PROVIDER, + functions: { + getProject: this.getProject.bind(this), + getChapterDeltaOperations: this.getChapterDeltaOperations.bind(this), + setChapterDeltaOperations: this.setChapterDeltaOperations.bind(this), + }, + }; + } + + async getProject(): Promise { + return (await SCRIPTURE_FORGE_BACK_END_CONNECTION.getProjectDoc(this.projectId)).data; + } + + async getChapterDeltaOperations(verseRef: SerializedVerseRef): Promise { + return (await SCRIPTURE_FORGE_BACK_END_CONNECTION.getChapterDoc(this.projectId, verseRef)).data + .ops; + } + + async setChapterDeltaOperations( + verseRef: SerializedVerseRef, + updatesToApply: DeltaOperation[], + ): Promise { + const doc = await SCRIPTURE_FORGE_BACK_END_CONNECTION.getChapterDoc(this.projectId, verseRef); + doc.submitOp(new Delta(updatesToApply)); + this.rpcClient.emitEventOnNetwork(this.dataUpdateEventName, 'ChapterDeltaOperations'); + } +} diff --git a/src/sf-pdp/src/sf-backend/custom-origin-websocket.ts b/src/sf-pdp/src/sf-backend/custom-origin-websocket.ts new file mode 100644 index 0000000..d730c70 --- /dev/null +++ b/src/sf-pdp/src/sf-backend/custom-origin-websocket.ts @@ -0,0 +1,27 @@ +import { ClientOptions, WebSocket } from 'ws'; + +let origin: string | undefined; + +/** + * Sets the origin for CustomOriginWebSocket. This must be called before creating any instances of + * CustomOriginWebSocket. + * + * @param newOrigin - The origin to set, e.g., 'https://example.com'. + */ +export function setOrigin(newOrigin: string): void { + origin = newOrigin; +} + +/** WebSocket class that allows setting a custom Origin header. */ +export class CustomOriginWebSocket extends WebSocket { + constructor(address: string, protocols?: string | string[], options: ClientOptions = {}) { + if (!origin) throw new Error('Origin must be set before creating a CustomOriginWebSocket'); + + // Add or override the Origin header + options.headers = { + ...(options.headers || {}), + origin, + }; + super(address, protocols, options); + } +} diff --git a/src/sf-pdp/src/sf-backend/rce-utils.ts b/src/sf-pdp/src/sf-backend/rce-utils.ts new file mode 100644 index 0000000..092f9ce --- /dev/null +++ b/src/sf-pdp/src/sf-backend/rce-utils.ts @@ -0,0 +1,28 @@ +import Delta, { Op } from 'quill-delta'; +import RichText from 'rich-text'; +import { Connection, Doc as ShareDbDoc, types as ShareDbTypes } from 'sharedb/lib/client'; +import { type Socket } from 'sharedb/lib/sharedb'; +import { SerializedVerseRef } from '@sillsdev/scripture'; + +/** This is the key that Scripture Forge uses for storing data about projects */ +export const SCRIPTURE_FORGE_PROJECTS_DOCUMENT = 'sf_projects'; + +/** This is the key that Scripture Forge uses for scripture data per project per chapter */ +export const SCRIPTURE_FORGE_CHAPTER_DOCUMENT = 'texts'; + +/** + * Generate the ID that stands for one specific chapter amongst all chapters of all books in all + * projects within Scripture Forge + */ +export function createTextId(projectId: string, serializedVerseRef: SerializedVerseRef): string { + return `${projectId}:${serializedVerseRef.book}:${serializedVerseRef.chapterNum}:target`; +} + +// Exports related to connecting to ShareDB +export { Connection, Socket }; + +// Exports related to ShareDB documents and document types +export { RichText, ShareDbDoc, ShareDbTypes }; + +// Exports related to data structures passed back and forth with ShareDB +export { Delta, type Op }; diff --git a/src/sf-pdp/src/sf-backend/scripture-forge-back-end-connection.ts b/src/sf-pdp/src/sf-backend/scripture-forge-back-end-connection.ts new file mode 100644 index 0000000..fbd7e1a --- /dev/null +++ b/src/sf-pdp/src/sf-backend/scripture-forge-back-end-connection.ts @@ -0,0 +1,234 @@ +import RichText from 'rich-text'; +import { Canon, SerializedVerseRef } from '@sillsdev/scripture'; +import { Chapter, ScriptureForgeProjectDocument, TextInfo } from 'scripture-forge'; +import { AsyncVariable, getErrorMessage } from 'platform-bible-utils'; +import { + Connection, + createTextId, + Delta, + SCRIPTURE_FORGE_CHAPTER_DOCUMENT, + SCRIPTURE_FORGE_PROJECTS_DOCUMENT, + ShareDbTypes, + ShareDbDoc, + Socket, +} from './rce-utils'; +import { ShareDBWebsocketAdapter } from './share-db-websocket-adapter'; +import { CustomOriginWebSocket } from './custom-origin-websocket'; +import * as logger from '../log'; + +// Timeout for connecting and retrieving documents +const SF_TIMEOUT_MS = 60000; + +let initialized = false; +function initialize() { + if (initialized) return; + ShareDbTypes.register(RichText.type); + initialized = true; +} + +let connectionAsyncVariable: AsyncVariable | undefined; +let socket: ShareDBWebsocketAdapter | undefined; +const projectDocuments = new Map>(); +const chapterDeltas = new Map>(); + +async function connect(wssBaseUrl: string, accessToken: string): Promise { + if (connectionAsyncVariable) { + await connectionAsyncVariable.promise; + return; + } + + initialize(); + + connectionAsyncVariable = new AsyncVariable('SF PDP connect', SF_TIMEOUT_MS); + + socket = new ShareDBWebsocketAdapter(`${wssBaseUrl}/?access_token=${accessToken}`, [], { + // ShareDB handles dropped messages, and buffering them while the socket + // is closed has undefined behavior + maxEnqueuedMessages: 0, + WebSocket: CustomOriginWebSocket, + }); + + // Socket requires onopen + const originalOnOpen = socket.onopen; + socket.onopen = (event) => { + // ShareDB's minimal WebSocket interface that we ensure is implemented in this function + // eslint-disable-next-line no-type-assertion/no-type-assertion + if (socket) connectionAsyncVariable?.resolveToValue(new Connection(socket as Socket)); + logger.info('RCE stream with SF PDP opened'); + if (originalOnOpen) originalOnOpen(event); + }; + + // Socket requires onclose + const originalOnClose = socket.onclose; + if (!originalOnClose) { + socket.onclose = () => { + logger.info('RCE stream with SF PDP closed'); + }; + } + + // Socket requires onerror + const originalOnError = socket.onerror; + socket.onerror = (event) => { + const errorMsg = `Error occurred in RCE stream with SF PDP: ${event.message}`; + logger.error(errorMsg); + if (connectionAsyncVariable && !connectionAsyncVariable.hasSettled) + connectionAsyncVariable.rejectWithReason(errorMsg); + if (originalOnError) originalOnError(event); + }; + + // Socket requires onmessage + const originalOnMessage = socket.onmessage; + if (!originalOnMessage) { + socket.onmessage = () => { + logger.verbose('RCE stream with SF PDP received message'); + }; + } + + // Don't return the connection object (keep it private) + await connectionAsyncVariable.promise; +} + +async function disconnect(): Promise { + const socketClosedAsyncVariable = new AsyncVariable('SF PDP disconnect', SF_TIMEOUT_MS); + if (socket) { + const originalOnClose = socket.onclose; + socket.onclose = (event: CloseEvent) => { + socketClosedAsyncVariable.resolveToValue(); + if (originalOnClose) originalOnClose(event); + }; + } else { + socketClosedAsyncVariable.resolveToValue(); + } + + if (connectionAsyncVariable) { + if (!connectionAsyncVariable.hasSettled) + connectionAsyncVariable.rejectWithReason('Disconnecting'); + else { + try { + const connection = await connectionAsyncVariable.promise; + connection.close(); + } catch (err) { + logger.warn(`Error while closing connection with SF PDP: ${getErrorMessage(err)}`); + } + } + } + connectionAsyncVariable = undefined; + + if (socket) { + socket.close(); + socket = undefined; + } + await socketClosedAsyncVariable.promise; + + projectDocuments.clear(); + chapterDeltas.clear(); +} + +async function getProjectDoc( + projectId: string, +): Promise> { + const existingDoc = projectDocuments.get(projectId); + if (existingDoc) return existingDoc; + + if (!connectionAsyncVariable) throw new Error('Must call connect() before calling this function'); + + const connection = await connectionAsyncVariable.promise; + const projectAsyncVar = new AsyncVariable(`SF PDP project ${projectId}`, SF_TIMEOUT_MS); + const newDoc = connection.get(SCRIPTURE_FORGE_PROJECTS_DOCUMENT, projectId); + newDoc.subscribe((error) => { + if (error) { + const errorMsg = `SF PDP error subscribing to project: ${getErrorMessage(error)}`; + logger.error(errorMsg); + projectAsyncVar.rejectWithReason(errorMsg); + } else projectAsyncVar.resolveToValue(); + }); + newDoc.fetch(); + await projectAsyncVar.promise; + projectDocuments.set(projectId, newDoc); + return newDoc; +} + +async function getChapterDoc( + projectId: string, + serializedVerseRef: SerializedVerseRef, +): Promise> { + const textId = createTextId(projectId, serializedVerseRef); + const existingDoc = chapterDeltas.get(textId); + if (existingDoc) return existingDoc; + + if (!connectionAsyncVariable) throw new Error('Must call connect() before calling this function'); + + const project = await getProjectDoc(projectId); + if ( + !project.data.texts.find((textInfo: TextInfo) => { + return ( + Canon.bookNumberToId(textInfo.bookNum) === serializedVerseRef.book && + textInfo.chapters.find((chapter: Chapter) => { + return chapter.number === serializedVerseRef.chapterNum && chapter.isValid; + }) + ); + }) + ) + throw new Error(`Verse ref ${serializedVerseRef} not found in project ${projectId}`); + + const connection = await connectionAsyncVariable.promise; + const docSubscriptionAsyncVar = new AsyncVariable(`SF PDP text ${textId}`, SF_TIMEOUT_MS); + const newDoc = connection.get(SCRIPTURE_FORGE_CHAPTER_DOCUMENT, textId); + newDoc.subscribe((error) => { + if (error) { + const errorMsg = `SF PDP error subscribing to document: ${getErrorMessage(error)}`; + logger.error(errorMsg); + docSubscriptionAsyncVar.rejectWithReason(errorMsg); + } else docSubscriptionAsyncVar.resolveToValue(); + }); + newDoc.fetch(); + await docSubscriptionAsyncVar.promise; + chapterDeltas.set(textId, newDoc); + return newDoc; +} + +/** + * Represents the connection to the Scripture Forge backend for reading and writing project-related + * data over the WebSocket. + */ +export const SCRIPTURE_FORGE_BACK_END_CONNECTION = { + /** + * Establishes a connection to the Scripture Forge backend. Ensures that the client is ready to + * interact with the backend services. + * + * @param wssBaseUrl The base WebSocket URL for the Scripture Forge backend. + * @param accessToken The token used to authenticate the current user to the backend. + * @returns Promise that resolves once the connection is established. If connect has already been + * called, the same promise is returned as the previous time regardless of whether the + * parameters are the same. To change the connection parameters, you must disconnect before + * connecting again. + */ + connect, + + /** + * Disconnects from the Scripture Forge backend. Cleans up resources and terminates the connection + * gracefully. + * + * @returns Promise that resolves once the WebSocket to the Scripture Forge backend has closed. + */ + disconnect, + + /** + * Retrieves the project document from the backend. + * + * @param projectId The unique identifier of the project to retrieve. + * @returns A promise that resolves to the ShareDB project document. + */ + getProjectDoc, + + /** + * Retrieves the chapter document from the backend. + * + * @param projectId The unique identifier of the project to retrieve. + * @param serializedVerseRef The serialized verse reference specifying the book and chapter to + * retrieve. + * @returns A promise that resolves to the ShareDB chapter document. + */ + getChapterDoc, +}; +Object.freeze(SCRIPTURE_FORGE_BACK_END_CONNECTION); diff --git a/src/sf-pdp/src/sf-backend/share-db-websocket-adapter.ts b/src/sf-pdp/src/sf-backend/share-db-websocket-adapter.ts new file mode 100644 index 0000000..500f87a --- /dev/null +++ b/src/sf-pdp/src/sf-backend/share-db-websocket-adapter.ts @@ -0,0 +1,92 @@ +import { WebSocket } from 'partysocket'; +import { hasStringProperty, isMessageSendingOp, tryParseJson } from './utils'; + +export type ScriptureForgeWebSocketMessage = string | ArrayBuffer | Blob | ArrayBufferView; + +/** + * This class was created to work around a problem with ShareDB and offline support. ShareDB is + * designed to work with a network that drops and then reconnects, but is not designed to persist + * data anywhere other than in memory. + * + * In order to make submitting an op idempotent, two properties are set on the op: + * + * - `src` is set to the value of the `id` property of the connection. (In practice this is usually + * omitted because it would be the same as the connection id; see below) + * - `seq` is set to a monotonically increasing number that is unique to the connection. If an op has + * been submitted but not acknowledged, then the op is submitted again, and the server will ignore + * the op if it already applied it. + * + * If the user closes the browser when an op has been sent and not acknowledged, the op needs to be + * stored in IndexedDB with the same `src` and `seq` properties so that when the user opens the + * browser again, the op can be submitted again idempotently. The problem is that ShareDB sets the + * `seq` property immediately before sending the op, so it is not possible to fully store the op in + * IndexedDB before the `seq` property is set. There is no event that can be subscribed to that will + * be triggered after the `seq` property is set and before the op is sent, and we cannot set the + * `seq` property ourselves when submitting the op to ShareDB (or at least no way was found when + * this route was investigated). + * + * ShareDB is even more lackadaisical about setting the `src` property on the op. When the op is + * first submitted, the `src` value would be set to the `id` property of the connection, which is + * known by the server, so ShareDB omits the `src` property and lets the server get the value from + * the connection. Immediately after submitting the op, ShareDB sets the `src` property to the value + * of the `id` property of the connection, so that if the op is later sent again after a new + * connection is established, the op can be correctly ignored. + * + * The workaround to these problems is to use a custom websocket adapter that will intercept the + * connection and store the op in IndexedDB before it is sent. When the op is intercepted it already + * has the `seq` property set, but lacks the `src` property. To work around this, the `src` property + * is added to a copy of the op just before it is stored in IndexedDB. + * + * - NP, 2023-03-07 + * + * Notes: + * + * - You will need to cast this as a ShareDB Socket to use with a ShareDB Connection. + * - This differs from the Scripture Forge implementation as it has no preventOpAcknowledgement + * support. + */ +export class ShareDBWebsocketAdapter extends WebSocket { + /** The listeners to call before sending an op. */ + private readonly beforeSendOpListeners: ((collection: string, docId: string) => Promise)[] = + []; + + /** + * Adds a listener that will be called before an op is sent to the server. This allows the op to + * be stored in IndexedDB before being sent. + * + * @param listener The listener + */ + subscribeToBeforeSendOp(listener: (collection: string, docId: string) => Promise): void { + this.beforeSendOpListeners.push(listener); + } + + /** + * Sends messages on through from the ShareDB client to the websocket, but ignores them if the + * message is an op and the feature flag to disable sending ops is turned on. + * + * If the message is an op and sending ops is not disabled, then the remote store is notified that + * an op is about to be sent, along with the collection and document id of the op, so that it can + * be stored in IndexedDB before being sent. + * + * @param data The message + * @returns A promise. + */ + async send(data: ScriptureForgeWebSocketMessage): Promise { + const msg: unknown = tryParseJson(data); + if (isMessageSendingOp(msg) && hasStringProperty(msg, 'c') && hasStringProperty(msg, 'd')) + await this.beforeSendOp(msg.c, msg.d); + super.send(data); + } + + /** + * Calls all listeners that have been added to be notified before an op is sent to the server. + * + * @param collection The collection that the op is for + * @param docId The identifier of the document being modified by the op. + */ + private async beforeSendOp(collection: string, docId: string): Promise { + await Promise.all( + this.beforeSendOpListeners.map(async (listener) => listener(collection, docId)), + ); + } +} diff --git a/src/sf-pdp/src/sf-backend/utils.ts b/src/sf-pdp/src/sf-backend/utils.ts new file mode 100644 index 0000000..718f339 --- /dev/null +++ b/src/sf-pdp/src/sf-backend/utils.ts @@ -0,0 +1,73 @@ +/** + * Determines whether a variable has the specified property on it. + * + * @param variable The variable to check. + * @param property Typically the name of a property, but could be a number of symbol. + * @returns `true` if the object has the property, `false` otherwise. + */ +export function hasProperty( + variable: TVariable, + property: TProperty, +): variable is TVariable & Record { + return ( + variable && + (typeof variable === 'object' || typeof variable === 'function') && + property in variable + ); +} + +/** + * Determines whether an object has the specified property and value. + * + * @param variable The variable to check. + * @param property Typically the name of a property, but could be a number of symbol. + * @param value The value of the property. + * @returns `true` if the object has the property with the specified value, `false` otherwise. + */ +export function hasPropertyWithValue( + variable: TVariable, + property: TProperty, + value: unknown, +): variable is TVariable & Record { + return hasProperty(variable, property) && variable[property] === value; +} + +/** + * Determines whether an object has the specified string property. + * + * @param variable The variable to check. + * @param property Typically the name of a property, but could be a number of symbol. + * @returns `true` if the object has the property with a string type, `false` otherwise. + */ +export function hasStringProperty( + variable: TVariable, + property: TProperty, +): variable is TVariable & Record { + return hasProperty(variable, property) && typeof variable[property] === 'string'; +} + +/** + * Determines whether the given value is a message sending an op to the server. + * + * @param data The data to be sent to the server. + * @returns `true` if the value is a message sending operation. + */ +export function isMessageSendingOp(value: unknown): value is { a: 'op' } { + return hasPropertyWithValue(value, 'a', 'op'); +} + +/** + * Attempts to parse a JSON string into an object of type T. + * + * @param value The value to parse as JSON. + * @returns The parsed object of type T, or `undefined` if the value is not a string or parsing + * fails. + */ +export function tryParseJson(value: unknown): T | undefined { + if (typeof value !== 'string') return undefined; + try { + return JSON.parse(value); + } catch (error) { + return undefined; + } +} diff --git a/src/sf-pdp/tsconfig.json b/src/sf-pdp/tsconfig.json new file mode 100644 index 0000000..bf6797e --- /dev/null +++ b/src/sf-pdp/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "CommonJS", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"] +} diff --git a/src/sf-pdp/tsconfig.lint.json b/src/sf-pdp/tsconfig.lint.json new file mode 100644 index 0000000..a870799 --- /dev/null +++ b/src/sf-pdp/tsconfig.lint.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig", + "include": [".eslintrc.js", "*.ts", "*.js", "src"] +}