diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 746198d4f..f0b0a304e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -155,8 +155,11 @@ jobs: - name: Generate Prisma client uses: ./.github/actions/generate-prisma-client - - name: Build everything - run: cargo build --release + - name: Cargo fetch + run: cargo fetch + + - name: Check core + run: cargo check -p sd-core --release - name: Bundle Desktop run: pnpm desktop tauri build diff --git a/.gitignore b/.gitignore index 1a7673277..41d99ca63 100644 --- a/.gitignore +++ b/.gitignore @@ -65,4 +65,4 @@ examples/*/*.lock /sdserver_data .spacedrive -dev.db-journal \ No newline at end of file +dev.db-journal diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 000000000..3e5b2cdae --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,20 @@ +{ + "pluginSearchDirs": [ + "." + ], + "useTabs": true, + "printWidth": 100, + "singleQuote": true, + "trailingComma": "none", + "bracketSameLine": false, + "semi": true, + "quoteProps": "consistent", + "importOrder": [ + "^[./]", + "^@sd/ui/(.*)$", + "^@sd/client/(.*)$", + "^@sd/interface/(.*)$" + ], + "importOrderSeparation": true, + "importOrderSortSpecifiers": true +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..6ff490004 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "tauri-apps.tauri-vscode", + "rust-lang.rust-analyzer", + "oscartbeaumont.rspc-vscode" + ] +} diff --git a/Cargo.lock b/Cargo.lock index 3724a84a0..5294fffd8 100644 Binary files a/Cargo.lock and b/Cargo.lock differ diff --git a/Cargo.toml b/Cargo.toml index 21fc6d3ef..c12745288 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,19 +12,22 @@ members = [ resolver = "2" [workspace.dependencies] -prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust.git", rev = "762ba6c06b9386102aea37538fdfe0b2aa24fd52", features = [ +prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust.git", rev = "078919e3724f35cc00c2402c66ba585b61c88d47", features = [ "rspc", "sqlite-create-many", "migrations", "sqlite", ], default-features = false } -prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client-rust.git", rev = "762ba6c06b9386102aea37538fdfe0b2aa24fd52", features = [ +prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client-rust.git", rev = "078919e3724f35cc00c2402c66ba585b61c88d47", features = [ "rspc", "sqlite-create-many", "migrations", "sqlite", ], default-features = false } +rspc = { version = "0.1.2" } [patch.crates-io] # We use this patch so we can compile for the IOS simulator on M1 -openssl-sys = { git = "https://github.com/spacedriveapp/rust-openssl" } +openssl-sys = { git = "https://github.com/spacedriveapp/rust-openssl", rev = "92c3dec225a9e984884d5b30a517e5d44a24d03b" } + +rspc = { git = "https://github.com/oscartbeaumont/rspc", rev = "08c19f5e485dfc6841c7e9877c80058402bf71d7" } # TODO: Move back to crates.io when new jsonrpc executor is release \ No newline at end of file diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 818905ec2..a567d20a5 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -11,32 +11,32 @@ "build": "tauri build" }, "dependencies": { - "@rspc/client": "^0.0.6", - "@rspc/tauri": "^0.0.6", + "@rspc/client": "^0.1.2", + "@rspc/tauri": "^0.1.2", "@sd/client": "workspace:*", "@sd/interface": "workspace:*", "@sd/ui": "workspace:*", - "@tanstack/react-query": "^4.0.10", - "@tauri-apps/api": "1.0.2", + "@tanstack/react-query": "^4.10.1", + "@tauri-apps/api": "1.1.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { - "@tauri-apps/cli": "1.0.5", + "@tauri-apps/cli": "1.1.1", "@tauri-apps/tauricon": "github:tauri-apps/tauricon", "@types/babel-core": "^6.25.7", "@types/byte-size": "^8.1.0", - "@types/react": "^18.0.15", + "@types/react": "^18.0.21", "@types/react-dom": "^18.0.6", "@types/react-router-dom": "^5.3.3", "@types/react-window": "^1.8.5", "@types/tailwindcss": "^3.1.0", - "@vitejs/plugin-react": "^2.0.0", - "concurrently": "^7.3.0", + "@vitejs/plugin-react": "^2.1.0", + "concurrently": "^7.4.0", "prettier": "^2.7.1", - "sass": "^1.54.0", - "typescript": "^4.7.4", - "vite": "^3.0.3", + "sass": "^1.55.0", + "typescript": "^4.8.4", + "vite": "^3.1.4", "vite-plugin-filter-replace": "^0.1.9", "vite-plugin-svgr": "^2.2.1" } diff --git a/apps/desktop/src-tauri/.env.example b/apps/desktop/src-tauri/.env.example deleted file mode 100644 index dcb15beb5..000000000 --- a/apps/desktop/src-tauri/.env.example +++ /dev/null @@ -1 +0,0 @@ -RUST_LOG=spacedrive=debug,sdcore=debug \ No newline at end of file diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 62f0b2a48..c249d17a8 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -10,19 +10,19 @@ edition = "2021" build = "build.rs" [dependencies] -tauri = { version = "1.0.4", features = ["api-all", "macos-private-api"] } -rspc = { version = "0.0.5", features = ["tauri"] } +tauri = { version = "1.1.1", features = ["api-all", "macos-private-api"] } +rspc = { workspace = true, features = ["tauri"] } sd-core = { path = "../../../core", features = ["ffmpeg"] } -tokio = { version = "1.17.0", features = ["sync"] } -window-shadows = "0.1.2" -tracing = "0.1.35" -serde = "1.0.144" +tokio = { version = "1.21.2", features = ["sync"] } +window-shadows = "0.2.0" +tracing = "0.1.36" +serde = "1.0.145" [target.'cfg(target_os = "macos")'.dependencies] swift-rs = { git = "https://github.com/Brendonovich/swift-rs.git", branch = "autorelease" } [build-dependencies] -tauri-build = { version = "1.0.0", features = [] } +tauri-build = { version = "1.1.1", features = [] } [target.'cfg(target_os = "macos")'.build-dependencies] swift-rs = { git = "https://github.com/Brendonovich/swift-rs.git", branch = "autorelease", features = [ diff --git a/apps/desktop/src/index.tsx b/apps/desktop/src/index.tsx index 2e5aa61b0..5bf37ecfb 100644 --- a/apps/desktop/src/index.tsx +++ b/apps/desktop/src/index.tsx @@ -1,6 +1,6 @@ import { createClient } from '@rspc/client'; import { TauriTransport } from '@rspc/tauri'; -import { OperatingSystem, Operations, PlatformProvider, queryClient, rspc } from '@sd/client'; +import { OperatingSystem, PlatformProvider, Procedures, queryClient, rspc } from '@sd/client'; import SpacedriveInterface, { Platform } from '@sd/interface'; import { KeybindEvent } from '@sd/interface'; import { dialog, invoke, os, shell } from '@tauri-apps/api'; @@ -10,7 +10,7 @@ import { createRoot } from 'react-dom/client'; import '@sd/ui/style'; -const client = createClient({ +const client = createClient({ transport: new TauriTransport() }); diff --git a/apps/mobile/android/app/build.gradle b/apps/mobile/android/app/build.gradle index d825f46f4..2b07ca96a 100644 --- a/apps/mobile/android/app/build.gradle +++ b/apps/mobile/android/app/build.gradle @@ -7,7 +7,7 @@ apply plugin: 'org.mozilla.rust-android-gradle.rust-android' cargo { module = "../../rust" - libname = "sdcore" + libname = "sd_core_mobile" // profile = 'release', pythonCommand = 'python3' targets = ["arm", "arm64", "x86", "x86_64"] diff --git a/apps/mobile/android/app/src/main/java/com/spacedrive/app/SDCore.java b/apps/mobile/android/app/src/main/java/com/spacedrive/app/SDCore.java index 782f44a1b..d6c9f011d 100644 --- a/apps/mobile/android/app/src/main/java/com/spacedrive/app/SDCore.java +++ b/apps/mobile/android/app/src/main/java/com/spacedrive/app/SDCore.java @@ -28,7 +28,7 @@ public class SDCore extends ReactContextBaseJavaModule { } static { - System.loadLibrary("sdcore"); + System.loadLibrary("sd_core_mobile"); } // is exposed by Rust and is used to register the subscription @@ -47,6 +47,11 @@ public class SDCore extends ReactContextBaseJavaModule { return getCurrentActivity().getFilesDir().toString(); } + public void print(String msg) + { + System.out.println(msg); + } + @ReactMethod public void addListener(String eventName) { diff --git a/apps/mobile/ios/Podfile.lock b/apps/mobile/ios/Podfile.lock index 5825a73f1..9c1fd60da 100644 --- a/apps/mobile/ios/Podfile.lock +++ b/apps/mobile/ios/Podfile.lock @@ -7,13 +7,13 @@ PODS: - ExpoModulesCore - EXFileSystem (14.1.0): - ExpoModulesCore - - EXFont (10.2.0): + - EXFont (10.2.1): - ExpoModulesCore - - Expo (46.0.10): + - Expo (46.0.15): - ExpoModulesCore - ExpoKeepAwake (10.2.0): - ExpoModulesCore - - ExpoModulesCore (0.11.5): + - ExpoModulesCore (0.11.7): - React-Core - ReactCommon/turbomodule/core - EXSplashScreen (0.16.2): @@ -31,7 +31,7 @@ PODS: - glog (0.3.5) - hermes-engine (0.69.4) - libevent (2.1.12) - - lottie-ios (3.4.2) + - lottie-ios (3.4.3) - lottie-react-native (5.1.4): - lottie-ios (~> 3.4.0) - React-Core @@ -333,7 +333,7 @@ PODS: - React-jsi (= 0.69.4) - React-logger (= 0.69.4) - React-perflogger (= 0.69.4) - - RNCAsyncStorage (1.17.7): + - RNCAsyncStorage (1.17.10): - React-Core - RNCMaskedView (0.2.7): - React-Core @@ -376,14 +376,14 @@ PODS: DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - - "EXApplication (from `../node_modules/.pnpm/expo-application@4.2.2_expo@46.0.10/node_modules/expo-application/ios`)" - - "EXConstants (from `../node_modules/.pnpm/expo-constants@13.2.4_expo@46.0.10/node_modules/expo-constants/ios`)" - - "EXFileSystem (from `../node_modules/.pnpm/expo-file-system@14.1.0_expo@46.0.10/node_modules/expo-file-system/ios`)" - - "EXFont (from `../node_modules/.pnpm/expo-font@10.2.0_expo@46.0.10/node_modules/expo-font/ios`)" - - "Expo (from `../node_modules/.pnpm/expo@46.0.10_@babel+core@7.18.10/node_modules/expo`)" - - "ExpoKeepAwake (from `../node_modules/.pnpm/expo-keep-awake@10.2.0_expo@46.0.10/node_modules/expo-keep-awake/ios`)" - - "ExpoModulesCore (from `../node_modules/.pnpm/expo-modules-core@0.11.5/node_modules/expo-modules-core/ios`)" - - "EXSplashScreen (from `../node_modules/.pnpm/expo-splash-screen@0.16.2_expo@46.0.10/node_modules/expo-splash-screen/ios`)" + - "EXApplication (from `../node_modules/.pnpm/expo-application@4.2.2_expo@46.0.15/node_modules/expo-application/ios`)" + - "EXConstants (from `../node_modules/.pnpm/expo-constants@13.2.4_expo@46.0.15/node_modules/expo-constants/ios`)" + - "EXFileSystem (from `../node_modules/.pnpm/expo-file-system@14.1.0_expo@46.0.15/node_modules/expo-file-system/ios`)" + - "EXFont (from `../node_modules/.pnpm/expo-font@10.2.1_expo@46.0.15/node_modules/expo-font/ios`)" + - "Expo (from `../node_modules/.pnpm/expo@46.0.15_@babel+core@7.19.3/node_modules/expo`)" + - "ExpoKeepAwake (from `../node_modules/.pnpm/expo-keep-awake@10.2.0_expo@46.0.15/node_modules/expo-keep-awake/ios`)" + - "ExpoModulesCore (from `../node_modules/.pnpm/expo-modules-core@0.11.7/node_modules/expo-modules-core/ios`)" + - "EXSplashScreen (from `../node_modules/.pnpm/expo-splash-screen@0.16.2_expo@46.0.15/node_modules/expo-splash-screen/ios`)" - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) - FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`) - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) @@ -439,21 +439,21 @@ EXTERNAL SOURCES: DoubleConversion: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" EXApplication: - :path: "../node_modules/.pnpm/expo-application@4.2.2_expo@46.0.10/node_modules/expo-application/ios" + :path: "../node_modules/.pnpm/expo-application@4.2.2_expo@46.0.15/node_modules/expo-application/ios" EXConstants: - :path: "../node_modules/.pnpm/expo-constants@13.2.4_expo@46.0.10/node_modules/expo-constants/ios" + :path: "../node_modules/.pnpm/expo-constants@13.2.4_expo@46.0.15/node_modules/expo-constants/ios" EXFileSystem: - :path: "../node_modules/.pnpm/expo-file-system@14.1.0_expo@46.0.10/node_modules/expo-file-system/ios" + :path: "../node_modules/.pnpm/expo-file-system@14.1.0_expo@46.0.15/node_modules/expo-file-system/ios" EXFont: - :path: "../node_modules/.pnpm/expo-font@10.2.0_expo@46.0.10/node_modules/expo-font/ios" + :path: "../node_modules/.pnpm/expo-font@10.2.1_expo@46.0.15/node_modules/expo-font/ios" Expo: - :path: "../node_modules/.pnpm/expo@46.0.10_@babel+core@7.18.10/node_modules/expo" + :path: "../node_modules/.pnpm/expo@46.0.15_@babel+core@7.19.3/node_modules/expo" ExpoKeepAwake: - :path: "../node_modules/.pnpm/expo-keep-awake@10.2.0_expo@46.0.10/node_modules/expo-keep-awake/ios" + :path: "../node_modules/.pnpm/expo-keep-awake@10.2.0_expo@46.0.15/node_modules/expo-keep-awake/ios" ExpoModulesCore: - :path: "../node_modules/.pnpm/expo-modules-core@0.11.5/node_modules/expo-modules-core/ios" + :path: "../node_modules/.pnpm/expo-modules-core@0.11.7/node_modules/expo-modules-core/ios" EXSplashScreen: - :path: "../node_modules/.pnpm/expo-splash-screen@0.16.2_expo@46.0.10/node_modules/expo-splash-screen/ios" + :path: "../node_modules/.pnpm/expo-splash-screen@0.16.2_expo@46.0.15/node_modules/expo-splash-screen/ios" FBLazyVector: :path: "../node_modules/react-native/Libraries/FBLazyVector" FBReactNativeSpec: @@ -541,10 +541,10 @@ SPEC CHECKSUMS: EXApplication: e418d737a036e788510f2c4ad6c10a7d54d18586 EXConstants: 7c44785d41d8e959d527d23d29444277a4d1ee73 EXFileSystem: 927e0a8885aa9c49e50fc38eaba2c2389f2f1019 - EXFont: a5d80bd9b3452b2d5abbce2487da89b0150e6487 - Expo: fcdb32274e2ca9c7638d3b21b30fb665c6869219 + EXFont: 06df627203afcb8a3b3152ec06eb2f11f46f0cff + Expo: 7e821e708a35d2720ef6baa658e224dd91c4821e ExpoKeepAwake: 0e8f18142e71bbf2c7f6aa66ebed249ba1420320 - ExpoModulesCore: 5a973701f4400d70254bc836305228731c829010 + ExpoModulesCore: 2d60ec04c49641afb55fee3faac86fb108c68fe0 EXSplashScreen: 799bece80089219b2c989c1082d70f3b00995cda FBLazyVector: c71b8c429a8af2aff1013934a7152e9d9d0c937d FBReactNativeSpec: 3cc5cff7d792e74a875be91e56d6242335016f50 @@ -552,7 +552,7 @@ SPEC CHECKSUMS: glog: 3d02b25ca00c2d456734d0bcff864cbc62f6ae1a hermes-engine: 761a544537e62df2a37189389b9d2654dc1f75af libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 - lottie-ios: 6bbc53eef6957e4744a50321507015fba72d8ca6 + lottie-ios: 9ae750cdc7820fecbd3c2f0cfc493038208fcdc4 lottie-react-native: b702fab740cdb952a8e2354713d3beda63ff97b0 RCT-Folly: b9d9fe1fc70114b751c076104e52f3b1b5e5a95a RCTRequired: bd9d2ab0fda10171fcbcf9ba61a7df4dc15a28f4 @@ -582,7 +582,7 @@ SPEC CHECKSUMS: React-RCTVibration: 9adb4a3cbb598d1bbd46a05256f445e4b8c70603 React-runtimeexecutor: 61ee22a8cdf8b6bb2a7fb7b4ba2cc763e5285196 ReactCommon: 8f67bd7e0a6afade0f20718f859dc8c2275f2e83 - RNCAsyncStorage: d81ee5c3db1060afd49ea7045ad460eff82d2b7d + RNCAsyncStorage: 0c357f3156fcb16c8589ede67cc036330b6698ca RNCMaskedView: cb9670ea9239998340eaab21df13fa12a1f9de15 RNGestureHandler: bad495418bcbd3ab47017a38d93d290ebd406f50 RNReanimated: 7faa787e8d4493fbc95fab2ad331fa7625828cfa diff --git a/apps/mobile/ios/Spacedrive.xcodeproj/project.pbxproj b/apps/mobile/ios/Spacedrive.xcodeproj/project.pbxproj index 003b6e4b7..ecaea1a36 100644 --- a/apps/mobile/ios/Spacedrive.xcodeproj/project.pbxproj +++ b/apps/mobile/ios/Spacedrive.xcodeproj/project.pbxproj @@ -366,6 +366,7 @@ "@executable_path/Frameworks", ); LIBRARY_SEARCH_PATHS = ( + "$(SDKROOT)/usr/lib/swift", "$(inherited)", "\"${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}\"", "\"${PODS_CONFIGURATION_BUILD_DIR}/DoubleConversion\"", @@ -455,6 +456,7 @@ "@executable_path/Frameworks", ); LIBRARY_SEARCH_PATHS = ( + "$(SDKROOT)/usr/lib/swift", "$(inherited)", "\"${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}\"", "\"${PODS_CONFIGURATION_BUILD_DIR}/DoubleConversion\"", @@ -579,7 +581,7 @@ /usr/lib/swift, "$(inherited)", ); - LIBRARY_SEARCH_PATHS = "\"$(inherited)\""; + LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; @@ -633,7 +635,7 @@ /usr/lib/swift, "$(inherited)", ); - LIBRARY_SEARCH_PATHS = "\"$(inherited)\""; + LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; MTL_ENABLE_DEBUG_INFO = NO; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; diff --git a/apps/mobile/package.json b/apps/mobile/package.json index f1c15ee93..49b065fb3 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -17,8 +17,8 @@ "@react-navigation/drawer": "^6.4.4", "@react-navigation/native": "^6.0.12", "@react-navigation/stack": "^6.2.3", - "@rspc/client": "^0.0.6", - "@rspc/react": "^0.0.6", + "@rspc/client": "^0.1.2", + "@rspc/react": "^0.1.2", "@sd/assets": "file:../../packages/assets", "@tanstack/react-query": "^4.2.3", "byte-size": "^8.1.0", diff --git a/apps/mobile/pnpm-lock.yaml b/apps/mobile/pnpm-lock.yaml index 84ec59b3d..f635ed63a 100644 Binary files a/apps/mobile/pnpm-lock.yaml and b/apps/mobile/pnpm-lock.yaml differ diff --git a/apps/mobile/rust/Cargo.toml b/apps/mobile/rust/Cargo.toml index dde522bbe..06eefe59e 100644 --- a/apps/mobile/rust/Cargo.toml +++ b/apps/mobile/rust/Cargo.toml @@ -8,18 +8,18 @@ rust-version = "1.64.0" crate-type = ["staticlib", "cdylib"] # staticlib for IOS and cdylib for Android [dependencies] -once_cell = "1.13.0" +once_cell = "1.15.0" sd-core = { path = "../../../core", features = [ "mobile", "p2p", ], default-features = false } -rspc = { version = "0.0.5", features = [] } -serde_json = "1.0.83" -tokio = "1.20.1" -openssl = { version = "0.10.41", features = [ +rspc = { workspace = true } +serde_json = "1.0.85" +tokio = "1.21.2" +openssl = { version = "0.10.42", features = [ "vendored", ] } # Override features of transitive dependencies -openssl-sys = { version = "0.9.75", features = [ +openssl-sys = { version = "0.9.76", features = [ "vendored", ] } # Override features of transitive dependencies to support IOS Simulator on M1 diff --git a/apps/mobile/rust/src/android.rs b/apps/mobile/rust/src/android.rs index e7af9be6f..ed93a44de 100644 --- a/apps/mobile/rust/src/android.rs +++ b/apps/mobile/rust/src/android.rs @@ -1,43 +1,72 @@ -use crate::{CLIENT_CONTEXT, EVENT_SENDER, NODE, RUNTIME}; -use jni::objects::{JClass, JObject, JString}; -use jni::JNIEnv; -use rspc::Request; +use std::panic; + +use crate::{EVENT_SENDER, NODE, RUNTIME, SUBSCRIPTIONS}; +use jni::objects::{GlobalRef, JClass, JObject, JString}; +use jni::{JNIEnv, JavaVM}; +use rspc::internal::jsonrpc::{handle_json_rpc, Request, Sender, SubscriptionMap}; use sd_core::Node; use tokio::sync::mpsc::unbounded_channel; +// fn print(jvm: &JavaVM, class: &GlobalRef, msg: &str) { +// let env = jvm.attach_current_thread().unwrap(); +// env.call_method( +// class, +// "print", +// "(Ljava/lang/String;)V", +// &[env +// .new_string(msg) +// .expect("Couldn't create java string!") +// .into()], +// ) +// .unwrap() +// .l() +// .unwrap(); +// } + #[no_mangle] pub extern "system" fn Java_com_spacedrive_app_SDCore_registerCoreEventListener( env: JNIEnv, class: JClass, ) { - let jvm = env.get_java_vm().unwrap(); - let class = env.new_global_ref(class).unwrap(); - let (tx, mut rx) = unbounded_channel(); - let _ = EVENT_SENDER.set(tx); + let result = panic::catch_unwind(|| { + let jvm = env.get_java_vm().unwrap(); + let class = env.new_global_ref(class).unwrap(); + let (tx, mut rx) = unbounded_channel(); + let _ = EVENT_SENDER.set(tx); - RUNTIME.spawn(async move { - while let Some(event) = rx.recv().await { - let data = match serde_json::to_string(&event) { - Ok(json) => json, - Err(err) => { - println!("Failed to serialize event: {}", err); - continue; - } - }; + RUNTIME.spawn(async move { + while let Some(event) = rx.recv().await { + let data = match serde_json::to_string(&event) { + Ok(json) => json, + Err(err) => { + println!("Failed to serialize event: {}", err); + continue; + } + }; - let env = jvm.attach_current_thread().unwrap(); - env.call_method( - &class, - "sendCoreEvent", - "(Ljava/lang/String;)V", - &[env - .new_string(data) - .expect("Couldn't create java string!") - .into()], - ) - .unwrap(); - } + let env = jvm.attach_current_thread().unwrap(); + env.call_method( + &class, + "sendCoreEvent", + "(Ljava/lang/String;)V", + &[env + .new_string(data) + .expect("Couldn't create java string!") + .into()], + ) + .unwrap(); + } + }); }); + + if let Err(err) = result { + // TODO: Send rspc error or something here so we can show this in the UI. + // TODO: Maybe reinitialise the core cause it could be in an invalid state? + println!( + "Error in Java_com_spacedrive_app_SDCore_registerCoreEventListener: {:?}", + err + ); + } } #[no_mangle] @@ -47,60 +76,75 @@ pub extern "system" fn Java_com_spacedrive_app_SDCore_handleCoreMsg( query: JString, callback: JObject, ) { - let jvm = env.get_java_vm().unwrap(); - let query: String = env - .get_string(query) - .expect("Couldn't get java string!") - .into(); - let class = env.new_global_ref(class).unwrap(); - let callback = env.new_global_ref(callback).unwrap(); + let result = panic::catch_unwind(|| { + let jvm = env.get_java_vm().unwrap(); + let query: String = env + .get_string(query) + .expect("Couldn't get java string!") + .into(); + let class = env.new_global_ref(class).unwrap(); + let callback = env.new_global_ref(callback).unwrap(); - RUNTIME.spawn(async move { - let request: Request = serde_json::from_str(&query).unwrap(); + RUNTIME.spawn(async move { + let request: Request = serde_json::from_str(&query).unwrap(); - let node = &mut *NODE.lock().await; - let (node, router) = match node { - Some(node) => node.clone(), - None => { - let data_dir: String = { + let node = &mut *NODE.lock().await; + let (node, router) = match node { + Some(node) => node.clone(), + None => { + let data_dir: String = { + let env = jvm.attach_current_thread().unwrap(); + let data_dir = env + .call_method(&class, "getDataDirectory", "()Ljava/lang/String;", &[]) + .unwrap() + .l() + .unwrap(); + + env.get_string(data_dir.into()).unwrap().into() + }; + + let new_node = Node::new(data_dir).await.unwrap(); + node.replace(new_node.clone()); + new_node + } + }; + + let mut channel = EVENT_SENDER.get().unwrap().clone(); + let mut resp = Sender::ResponseAndChannel(None, &mut channel); + handle_json_rpc( + node.get_request_context(), + request, + &router, + &mut resp, + &mut SubscriptionMap::Mutex(&SUBSCRIPTIONS), + ) + .await; + + match resp { + Sender::Response(Some(resp)) => { let env = jvm.attach_current_thread().unwrap(); - let data_dir = env - .call_method(&class, "getDataDirectory", "()Ljava/lang/String;", &[]) - .unwrap() - .l() - .unwrap(); - - env.get_string(data_dir.into()).unwrap().into() - }; - - let new_node = Node::new(data_dir).await.expect("Unable to create node"); - node.replace(new_node.clone()); - new_node + env.call_method( + &callback, + "resolve", + "(Ljava/lang/Object;)V", + &[env + .new_string(serde_json::to_string(&resp).unwrap()) + .expect("Couldn't create java string!") + .into()], + ) + .unwrap(); + } + _ => unreachable!(), } - }; - - let resp = serde_json::to_string( - &request - .handle( - node.get_request_context(), - &router, - &CLIENT_CONTEXT, - EVENT_SENDER.get(), - ) - .await, - ) - .unwrap(); - - let env = jvm.attach_current_thread().unwrap(); - env.call_method( - &callback, - "resolve", - "(Ljava/lang/Object;)V", - &[env - .new_string(resp) - .expect("Couldn't create java string!") - .into()], - ) - .unwrap(); + }); }); + + if let Err(err) = result { + // TODO: Send rspc error or something here so we can show this in the UI. + // TODO: Maybe reinitialise the core cause it could be in an invalid state? + println!( + "Error in Java_com_spacedrive_app_SDCore_registerCoreEventListener: {:?}", + err + ); + } } diff --git a/apps/mobile/rust/src/ios.rs b/apps/mobile/rust/src/ios.rs index 19d26d8eb..a21ead310 100644 --- a/apps/mobile/rust/src/ios.rs +++ b/apps/mobile/rust/src/ios.rs @@ -1,16 +1,16 @@ -use crate::{CLIENT_CONTEXT, EVENT_SENDER, NODE, RUNTIME}; +use crate::{EVENT_SENDER, NODE, RUNTIME, SUBSCRIPTIONS}; +use objc::{msg_send, runtime::Object, sel, sel_impl}; +use objc_foundation::{INSString, NSString}; +use objc_id::Id; +use rspc::internal::jsonrpc::{handle_json_rpc, Request, Sender, SubscriptionMap}; +use sd_core::Node; use std::{ ffi::{CStr, CString}, os::raw::{c_char, c_void}, + panic, }; use tokio::sync::mpsc::unbounded_channel; -use objc::{class, msg_send, runtime::Object, sel, sel_impl}; -use objc_foundation::{INSString, NSString}; -use objc_id::Id; -use rspc::Request; -use sd_core::Node; - extern "C" { fn get_data_directory() -> *const c_char; fn call_resolve(resolve: *const c_void, result: *const c_char); @@ -33,64 +33,81 @@ impl RNPromise { #[no_mangle] pub unsafe extern "C" fn register_core_event_listener(id: *mut Object) { - let id = Id::::from_ptr(id); + let result = panic::catch_unwind(|| { + let id = Id::::from_ptr(id); - let (tx, mut rx) = unbounded_channel(); - let _ = EVENT_SENDER.set(tx); + let (tx, mut rx) = unbounded_channel(); + let _ = EVENT_SENDER.set(tx); - RUNTIME.spawn(async move { - while let Some(event) = rx.recv().await { - let data = match serde_json::to_string(&event) { - Ok(json) => json, - Err(err) => { - println!("Failed to serialize event: {}", err); - continue; - } - }; - let data = NSString::from_str(&data); - let _: () = msg_send![id, sendCoreEvent: data]; - } + RUNTIME.spawn(async move { + while let Some(event) = rx.recv().await { + let data = match serde_json::to_string(&event) { + Ok(json) => json, + Err(err) => { + println!("Failed to serialize event: {}", err); + continue; + } + }; + let data = NSString::from_str(&data); + let _: () = msg_send![id, sendCoreEvent: data]; + } + }); }); + + if let Err(err) = result { + // TODO: Send rspc error or something here so we can show this in the UI. + // TODO: Maybe reinitialise the core cause it could be in an invalid state? + println!("Error in register_core_event_listener: {:?}", err); + } } #[no_mangle] pub unsafe extern "C" fn sd_core_msg(query: *const c_char, resolve: *const c_void) { - // This string is cloned to the Rust heap. This is important as Objective-C may remove the query once this function completions but prior to the async block finishing. - let query = CStr::from_ptr(query).to_str().unwrap().to_string(); + let result = panic::catch_unwind(|| { + // This string is cloned to the Rust heap. This is important as Objective-C may remove the query once this function completions but prior to the async block finishing. + let query = CStr::from_ptr(query).to_str().unwrap().to_string(); - let resolve = RNPromise(resolve); - RUNTIME.spawn(async move { - let request: Request = serde_json::from_str(&query).unwrap(); + let resolve = RNPromise(resolve); + RUNTIME.spawn(async move { + let request: Request = serde_json::from_str(&query).unwrap(); - let node = &mut *NODE.lock().await; - let (node, router) = match node { - Some(node) => node.clone(), - None => { - let doc_dir = CStr::from_ptr(get_data_directory()) - .to_str() - .unwrap() - .to_string(); - let new_node = Node::new(doc_dir).await.unwrap(); - node.replace(new_node.clone()); - new_node - } - }; + let node = &mut *NODE.lock().await; + let (node, router) = match node { + Some(node) => node.clone(), + None => { + let doc_dir = CStr::from_ptr(get_data_directory()) + .to_str() + .unwrap() + .to_string(); + let new_node = Node::new(doc_dir).await.unwrap(); + node.replace(new_node.clone()); + new_node + } + }; - resolve.resolve( - CString::new( - serde_json::to_vec( - &request - .handle( - node.get_request_context(), - &router, - &CLIENT_CONTEXT, - EVENT_SENDER.get(), - ) - .await, - ) - .unwrap(), + let mut channel = EVENT_SENDER.get().unwrap().clone(); + let mut resp = Sender::ResponseAndChannel(None, &mut channel); + handle_json_rpc( + node.get_request_context(), + request, + &router, + &mut resp, + &mut SubscriptionMap::Mutex(&SUBSCRIPTIONS), ) - .unwrap(), - ) + .await; + + match resp { + Sender::ResponseAndChannel(Some(resp), _) => { + resolve.resolve(CString::new(serde_json::to_vec(&resp).unwrap()).unwrap()); + } + _ => unreachable!(), + } + }); }); + + if let Err(err) = result { + // TODO: Send rspc error or something here so we can show this in the UI. + // TODO: Maybe reinitialise the core cause it could be in an invalid state? + println!("Error in sd_core_msg: {:?}", err); + } } diff --git a/apps/mobile/rust/src/lib.rs b/apps/mobile/rust/src/lib.rs index fb837282c..b887d887e 100644 --- a/apps/mobile/rust/src/lib.rs +++ b/apps/mobile/rust/src/lib.rs @@ -1,24 +1,24 @@ -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; use once_cell::sync::{Lazy, OnceCell}; -use rspc::{ClientContext, Response}; +use rspc::internal::jsonrpc::{RequestId, Response}; use sd_core::{api::Router, Node}; use tokio::{ runtime::Runtime, - sync::{mpsc::UnboundedSender, Mutex}, + sync::{mpsc::UnboundedSender, oneshot, Mutex}, }; #[allow(dead_code)] pub(crate) static RUNTIME: Lazy = Lazy::new(|| Runtime::new().unwrap()); -type LazyNode = Lazy, Arc)>>>; -#[allow(dead_code)] -pub(crate) static NODE: LazyNode = Lazy::new(|| Mutex::new(None)); +type NodeType = Lazy, Arc)>>>; #[allow(dead_code)] -pub(crate) static CLIENT_CONTEXT: Lazy = Lazy::new(|| ClientContext { - subscriptions: Default::default(), -}); +pub(crate) static NODE: NodeType = Lazy::new(|| Mutex::new(None)); + +#[allow(dead_code)] +pub(crate) static SUBSCRIPTIONS: Lazy>>> = + Lazy::new(Default::default); #[allow(dead_code)] pub(crate) static EVENT_SENDER: OnceCell> = OnceCell::new(); diff --git a/apps/mobile/src/App.tsx b/apps/mobile/src/App.tsx index 08ecaa158..7dfa19e47 100644 --- a/apps/mobile/src/App.tsx +++ b/apps/mobile/src/App.tsx @@ -22,9 +22,9 @@ import RootNavigator from './navigation'; import OnboardingNavigator from './navigation/OnboardingNavigator'; import { libraryStore } from './stores/libraryStore'; import { onboardingStore } from './stores/onboardingStore'; -import type { Operations } from './types/bindings'; +import type { Procedures } from './types/bindings'; -const client = createClient({ +const client = createClient({ transport: new ReactNativeTransport() }); diff --git a/apps/mobile/src/components/modals/FileModal.tsx b/apps/mobile/src/components/modals/FileModal.tsx index f46017c73..35d4434d0 100644 --- a/apps/mobile/src/components/modals/FileModal.tsx +++ b/apps/mobile/src/components/modals/FileModal.tsx @@ -1,5 +1,5 @@ import { BottomSheetModal, BottomSheetScrollView } from '@gorhom/bottom-sheet'; -import { format } from 'date-fns'; +import dayjs from 'dayjs'; import React, { useRef } from 'react'; import { Button, Pressable, Text, View } from 'react-native'; import { ChevronLeftIcon } from 'react-native-heroicons/outline'; @@ -98,12 +98,12 @@ export const FileModal = () => { diff --git a/apps/mobile/src/hooks/rspc.ts b/apps/mobile/src/hooks/rspc.ts index 0df3939c0..774f03b7c 100644 --- a/apps/mobile/src/hooks/rspc.ts +++ b/apps/mobile/src/hooks/rspc.ts @@ -1,144 +1,109 @@ -import { ClientTransformer, OperationKey, OperationType, RSPCError, Transport } from '@rspc/client'; +import { OperationType, ProcedureDef, RSPCError, Transport } from '@rspc/client'; import { createReactQueryHooks } from '@rspc/react'; -import { - QueryClient, - UseMutationOptions, - UseMutationResult, - UseQueryOptions, - UseQueryResult, - useMutation as _useMutation -} from '@tanstack/react-query'; +import { QueryClient } from '@tanstack/react-query'; import { NativeEventEmitter, NativeModules } from 'react-native'; -import { useSnapshot } from 'valtio'; -import { libraryStore } from '../stores/libraryStore'; -import type { LibraryArgs, Operations } from '../types/bindings'; +import { getLibraryIdRaw } from '../stores/libraryStore'; +import { LibraryArgs, Procedures } from '../types/bindings'; export const queryClient = new QueryClient(); -export const rspc = createReactQueryHooks(); +export const rspc = createReactQueryHooks(); const { SDCore } = NativeModules; const eventEmitter = new NativeEventEmitter(NativeModules.SDCore); // TODO(@Oscar): Replace this with a better abstraction when it's released in rspc. This relies on internal details of rspc which will change without warning. export class ReactNativeTransport implements Transport { - transformer?: ClientTransformer; - clientSubscriptionCallback?: (id: string, key: string, value: any) => void; + clientSubscriptionCallback?: (id: string, value: any) => void; constructor() { const subscriptionEventListener = eventEmitter.addListener('SDCoreEvent', (event) => { - const body = JSON.parse(event); - if (body.type === 'event') { - const { id, key, result } = body; - this.clientSubscriptionCallback(id, key, result); - } else if (body.type === 'response' || body.type === 'error') { + const { id, result } = JSON.parse(event); + if (result.type === 'event') { + if (this.clientSubscriptionCallback) this.clientSubscriptionCallback(id, result.data); + } else if (result.type === 'response' || result.type === 'error') { throw new Error( - `Recieved event of type '${body.type}'. This should be impossible with the React Native transport!` + `Recieved event of type '${result.type}'. This should be impossible with the React Native transport!` ); } else { - console.error(`Received event of unknown method '${body.type}'`); + console.error(`Received event of unknown method '${result.type}'`); } }); } - async doRequest(operation: OperationType, key: OperationKey): Promise { - const body = JSON.parse( + async doRequest(operation: OperationType, key: string, input: any): Promise { + const resp = JSON.parse( await SDCore.sd_core_msg( JSON.stringify({ - operation, - key: this.transformer?.serialize(operation, key) || key + id: null, + method: operation, + params: { + path: key, + input + } }) ) ); + + const body = resp.result; if (body.type === 'error') { - const { status_code, message } = body; - throw new RSPCError(status_code, message); + const { code, message } = body; + throw new RSPCError(code, message); } else if (body.type === 'response') { - return this.transformer?.deserialize(operation, key, body.result) || body.result; + return body.data; } else if (body.type !== 'none') { throw new Error(`RSPC ReactNative doRequest received invalid body type '${body?.type}'`); } } } -type NonLibraryQueries = Exclude] }> & - Extract; -type NonLibraryQuery = Extract; -type NonLibraryQueryKey = NonLibraryQueries['key'][0]; -type NonLibraryQueryResult = NonLibraryQuery['result']; +type NonLibraryProcedure = + | Exclude }> + | Extract; -export function useBridgeQuery( - key: K, - options?: UseQueryOptions, RSPCError> -): UseQueryResult, RSPCError> { - // @ts-ignore - return rspc.useQuery(key, options); -} - -type LibraryQueries = Extract] }>; -type LibraryQuery = Extract; -type LibraryQueryKey = LibraryQueries['key'][0]; -type LibraryQueryArgs = LibraryQuery['key'][1] extends LibraryArgs - ? A - : never; -type LibraryQueryResult = LibraryQuery['result']; - -export function useLibraryQuery( - key: LibraryQueryArgs extends null | undefined ? [K] : [K, LibraryQueryArgs], - options?: UseQueryOptions, RSPCError> -): UseQueryResult, RSPCError> { - const store = useSnapshot(libraryStore); - const library_id = store.currentLibraryUuid; - if (!library_id) throw new Error(`Attempted to do library query with no library set!`); - // @ts-ignore - return rspc.useQuery([key[0], { library_id: library_id || '', arg: key[1] || null }], options); -} - -type LibraryMutations = Extract] }>; -type LibraryMutation = Extract; -type LibraryMutationKey = LibraryMutations['key'][0]; -type LibraryMutationArgs = - LibraryMutation['key'][1] extends LibraryArgs ? A : never; -type LibraryMutationResult = LibraryMutation['result']; -export function useLibraryMutation( - key: K, - options?: UseMutationOptions, RSPCError> -) { - const ctx = rspc.useContext(); - const store = useSnapshot(libraryStore); - const library_id = store.currentLibraryUuid; - if (!library_id) throw new Error(`Attempted to do library query with no library set!`); - - // @ts-ignore - return _useMutation, RSPCError, LibraryMutationArgs>( - async (data) => ctx.client.mutation([key, { library_id: library_id || '', arg: data || null }]), - { - ...options, - context: rspc.ReactQueryContext - } - ); -} - -type NonLibraryMutations = Exclude] }>; -type NonLibraryMutation = Extract< - NonLibraryMutations, - { key: [K] | [K, any] } +type LibraryProcedures = Exclude< + Extract }>, + { input: never } >; -type NonLibraryMutationKey = NonLibraryMutations['key'][0]; -type NonLibraryMutationArgs = NonLibraryMutation['key'][1]; -type NonLibraryMutationResult = NonLibraryMutation['result']; -export function useBridgeMutation( - key: K, - options?: UseMutationOptions, RSPCError> -): UseMutationResult, RSPCError, NonLibraryMutationArgs> { - // @ts-ignore - return rspc.useMutation(key, options); -} + +type MoreConstrainedQueries = T extends any + ? T['input'] extends LibraryArgs + ? { + key: T['key']; + input: E; + result: T['result']; + } + : never + : never; + +export const useBridgeQuery = rspc.customQuery>( + (keyAndInput) => keyAndInput as any +); + +export const useBridgeMutation = rspc.customMutation>( + (keyAndInput) => keyAndInput +); + +export const useLibraryQuery = rspc.customQuery< + MoreConstrainedQueries> +>((keyAndInput) => { + const library_id = getLibraryIdRaw(); + if (library_id === null) throw new Error('Attempted to do library query with no library set!'); + return [keyAndInput[0], { library_id, arg: keyAndInput[1] || null }]; +}); + +export const useLibraryMutation = rspc.customMutation< + MoreConstrainedQueries> +>((keyAndInput) => { + const library_id = getLibraryIdRaw(); + if (library_id === null) throw new Error('Attempted to do library query with no library set!'); + return [keyAndInput[0], { library_id, arg: keyAndInput[1] || null }]; +}); export function useInvalidateQuery() { const context = rspc.useContext(); rspc.useSubscription(['invalidateQuery'], { - onNext: (invalidateOperation) => { + onData: (invalidateOperation) => { const key = [invalidateOperation.key]; if (invalidateOperation.arg !== null) { key.concat(invalidateOperation.arg); diff --git a/apps/mobile/src/stores/libraryStore.ts b/apps/mobile/src/stores/libraryStore.ts index 73cf04544..20f6197c8 100644 --- a/apps/mobile/src/stores/libraryStore.ts +++ b/apps/mobile/src/stores/libraryStore.ts @@ -27,6 +27,10 @@ export const libraryStore = proxyWithPersist({ getStorage: () => StorageEngine }); +export function getLibraryIdRaw(): string | null { + return libraryStore.currentLibraryUuid; +} + // this must be used at least once in the app to correct the initial state // is memorized and can be used safely in any component export const useCurrentLibrary = () => { diff --git a/apps/mobile/src/types/bindings.ts b/apps/mobile/src/types/bindings.ts index 454d21f64..5ad4ffda2 100644 --- a/apps/mobile/src/types/bindings.ts +++ b/apps/mobile/src/types/bindings.ts @@ -1,48 +1,48 @@ /* eslint-disable */ // This file was generated by [rspc](https://github.com/oscartbeaumont/rspc). Do not edit this file manually. -export type Operations = { +export type Procedures = { queries: - { key: ["files.readMetadata", LibraryArgs], result: null } | - { key: ["getNode"], result: NodeState } | - { key: ["jobs.getHistory", LibraryArgs], result: Array } | - { key: ["jobs.getRunning", LibraryArgs], result: Array } | - { key: ["library.getStatistics", LibraryArgs], result: Statistics } | - { key: ["library.list"], result: Array } | - { key: ["locations.getById", LibraryArgs], result: Location | null } | - { key: ["locations.getExplorerData", LibraryArgs], result: ExplorerData } | - { key: ["locations.indexer_rules.get", LibraryArgs], result: IndexerRule } | - { key: ["locations.indexer_rules.list", LibraryArgs], result: Array } | - { key: ["locations.list", LibraryArgs], result: Array<{ id: number, pub_id: Array, node_id: number, name: string | null, local_path: string | null, total_capacity: number | null, available_capacity: number | null, filesystem: string | null, disk_type: number | null, is_removable: boolean | null, is_online: boolean, is_archived: boolean, date_created: string, node: Node }> } | - { key: ["tags.get", LibraryArgs], result: Tag | null } | - { key: ["tags.getExplorerData", LibraryArgs], result: ExplorerData } | - { key: ["tags.getForFile", LibraryArgs], result: Array } | - { key: ["tags.list", LibraryArgs], result: Array } | - { key: ["version"], result: string } | - { key: ["volumes.list"], result: Array }, + { key: "files.readMetadata", input: LibraryArgs, result: null } | + { key: "getNode", input: never, result: NodeState } | + { key: "jobs.getHistory", input: LibraryArgs, result: Array } | + { key: "jobs.getRunning", input: LibraryArgs, result: Array } | + { key: "library.getStatistics", input: LibraryArgs, result: Statistics } | + { key: "library.list", input: never, result: Array } | + { key: "locations.getById", input: LibraryArgs, result: Location | null } | + { key: "locations.getExplorerData", input: LibraryArgs, result: ExplorerData } | + { key: "locations.indexer_rules.get", input: LibraryArgs, result: IndexerRule } | + { key: "locations.indexer_rules.list", input: LibraryArgs, result: Array } | + { key: "locations.list", input: LibraryArgs, result: Array<{ id: number, pub_id: Array, node_id: number, name: string | null, local_path: string | null, total_capacity: number | null, available_capacity: number | null, filesystem: string | null, disk_type: number | null, is_removable: boolean | null, is_online: boolean, is_archived: boolean, date_created: string, node: Node }> } | + { key: "tags.get", input: LibraryArgs, result: Tag | null } | + { key: "tags.getExplorerData", input: LibraryArgs, result: ExplorerData } | + { key: "tags.getForObject", input: LibraryArgs, result: Array } | + { key: "tags.list", input: LibraryArgs, result: Array } | + { key: "version", input: never, result: string } | + { key: "volumes.list", input: never, result: Array }, mutations: - { key: ["files.delete", LibraryArgs], result: null } | - { key: ["files.setFavorite", LibraryArgs], result: null } | - { key: ["files.setNote", LibraryArgs], result: null } | - { key: ["jobs.generateThumbsForLocation", LibraryArgs], result: null } | - { key: ["jobs.identifyUniqueFiles", LibraryArgs], result: null } | - { key: ["library.create", string], result: LibraryConfigWrapped } | - { key: ["library.delete", string], result: null } | - { key: ["library.edit", EditLibraryArgs], result: null } | - { key: ["locations.create", LibraryArgs], result: null } | - { key: ["locations.delete", LibraryArgs], result: null } | - { key: ["locations.fullRescan", LibraryArgs], result: null } | - { key: ["locations.indexer_rules.create", LibraryArgs], result: IndexerRule } | - { key: ["locations.indexer_rules.delete", LibraryArgs], result: null } | - { key: ["locations.quickRescan", LibraryArgs], result: null } | - { key: ["locations.update", LibraryArgs], result: null } | - { key: ["tags.assign", LibraryArgs], result: null } | - { key: ["tags.create", LibraryArgs], result: Tag } | - { key: ["tags.delete", LibraryArgs], result: null } | - { key: ["tags.update", LibraryArgs], result: null }, + { key: "files.delete", input: LibraryArgs, result: null } | + { key: "files.setFavorite", input: LibraryArgs, result: null } | + { key: "files.setNote", input: LibraryArgs, result: null } | + { key: "jobs.generateThumbsForLocation", input: LibraryArgs, result: null } | + { key: "jobs.identifyUniqueFiles", input: LibraryArgs, result: null } | + { key: "library.create", input: string, result: LibraryConfigWrapped } | + { key: "library.delete", input: string, result: null } | + { key: "library.edit", input: EditLibraryArgs, result: null } | + { key: "locations.create", input: LibraryArgs, result: null } | + { key: "locations.delete", input: LibraryArgs, result: null } | + { key: "locations.fullRescan", input: LibraryArgs, result: null } | + { key: "locations.indexer_rules.create", input: LibraryArgs, result: IndexerRule } | + { key: "locations.indexer_rules.delete", input: LibraryArgs, result: null } | + { key: "locations.quickRescan", input: LibraryArgs, result: null } | + { key: "locations.update", input: LibraryArgs, result: null } | + { key: "tags.assign", input: LibraryArgs, result: null } | + { key: "tags.create", input: LibraryArgs, result: Tag } | + { key: "tags.delete", input: LibraryArgs, result: null } | + { key: "tags.update", input: LibraryArgs, result: null }, subscriptions: - { key: ["invalidateQuery"], result: InvalidateOperationEvent } | - { key: ["jobs.newThumbnail", LibraryArgs], result: string } + { key: "invalidateQuery", input: never, result: InvalidateOperationEvent } | + { key: "jobs.newThumbnail", input: LibraryArgs, result: string } }; export interface ConfigMetadata { version: string | null } diff --git a/apps/server/Cargo.toml b/apps/server/Cargo.toml index 1344cc92d..115adbfa4 100644 --- a/apps/server/Cargo.toml +++ b/apps/server/Cargo.toml @@ -5,8 +5,8 @@ edition = "2021" [dependencies] sd-core = { path = "../../core", features = [] } -rspc = { version = "0.0.5", features = ["axum"] } -axum = "0.5.13" -tokio = { version = "1.17.0", features = ["sync", "rt-multi-thread", "signal"] } -tracing = "0.1.35" -ctrlc = "3.2.2" +rspc = { workspace = true, features = ["axum"] } +axum = "0.5.16" +tokio = { version = "1.21.2", features = ["sync", "rt-multi-thread", "signal"] } +tracing = "0.1.36" +ctrlc = "3.2.3" diff --git a/apps/server/src/main.rs b/apps/server/src/main.rs index d4063c43d..5b19a4d7a 100644 --- a/apps/server/src/main.rs +++ b/apps/server/src/main.rs @@ -55,8 +55,8 @@ async fn main() { }) }) .route( - "/rspcws", - router.axum_ws_handler(move || node.get_request_context()), + "/rspc/:id", + router.endpoint(move || node.get_request_context()).axum(), ) .fallback((|| async { "404 Not Found: We're past the event horizon..." }).into_service()); diff --git a/apps/web/package.json b/apps/web/package.json index 67d29d392..15588492d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -8,25 +8,27 @@ "preview": "vite preview" }, "dependencies": { - "@fontsource/inter": "^4.5.11", - "@rspc/client": "^0.0.6", + "@fontsource/inter": "^4.5.13", + "@rspc/client": "^0.1.2", "@sd/client": "workspace:*", "@sd/interface": "workspace:*", "@sd/ui": "workspace:*", - "@tanstack/react-query": "^4.0.10", + "@tanstack/react-query": "^4.10.1", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { - "@types/react": "^18.0.15", + "@types/react": "^18.0.21", "@types/react-dom": "^18.0.6", - "@vitejs/plugin-react": "^2.0.0", - "autoprefixer": "^10.4.7", - "postcss": "^8.4.14", + "@vitejs/plugin-react": "^2.1.0", + "autoprefixer": "^10.4.12", + "postcss": "^8.4.17", + "rollup-plugin-visualizer": "^5.8.2", "tailwind": "^4.0.0", - "typescript": "^4.7.4", - "vite": "^3.0.3", + "typescript": "^4.8.4", + "vite": "^3.1.4", + "vite-plugin-html": "^3.2.0", "vite-plugin-svgr": "^2.2.1", - "vite-plugin-tsconfig-paths": "^1.1.0" + "vite-plugin-tsconfig-paths": "^1.2.0" } } diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 52090595a..f944d8a85 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,11 +1,11 @@ import { WebsocketTransport, createClient } from '@rspc/client'; -import { Operations, PlatformProvider, queryClient, rspc } from '@sd/client'; +import { PlatformProvider, Procedures, queryClient, rspc } from '@sd/client'; import SpacedriveInterface, { Platform } from '@sd/interface'; import { useEffect } from 'react'; -const client = createClient({ +const client = createClient({ transport: new WebsocketTransport( - import.meta.env.VITE_SDSERVER_BASE_URL || 'ws://localhost:8080/rspcws' + import.meta.env.VITE_SDSERVER_BASE_URL || 'ws://localhost:8080/rspc/ws' ) }); diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index b22575e2e..affc565fe 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -1,5 +1,7 @@ import react from '@vitejs/plugin-react'; +import { visualizer } from 'rollup-plugin-visualizer'; import { defineConfig } from 'vite'; +import { createHtmlPlugin } from 'vite-plugin-html'; import svg from 'vite-plugin-svgr'; import tsconfigPaths from 'vite-plugin-tsconfig-paths'; @@ -10,7 +12,18 @@ export default defineConfig({ server: { port: 8002 }, - plugins: [react(), svg({ svgrOptions: { icon: true } }), tsconfigPaths()], + plugins: [ + react(), + svg({ svgrOptions: { icon: true } }), + tsconfigPaths(), + createHtmlPlugin({ + minify: true + }), + visualizer({ + gzipSize: true, + brotliSize: true + }) + ], root: 'src', publicDir: '../../packages/interface/src/assets', define: { diff --git a/core/Cargo.toml b/core/Cargo.toml index 7b7d898a9..32157f382 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -25,43 +25,45 @@ hostname = "0.3.1" # Universal Dependencies base64 = "0.13.0" serde = { version = "1.0", features = ["derive"] } -chrono = { version = "0.4.19", features = ["serde"] } +chrono = { version = "0.4.22", features = ["serde"] } serde_json = "1.0" futures = "0.3" data-encoding = "2.3.2" -ring = "0.17.0-alpha.10" +ring = "0.17.0-alpha.11" int-enum = "0.4.0" rmp = "^0.8.11" -rmp-serde = "^1.1.0" +rmp-serde = "^1.1.1" # Project dependencies +rspc = { workspace = true, features = ["uuid", "chrono", "tracing"] } prisma-client-rust = { workspace = true } -rspc = { version = "0.0.5", features = ["uuid", "chrono", "tracing"] } uuid = { version = "1.1.2", features = ["v4", "serde"] } -sysinfo = "0.23.9" -thiserror = "1.0.30" +sysinfo = "0.26.4" +thiserror = "1.0.37" -tokio = { version = "1.17.0", features = [ +tokio = { version = "1.21.2", features = [ "sync", "rt-multi-thread", "io-util", ] } include_dir = { version = "0.7.2", features = ["glob"] } -async-trait = "^0.1.52" -image = "0.24.1" +async-trait = "^0.1.57" +image = "0.24.4" webp = "0.2.2" -ffmpeg-next = { version = "5.0.3", optional = true, features = [] } +ffmpeg-next = { version = "5.1.1", optional = true, features = [] } sd-ffmpeg = { path = "../crates/ffmpeg", optional = true } fs_extra = "1.2.0" -tracing = "0.1.35" -tracing-subscriber = { version = "0.3.14", features = ["env-filter"] } +tracing = "0.1.36" +tracing-subscriber = { version = "0.3.15", features = ["env-filter"] } async-stream = "0.3.3" -once_cell = "1.13.0" -ctor = "0.1.22" +once_cell = "1.15.0" +ctor = "0.1.23" globset = { version = "^0.4.9", features = ["serde1"] } -itertools = "^0.10.3" +itertools = "^0.10.5" enumflags2 = "0.7.5" +openssl-sys = "0.9.76" # We don't use this in the core but it exists so that the workspace level patch doesn't complain that the dependency isn't used. + [dev-dependencies] tempfile = "^3.3.0" tracing-test = "^0.2.3" diff --git a/core/src/api/files.rs b/core/src/api/files.rs index 251b5234c..e973fb299 100644 --- a/core/src/api/files.rs +++ b/core/src/api/files.rs @@ -5,42 +5,45 @@ use serde::Deserialize; use super::{utils::LibraryRequest, RouterBuilder}; -#[derive(Type, Deserialize)] -pub struct SetNoteArgs { - pub id: i32, - pub note: Option, -} - -#[derive(Type, Deserialize)] -pub struct SetFavoriteArgs { - pub id: i32, - pub favorite: bool, -} - pub(crate) fn mount() -> RouterBuilder { ::new() - .library_query("readMetadata", |_, _id: i32, _| async move { - #[allow(unreachable_code)] - Ok(todo!()) + .library_query("readMetadata", |t| { + t(|_, _id: i32, _| async move { + #[allow(unreachable_code)] + Ok(todo!()) + }) }) - .library_mutation("setNote", |_, args: SetNoteArgs, library| async move { - library - .db - .object() - .update( - object::id::equals(args.id), - vec![object::note::set(args.note)], - ) - .exec() - .await?; + .library_mutation("setNote", |t| { + #[derive(Type, Deserialize)] + pub struct SetNoteArgs { + pub id: i32, + pub note: Option, + } - invalidate_query!(library, "locations.getExplorerData"); + t(|_, args: SetNoteArgs, library| async move { + library + .db + .object() + .update( + object::id::equals(args.id), + vec![object::note::set(args.note)], + ) + .exec() + .await?; - Ok(()) + invalidate_query!(library, "locations.getExplorerData"); + + Ok(()) + }) }) - .library_mutation( - "setFavorite", - |_, args: SetFavoriteArgs, library| async move { + .library_mutation("setFavorite", |t| { + #[derive(Type, Deserialize)] + pub struct SetFavoriteArgs { + pub id: i32, + pub favorite: bool, + } + + t(|_, args: SetFavoriteArgs, library| async move { library .db .object() @@ -54,17 +57,19 @@ pub(crate) fn mount() -> RouterBuilder { invalidate_query!(library, "locations.getExplorerData"); Ok(()) - }, - ) - .library_mutation("delete", |_, id: i32, library| async move { - library - .db - .object() - .delete(object::id::equals(id)) - .exec() - .await?; + }) + }) + .library_mutation("delete", |t| { + t(|_, id: i32, library| async move { + library + .db + .object() + .delete(object::id::equals(id)) + .exec() + .await?; - invalidate_query!(library, "locations.getExplorerData"); - Ok(()) + invalidate_query!(library, "locations.getExplorerData"); + Ok(()) + }) }) } diff --git a/core/src/api/jobs.rs b/core/src/api/jobs.rs index 95525f3ab..c27092f47 100644 --- a/core/src/api/jobs.rs +++ b/core/src/api/jobs.rs @@ -14,56 +14,56 @@ use std::path::PathBuf; use super::{utils::LibraryRequest, CoreEvent, RouterBuilder}; -#[derive(Type, Deserialize)] -pub struct GenerateThumbsForLocationArgs { - pub id: i32, - pub path: PathBuf, -} - -#[derive(Type, Deserialize)] -pub struct IdentifyUniqueFilesArgs { - pub id: i32, - pub path: PathBuf, -} - pub(crate) fn mount() -> RouterBuilder { ::new() - .library_query("getRunning", |ctx, _: (), _| async move { - Ok(ctx.jobs.get_running().await) + .library_query("getRunning", |t| { + t(|ctx, _: (), _| async move { Ok(ctx.jobs.get_running().await) }) }) - .library_query("getHistory", |_, _: (), library| async move { - Ok(JobManager::get_history(&library).await?) + .library_query("getHistory", |t| { + t(|_, _: (), library| async move { Ok(JobManager::get_history(&library).await?) }) }) - .library_mutation( - "generateThumbsForLocation", - |_, args: GenerateThumbsForLocationArgs, library| async move { - if library - .db - .location() - .count(vec![location::id::equals(args.id)]) - .exec() - .await? == 0 - { - return Err(LocationError::IdNotFound(args.id).into()); - } + .library_mutation("generateThumbsForLocation", |t| { + #[derive(Type, Deserialize)] + pub struct GenerateThumbsForLocationArgs { + pub id: i32, + pub path: PathBuf, + } - library - .spawn_job(Job::new( - ThumbnailJobInit { - location_id: args.id, - path: PathBuf::new(), - background: true, - }, - Box::new(ThumbnailJob {}), - )) - .await; + t( + |_, args: GenerateThumbsForLocationArgs, library| async move { + if library + .db + .location() + .count(vec![location::id::equals(args.id)]) + .exec() + .await? == 0 + { + return Err(LocationError::IdNotFound(args.id).into()); + } - Ok(()) - }, - ) - .library_mutation( - "identifyUniqueFiles", - |_, args: IdentifyUniqueFilesArgs, library| async move { + library + .spawn_job(Job::new( + ThumbnailJobInit { + location_id: args.id, + path: PathBuf::new(), + background: true, + }, + Box::new(ThumbnailJob {}), + )) + .await; + + Ok(()) + }, + ) + }) + .library_mutation("identifyUniqueFiles", |t| { + #[derive(Type, Deserialize)] + pub struct IdentifyUniqueFilesArgs { + pub id: i32, + pub path: PathBuf, + } + + t(|_, args: IdentifyUniqueFilesArgs, library| async move { if fetch_location(&library, args.id).exec().await?.is_none() { return Err(rspc::Error::new( ErrorCode::NotFound, @@ -82,19 +82,21 @@ pub(crate) fn mount() -> RouterBuilder { .await; Ok(()) - }, - ) - .library_subscription("newThumbnail", |ctx, _: (), _| { - // TODO: Only return event for the library that was subscribed to + }) + }) + .library_subscription("newThumbnail", |t| { + t(|ctx, _: (), _| { + // TODO: Only return event for the library that was subscribed to - let mut event_bus_rx = ctx.event_bus.subscribe(); - async_stream::stream! { - while let Ok(event) = event_bus_rx.recv().await { - match event { - CoreEvent::NewThumbnail { cas_id } => yield cas_id, - _ => {} + let mut event_bus_rx = ctx.event_bus.subscribe(); + async_stream::stream! { + while let Ok(event) = event_bus_rx.recv().await { + match event { + CoreEvent::NewThumbnail { cas_id } => yield cas_id, + _ => {} + } } } - } + }) }) } diff --git a/core/src/api/libraries.rs b/core/src/api/libraries.rs index c89e00663..44d0180fc 100644 --- a/core/src/api/libraries.rs +++ b/core/src/api/libraries.rs @@ -12,87 +12,93 @@ use serde::Deserialize; use tokio::fs; use uuid::Uuid; -#[derive(Type, Deserialize)] -pub struct EditLibraryArgs { - pub id: Uuid, - pub name: Option, - pub description: Option, -} - pub(crate) fn mount() -> RouterBuilder { ::new() - .query("list", |ctx, _: ()| async move { - ctx.library_manager.get_all_libraries_config().await + .query("list", |t| { + t(|ctx, _: ()| async move { ctx.library_manager.get_all_libraries_config().await }) }) - .library_query("getStatistics", |_, _: (), library| async move { - let _statistics = library - .db - .statistics() - .find_unique(statistics::id::equals(library.node_local_id)) - .exec() - .await?; + .library_query("getStatistics", |t| { + t(|_, _: (), library| async move { + let _statistics = library + .db + .statistics() + .find_unique(statistics::id::equals(library.node_local_id)) + .exec() + .await?; - // TODO: get from database, not sys - let volumes = get_volumes(); - save_volume(&library).await?; + // TODO: get from database, not sys + let volumes = get_volumes(); + save_volume(&library).await?; - let mut available_capacity: u64 = 0; - let mut total_capacity: u64 = 0; - if volumes.is_ok() { - for volume in volumes? { - total_capacity += volume.total_capacity; - available_capacity += volume.available_capacity; + let mut available_capacity: u64 = 0; + let mut total_capacity: u64 = 0; + if volumes.is_ok() { + for volume in volumes? { + total_capacity += volume.total_capacity; + available_capacity += volume.available_capacity; + } } + + let library_db_size = match fs::metadata(library.config().data_directory()).await { + Ok(metadata) => metadata.len(), + Err(_) => 0, + }; + + let thumbnail_folder_size = + get_size(library.config().data_directory().join("thumbnails")); + + use statistics::*; + let params = vec![ + id::set(1), // Each library is a database so only one of these ever exists + date_captured::set(Utc::now().into()), + total_object_count::set(0), + library_db_size::set(library_db_size.to_string()), + total_bytes_used::set(0.to_string()), + total_bytes_capacity::set(total_capacity.to_string()), + total_unique_bytes::set(0.to_string()), + total_bytes_free::set(available_capacity.to_string()), + preview_media_bytes::set(thumbnail_folder_size.unwrap_or(0).to_string()), + ]; + + Ok(library + .db + .statistics() + .upsert( + statistics::id::equals(1), // Each library is a database so only one of these ever exists + params.clone(), + params, + ) + .exec() + .await?) + }) + }) + .mutation("create", |t| { + t(|ctx, name: String| async move { + Ok(ctx + .library_manager + .create(LibraryConfig { + name: name.to_string(), + ..Default::default() + }) + .await?) + }) + }) + .mutation("edit", |t| { + #[derive(Type, Deserialize)] + pub struct EditLibraryArgs { + pub id: Uuid, + pub name: Option, + pub description: Option, } - let library_db_size = match fs::metadata(library.config().data_directory()).await { - Ok(metadata) => metadata.len(), - Err(_) => 0, - }; - - let thumbnail_folder_size = - get_size(library.config().data_directory().join("thumbnails")); - - use statistics::*; - let params = vec![ - id::set(1), // Each library is a database so only one of these ever exists - date_captured::set(Utc::now().into()), - total_object_count::set(0), - library_db_size::set(library_db_size.to_string()), - total_bytes_used::set(0.to_string()), - total_bytes_capacity::set(total_capacity.to_string()), - total_unique_bytes::set(0.to_string()), - total_bytes_free::set(available_capacity.to_string()), - preview_media_bytes::set(thumbnail_folder_size.unwrap_or(0).to_string()), - ]; - - Ok(library - .db - .statistics() - .upsert( - statistics::id::equals(1), // Each library is a database so only one of these ever exists - params.clone(), - params, - ) - .exec() - .await?) + t(|ctx, args: EditLibraryArgs| async move { + Ok(ctx + .library_manager + .edit(args.id, args.name, args.description) + .await?) + }) }) - .mutation("create", |ctx, name: String| async move { - Ok(ctx - .library_manager - .create(LibraryConfig { - name: name.to_string(), - ..Default::default() - }) - .await?) - }) - .mutation("edit", |ctx, args: EditLibraryArgs| async move { - Ok(ctx - .library_manager - .edit(args.id, args.name, args.description) - .await?) - }) - .mutation("delete", |ctx, id: Uuid| async move { - Ok(ctx.library_manager.delete_library(id).await?) + .mutation("delete", |t| { + t(|ctx, id: Uuid| async move { Ok(ctx.library_manager.delete_library(id).await?) }) }) } diff --git a/core/src/api/locations.rs b/core/src/api/locations.rs index 18754df3d..d5a0b617f 100644 --- a/core/src/api/locations.rs +++ b/core/src/api/locations.rs @@ -9,17 +9,11 @@ use crate::{ prisma::{file_path, indexer_rule, indexer_rules_in_location, location, object, tag}, }; -use rspc::{self, ErrorCode, Type}; +use rspc::{self, internal::MiddlewareBuilderLike, ErrorCode, Type}; use serde::{Deserialize, Serialize}; use tracing::info; -use super::{utils::LibraryRequest, RouterBuilder}; - -#[derive(Serialize, Deserialize, Type, Debug)] -pub struct ExplorerData { - pub context: ExplorerContext, - pub items: Vec, -} +use super::{utils::LibraryRequest, Ctx, RouterBuilder}; #[derive(Serialize, Deserialize, Type, Debug)] #[serde(tag = "type")] @@ -29,9 +23,6 @@ pub enum ExplorerContext { // Space(object_in_space::Data), } -file_path::include!(file_path_with_object { object }); -object::include!(object_with_file_paths { file_paths }); - #[derive(Serialize, Deserialize, Type, Debug)] #[serde(tag = "type")] pub enum ExplorerItem { @@ -39,36 +30,53 @@ pub enum ExplorerItem { Object(Box), } -#[derive(Clone, Serialize, Deserialize, Type, Debug)] -pub struct LocationExplorerArgs { - pub location_id: i32, - pub path: String, - pub limit: i32, - pub cursor: Option, +#[derive(Serialize, Deserialize, Type, Debug)] +pub struct ExplorerData { + pub context: ExplorerContext, + pub items: Vec, } -pub(crate) fn mount() -> RouterBuilder { +file_path::include!(file_path_with_object { object }); +object::include!(object_with_file_paths { file_paths }); + +// TODO(@Oscar): This return type sucks. Add an upstream rspc solution. +pub(crate) fn mount() -> rspc::RouterBuilder< + Ctx, + (), + impl MiddlewareBuilderLike + Send + 'static, +> { ::new() - .library_query("list", |_, _: (), library| async move { - Ok(library - .db - .location() - .find_many(vec![]) - .include(location::include!({ node })) - .exec() - .await?) + .library_query("list", |t| { + t(|_, _: (), library| async move { + Ok(library + .db + .location() + .find_many(vec![]) + .include(location::include!({ node })) + .exec() + .await?) + }) }) - .library_query("getById", |_, location_id: i32, library| async move { - Ok(library - .db - .location() - .find_unique(location::id::equals(location_id)) - .exec() - .await?) + .library_query("getById", |t| { + t(|_, location_id: i32, library| async move { + Ok(library + .db + .location() + .find_unique(location::id::equals(location_id)) + .exec() + .await?) + }) }) - .library_query( - "getExplorerData", - |_, args: LocationExplorerArgs, library| async move { + .library_query("getExplorerData", |t| { + #[derive(Clone, Serialize, Deserialize, Type, Debug)] + pub struct LocationExplorerArgs { + pub location_id: i32, + pub path: String, + pub limit: i32, + pub cursor: Option, + } + + t(|_, args: LocationExplorerArgs, library| async move { let location = library .db .location() @@ -124,119 +132,128 @@ pub(crate) fn mount() -> RouterBuilder { }) .collect(), }) - }, - ) - .library_mutation( - "create", - |_, args: LocationCreateArgs, library| async move { + }) + }) + .library_mutation("create", |t| { + t(|_, args: LocationCreateArgs, library| async move { let location = args.create(&library).await?; scan_location(&library, location).await?; Ok(()) - }, - ) - .library_mutation( - "update", - |_, args: LocationUpdateArgs, library| async move { + }) + }) + .library_mutation("update", |t| { + t(|_, args: LocationUpdateArgs, library| async move { args.update(&library).await.map_err(Into::into) - }, - ) - .library_mutation("delete", |_, location_id: i32, library| async move { - library - .db - .file_path() - .delete_many(vec![file_path::location_id::equals(location_id)]) - .exec() - .await?; - - library - .db - .indexer_rules_in_location() - .delete_many(vec![indexer_rules_in_location::location_id::equals( - location_id, - )]) - .exec() - .await?; - - library - .db - .location() - .delete(location::id::equals(location_id)) - .exec() - .await?; - - invalidate_query!(library, "locations.list"); - - info!("Location {} deleted", location_id); - - Ok(()) + }) }) - .library_mutation("fullRescan", |_, location_id: i32, library| async move { - scan_location( - &library, - fetch_location(&library, location_id) - .include(indexer_job_location::include()) + .library_mutation("delete", |t| { + t(|_, location_id: i32, library| async move { + library + .db + .file_path() + .delete_many(vec![file_path::location_id::equals(location_id)]) .exec() - .await? - .ok_or(LocationError::IdNotFound(location_id))?, - ) - .await - .map_err(Into::into) + .await?; + + library + .db + .indexer_rules_in_location() + .delete_many(vec![indexer_rules_in_location::location_id::equals( + location_id, + )]) + .exec() + .await?; + + library + .db + .location() + .delete(location::id::equals(location_id)) + .exec() + .await?; + + invalidate_query!(library, "locations.list"); + + info!("Location {} deleted", location_id); + + Ok(()) + }) }) - .library_mutation("quickRescan", |_, _: (), _| async move { - #[allow(unreachable_code)] - Ok(todo!()) + .library_mutation("fullRescan", |t| { + t(|_, location_id: i32, library| async move { + scan_location( + &library, + fetch_location(&library, location_id) + .include(indexer_job_location::include()) + .exec() + .await? + .ok_or(LocationError::IdNotFound(location_id))?, + ) + .await + .map_err(Into::into) + }) + }) + .library_mutation("quickRescan", |t| { + t(|_, _: (), _| async move { + #[allow(unreachable_code)] + Ok(todo!()) + }) }) .merge("indexer_rules.", mount_indexer_rule_routes()) } fn mount_indexer_rule_routes() -> RouterBuilder { ::new() - .library_mutation( - "create", - |_, args: IndexerRuleCreateArgs, library| async move { + .library_mutation("create", |t| { + t(|_, args: IndexerRuleCreateArgs, library| async move { args.create(&library).await.map_err(Into::into) - }, - ) - .library_mutation("delete", |_, indexer_rule_id: i32, library| async move { - library - .db - .indexer_rules_in_location() - .delete_many(vec![indexer_rules_in_location::indexer_rule_id::equals( - indexer_rule_id, - )]) - .exec() - .await?; - - library - .db - .indexer_rule() - .delete(indexer_rule::id::equals(indexer_rule_id)) - .exec() - .await?; - - Ok(()) + }) }) - .library_query("get", |_, indexer_rule_id: i32, library| async move { - library - .db - .indexer_rule() - .find_unique(indexer_rule::id::equals(indexer_rule_id)) - .exec() - .await? - .ok_or_else(|| { - rspc::Error::new( - ErrorCode::NotFound, - format!("Indexer rule not found"), - ) - }) + .library_mutation("delete", |t| { + t(|_, indexer_rule_id: i32, library| async move { + library + .db + .indexer_rules_in_location() + .delete_many(vec![indexer_rules_in_location::indexer_rule_id::equals( + indexer_rule_id, + )]) + .exec() + .await?; + + library + .db + .indexer_rule() + .delete(indexer_rule::id::equals(indexer_rule_id)) + .exec() + .await?; + + Ok(()) + }) }) - .library_query("list", |_, _: (), library| async move { - library - .db - .indexer_rule() - .find_many(vec![]) - .exec() - .await - .map_err(Into::into) + .library_query("get", |t| { + t(|_, indexer_rule_id: i32, library| async move { + library + .db + .indexer_rule() + .find_unique(indexer_rule::id::equals(indexer_rule_id)) + .exec() + .await? + .ok_or_else(|| { + rspc::Error::new( + ErrorCode::NotFound, + format!("Indexer rule not found"), + ) + }) + }) + }) + .library_query("list", |t| { + t(|_, _: (), library| async move { + library + .db + .indexer_rule() + .find_many(vec![]) + .exec() + .await + .map_err(Into::into) + }) }) } diff --git a/core/src/api/mod.rs b/core/src/api/mod.rs index 2266bf330..a50376443 100644 --- a/core/src/api/mod.rs +++ b/core/src/api/mod.rs @@ -1,4 +1,5 @@ use std::{ + path::PathBuf, sync::Arc, time::{Duration, Instant}, }; @@ -62,12 +63,14 @@ pub(crate) fn mount() -> Arc { // .export_ts_bindings(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("./index.ts")), .set_ts_bindings_header("/* eslint-disable */"), ) - .query("version", |_, _: ()| env!("CARGO_PKG_VERSION")) - .query("getNode", |ctx, _: ()| async move { - Ok(NodeState { - config: ctx.config.get().await, - // We are taking the assumption here that this value is only used on the frontend for display purposes - data_path: ctx.config.data_directory().to_string_lossy().into_owned(), + .query("version", |t| t(|_, _: ()| env!("CARGO_PKG_VERSION"))) + .query("getNode", |t| { + t(|ctx, _: ()| async move { + Ok(NodeState { + config: ctx.config.get().await, + // We are taking the assumption here that this value is only used on the frontend for display purposes + data_path: ctx.config.data_directory().to_string_lossy().into_owned(), + }) }) }) .merge("library.", libraries::mount()) @@ -77,46 +80,48 @@ pub(crate) fn mount() -> Arc { .merge("files.", files::mount()) .merge("jobs.", jobs::mount()) // TODO: Scope the invalidate queries to a specific library (filtered server side) - .subscription("invalidateQuery", |ctx, _: ()| { - let mut event_bus_rx = ctx.event_bus.subscribe(); - let mut last = Instant::now(); - async_stream::stream! { - while let Ok(event) = event_bus_rx.recv().await { - match event { - CoreEvent::InvalidateOperation(op) => yield op, - CoreEvent::InvalidateOperationDebounced(op) => { - let current = Instant::now(); - if current.duration_since(last) > Duration::from_millis(1000 / 60) { - last = current; - yield op; - } - }, - _ => {} + .subscription("invalidateQuery", |t| { + t(|ctx, _: ()| { + let mut event_bus_rx = ctx.event_bus.subscribe(); + let mut last = Instant::now(); + async_stream::stream! { + while let Ok(event) = event_bus_rx.recv().await { + match event { + CoreEvent::InvalidateOperation(op) => yield op, + CoreEvent::InvalidateOperationDebounced(op) => { + let current = Instant::now(); + if current.duration_since(last) > Duration::from_millis(1000 / 60) { + last = current; + yield op; + } + }, + _ => {} + } } } - } + }) }) .build() .arced(); InvalidRequests::validate(r.clone()); // This validates all invalidation calls. + export_ts_bindings(&r); r } +pub fn export_ts_bindings(r: &Router) { + r.export_ts(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../packages/client/src/core.ts")) + .expect("Error exporting rspc Typescript bindings!"); + r.export_ts( + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../apps/mobile/src/types/bindings.ts"), + ) + .expect("Error exporting rspc Typescript bindings!"); +} + #[cfg(test)] mod tests { - use std::path::PathBuf; - /// This test will ensure the rspc router and all calls to `invalidate_query` are valid and also export an updated version of the Typescript bindings. #[test] fn test_and_export_rspc_bindings() { - let r = super::mount(); - r.export_ts( - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../packages/client/src/core.ts"), - ) - .expect("Error exporting rspc Typescript bindings!"); - r.export_ts( - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../apps/mobile/src/types/bindings.ts"), - ) - .expect("Error exporting rspc Typescript bindings!"); + super::export_ts_bindings(&super::mount()); } } diff --git a/core/src/api/tags.rs b/core/src/api/tags.rs index acc727fc5..b69238220 100644 --- a/core/src/api/tags.rs +++ b/core/src/api/tags.rs @@ -12,168 +12,187 @@ use crate::{ use super::{utils::LibraryRequest, RouterBuilder}; -#[derive(Type, Deserialize)] -pub struct TagCreateArgs { - pub name: String, - pub color: String, -} - -#[derive(Debug, Type, Deserialize)] -pub struct TagAssignArgs { - pub object_id: i32, - pub tag_id: i32, - pub unassign: bool, -} - -#[derive(Type, Deserialize)] -pub struct TagUpdateArgs { - pub id: i32, - pub name: Option, - pub color: Option, -} - pub(crate) fn mount() -> RouterBuilder { RouterBuilder::new() - .library_query("list", |_, _: (), library| async move { - Ok(library.db.tag().find_many(vec![]).exec().await?) + .library_query("list", |t| { + t( + |_, _: (), library| async move { Ok(library.db.tag().find_many(vec![]).exec().await?) }, + ) }) - .library_query("getExplorerData", |_, tag_id: i32, library| async move { - info!("Getting files for tag {}", tag_id); + .library_query("getExplorerData", |t| { + t(|_, tag_id: i32, library| async move { + info!("Getting files for tag {}", tag_id); - let tag = library - .db - .tag() - .find_unique(tag::id::equals(tag_id)) - .exec() - .await? - .ok_or_else(|| { - rspc::Error::new(ErrorCode::NotFound, format!("Tag not found")) - })?; + let tag = library + .db + .tag() + .find_unique(tag::id::equals(tag_id)) + .exec() + .await? + .ok_or_else(|| { + rspc::Error::new( + ErrorCode::NotFound, + format!("Tag not found"), + ) + })?; - let files: Vec = library - .db - .object() - .find_many(vec![object::tags::some(vec![ - tag_on_object::tag_id::equals(tag_id), - ])]) - .include(object_with_file_paths::include()) - .exec() - .await? - .into_iter() - .map(|mut object| { - // sorry brendan - // grab the first path and tac on the name - let oldest_path = &object.file_paths[0]; - object.name = Some(oldest_path.name.clone()); - object.extension = oldest_path.extension.clone(); - // a long term fix for this would be to have the indexer give the Object a name and extension, sacrificing its own and only store newly found Path names that differ from the Object name + let objects: Vec = library + .db + .object() + .find_many(vec![object::tags::some(vec![ + tag_on_object::tag_id::equals(tag_id), + ])]) + .include(object_with_file_paths::include()) + .exec() + .await? + .into_iter() + .map(|mut object| { + // sorry brendan + // grab the first path and tac on the name + let oldest_path = &object.file_paths[0]; + object.name = Some(oldest_path.name.clone()); + object.extension = oldest_path.extension.clone(); + // a long term fix for this would be to have the indexer give the Object a name and extension, sacrificing its own and only store newly found Path names that differ from the Object name - let thumb_path = library - .config() - .data_directory() - .join(THUMBNAIL_CACHE_DIR_NAME) - .join(&object.cas_id) - .with_extension("webp"); + let thumb_path = library + .config() + .data_directory() + .join(THUMBNAIL_CACHE_DIR_NAME) + .join(&object.cas_id) + .with_extension("webp"); - object.has_thumbnail = thumb_path.exists(); + object.has_thumbnail = thumb_path.exists(); - ExplorerItem::Object(Box::new(object)) + ExplorerItem::Object(Box::new(object)) + }) + .collect(); + + info!("Got objects {}", objects.len()); + + Ok(ExplorerData { + context: ExplorerContext::Tag(tag), + items: objects, }) - .collect(); - - info!("Got files {}", files.len()); - - Ok(ExplorerData { - context: ExplorerContext::Tag(tag), - items: files, }) }) - .library_query("getForFile", |_, object_id: i32, library| async move { - Ok(library - .db - .tag() - .find_many(vec![tag::tag_objects::some(vec![ - tag_on_object::object_id::equals(object_id), - ])]) - .exec() - .await?) - }) - .library_query("get", |_, tag_id: i32, library| async move { - Ok(library - .db - .tag() - .find_unique(tag::id::equals(tag_id)) - .exec() - .await?) - }) - .library_mutation("create", |_, args: TagCreateArgs, library| async move { - let created_tag = library - .db - .tag() - .create( - Uuid::new_v4().as_bytes().to_vec(), - vec![ - tag::name::set(Some(args.name)), - tag::color::set(Some(args.color)), - ], - ) - .exec() - .await?; - - invalidate_query!(library, "tags.list"); - - Ok(created_tag) - }) - .library_mutation("assign", |_, args: TagAssignArgs, library| async move { - if args.unassign { - library + .library_query("getForObject", |t| { + t(|_, object_id: i32, library| async move { + Ok(library .db - .tag_on_object() - .delete(tag_on_object::tag_id_object_id(args.tag_id, args.object_id)) + .tag() + .find_many(vec![tag::tag_objects::some(vec![ + tag_on_object::object_id::equals(object_id), + ])]) .exec() - .await?; - } else { - library + .await?) + }) + }) + .library_query("get", |t| { + t(|_, tag_id: i32, library| async move { + Ok(library .db - .tag_on_object() + .tag() + .find_unique(tag::id::equals(tag_id)) + .exec() + .await?) + }) + }) + .library_mutation("create", |t| { + #[derive(Type, Deserialize)] + pub struct TagCreateArgs { + pub name: String, + pub color: String, + } + + t(|_, args: TagCreateArgs, library| async move { + let created_tag = library + .db + .tag() .create( - tag::id::equals(args.tag_id), - object::id::equals(args.object_id), - vec![], + Uuid::new_v4().as_bytes().to_vec(), + vec![ + tag::name::set(Some(args.name)), + tag::color::set(Some(args.color)), + ], ) .exec() .await?; + + invalidate_query!(library, "tags.list"); + + Ok(created_tag) + }) + }) + .library_mutation("assign", |t| { + #[derive(Debug, Type, Deserialize)] + pub struct TagAssignArgs { + pub object_id: i32, + pub tag_id: i32, + pub unassign: bool, } - invalidate_query!(library, "tags.getForFile"); + t(|_, args: TagAssignArgs, library| async move { + if args.unassign { + library + .db + .tag_on_object() + .delete(tag_on_object::tag_id_object_id(args.tag_id, args.object_id)) + .exec() + .await?; + } else { + library + .db + .tag_on_object() + .create( + tag::id::equals(args.tag_id), + object::id::equals(args.object_id), + vec![], + ) + .exec() + .await?; + } - Ok(()) + invalidate_query!(library, "tags.getForObject"); + + Ok(()) + }) }) - .library_mutation("update", |_, args: TagUpdateArgs, library| async move { - library - .db - .tag() - .update( - tag::id::equals(args.id), - vec![tag::name::set(args.name), tag::color::set(args.color)], - ) - .exec() - .await?; + .library_mutation("update", |t| { + #[derive(Type, Deserialize)] + pub struct TagUpdateArgs { + pub id: i32, + pub name: Option, + pub color: Option, + } - invalidate_query!(library, "tags.list"); + t(|_, args: TagUpdateArgs, library| async move { + library + .db + .tag() + .update( + tag::id::equals(args.id), + vec![tag::name::set(args.name), tag::color::set(args.color)], + ) + .exec() + .await?; - Ok(()) + invalidate_query!(library, "tags.list"); + + Ok(()) + }) }) - .library_mutation("delete", |_, tag_id: i32, library| async move { - library - .db - .tag() - .delete(tag::id::equals(tag_id)) - .exec() - .await?; + .library_mutation("delete", |t| { + t(|_, tag_id: i32, library| async move { + library + .db + .tag() + .delete(tag::id::equals(tag_id)) + .exec() + .await?; - invalidate_query!(library, "tags.list"); + invalidate_query!(library, "tags.list"); - Ok(()) + Ok(()) + }) }) } diff --git a/core/src/api/utils/library.rs b/core/src/api/utils/library.rs index 4062b94d4..aff888476 100644 --- a/core/src/api/utils/library.rs +++ b/core/src/api/utils/library.rs @@ -1,5 +1,13 @@ +use std::sync::Arc; + use futures::{Future, Stream}; -use rspc::{internal::specta, ErrorCode, IntoLayerResult, Type}; +use rspc::{ + internal::{ + specta, BuiltProcedureBuilder, MiddlewareBuilderLike, RequestResult, + UnbuiltProcedureBuilder, + }, + ErrorCode, Type, +}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use uuid::Uuid; @@ -12,116 +20,172 @@ pub(crate) struct LibraryArgs { pub arg: T, } +// WARNING: This is system is using internal API's which means it will break between rspc release. I would avoid copying it unless you understand the cost of maintaining it! pub trait LibraryRequest { - fn library_query( + fn library_query( self, key: &'static str, - resolver: fn(Ctx, TArg, LibraryContext) -> TResult, + builder: impl FnOnce( + UnbuiltProcedureBuilder, + ) -> BuiltProcedureBuilder, ) -> Self where - TArg: DeserializeOwned + specta::Type + Send + 'static, - TResult: Future> + Send + 'static, - T: IntoLayerResult + Send + Serialize + specta::Type; + TUnbuiltResolver: Fn(Ctx, TArg, LibraryContext) -> TUnbuiltResult + Send, + TBuiltResolver: Fn(Ctx, TArg, LibraryContext) -> TUnbuiltResult + Send + Sync + 'static, + TUnbuiltResult: RequestResult + Send, + TArg: DeserializeOwned + specta::Type + Send + 'static; - fn library_mutation( + fn library_mutation< + TUnbuiltResolver, + TUnbuiltResult, + TUnbuiltResultMarker, + TBuiltResolver, + TArg, + >( self, key: &'static str, - resolver: fn(Ctx, TArg, LibraryContext) -> TResult, + builder: impl FnOnce( + UnbuiltProcedureBuilder, + ) -> BuiltProcedureBuilder, ) -> Self where - TArg: DeserializeOwned + specta::Type + Send + 'static, - TResult: Future> + Send + 'static, - T: IntoLayerResult + Send + Serialize + specta::Type; + TUnbuiltResolver: Fn(Ctx, TArg, LibraryContext) -> TUnbuiltResult + Send, + TBuiltResolver: Fn(Ctx, TArg, LibraryContext) -> TUnbuiltResult + Send + Sync + 'static, + TUnbuiltResult: RequestResult + Send, + TArg: DeserializeOwned + specta::Type + Send + 'static; - fn library_subscription( + fn library_subscription( self, key: &'static str, - resolver: fn(Ctx, TArg, Uuid) -> T, + builder: impl Fn(UnbuiltProcedureBuilder) -> BuiltProcedureBuilder, ) -> Self where - TArg: DeserializeOwned + specta::Type + Send + 'static, - T: Stream + Send + 'static, - TResult: Serialize + specta::Type; + TArg: DeserializeOwned + specta::Type + 'static, + TStream: Stream + Send + Sync + 'static, + TResult: Serialize + specta::Type, + TResolver: Fn(Ctx, TArg, Uuid) -> TStream + Send + Sync + 'static; } // Note: This will break with middleware context switching but that's fine for now -impl LibraryRequest for rspc::RouterBuilder { - fn library_query( +impl LibraryRequest for rspc::RouterBuilder +where + TMiddleware: MiddlewareBuilderLike + Send + 'static, +{ + fn library_query( self, key: &'static str, - resolver: fn(Ctx, TArg, LibraryContext) -> TResult, + builder: impl FnOnce( + UnbuiltProcedureBuilder, + ) -> BuiltProcedureBuilder, ) -> Self where + TUnbuiltResolver: Fn(Ctx, TArg, LibraryContext) -> TUnbuiltResult + Send, + TBuiltResolver: Fn(Ctx, TArg, LibraryContext) -> TUnbuiltResult + Send + Sync + 'static, + TUnbuiltResult: RequestResult + Send, TArg: DeserializeOwned + specta::Type + Send + 'static, - TResult: Future> + Send + 'static, - T: IntoLayerResult + Send + Serialize + specta::Type, { - self.query(key, move |ctx, arg: LibraryArgs| async move { - let library = ctx - .library_manager - .get_ctx(arg.library_id) - .await - .ok_or_else(|| { - rspc::Error::new( - ErrorCode::BadRequest, - "You must specify a valid library to use this operation.".to_string(), - ) - })?; + self.query(key, move |t| { + let resolver = Arc::new(builder(UnbuiltProcedureBuilder::new(t.data())).resolver); - resolver(ctx, arg.arg, library).await + t(move |ctx, arg: LibraryArgs| { + let resolver = resolver.clone(); + async move { + let library = ctx + .library_manager + .get_ctx(arg.library_id) + .await + .ok_or_else(|| { + rspc::Error::new( + ErrorCode::BadRequest, + "You must specify a valid library to use this operation." + .to_string(), + ) + })?; + + Ok(resolver(ctx, arg.arg, library) + .into_request_future()? + .exec() + .await?) + } + }) }) } - fn library_mutation( + fn library_mutation< + TUnbuiltResolver, + TUnbuiltResult, + TUnbuiltResultMarker, + TBuiltResolver, + TArg, + >( self, key: &'static str, - resolver: fn(Ctx, TArg, LibraryContext) -> TResult, + builder: impl FnOnce( + UnbuiltProcedureBuilder, + ) -> BuiltProcedureBuilder, ) -> Self where + TUnbuiltResolver: Fn(Ctx, TArg, LibraryContext) -> TUnbuiltResult + Send, + TBuiltResolver: Fn(Ctx, TArg, LibraryContext) -> TUnbuiltResult + Send + Sync + 'static, + TUnbuiltResult: RequestResult + Send, TArg: DeserializeOwned + specta::Type + Send + 'static, - TResult: Future> + Send + 'static, - T: IntoLayerResult + Send + Serialize + specta::Type, { - self.mutation(key, move |ctx, arg: LibraryArgs| async move { - let library = ctx - .library_manager - .get_ctx(arg.library_id) - .await - .ok_or_else(|| { - rspc::Error::new( - ErrorCode::BadRequest, - "You must specify a valid library to use this operation.".to_string(), - ) - })?; + self.mutation(key, move |t| { + let resolver = Arc::new(builder(UnbuiltProcedureBuilder::new(t.data())).resolver); - resolver(ctx, arg.arg, library).await + t(move |ctx, arg: LibraryArgs| { + let resolver = resolver.clone(); + async move { + let library = ctx + .library_manager + .get_ctx(arg.library_id) + .await + .ok_or_else(|| { + rspc::Error::new( + ErrorCode::BadRequest, + "You must specify a valid library to use this operation." + .to_string(), + ) + })?; + + Ok(resolver(ctx, arg.arg, library) + .into_request_future()? + .exec() + .await?) + } + }) }) } - fn library_subscription( + fn library_subscription( self, key: &'static str, - resolver: fn(Ctx, TArg, Uuid) -> T, + builder: impl Fn(UnbuiltProcedureBuilder) -> BuiltProcedureBuilder, ) -> Self where - TArg: DeserializeOwned + specta::Type + Send + 'static, - T: Stream + Send + 'static, + TArg: DeserializeOwned + specta::Type + 'static, + TStream: Stream + Send + Sync + 'static, TResult: Serialize + specta::Type, + TResolver: Fn(Ctx, TArg, Uuid) -> TStream + Send + Sync + 'static, { - self.subscription(key, move |ctx, arg: LibraryArgs| { - // TODO: Make this fetch the library like the other functions. This needs upstream rspc work to be supported. - // let library = ctx - // .library_manager - // .get_ctx(arg.library_id) - // .await - // .ok_or_else(|| { - // rspc::Error::new( - // ErrorCode::BadRequest, - // "You must specify a valid library to use this operation.".to_string(), - // ) - // })?; + self.subscription(key, |t| { + let resolver = Arc::new(builder(UnbuiltProcedureBuilder::new(t.data())).resolver); - resolver(ctx, arg.arg, arg.library_id) + t(move |ctx, arg: LibraryArgs| { + // TODO(@Oscar): Upstream rspc work to allow this to work + // let library = ctx + // .library_manager + // .get_ctx(arg.library_id) + // .await + // .ok_or_else(|| { + // rspc::Error::new( + // ErrorCode::BadRequest, + // "You must specify a valid library to use this operation.".to_string(), + // ) + // })?; + + resolver(ctx, arg.arg, arg.library_id) + }) }) } } diff --git a/core/src/api/volumes.rs b/core/src/api/volumes.rs index 6cd783056..f9a9724be 100644 --- a/core/src/api/volumes.rs +++ b/core/src/api/volumes.rs @@ -1,7 +1,7 @@ use crate::volume::get_volumes; -use super::{Router, RouterBuilder}; +use super::RouterBuilder; pub(crate) fn mount() -> RouterBuilder { - ::new().query("list", |_, _: ()| Ok(get_volumes()?)) + RouterBuilder::new().query("list", |t| t(|_, _: ()| Ok(get_volumes()?))) } diff --git a/core/src/lib.rs b/core/src/lib.rs index fae1da894..db2ebf73f 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -77,7 +77,11 @@ impl Node { "desktop=debug" .parse() .expect("Error invalid tracing directive!"), - ), + ), // .add_directive( + // "rspc=debug" + // .parse() + // .expect("Error invalid tracing directive!"), + // ), ) .with(fmt::layer().with_filter(CONSOLE_LOG_FILTER)) // .with( diff --git a/core/src/util/db.rs b/core/src/util/db.rs index 9c600b527..41ceff5b1 100644 --- a/core/src/util/db.rs +++ b/core/src/util/db.rs @@ -29,7 +29,7 @@ pub async fn load_and_migrate(db_url: &str) -> Result], result: null } | - { key: ["getNode"], result: NodeState } | - { key: ["jobs.getHistory", LibraryArgs], result: Array } | - { key: ["jobs.getRunning", LibraryArgs], result: Array } | - { key: ["library.getStatistics", LibraryArgs], result: Statistics } | - { key: ["library.list"], result: Array } | - { key: ["locations.getById", LibraryArgs], result: Location | null } | - { key: ["locations.getExplorerData", LibraryArgs], result: ExplorerData } | - { key: ["locations.indexer_rules.get", LibraryArgs], result: IndexerRule } | - { key: ["locations.indexer_rules.list", LibraryArgs], result: Array } | - { key: ["locations.list", LibraryArgs], result: Array<{ id: number, pub_id: Array, node_id: number, name: string | null, local_path: string | null, total_capacity: number | null, available_capacity: number | null, filesystem: string | null, disk_type: number | null, is_removable: boolean | null, is_online: boolean, is_archived: boolean, date_created: string, node: Node }> } | - { key: ["tags.get", LibraryArgs], result: Tag | null } | - { key: ["tags.getExplorerData", LibraryArgs], result: ExplorerData } | - { key: ["tags.getForFile", LibraryArgs], result: Array } | - { key: ["tags.list", LibraryArgs], result: Array } | - { key: ["version"], result: string } | - { key: ["volumes.list"], result: Array }, + { key: "files.readMetadata", input: LibraryArgs, result: null } | + { key: "getNode", input: never, result: NodeState } | + { key: "jobs.getHistory", input: LibraryArgs, result: Array } | + { key: "jobs.getRunning", input: LibraryArgs, result: Array } | + { key: "library.getStatistics", input: LibraryArgs, result: Statistics } | + { key: "library.list", input: never, result: Array } | + { key: "locations.getById", input: LibraryArgs, result: Location | null } | + { key: "locations.getExplorerData", input: LibraryArgs, result: ExplorerData } | + { key: "locations.indexer_rules.get", input: LibraryArgs, result: IndexerRule } | + { key: "locations.indexer_rules.list", input: LibraryArgs, result: Array } | + { key: "locations.list", input: LibraryArgs, result: Array<{ id: number, pub_id: Array, node_id: number, name: string | null, local_path: string | null, total_capacity: number | null, available_capacity: number | null, filesystem: string | null, disk_type: number | null, is_removable: boolean | null, is_online: boolean, is_archived: boolean, date_created: string, node: Node }> } | + { key: "tags.get", input: LibraryArgs, result: Tag | null } | + { key: "tags.getExplorerData", input: LibraryArgs, result: ExplorerData } | + { key: "tags.getForObject", input: LibraryArgs, result: Array } | + { key: "tags.list", input: LibraryArgs, result: Array } | + { key: "version", input: never, result: string } | + { key: "volumes.list", input: never, result: Array }, mutations: - { key: ["files.delete", LibraryArgs], result: null } | - { key: ["files.setFavorite", LibraryArgs], result: null } | - { key: ["files.setNote", LibraryArgs], result: null } | - { key: ["jobs.generateThumbsForLocation", LibraryArgs], result: null } | - { key: ["jobs.identifyUniqueFiles", LibraryArgs], result: null } | - { key: ["library.create", string], result: LibraryConfigWrapped } | - { key: ["library.delete", string], result: null } | - { key: ["library.edit", EditLibraryArgs], result: null } | - { key: ["locations.create", LibraryArgs], result: null } | - { key: ["locations.delete", LibraryArgs], result: null } | - { key: ["locations.fullRescan", LibraryArgs], result: null } | - { key: ["locations.indexer_rules.create", LibraryArgs], result: IndexerRule } | - { key: ["locations.indexer_rules.delete", LibraryArgs], result: null } | - { key: ["locations.quickRescan", LibraryArgs], result: null } | - { key: ["locations.update", LibraryArgs], result: null } | - { key: ["tags.assign", LibraryArgs], result: null } | - { key: ["tags.create", LibraryArgs], result: Tag } | - { key: ["tags.delete", LibraryArgs], result: null } | - { key: ["tags.update", LibraryArgs], result: null }, + { key: "files.delete", input: LibraryArgs, result: null } | + { key: "files.setFavorite", input: LibraryArgs, result: null } | + { key: "files.setNote", input: LibraryArgs, result: null } | + { key: "jobs.generateThumbsForLocation", input: LibraryArgs, result: null } | + { key: "jobs.identifyUniqueFiles", input: LibraryArgs, result: null } | + { key: "library.create", input: string, result: LibraryConfigWrapped } | + { key: "library.delete", input: string, result: null } | + { key: "library.edit", input: EditLibraryArgs, result: null } | + { key: "locations.create", input: LibraryArgs, result: null } | + { key: "locations.delete", input: LibraryArgs, result: null } | + { key: "locations.fullRescan", input: LibraryArgs, result: null } | + { key: "locations.indexer_rules.create", input: LibraryArgs, result: IndexerRule } | + { key: "locations.indexer_rules.delete", input: LibraryArgs, result: null } | + { key: "locations.quickRescan", input: LibraryArgs, result: null } | + { key: "locations.update", input: LibraryArgs, result: null } | + { key: "tags.assign", input: LibraryArgs, result: null } | + { key: "tags.create", input: LibraryArgs, result: Tag } | + { key: "tags.delete", input: LibraryArgs, result: null } | + { key: "tags.update", input: LibraryArgs, result: null }, subscriptions: - { key: ["invalidateQuery"], result: InvalidateOperationEvent } | - { key: ["jobs.newThumbnail", LibraryArgs], result: string } + { key: "invalidateQuery", input: never, result: InvalidateOperationEvent } | + { key: "jobs.newThumbnail", input: LibraryArgs, result: string } }; export interface ConfigMetadata { version: string | null } diff --git a/packages/client/src/hooks/useCurrentLibrary.tsx b/packages/client/src/hooks/useCurrentLibrary.tsx index 53cc877a3..d9d02e43f 100644 --- a/packages/client/src/hooks/useCurrentLibrary.tsx +++ b/packages/client/src/hooks/useCurrentLibrary.tsx @@ -22,6 +22,10 @@ export const LibraryContextProvider = ({ return {children}; }; +export function getLibraryIdRaw(): string | null { + return currentLibraryUuidStore.id; +} + // this is a hook to get the current library loaded into the UI. It takes care of a bunch of invariants under the hood. export const useCurrentLibrary = () => { const currentLibraryUuid = useSnapshot(currentLibraryUuidStore).id; diff --git a/packages/client/src/rspc.ts b/packages/client/src/rspc.ts index fa2aff113..086363f16 100644 --- a/packages/client/src/rspc.ts +++ b/packages/client/src/rspc.ts @@ -1,120 +1,61 @@ -import { RSPCError } from '@rspc/client'; +import { ProcedureDef } from '@rspc/client'; import { createReactQueryHooks } from '@rspc/react'; -import { - QueryClient, - UseInfiniteQueryOptions, - UseInfiniteQueryResult, - UseMutationOptions, - UseMutationResult, - UseQueryOptions, - UseQueryResult, - useMutation as _useMutation -} from '@tanstack/react-query'; +import { QueryClient } from '@tanstack/react-query'; -import { LibraryArgs, Operations } from './core'; -import { useCurrentLibrary } from './index'; +import { LibraryArgs, Procedures } from './core'; +import { getLibraryIdRaw } from './index'; export const queryClient = new QueryClient(); -export const rspc = createReactQueryHooks(); +export const rspc = createReactQueryHooks(); -type NonLibraryQueries = Exclude] }> & - Extract; -type NonLibraryQuery = Extract; -type NonLibraryQueryKey = NonLibraryQueries['key'][0]; -type NonLibraryQueryResult = NonLibraryQuery['result']; +type NonLibraryProcedure = + | Exclude }> + | Extract; -export function useBridgeQuery( - key: K, - options?: UseQueryOptions, RSPCError> -): UseQueryResult, RSPCError> { - // @ts-ignore - return rspc.useQuery(key, options); -} - -type LibraryQueries = Extract] }>; -type LibraryQuery = Extract; -type LibraryQueryKey = LibraryQueries['key'][0]; -type LibraryQueryArgs = LibraryQuery['key'][1] extends LibraryArgs - ? A - : never; -type LibraryQueryResult = LibraryQuery['result']; - -export function useLibraryQuery( - key: LibraryQueryArgs extends null | undefined ? [K] : [K, LibraryQueryArgs], - options?: UseQueryOptions, RSPCError> -): UseQueryResult, RSPCError> { - const { library } = useCurrentLibrary(); - - if (!library?.uuid) throw new Error(`Attempted to do library query with no library set!`); - // @ts-ignore - return rspc.useQuery( - // @ts-ignore - [key[0], { library_id: library?.uuid || '', arg: key[1] || null }], - options - ); -} - -export function useInfiniteLibraryQuery( - key: LibraryQueryArgs extends null | undefined ? [K] : [K, LibraryQueryArgs], - options?: UseInfiniteQueryOptions, RSPCError> -): UseInfiniteQueryResult, RSPCError> { - const { library } = useCurrentLibrary(); - - if (!library?.uuid) throw new Error(`Attempted to do library query with no library set!`); - // @ts-ignore - return rspc.useInfiniteQuery( - // @ts-ignore - [key[0], { library_id: library?.uuid || '', arg: key[1] || null }], - options - ); -} - -type LibraryMutations = Extract] }>; -type LibraryMutation = Extract; -type LibraryMutationKey = LibraryMutations['key'][0]; -type LibraryMutationArgs = - LibraryMutation['key'][1] extends LibraryArgs ? A : never; -type LibraryMutationResult = LibraryMutation['result']; -export function useLibraryMutation( - key: K, - options?: UseMutationOptions, RSPCError> -) { - const ctx = rspc.useContext(); - const { library } = useCurrentLibrary(); - if (!library?.uuid) throw new Error(`Attempted to do library query with no library set!`); - - // @ts-ignore - return _useMutation, RSPCError, LibraryMutationArgs>( - async (data) => - ctx.client.mutation([key, { library_id: library?.uuid || '', arg: data || null }]), - { - ...options, - context: rspc.ReactQueryContext - } - ); -} - -type NonLibraryMutations = Exclude] }>; -type NonLibraryMutation = Extract< - NonLibraryMutations, - { key: [K] | [K, any] } +type LibraryProcedures = Exclude< + Extract }>, + { input: never } >; -type NonLibraryMutationKey = NonLibraryMutations['key'][0]; -type NonLibraryMutationArgs = NonLibraryMutation['key'][1]; -type NonLibraryMutationResult = NonLibraryMutation['result']; -export function useBridgeMutation( - key: K, - options?: UseMutationOptions, RSPCError> -): UseMutationResult, RSPCError, NonLibraryMutationArgs> { - // @ts-ignore - return rspc.useMutation(key, options); -} + +type MoreConstrainedQueries = T extends any + ? T['input'] extends LibraryArgs + ? { + key: T['key']; + input: E; + result: T['result']; + } + : never + : never; + +export const useBridgeQuery = rspc.customQuery>( + (keyAndInput) => keyAndInput as any +); + +export const useBridgeMutation = rspc.customMutation>( + (keyAndInput) => keyAndInput +); + +export const useLibraryQuery = rspc.customQuery< + MoreConstrainedQueries> +>((keyAndInput) => { + const library_id = getLibraryIdRaw(); + if (library_id === null) throw new Error('Attempted to do library query with no library set!'); + return [keyAndInput[0], { library_id, arg: keyAndInput[1] || null }]; +}); + +export const useLibraryMutation = rspc.customMutation< + MoreConstrainedQueries> +>((keyAndInput) => { + const library_id = getLibraryIdRaw(); + if (library_id === null) throw new Error('Attempted to do library query with no library set!'); + return [keyAndInput[0], { library_id, arg: keyAndInput[1] || null }]; +}); export function useInvalidateQuery() { const context = rspc.useContext(); rspc.useSubscription(['invalidateQuery'], { - onNext: (invalidateOperation) => { - let key = [invalidateOperation.key]; + onData: (invalidateOperation) => { + const key = [invalidateOperation.key]; if (invalidateOperation.arg !== null) { key.concat(invalidateOperation.arg); } diff --git a/packages/config/base.tsconfig.json b/packages/config/base.tsconfig.json index 192c236c5..819f5cfbd 100644 --- a/packages/config/base.tsconfig.json +++ b/packages/config/base.tsconfig.json @@ -15,7 +15,7 @@ "noUnusedLocals": false, "noUnusedParameters": false, "preserveWatchOutput": true, - "skipLibCheck": false, + "skipLibCheck": true, "strict": true, "allowSyntheticDefaultImports": true, "resolveJsonModule": true, diff --git a/packages/config/package.json b/packages/config/package.json index 6d9f656ba..ddc8bc105 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -7,11 +7,11 @@ "eslint-react.js" ], "devDependencies": { - "eslint": "^8.21.0", - "@typescript-eslint/eslint-plugin": "^5.30.7", - "@typescript-eslint/parser": "^5.30.7", + "@typescript-eslint/eslint-plugin": "^5.39.0", + "@typescript-eslint/parser": "^5.39.0", + "eslint": "^8.24.0", "eslint-config-prettier": "^8.5.0", - "eslint-plugin-react": "^7.30.1", + "eslint-plugin-react": "^7.31.8", "eslint-plugin-react-hooks": "^4.6.0" } } diff --git a/packages/interface/package.json b/packages/interface/package.json index 01384e648..9563c9469 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -15,80 +15,81 @@ "lint": "eslint src/**/*.{ts,tsx} && tsc --noEmit" }, "dependencies": { - "@fontsource/inter": "^4.5.11", - "@headlessui/react": "^1.6.6", - "@heroicons/react": "^2.0.10", + "@fontsource/inter": "^4.5.13", + "@headlessui/react": "^1.7.3", + "@heroicons/react": "^2.0.12", "@radix-ui/react-dialog": "^1.0.0", "@radix-ui/react-dropdown-menu": "^1.0.0", "@radix-ui/react-icons": "^1.1.1", - "@radix-ui/react-progress": "^0.1.4", - "@radix-ui/react-slider": "^0.1.4", + "@radix-ui/react-progress": "^1.0.0", + "@radix-ui/react-slider": "^1.0.0", "@radix-ui/react-tabs": "^1.0.0", "@radix-ui/react-tooltip": "^1.0.0", "@sd/assets": "workspace:*", "@sd/client": "workspace:*", "@sd/ui": "workspace:*", - "@tailwindcss/forms": "^0.5.2", - "@tanstack/react-query": "^4.2.3", - "@tanstack/react-query-devtools": "^4.0.10", + "@tailwindcss/forms": "^0.5.3", + "@tanstack/react-query": "^4.10.1", + "@tanstack/react-query-devtools": "^4.10.1", "@tanstack/react-virtual": "3.0.0-beta.18", - "@vitejs/plugin-react": "^2.0.0", - "autoprefixer": "^10.4.7", + "@vitejs/plugin-react": "^2.1.0", + "autoprefixer": "^10.4.12", "byte-size": "^8.1.0", "clsx": "^1.2.1", - "date-fns": "^2.29.2", + "date-fns": "^2.29.3", + "dayjs": "^1.11.5", "immer": "^9.0.15", - "jotai": "^1.7.6", + "jotai": "^1.8.4", "lodash": "^4.17.21", "moment": "^2.29.4", "phosphor-react": "^1.4.1", "pretty-bytes": "^6.0.0", "react": "^18.2.0", - "react-colorful": "^5.5.1", - "react-countup": "^6.3.0", + "react-colorful": "^5.6.1", + "react-countup": "^6.3.1", "react-dom": "^18.2.0", "react-dropzone": "^14.2.2", "react-error-boundary": "^3.1.4", - "react-hook-form": "^7.33.1", + "react-hook-form": "^7.36.1", "react-hotkeys-hook": "^3.4.7", "react-json-view": "^1.21.3", "react-loading-icons": "^1.1.0", "react-loading-skeleton": "^3.1.0", "react-portal": "^4.2.2", - "react-query": "^4.0.0", - "react-router": "6.3.0", - "react-router-dom": "6.3.0", - "react-scrollbars-custom": "^4.1.0", + "react-query": "^3.39.2", + "react-router": "6.4.2", + "react-router-dom": "6.4.2", + "react-scrollbars-custom": "^4.1.1", "react-spline": "^1.2.1", - "react-transition-group": "^4.4.2", - "react-virtuoso": "^2.16.5", + "react-transition-group": "^4.4.5", + "react-virtuoso": "^2.19.1", "rooks": "^5.14.0", - "tailwindcss": "^3.1.6", + "tailwindcss": "^3.1.8", "use-count-up": "^3.0.1", - "use-debounce": "^8.0.3", + "use-debounce": "^8.0.4", "valtio": "^1.7.0", "valtio-persist": "^1.0.2", - "zod": "^3.18.0", - "zustand": "4.0.0" + "zod": "^3.19.1", + "zustand": "4.1.1" }, "devDependencies": { "@sd/config": "workspace:*", "@types/babel-core": "^6.25.7", "@types/byte-size": "^8.1.0", - "@types/lodash": "^4.14.182", - "@types/node": "^18.6.1", + "@types/lodash": "^4.14.186", + "@types/node": "^18.8.2", "@types/pretty-bytes": "^5.2.0", - "@types/react": "^18.0.15", + "@types/react": "^18.0.21", "@types/react-dom": "^18.0.6", "@types/react-router-dom": "^5.3.3", "@types/react-table": "^7.7.12", "@types/react-window": "^1.8.5", "@types/tailwindcss": "^3.1.0", "@vitejs/plugin-react": "^1.3.1", - "concurrently": "^7.3.0", + "concurrently": "^7.4.0", "prettier": "^2.7.1", - "typescript": "^4.7.4", - "vite": "^3.0.3", + "typescript": "^4.8.4", + "vite": "^3.1.4", "vite-plugin-svgr": "^2.2.1" } } diff --git a/packages/interface/src/App.tsx b/packages/interface/src/App.tsx index 07a4dedff..84e3fd591 100644 --- a/packages/interface/src/App.tsx +++ b/packages/interface/src/App.tsx @@ -2,6 +2,10 @@ import '@fontsource/inter/variable.css'; import { LibraryContextProvider, queryClient } from '@sd/client'; import { QueryClientProvider, defaultContext } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import dayjs from 'dayjs'; +import advancedFormat from 'dayjs/plugin/advancedFormat'; +import duration from 'dayjs/plugin/duration'; +import relativeTime from 'dayjs/plugin/relativeTime'; import { ErrorBoundary } from 'react-error-boundary'; import { MemoryRouter, useNavigate } from 'react-router-dom'; @@ -9,6 +13,10 @@ import { AppRouter } from './AppRouter'; import { ErrorFallback } from './ErrorFallback'; import './style.scss'; +dayjs.extend(advancedFormat); +dayjs.extend(relativeTime); +dayjs.extend(duration); + export default function SpacedriveInterface() { return ( diff --git a/packages/interface/src/AppLayout.tsx b/packages/interface/src/AppLayout.tsx index 14a9ab835..263173dae 100644 --- a/packages/interface/src/AppLayout.tsx +++ b/packages/interface/src/AppLayout.tsx @@ -1,5 +1,6 @@ import { useCurrentLibrary } from '@sd/client'; import clsx from 'clsx'; +import { Suspense } from 'react'; import { Outlet } from 'react-router-dom'; import { Sidebar } from './components/layout/Sidebar'; @@ -29,7 +30,9 @@ export function AppLayout() { >
- + Loading...

}> + +
); diff --git a/packages/interface/src/AppRouter.tsx b/packages/interface/src/AppRouter.tsx index fc8c33ca6..f2059d7f7 100644 --- a/packages/interface/src/AppRouter.tsx +++ b/packages/interface/src/AppRouter.tsx @@ -1,37 +1,41 @@ +import { AppLayout } from './AppLayout'; +import { useKeybindHandler } from './hooks/useKeyboardHandler'; import { useCurrentLibrary, useInvalidateQuery } from '@sd/client'; +import { lazy, Suspense } from 'react'; import { Navigate, Route, Routes } from 'react-router-dom'; -import { AppLayout } from './AppLayout'; -import { NotFound } from './NotFound'; -import OnboardingScreen from './components/onboarding/Onboarding'; -import { useKeybindHandler } from './hooks/useKeyboardHandler'; -import { ContentScreen } from './screens/Content'; -import { DebugScreen } from './screens/Debug'; -import { LocationExplorer } from './screens/LocationExplorer'; -import { OverviewScreen } from './screens/Overview'; -import { PhotosScreen } from './screens/Photos'; -import { TagExplorer } from './screens/TagExplorer'; -import { SettingsScreen } from './screens/settings/Settings'; -import AppearanceSettings from './screens/settings/client/AppearanceSettings'; -import ExtensionSettings from './screens/settings/client/ExtensionsSettings'; -import GeneralSettings from './screens/settings/client/GeneralSettings'; -import KeybindingSettings from './screens/settings/client/KeybindingSettings'; -import PrivacySettings from './screens/settings/client/PrivacySettings'; -import AboutSpacedrive from './screens/settings/info/AboutSpacedrive'; -import Changelog from './screens/settings/info/Changelog'; -import Support from './screens/settings/info/Support'; -import ContactsSettings from './screens/settings/library/ContactsSettings'; -import KeysSettings from './screens/settings/library/KeysSetting'; -import LibraryGeneralSettings from './screens/settings/library/LibraryGeneralSettings'; -import LocationSettings from './screens/settings/library/LocationSettings'; -import NodesSettings from './screens/settings/library/NodesSettings'; -import SecuritySettings from './screens/settings/library/SecuritySettings'; -import SharingSettings from './screens/settings/library/SharingSettings'; -import SyncSettings from './screens/settings/library/SyncSettings'; -import TagsSettings from './screens/settings/library/TagsSettings'; -import ExperimentalSettings from './screens/settings/node/ExperimentalSettings'; -import LibrarySettings from './screens/settings/node/LibrariesSettings'; -import P2PSettings from './screens/settings/node/P2PSettings'; +const DebugScreen = lazy(() => import('./screens/Debug')); +const SettingsScreen = lazy(() => import('./screens/settings/Settings')); +const TagExplorer = lazy(() => import('./screens/TagExplorer')); +const PhotosScreen = lazy(() => import('./screens/Photos')); +const OverviewScreen = lazy(() => import('./screens/Overview')); +const ContentScreen = lazy(() => import('./screens/Content')); +const LocationExplorer = lazy(() => import('./screens/LocationExplorer')); +const OnboardingScreen = lazy(() => import('./components/onboarding/Onboarding')); +const NotFound = lazy(() => import('./NotFound')); + +const AppearanceSettings = lazy(() => import('./screens/settings/client/AppearanceSettings')); +const ExtensionSettings = lazy(() => import('./screens/settings/client/ExtensionsSettings')); +const GeneralSettings = lazy(() => import('./screens/settings/client/GeneralSettings')); +const KeybindingSettings = lazy(() => import('./screens/settings/client/KeybindingSettings')); +const PrivacySettings = lazy(() => import('./screens/settings/client/PrivacySettings')); +const AboutSpacedrive = lazy(() => import('./screens/settings/info/AboutSpacedrive')); +const Changelog = lazy(() => import('./screens/settings/info/Changelog')); +const Support = lazy(() => import('./screens/settings/info/Support')); +const ContactsSettings = lazy(() => import('./screens/settings/library/ContactsSettings')); +const KeysSettings = lazy(() => import('./screens/settings/library/KeysSetting')); +const LibraryGeneralSettings = lazy( + () => import('./screens/settings/library/LibraryGeneralSettings') +); +const LocationSettings = lazy(() => import('./screens/settings/library/LocationSettings')); +const NodesSettings = lazy(() => import('./screens/settings/library/NodesSettings')); +const SecuritySettings = lazy(() => import('./screens/settings/library/SecuritySettings')); +const SharingSettings = lazy(() => import('./screens/settings/library/SharingSettings')); +const SyncSettings = lazy(() => import('./screens/settings/library/SyncSettings')); +const TagsSettings = lazy(() => import('./screens/settings/library/TagsSettings')); +const ExperimentalSettings = lazy(() => import('./screens/settings/node/ExperimentalSettings')); +const LibrarySettings = lazy(() => import('./screens/settings/node/LibrariesSettings')); +const P2PSettings = lazy(() => import('./screens/settings/node/P2PSettings')); export function AppRouter() { const { library } = useCurrentLibrary(); @@ -40,56 +44,60 @@ export function AppRouter() { useInvalidateQuery(); return ( - - } /> - }> - {/* As we are caching the libraries in localStore so this *shouldn't* result is visual problems unless something else is wrong */} - {library === undefined ? ( - Please select or create a library in the sidebar. - } - /> - ) : ( - <> - } /> - } /> - } /> - } /> - } /> - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - } /> - } /> - } /> - - )} - - + Loading...

}> + + } /> + }> + {/* As we are caching the libraries in localStore so this *shouldn't* result is visual problems unless something else is wrong */} + {library === undefined ? ( + + Please select or create a library in the sidebar. + + } + /> + ) : ( + <> + } /> + } /> + } /> + } /> + } /> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + } /> + } /> + + )} + + +
); } diff --git a/packages/interface/src/NotFound.tsx b/packages/interface/src/NotFound.tsx index aeec54c35..ad732dac1 100644 --- a/packages/interface/src/NotFound.tsx +++ b/packages/interface/src/NotFound.tsx @@ -1,7 +1,7 @@ import { Button } from '@sd/ui'; import { useNavigate } from 'react-router'; -export function NotFound() { +export default function NotFound() { const navigate = useNavigate(); return (
{ + onData: (cas_id) => { expStore.addNewThumbnail(cas_id); } }); diff --git a/packages/interface/src/components/explorer/ExplorerContextMenu.tsx b/packages/interface/src/components/explorer/ExplorerContextMenu.tsx index 8676dd0b7..e39fe4874 100644 --- a/packages/interface/src/components/explorer/ExplorerContextMenu.tsx +++ b/packages/interface/src/components/explorer/ExplorerContextMenu.tsx @@ -21,14 +21,14 @@ import { useSnapshot } from 'valtio'; const AssignTagMenuItems = (props: { objectId: number }) => { const tags = useLibraryQuery(['tags.list'], { suspense: true }); - const tagsForFile = useLibraryQuery(['tags.getForFile', props.objectId], { suspense: true }); + const tagsForObject = useLibraryQuery(['tags.getForObject', props.objectId], { suspense: true }); const { mutate: assignTag } = useLibraryMutation('tags.assign'); return ( <> {tags.data?.map((tag) => { - const active = !!tagsForFile.data?.find((t) => t.id === tag.id); + const active = !!tagsForObject.data?.find((t) => t.id === tag.id); return ( ; + const Icon = useMemo(() => { + const icon = icons[`../../../../assets/icons/${data.extension as any}.svg`]; + + const Icon = icon + ? lazy(() => icon().then((v) => ({ default: (v as any).ReactComponent }))) + : undefined; + return Icon; + }, [data.extension]); + + if (isPath(data) && data.is_dir) return ; const cas_id = isObject(data) ? data.cas_id : data.object?.cas_id; - if (cas_id) { - // this won't work - const new_thumbnail = !!getExplorerStore().newThumbnails[cas_id]; + if (!cas_id) return
; - const has_thumbnail = isObject(data) - ? data.has_thumbnail - : isPath(data) - ? data.object?.has_thumbnail - : new_thumbnail; + const has_thumbnail = isObject(data) + ? data.has_thumbnail + : isPath(data) + ? data.object?.has_thumbnail + : !!store.newThumbnails[cas_id]; - const url = platform.getThumbnailUrlById(cas_id); + const url = platform.getThumbnailUrlById(cas_id); - if (has_thumbnail && url) - return ( + if (has_thumbnail && url) + return ( + + ); + + if (props.kind === 'video') { + return ( +
- ); - - if (props.kind === 'video') { - return ( -
- -
- ); - } - if (props.kind === 'zip') { - return ( -
- -
- ); - } +
+ ); + } + if (props.kind === 'zip') { + return ( +
+ +
+ ); } - - const Icon = icons[data.extension as keyof typeof icons]; return (
{Icon && (
- + }> + + {data.extension} diff --git a/packages/interface/src/components/explorer/Inspector.tsx b/packages/interface/src/components/explorer/Inspector.tsx index 8db5116ff..3aef88989 100644 --- a/packages/interface/src/components/explorer/Inspector.tsx +++ b/packages/interface/src/components/explorer/Inspector.tsx @@ -1,13 +1,4 @@ -import { ShareIcon } from '@heroicons/react/24/solid'; -import { useLibraryQuery } from '@sd/client'; -import { ExplorerContext, ExplorerItem, FilePath, Location, Object } from '@sd/client'; -import { Button, TextArea } from '@sd/ui'; -import clsx from 'clsx'; -import moment from 'moment'; -import { Link } from 'phosphor-react'; -import { useEffect, useState } from 'react'; - -import types from '../../constants/file-types.json'; +// import types from '../../constants/file-types.json'; import { Tooltip } from '../tooltip/Tooltip'; import FileThumb from './FileThumb'; import { Divider } from './inspector/Divider'; @@ -15,6 +6,15 @@ import FavoriteButton from './inspector/FavoriteButton'; import { MetaItem } from './inspector/MetaItem'; import Note from './inspector/Note'; import { isObject } from './utils'; +import { ShareIcon } from '@heroicons/react/24/solid'; +import { useLibraryQuery } from '@sd/client'; +import { ExplorerContext, ExplorerItem } from '@sd/client'; +import { Button } from '@sd/ui'; +import { useQuery } from '@tanstack/react-query'; +import clsx from 'clsx'; +import dayjs from 'dayjs'; +import { Link } from 'phosphor-react'; +import { useEffect, useState } from 'react'; interface Props { context?: ExplorerContext; @@ -22,6 +22,11 @@ interface Props { } export const Inspector = (props: Props) => { + const { data: types } = useQuery( + ['_file-types'], + () => import('../../constants/file-types.json') + ); + const is_dir = props.data?.type === 'Path' ? props.data.is_dir : false; const objectData = props.data ? (isObject(props.data) ? props.data : props.data.object) : null; @@ -36,120 +41,118 @@ export const Inspector = (props: Props) => { }, [props.data?.id]); // this is causing LAG - const { data: tags } = useLibraryQuery(['tags.getForFile', objectData?.id || -1], { + const tags = useLibraryQuery(['tags.getForObject', objectData?.id || -1], { enabled: readyToFetch }); return ( -
-
- {!!props.data && ( - <> -
- -
-
-

- {props.data?.name} - {props.data?.extension && `.${props.data.extension}`} -

- {objectData && ( -
- - - - - - - - - -
- )} - {!!tags?.length && ( - <> - - - {tags?.map((tag) => ( -
setSelectedTag(tag.id === selectedTag ? null : tag.id)} - key={tag.id} - className={clsx( - 'flex items-center rounded px-1.5 py-0.5' - // selectedTag === tag.id && 'ring' - )} - style={{ backgroundColor: tag.color + 'CC' }} - > - {tag.name} -
- ))} -
- } - /> - - )} - {props.context?.type == 'Location' && props.data?.type === 'Path' && ( - <> - - - - )} - - - - - {!is_dir && ( - <> - -
- {props.data?.extension && ( - - {props.data?.extension} - - )} -

- {props.data?.extension - ? //@ts-ignore - types[props.data.extension.toUpperCase()]?.descriptions.join(' / ') - : 'Unknown'} -

-
- {objectData && ( - <> - - - {objectData.cas_id && ( - - )} - +
+ {!!props.data && ( + <> +
+ +
+
+

+ {props.data?.name} + {props.data?.extension && `.${props.data.extension}`} +

+ {objectData && ( +
+ + + + + + + + + +
+ )} + {tags?.data && tags.data.length > 0 && ( + <> + + + {tags?.data?.map((tag) => ( +
setSelectedTag(tag.id === selectedTag ? null : tag.id)} + key={tag.id} + className={clsx( + 'flex items-center rounded px-1.5 py-0.5' + // selectedTag === tag.id && 'ring' + )} + style={{ backgroundColor: tag.color + 'CC' }} + > + {tag.name} +
+ ))} +
+ } + /> + + )} + {props.context?.type == 'Location' && props.data?.type === 'Path' && ( + <> + + + + )} + + + + + {!is_dir && ( + <> + +
+ {props.data?.extension && ( + + {props.data?.extension} + )} - - )} -
- - )} -
+

+ {props.data?.extension + ? //@ts-ignore + types[props.data.extension.toUpperCase()]?.descriptions.join(' / ') + : 'Unknown'} +

+
+ {objectData && ( + <> + + + {objectData.cas_id && ( + + )} + + )} + + )} +
+ + )}
); }; diff --git a/packages/interface/src/components/explorer/VirtualizedList.tsx b/packages/interface/src/components/explorer/VirtualizedList.tsx index 6c000db03..1e6e7c500 100644 --- a/packages/interface/src/components/explorer/VirtualizedList.tsx +++ b/packages/interface/src/components/explorer/VirtualizedList.tsx @@ -1,14 +1,12 @@ -import { ExplorerLayoutMode, getExplorerStore, useExplorerStore } from '@sd/client'; -import { ExplorerContext, ExplorerItem, FilePath } from '@sd/client'; -import { useVirtualizer } from '@tanstack/react-virtual'; -import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; -import { useSearchParams } from 'react-router-dom'; -import { useKey, useOnWindowResize, useWindowSize } from 'rooks'; -import { useSnapshot } from 'valtio'; - import FileItem from './FileItem'; import FileRow from './FileRow'; import { isPath } from './utils'; +import { ExplorerLayoutMode, getExplorerStore, useExplorerStore } from '@sd/client'; +import { ExplorerContext, ExplorerItem } from '@sd/client'; +import { useVirtualizer } from '@tanstack/react-virtual'; +import { memo, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { useKey, useOnWindowResize } from 'rooks'; const TOP_BAR_HEIGHT = 50; const GRID_TEXT_AREA_HEIGHT = 25; diff --git a/packages/interface/src/components/explorer/inspector/Note.tsx b/packages/interface/src/components/explorer/inspector/Note.tsx index 1eaa0e1be..5010f4fd9 100644 --- a/packages/interface/src/components/explorer/inspector/Note.tsx +++ b/packages/interface/src/components/explorer/inspector/Note.tsx @@ -1,7 +1,7 @@ import { useLibraryMutation } from '@sd/client'; import { Object as SDObject } from '@sd/client'; import { TextArea } from '@sd/ui'; -import { debounce } from 'lodash'; +import debounce from 'lodash/debounce'; import { useCallback, useState } from 'react'; import { Divider } from './Divider'; diff --git a/packages/interface/src/components/jobs/JobManager.tsx b/packages/interface/src/components/jobs/JobManager.tsx index 70d4a8349..8da2c5037 100644 --- a/packages/interface/src/components/jobs/JobManager.tsx +++ b/packages/interface/src/components/jobs/JobManager.tsx @@ -3,7 +3,7 @@ import { useLibraryQuery } from '@sd/client'; import { JobReport } from '@sd/client'; import { Button } from '@sd/ui'; import clsx from 'clsx'; -import { formatDistanceToNow, formatDuration } from 'date-fns'; +import dayjs from 'dayjs'; import { ArrowsClockwise } from 'phosphor-react'; import { Tooltip } from '../tooltip/Tooltip'; @@ -70,12 +70,12 @@ export function JobsManager() { {job.status === 'Failed' ? 'Failed after' : 'Took'}{' '} {job.seconds_elapsed - ? formatDuration({ seconds: job.seconds_elapsed }) + ? dayjs.duration({ seconds: job.seconds_elapsed }).humanize() : 'less than a second'} - {formatDistanceToNow(new Date(job.date_created))} ago + {dayjs(job.date_created).toNow(true)} ago
{job.data} diff --git a/packages/interface/src/screens/Content.tsx b/packages/interface/src/screens/Content.tsx index 47c83eb78..c1722ca4b 100644 --- a/packages/interface/src/screens/Content.tsx +++ b/packages/interface/src/screens/Content.tsx @@ -1,4 +1,4 @@ -export const ContentScreen: React.FC = (props) => { +export default function ContentScreen() { // const [address, setAddress] = React.useState(''); return
; -}; +} diff --git a/packages/interface/src/screens/Debug.tsx b/packages/interface/src/screens/Debug.tsx index 15a9aea69..cb1c33dea 100644 --- a/packages/interface/src/screens/Debug.tsx +++ b/packages/interface/src/screens/Debug.tsx @@ -3,7 +3,7 @@ import { useBridgeQuery, useLibraryMutation, useLibraryQuery, usePlatform } from import CodeBlock from '../components/primitive/Codeblock'; // TODO: Bring this back with a button in the sidebar near settings at the bottom -export const DebugScreen: React.FC = (props) => { +export default function DebugScreen() { const platform = usePlatform(); const { data: nodeState } = useBridgeQuery(['getNode']); const { data: libraryState } = useBridgeQuery(['library.list']); @@ -45,4 +45,4 @@ export const DebugScreen: React.FC = (props) => {
); -}; +} diff --git a/packages/interface/src/screens/LocationExplorer.tsx b/packages/interface/src/screens/LocationExplorer.tsx index 2820056da..ef6a9db95 100644 --- a/packages/interface/src/screens/LocationExplorer.tsx +++ b/packages/interface/src/screens/LocationExplorer.tsx @@ -15,7 +15,7 @@ export function useExplorerParams() { return { location_id, path, limit }; } -export const LocationExplorer: React.FC = () => { +export default function LocationExplorer() { const { location_id, path } = useExplorerParams(); const { library } = useCurrentLibrary(); @@ -38,4 +38,4 @@ export const LocationExplorer: React.FC = () => { ); -}; +} diff --git a/packages/interface/src/screens/Overview.tsx b/packages/interface/src/screens/Overview.tsx index 7bf735605..edf5b1767 100644 --- a/packages/interface/src/screens/Overview.tsx +++ b/packages/interface/src/screens/Overview.tsx @@ -89,7 +89,7 @@ const StatItem: React.FC = (props) => { ); }; -export const OverviewScreen = () => { +export default function OverviewScreen() { const platform = usePlatform(); const { data: libraryStatistics, isLoading: isStatisticsLoading } = useLibraryQuery([ 'library.getStatistics' @@ -191,4 +191,4 @@ export const OverviewScreen = () => { ); -}; +} diff --git a/packages/interface/src/screens/Photos.tsx b/packages/interface/src/screens/Photos.tsx index 17acd70b6..264677b64 100644 --- a/packages/interface/src/screens/Photos.tsx +++ b/packages/interface/src/screens/Photos.tsx @@ -1,4 +1,4 @@ -export const PhotosScreen: React.FC = (props) => { +export default function PhotosScreen() { return (
@@ -9,4 +9,4 @@ export const PhotosScreen: React.FC = (props) => {
); -}; +} diff --git a/packages/interface/src/screens/TagExplorer.tsx b/packages/interface/src/screens/TagExplorer.tsx index 134750d8d..0684dea8a 100644 --- a/packages/interface/src/screens/TagExplorer.tsx +++ b/packages/interface/src/screens/TagExplorer.tsx @@ -3,7 +3,7 @@ import { useParams } from 'react-router-dom'; import Explorer from '../components/explorer/Explorer'; -export const TagExplorer: React.FC = () => { +export default function TagExplorer() { const { id } = useParams(); const { library } = useCurrentLibrary(); @@ -16,4 +16,4 @@ export const TagExplorer: React.FC = () => { )} ); -}; +} diff --git a/packages/interface/src/screens/settings/Settings.tsx b/packages/interface/src/screens/settings/Settings.tsx index 34a095ffa..535338c30 100644 --- a/packages/interface/src/screens/settings/Settings.tsx +++ b/packages/interface/src/screens/settings/Settings.tsx @@ -17,7 +17,7 @@ import { SettingsScreenContainer } from '../../components/settings/SettingsScreenContainer'; -export const SettingsScreen: React.FC = () => { +export default function SettingsScreen() { return ( Client @@ -100,4 +100,4 @@ export const SettingsScreen: React.FC = () => { ); -}; +} diff --git a/packages/ui/package.json b/packages/ui/package.json index 751319d17..ff8a168ff 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -17,22 +17,22 @@ "storybook:build": "build-storybook" }, "dependencies": { - "@headlessui/react": "^1.6.6", - "@heroicons/react": "^2.0.10", + "@headlessui/react": "^1.7.3", + "@heroicons/react": "^2.0.12", "@radix-ui/react-context-menu": "^1.0.0", "@radix-ui/react-dropdown-menu": "^1.0.0", - "@tailwindcss/forms": "^0.5.2", + "@tailwindcss/forms": "^0.5.3", "class-variance-authority": "^0.2.3", "clsx": "^1.2.1", "phosphor-react": "^1.4.1", - "postcss": "^8.4.14", + "postcss": "^8.4.17", "react": "^18.2.0", "react-dom": "^18.2.0", - "storybook": "^6.5.10", - "tailwindcss": "^3.1.6" + "storybook": "^6.5.12", + "tailwindcss": "^3.1.8" }, "devDependencies": { - "@babel/core": "^7.18.9", + "@babel/core": "^7.19.3", "@sd/config": "workspace:*", "@storybook/addon-actions": "^6.5.12", "@storybook/addon-essentials": "^6.5.12", @@ -44,18 +44,18 @@ "@storybook/preset-scss": "^1.0.3", "@storybook/react": "^6.5.12", "@storybook/testing-library": "^0.0.13", - "@tailwindcss/line-clamp": "^0.4.0", - "@tailwindcss/typography": "^0.5.4", - "@types/react": "^18.0.15", + "@tailwindcss/line-clamp": "^0.4.2", + "@tailwindcss/typography": "^0.5.7", + "@types/react": "^18.0.21", "@types/react-dom": "^18.0.6", - "autoprefixer": "^10.4.7", + "autoprefixer": "^10.4.12", "babel-loader": "^8.2.5", "css-loader": "^6.7.1", "postcss-loader": "^7.0.1", - "sass": "^1.54.0", + "sass": "^1.55.0", "sass-loader": "^13.0.2", - "storybook-tailwind-dark-mode": "^1.0.12", + "storybook-tailwind-dark-mode": "^1.0.15", "style-loader": "^3.3.1", - "typescript": "^4.7.4" + "typescript": "^4.8.4" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55754f217..d60c0a145 100644 Binary files a/pnpm-lock.yaml and b/pnpm-lock.yaml differ