From a4a4f3d9b1389a57785d6aaee210f41b223933b7 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Wed, 25 Dec 2024 12:53:05 +0800 Subject: [PATCH] feat(mobile-native): add nbstore binding --- .github/workflows/build-test.yml | 2 +- .github/workflows/release-mobile.yml | 11 +- Cargo.lock | 169 +- Cargo.toml | 72 +- .../server/migrations/migration_lock.toml | 2 +- packages/common/native/Cargo.toml | 6 +- .../common/nbstore/src/storage/storage.ts | 11 +- .../ios/App/App.xcodeproj/project.pbxproj | 8 + .../ios/App/App/AffineViewController.swift | 1 + .../App/Plugins/NBStore/NBStorePlugin.swift | 402 ++++ .../App/App/uniffi/affine_mobile_native.swift | 1800 +++++++++++++++-- .../App/App/uniffi/affine_mobile_nativeFFI.h | 329 +++ .../apps/ios/App/xc-universal-binary.sh | 2 +- packages/frontend/apps/ios/package.json | 4 +- .../apps/ios/src/plugins/nbstore/blob.ts | 33 + .../apps/ios/src/plugins/nbstore/db.ts | 60 + .../ios/src/plugins/nbstore/definitions.ts | 144 ++ .../apps/ios/src/plugins/nbstore/doc.ts | 83 + .../apps/ios/src/plugins/nbstore/handlers.ts | 128 ++ .../apps/ios/src/plugins/nbstore/index.ts | 5 + .../apps/ios/src/plugins/nbstore/plugin.ts | 312 +++ .../apps/ios/src/plugins/nbstore/storage.ts | 83 + .../apps/ios/src/plugins/nbstore/sync.ts | 70 + packages/frontend/apps/ios/tsconfig.json | 6 +- .../src/modules/workspace-engine/index.ts | 1 + packages/frontend/mobile-native/Cargo.toml | 20 +- packages/frontend/mobile-native/src/error.rs | 29 + packages/frontend/mobile-native/src/lib.rs | 875 ++++++++ packages/frontend/mobile-native/src/utils.rs | 141 ++ packages/frontend/native/Cargo.toml | 22 +- packages/frontend/native/nbstore/Cargo.toml | 12 +- packages/frontend/native/nbstore/src/blob.rs | 11 +- packages/frontend/native/nbstore/src/doc.rs | 13 +- packages/frontend/native/nbstore/src/lib.rs | 114 +- packages/frontend/native/package.json | 4 +- packages/frontend/native/schema/Cargo.toml | 2 +- packages/frontend/native/sqlite_v1/Cargo.toml | 4 +- tools/commitlint/.commitlintrc.json | 1 + tools/utils/src/workspace.gen.ts | 1 + yarn.lock | 2 + 40 files changed, 4746 insertions(+), 249 deletions(-) create mode 100644 packages/frontend/apps/ios/App/App/Plugins/NBStore/NBStorePlugin.swift create mode 100644 packages/frontend/apps/ios/src/plugins/nbstore/blob.ts create mode 100644 packages/frontend/apps/ios/src/plugins/nbstore/db.ts create mode 100644 packages/frontend/apps/ios/src/plugins/nbstore/definitions.ts create mode 100644 packages/frontend/apps/ios/src/plugins/nbstore/doc.ts create mode 100644 packages/frontend/apps/ios/src/plugins/nbstore/handlers.ts create mode 100644 packages/frontend/apps/ios/src/plugins/nbstore/index.ts create mode 100644 packages/frontend/apps/ios/src/plugins/nbstore/plugin.ts create mode 100644 packages/frontend/apps/ios/src/plugins/nbstore/storage.ts create mode 100644 packages/frontend/apps/ios/src/plugins/nbstore/sync.ts create mode 100644 packages/frontend/mobile-native/src/error.rs create mode 100644 packages/frontend/mobile-native/src/utils.rs diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 25f5253a637a5..5777470de38a4 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -404,7 +404,7 @@ jobs: uses: taiki-e/install-action@nextest - name: Run tests - run: cargo nextest run --release + run: cargo nextest run --release --no-fail-fast copilot-api-test: name: Server Copilot Api Test diff --git a/.github/workflows/release-mobile.yml b/.github/workflows/release-mobile.yml index 5327806192944..fa9249f4aaca9 100644 --- a/.github/workflows/release-mobile.yml +++ b/.github/workflows/release-mobile.yml @@ -137,6 +137,9 @@ jobs: electron-install: false hard-link-nm: false enableScripts: false + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable - name: Cap sync run: yarn workspace @affine/ios cap sync - name: Signing By Apple Developer ID @@ -145,13 +148,15 @@ jobs: with: p12-file-base64: ${{ secrets.CERTIFICATES_P12_MOBILE }} p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD_MOBILE }} - - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: latest-stable - name: Setup Rust uses: ./.github/actions/setup-rust with: targets: 'aarch64-apple-ios' + - name: Build Rust + run: | + brew install swiftformat + cargo build -p affine_mobile_native --lib --release --target aarch64-apple-ios + cargo run -p affine_mobile_native --bin uniffi-bindgen generate --library target/aarch64-apple-ios/release/libaffine_mobile_native.a --language swift --out-dir packages/frontend/apps/ios/App/App/uniffi - name: Testflight if: ${{ env.BUILD_TYPE != 'stable' }} working-directory: packages/frontend/apps/ios/App diff --git a/Cargo.lock b/Cargo.lock index ce1b2f956d508..83c143a24a8ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,6 +33,16 @@ name = "affine_mobile_native" version = "0.0.0" dependencies = [ "affine_common", + "affine_nbstore", + "anyhow", + "base64-simd", + "chrono", + "dashmap", + "homedir", + "objc2", + "objc2-foundation", + "sqlx", + "thiserror 2.0.9", "uniffi", ] @@ -64,6 +74,7 @@ dependencies = [ "napi-derive", "sqlx", "tokio", + "uniffi", ] [[package]] @@ -316,6 +327,16 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + [[package]] name = "base64ct" version = "1.6.0" @@ -385,6 +406,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2", +] + [[package]] name = "borsh" version = "1.5.3" @@ -970,7 +1000,7 @@ dependencies = [ "libc", "log", "rustversion", - "windows", + "windows 0.58.0", ] [[package]] @@ -1091,6 +1121,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "homedir" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bdbbd5bc8c5749697ccaa352fa45aff8730cf21c68029c0eef1ffed7c3d6ba2" +dependencies = [ + "cfg-if", + "nix", + "widestring", + "windows 0.57.0", +] + [[package]] name = "iana-time-zone" version = "0.1.61" @@ -1541,6 +1583,18 @@ dependencies = [ "libloading", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -1608,6 +1662,40 @@ dependencies = [ "libm", ] +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", +] + [[package]] name = "object" version = "0.36.7" @@ -1639,6 +1727,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "outref" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a" + [[package]] name = "overload" version = "0.1.1" @@ -2968,6 +3062,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "walkdir" version = "2.5.0" @@ -3072,6 +3172,12 @@ dependencies = [ "wasite", ] +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + [[package]] name = "winapi" version = "0.3.9" @@ -3103,6 +3209,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.58.0" @@ -3122,19 +3238,42 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" dependencies = [ - "windows-implement", - "windows-interface", - "windows-result", + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", "windows-strings", "windows-targets 0.52.6", ] +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-implement" version = "0.58.0" @@ -3146,6 +3285,17 @@ dependencies = [ "syn", ] +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-interface" version = "0.58.0" @@ -3157,6 +3307,15 @@ dependencies = [ "syn", ] +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.2.0" @@ -3172,7 +3331,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ - "windows-result", + "windows-result 0.2.0", "windows-targets 0.52.6", ] diff --git a/Cargo.toml b/Cargo.toml index 182327176791f..8fbe43cc9530e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,40 +1,48 @@ [workspace] -members = [ - "./packages/backend/native", - "./packages/common/native", - "./packages/frontend/native", - "./packages/frontend/native/sqlite_v1", - "./packages/frontend/native/nbstore", - "./packages/frontend/native/schema", - "./packages/frontend/mobile-native", +members = [ + "./packages/backend/native", + "./packages/common/native", + "./packages/frontend/mobile-native", + "./packages/frontend/native", + "./packages/frontend/native/nbstore", + "./packages/frontend/native/schema", + "./packages/frontend/native/sqlite_v1", ] resolver = "2" [workspace.dependencies] -affine_common = { path = "./packages/common/native" } -criterion2 = { version = "2", default-features = false } -anyhow = "1" -chrono = "0.4" -dotenvy = "0.15" -file-format = { version = "0.26", features = ["reader"] } -mimalloc = "0.1" -napi = { version = "3.0.0-alpha.12", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] } -napi-build = { version = "2" } -napi-derive = { version = "3.0.0-alpha.12" } -notify = { version = "7", features = ["serde"] } -once_cell = "1" -parking_lot = "0.12" -rand = "0.8" -rayon = "1.10" -serde = "1" -serde_json = "1" -sha3 = "0.10" -sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] } -tiktoken-rs = "0.6" -tokio = "1.37" -uuid = "1.8" -v_htmlescape = "0.15" -y-octo = { git = "https://github.com/y-crdt/y-octo.git", branch = "main" } +affine_common = { path = "./packages/common/native" } +affine_nbstore = { path = "./packages/frontend/native/nbstore" } +anyhow = "1" +base64-simd = "0.8" +chrono = "0.4" +criterion2 = { version = "2", default-features = false } +dashmap = "6" +dotenvy = "0.15" +file-format = { version = "0.26", features = ["reader"] } +mimalloc = "0.1" +napi = { version = "3.0.0-alpha.12", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] } +napi-build = { version = "2" } +napi-derive = { version = "3.0.0-alpha.12" } +notify = { version = "7", features = ["serde"] } +objc2 = "0.5.2" +objc2-foundation = "0.2.2" +once_cell = "1" +parking_lot = "0.12" +homedir = "0.3" +rand = "0.8" +rayon = "1.10" +serde = "1" +serde_json = "1" +sha3 = "0.10" +sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] } +thiserror = "2" +tiktoken-rs = "0.6" +tokio = "1.37" +uniffi = "0.28" +uuid = "1.8" +v_htmlescape = "0.15" +y-octo = { git = "https://github.com/y-crdt/y-octo.git", branch = "main" } [profile.dev.package.sqlx-macros] opt-level = 3 diff --git a/packages/backend/server/migrations/migration_lock.toml b/packages/backend/server/migrations/migration_lock.toml index fbffa92c2bb7c..99e4f20090794 100644 --- a/packages/backend/server/migrations/migration_lock.toml +++ b/packages/backend/server/migrations/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually # It should be added in your version-control system (i.e. Git) -provider = "postgresql" \ No newline at end of file +provider = "postgresql" diff --git a/packages/common/native/Cargo.toml b/packages/common/native/Cargo.toml index 7693d7e486e18..5d1ad3e3eb223 100644 --- a/packages/common/native/Cargo.toml +++ b/packages/common/native/Cargo.toml @@ -9,9 +9,9 @@ rand = { workspace = true } sha3 = { workspace = true } [dev-dependencies] -rayon = { workspace = true } -criterion2 = { workspace = true } +criterion2 = { workspace = true } +rayon = { workspace = true } [[bench]] -name = "hashcash" harness = false +name = "hashcash" diff --git a/packages/common/nbstore/src/storage/storage.ts b/packages/common/nbstore/src/storage/storage.ts index 8cc5a7e87eb62..9f54fbd222297 100644 --- a/packages/common/nbstore/src/storage/storage.ts +++ b/packages/common/nbstore/src/storage/storage.ts @@ -13,6 +13,10 @@ export function universalId({ peer, type, id }: StorageOptions) { return `@peer(${peer});@type(${type});@id(${id});`; } +export function isValidSpaceType(type: string): type is SpaceType { + return type === 'workspace' || type === 'userspace'; +} + export function isValidUniversalId(opts: Record): boolean { const requiredKeys: Array = [ 'peer', @@ -26,11 +30,11 @@ export function isValidUniversalId(opts: Record): boolean { } } - return opts.type === 'userspace' || opts.type === 'workspace'; + return isValidSpaceType(opts.type); } export function parseUniversalId(id: string) { - const result: Record = {}; + const result: Partial = {}; let key = ''; let value = ''; let isInValue = false; @@ -44,6 +48,7 @@ export function parseUniversalId(id: string) { // when we are in value string, we only care about ch and next char to be [')', ';'] to end the id part if (isInValue) { if (ch === ')' && nextCh === ';') { + // @ts-expect-error we know the key is valid result[key] = value; key = ''; value = ''; @@ -77,7 +82,7 @@ export function parseUniversalId(id: string) { ); } - return result as any; + return result as StorageOptions; } export interface Storage { diff --git a/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj b/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj index fe3bec13be596..6d9f0ec1c5f7b 100644 --- a/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj +++ b/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj @@ -60,6 +60,10 @@ FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + C45499AB2D140B5000E21978 /* NBStore */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = NBStore; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 504EC3011FED79650016851F /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -139,6 +143,7 @@ 9D90BE1A2CCB9876006677DB /* Plugins */ = { isa = PBXGroup; children = ( + C45499AB2D140B5000E21978 /* NBStore */, E93B276A2CED9298001409B8 /* NavigationGesture */, 9D90BE192CCB9876006677DB /* Cookie */, ); @@ -201,6 +206,9 @@ ); dependencies = ( ); + fileSystemSynchronizedGroups = ( + C45499AB2D140B5000E21978 /* NBStore */, + ); name = App; productName = App; productReference = 504EC3041FED79650016851F /* App.app */; diff --git a/packages/frontend/apps/ios/App/App/AffineViewController.swift b/packages/frontend/apps/ios/App/App/AffineViewController.swift index 2e55195706be6..d504f0a885dbe 100644 --- a/packages/frontend/apps/ios/App/App/AffineViewController.swift +++ b/packages/frontend/apps/ios/App/App/AffineViewController.swift @@ -20,6 +20,7 @@ class AFFiNEViewController: CAPBridgeViewController { HashcashPlugin(), NavigationGesturePlugin(), IntelligentsPlugin(representController: self), + NbStorePlugin(), ] plugins.forEach { bridge?.registerPluginInstance($0) } } diff --git a/packages/frontend/apps/ios/App/App/Plugins/NBStore/NBStorePlugin.swift b/packages/frontend/apps/ios/App/App/Plugins/NBStore/NBStorePlugin.swift new file mode 100644 index 0000000000000..706e1b5c09973 --- /dev/null +++ b/packages/frontend/apps/ios/App/App/Plugins/NBStore/NBStorePlugin.swift @@ -0,0 +1,402 @@ +import Capacitor +import Foundation + +@objc(NbStorePlugin) +public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin { + private let docStoragePool: DocStoragePool = .init(noPointer: DocStoragePool.NoPointer()) + + public let identifier = "NbStorePlugin" + public let jsName = "NbStoreDocStorage" + public let pluginMethods: [CAPPluginMethod] = [ + CAPPluginMethod(name: "getSpaceDBPath", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "connect", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "close", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "isClosed", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "checkpoint", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "validate", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "setSpaceId", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "pushUpdate", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "getDocSnapshot", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "setDocSnapshot", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "getDocUpdates", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "markUpdatesMerged", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "deleteDoc", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "getDocClocks", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "getDocClock", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "getBlob", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "setBlob", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "deleteBlob", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "releaseBlobs", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "listBlobs", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "getPeerRemoteClocks", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "getPeerRemoteClock", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "setPeerRemoteClock", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "getPeerPulledRemoteClocks", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "getPeerPulledRemoteClock", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "setPeerPulledRemoteClock", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "getPeerPushedClocks", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "setPeerPushedClock", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "clearClocks", returnType: CAPPluginReturnPromise), + ] + + @objc func getSpaceDBPath(_ call: CAPPluginCall) { + let peer = call.getString("peer") ?? "" + let spaceType = call.getString("spaceType") ?? "" + let id = call.getString("id") ?? "" + + do { + let path = try getDbPath(peer: peer, spaceType: spaceType, id: id) + call.resolve(["path": path]) + } catch { + call.reject("Failed to get space DB path", nil, error) + } + } + + @objc func connect(_ call: CAPPluginCall) async { + let id = call.getString("id") ?? "" + try? await docStoragePool.connect(universalId: id) + } + + @objc func close(_ call: CAPPluginCall) async { + let id = call.getString("id") ?? "" + try? await docStoragePool.close(universalId: id) + } + + @objc func isClosed(_ call: CAPPluginCall) { + let id = call.getString("id") ?? "" + call.resolve(["isClosed": docStoragePool.isClosed(universalId: id)]) + } + + @objc func checkpoint(_ call: CAPPluginCall) async { + let id = call.getString("id") ?? "" + try? await docStoragePool.checkpoint(universalId: id) + } + + @objc func validate(_ call: CAPPluginCall) async { + let id = call.getString("id") ?? "" + let validate = (try? await docStoragePool.validate(universalId: id)) ?? false + call.resolve(["isValidate": validate]) + } + + @objc func setSpaceId(_ call: CAPPluginCall) async { + let id = call.getString("id") ?? "" + let spaceId = call.getString("spaceId") ?? "" + do { + try await docStoragePool.setSpaceId(universalId: id, spaceId: spaceId) + call.resolve() + } catch { + call.reject("Failed to set space id", nil, error) + } + } + + @objc func pushUpdate(_ call: CAPPluginCall) async { + let id = call.getString("id") ?? "" + let docId = call.getString("docId") ?? "" + let data = call.getString("data") ?? "" + do { + let timestamp = try await docStoragePool.pushUpdate(universalId: id, docId: docId, update: data) + call.resolve(["timestamp": timestamp.timeIntervalSince1970]) + + } catch { + call.reject("Failed to push update", nil, error) + } + } + + @objc func getDocSnapshot(_ call: CAPPluginCall) async { + let id = call.getString("id") ?? "" + let docId = call.getString("docId") ?? "" + do { + if let record = try await docStoragePool.getDocSnapshot(universalId: id, docId: docId) { + call.resolve([ + "docId": record.docId, + "data": record.data, + "timestamp": record.timestamp.timeIntervalSince1970, + ]) + } else { + call.resolve() + } + } catch { + call.reject("Failed to get doc snapshot", nil, error) + } + } + + @objc func setDocSnapshot(_ call: CAPPluginCall) async { + let id = call.getString("id") ?? "" + let docId = call.getString("docId") ?? "" + let data = call.getString("data") ?? "" + let timestamp = Date() + do { + let success = try await docStoragePool.setDocSnapshot( + universalId: id, + snapshot: DocRecord(docId: docId, data: data, timestamp: timestamp) + ) + call.resolve(["success": success]) + } catch { + call.reject("Failed to set doc snapshot", nil, error) + } + } + + @objc func getDocUpdates(_ call: CAPPluginCall) async { + let id = call.getString("id") ?? "" + let docId = call.getString("docId") ?? "" + do { + let updates = try await docStoragePool.getDocUpdates(universalId: id, docId: docId) + let mapped = updates.map { [ + "docId": $0.docId, + "createdAt": $0.createdAt.timeIntervalSince1970, + "data": $0.data, + ] } + call.resolve(["updates": mapped]) + } catch { + call.reject("Failed to get doc updates", nil, error) + } + } + + @objc func markUpdatesMerged(_ call: CAPPluginCall) async { + let id = call.getString("id") ?? "" + let docId = call.getString("docId") ?? "" + let times = call.getArray("timestamps", Double.self) ?? [] + let dateArray = times.map { Date(timeIntervalSince1970: $0) } + do { + let count = try await docStoragePool.markUpdatesMerged(universalId: id, docId: docId, updates: dateArray) + call.resolve(["count": count]) + } catch { + call.reject("Failed to mark updates merged", nil, error) + } + } + + @objc func deleteDoc(_ call: CAPPluginCall) async { + let id = call.getString("id") ?? "" + let docId = call.getString("docId") ?? "" + do { + try await docStoragePool.deleteDoc(universalId: id, docId: docId) + call.resolve() + } catch { + call.reject("Failed to delete doc", nil, error) + } + } + + @objc func getDocClocks(_ call: CAPPluginCall) async { + let id = call.getString("id") ?? "" + let after = call.getInt("after") + do { + let docClocks = try await docStoragePool.getDocClocks( + universalId: id, + after: after != nil ? Date(timeIntervalSince1970: TimeInterval(after!)) : nil + ) + let mapped = docClocks.map { [ + "docId": $0.docId, + "timestamp": $0.timestamp.timeIntervalSince1970, + ] } + call.resolve(["clocks": mapped]) + } catch { + call.reject("Failed to get doc clocks", nil, error) + } + } + + @objc func getDocClock(_ call: CAPPluginCall) async { + let id = call.getString("id") ?? "" + let docId = call.getString("docId") ?? "" + do { + if let docClock = try await docStoragePool.getDocClock(universalId: id, docId: docId) { + call.resolve([ + "docId": docClock.docId, + "timestamp": docClock.timestamp.timeIntervalSince1970, + ]) + } else { + call.resolve() + } + } catch { + call.reject("Failed to get doc clock for docId: \(docId)", nil, error) + } + } + + @objc func getBlob(_ call: CAPPluginCall) async { + let id = call.getString("id") ?? "" + let key = call.getString("key") ?? "" + if let blob = try? await docStoragePool.getBlob(universalId: id, key: key) { + call.resolve(["blob": blob]) + } else { + call.resolve() + } + } + + @objc func setBlob(_ call: CAPPluginCall) async { + let id = call.getString("id") ?? "" + let key = call.getString("key") ?? "" + let data = call.getString("data") ?? "" + let mime = call.getString("mime") ?? "" + try? await docStoragePool.setBlob(universalId: id, blob: SetBlob(key: key, data: data, mime: mime)) + } + + @objc func deleteBlob(_ call: CAPPluginCall) async { + let id = call.getString("id") ?? "" + let key = call.getString("key") ?? "" + let permanently = call.getBool("permanently") ?? false + try? await docStoragePool.deleteBlob(universalId: id, key: key, permanently: permanently) + } + + @objc func releaseBlobs(_ call: CAPPluginCall) async { + let id = call.getString("id") ?? "" + try? await docStoragePool.releaseBlobs(universalId: id) + } + + @objc func listBlobs(_ call: CAPPluginCall) async { + let id = call.getString("id") ?? "" + if let blobs = try? await docStoragePool.listBlobs(universalId: id) { + let mapped = blobs.map { [ + "key": $0.key, + "size": $0.size, + "mime": $0.mime, + "createdAt": $0.createdAt.timeIntervalSince1970, + ] } + call.resolve(["blobs": mapped]) + } else { + call.resolve() + } + } + + @objc func getPeerRemoteClocks(_ call: CAPPluginCall) async { + let id = call.getString("id") ?? "" + let peer = call.getString("peer") ?? "" + do { + let clocks = try await docStoragePool.getPeerRemoteClocks(universalId: id, peer: peer) + let mapped = clocks.map { [ + "docId": $0.docId, + "timestamp": $0.timestamp.timeIntervalSince1970, + ] } + call.resolve(["clocks": mapped]) + + } catch { + call.reject("Failed to get peer remote clocks", nil, error) + } + } + + @objc func getPeerRemoteClock(_ call: CAPPluginCall) async { + let id = call.getString("id") ?? "" + let peer = call.getString("peer") ?? "" + let docId = call.getString("docId") ?? "" + do { + let clock = try await docStoragePool.getPeerRemoteClock(universalId: id, peer: peer, docId: docId) + call.resolve([ + "docId": clock.docId, + "timestamp": clock.timestamp.timeIntervalSince1970, + ]) + + } catch { + call.reject("Failed to get peer remote clock", nil, error) + } + } + + @objc func setPeerRemoteClock(_ call: CAPPluginCall) async { + let id = call.getString("id") ?? "" + let peer = call.getString("peer") ?? "" + let docId = call.getString("docId") ?? "" + let timestamp = call.getDouble("timestamp") ?? 0 + do { + try await docStoragePool.setPeerRemoteClock( + universalId: id, + peer: peer, + docId: docId, + clock: Date(timeIntervalSince1970: timestamp) + ) + call.resolve() + } catch { + call.reject("Failed to set peer remote clock", nil, error) + } + } + + @objc func getPeerPulledRemoteClocks(_ call: CAPPluginCall) async { + let id = call.getString("id") ?? "" + let peer = call.getString("peer") ?? "" + do { + let clocks = try await docStoragePool.getPeerPulledRemoteClocks(universalId: id, peer: peer) + let mapped = clocks.map { [ + "docId": $0.docId, + "timestamp": $0.timestamp.timeIntervalSince1970, + ] } + call.resolve(["clocks": mapped]) + + } catch { + call.reject("Failed to get peer pulled remote clocks", nil, error) + } + } + + @objc func getPeerPulledRemoteClock(_ call: CAPPluginCall) async { + let id = call.getString("id") ?? "" + let peer = call.getString("peer") ?? "" + let docId = call.getString("docId") ?? "" + do { + let clock = try await docStoragePool.getPeerPulledRemoteClock(universalId: id, peer: peer, docId: docId) + call.resolve([ + "docId": clock.docId, + "timestamp": clock.timestamp.timeIntervalSince1970, + ]) + + } catch { + call.reject("Failed to get peer pulled remote clock", nil, error) + } + } + + @objc func setPeerPulledRemoteClock(_ call: CAPPluginCall) async { + let id = call.getString("id") ?? "" + let peer = call.getString("peer") ?? "" + let docId = call.getString("docId") ?? "" + let timestamp = call.getDouble("timestamp") ?? 0 + do { + try await docStoragePool.setPeerPulledRemoteClock( + universalId: id, + peer: peer, + docId: docId, + clock: Date(timeIntervalSince1970: timestamp) + ) + call.resolve() + } catch { + call.reject("Failed to set peer pulled remote clock", nil, error) + } + } + + @objc func getPeerPushedClocks(_ call: CAPPluginCall) async { + let id = call.getString("id") ?? "" + let peer = call.getString("peer") ?? "" + do { + let clocks = try await docStoragePool.getPeerPushedClocks(universalId: id, peer: peer) + let mapped = clocks.map { [ + "docId": $0.docId, + "timestamp": $0.timestamp.timeIntervalSince1970, + ] } + call.resolve(["clocks": mapped]) + + } catch { + call.reject("Failed to get peer pushed clocks", nil, error) + } + } + + @objc func setPeerPushedClock(_ call: CAPPluginCall) async { + let id = call.getString("id") ?? "" + let peer = call.getString("peer") ?? "" + let docId = call.getString("docId") ?? "" + let timestamp = call.getDouble("timestamp") ?? 0 + do { + try await docStoragePool.setPeerPushedClock( + universalId: id, + peer: peer, + docId: docId, + clock: Date(timeIntervalSince1970: timestamp) + ) + call.resolve() + } catch { + call.reject("Failed to set peer pushed clock", nil, error) + } + } + + @objc func clearClocks(_ call: CAPPluginCall) async { + let id = call.getString("id") ?? "" + do { + try await docStoragePool.clearClocks(universalId: id) + call.resolve() + } catch { + call.reject("Failed to clear clocks", nil, error) + } + } +} diff --git a/packages/frontend/apps/ios/App/App/uniffi/affine_mobile_native.swift b/packages/frontend/apps/ios/App/App/uniffi/affine_mobile_native.swift index 30d7930ce7165..f318d6d36f748 100644 --- a/packages/frontend/apps/ios/App/App/uniffi/affine_mobile_native.swift +++ b/packages/frontend/apps/ios/App/App/uniffi/affine_mobile_native.swift @@ -8,10 +8,10 @@ import Foundation // might be in a separate module, or it might be compiled inline into // this module. This is a bit of light hackery to work with both. #if canImport(affine_mobile_nativeFFI) -import affine_mobile_nativeFFI + import affine_mobile_nativeFFI #endif -fileprivate extension RustBuffer { +private extension RustBuffer { // Allocate a new buffer, copying the contents of a `UInt8` array. init(bytes: [UInt8]) { let rbuf = bytes.withUnsafeBufferPointer { ptr in @@ -21,7 +21,7 @@ fileprivate extension RustBuffer { } static func empty() -> RustBuffer { - RustBuffer(capacity: 0, len:0, data: nil) + RustBuffer(capacity: 0, len: 0, data: nil) } static func from(_ ptr: UnsafeBufferPointer) -> RustBuffer { @@ -35,7 +35,7 @@ fileprivate extension RustBuffer { } } -fileprivate extension ForeignBytes { +private extension ForeignBytes { init(bufferPointer: UnsafeBufferPointer) { self.init(len: Int32(bufferPointer.count), data: bufferPointer.baseAddress) } @@ -48,7 +48,7 @@ fileprivate extension ForeignBytes { // Helper classes/extensions that don't change. // Someday, this will be in a library of its own. -fileprivate extension Data { +private extension Data { init(rustBuffer: RustBuffer) { self.init( bytesNoCopy: rustBuffer.data!, @@ -72,15 +72,15 @@ fileprivate extension Data { // // Instead, the read() method and these helper functions input a tuple of data -fileprivate func createReader(data: Data) -> (data: Data, offset: Data.Index) { +private func createReader(data: Data) -> (data: Data, offset: Data.Index) { (data: data, offset: 0) } // Reads an integer at the current offset, in big-endian order, and advances // the offset on success. Throws if reading the integer would move the // offset past the end of the buffer. -fileprivate func readInt(_ reader: inout (data: Data, offset: Data.Index)) throws -> T { - let range = reader.offset...size +private func readInt(_ reader: inout (data: Data, offset: Data.Index)) throws -> T { + let range = reader.offset ..< reader.offset + MemoryLayout.size guard reader.data.count >= range.upperBound else { throw UniffiInternalError.bufferOverflow } @@ -90,38 +90,38 @@ fileprivate func readInt(_ reader: inout (data: Data, offs return value as! T } var value: T = 0 - let _ = withUnsafeMutableBytes(of: &value, { reader.data.copyBytes(to: $0, from: range)}) + let _ = withUnsafeMutableBytes(of: &value) { reader.data.copyBytes(to: $0, from: range) } reader.offset = range.upperBound return value.bigEndian } // Reads an arbitrary number of bytes, to be used to read // raw bytes, this is useful when lifting strings -fileprivate func readBytes(_ reader: inout (data: Data, offset: Data.Index), count: Int) throws -> Array { - let range = reader.offset..<(reader.offset+count) +private func readBytes(_ reader: inout (data: Data, offset: Data.Index), count: Int) throws -> [UInt8] { + let range = reader.offset ..< (reader.offset + count) guard reader.data.count >= range.upperBound else { throw UniffiInternalError.bufferOverflow } var value = [UInt8](repeating: 0, count: count) - value.withUnsafeMutableBufferPointer({ buffer in + value.withUnsafeMutableBufferPointer { buffer in reader.data.copyBytes(to: buffer, from: range) - }) + } reader.offset = range.upperBound return value } // Reads a float at the current offset. -fileprivate func readFloat(_ reader: inout (data: Data, offset: Data.Index)) throws -> Float { - return Float(bitPattern: try readInt(&reader)) +private func readFloat(_ reader: inout (data: Data, offset: Data.Index)) throws -> Float { + return try Float(bitPattern: readInt(&reader)) } // Reads a float at the current offset. -fileprivate func readDouble(_ reader: inout (data: Data, offset: Data.Index)) throws -> Double { - return Double(bitPattern: try readInt(&reader)) +private func readDouble(_ reader: inout (data: Data, offset: Data.Index)) throws -> Double { + return try Double(bitPattern: readInt(&reader)) } // Indicates if the offset has reached the end of the buffer. -fileprivate func hasRemaining(_ reader: (data: Data, offset: Data.Index)) -> Bool { +private func hasRemaining(_ reader: (data: Data, offset: Data.Index)) -> Bool { return reader.offset < reader.data.count } @@ -129,11 +129,11 @@ fileprivate func hasRemaining(_ reader: (data: Data, offset: Data.Index)) -> Boo // struct, but we use standalone functions instead in order to make external // types work. See the above discussion on Readers for details. -fileprivate func createWriter() -> [UInt8] { +private func createWriter() -> [UInt8] { return [] } -fileprivate func writeBytes(_ writer: inout [UInt8], _ byteArr: S) where S: Sequence, S.Element == UInt8 { +private func writeBytes(_ writer: inout [UInt8], _ byteArr: S) where S: Sequence, S.Element == UInt8 { writer.append(contentsOf: byteArr) } @@ -141,22 +141,22 @@ fileprivate func writeBytes(_ writer: inout [UInt8], _ byteArr: S) where S: S // // Warning: make sure what you are trying to write // is in the correct type! -fileprivate func writeInt(_ writer: inout [UInt8], _ value: T) { +private func writeInt(_ writer: inout [UInt8], _ value: T) { var value = value.bigEndian withUnsafeBytes(of: &value) { writer.append(contentsOf: $0) } } -fileprivate func writeFloat(_ writer: inout [UInt8], _ value: Float) { +private func writeFloat(_ writer: inout [UInt8], _ value: Float) { writeInt(&writer, value.bitPattern) } -fileprivate func writeDouble(_ writer: inout [UInt8], _ value: Double) { +private func writeDouble(_ writer: inout [UInt8], _ value: Double) { writeInt(&writer, value.bitPattern) } // Protocol for types that transfer other types across the FFI. This is // analogous to the Rust trait of the same name. -fileprivate protocol FfiConverter { +private protocol FfiConverter { associatedtype FfiType associatedtype SwiftType @@ -167,19 +167,19 @@ fileprivate protocol FfiConverter { } // Types conforming to `Primitive` pass themselves directly over the FFI. -fileprivate protocol FfiConverterPrimitive: FfiConverter where FfiType == SwiftType { } +private protocol FfiConverterPrimitive: FfiConverter where FfiType == SwiftType {} extension FfiConverterPrimitive { -#if swift(>=5.8) - @_documentation(visibility: private) -#endif + #if swift(>=5.8) + @_documentation(visibility: private) + #endif public static func lift(_ value: FfiType) throws -> SwiftType { return value } -#if swift(>=5.8) - @_documentation(visibility: private) -#endif + #if swift(>=5.8) + @_documentation(visibility: private) + #endif public static func lower(_ value: SwiftType) -> FfiType { return value } @@ -187,12 +187,12 @@ extension FfiConverterPrimitive { // Types conforming to `FfiConverterRustBuffer` lift and lower into a `RustBuffer`. // Used for complex types where it's hard to write a custom lift/lower. -fileprivate protocol FfiConverterRustBuffer: FfiConverter where FfiType == RustBuffer {} +private protocol FfiConverterRustBuffer: FfiConverter where FfiType == RustBuffer {} extension FfiConverterRustBuffer { -#if swift(>=5.8) - @_documentation(visibility: private) -#endif + #if swift(>=5.8) + @_documentation(visibility: private) + #endif public static func lift(_ buf: RustBuffer) throws -> SwiftType { var reader = createReader(data: Data(rustBuffer: buf)) let value = try read(from: &reader) @@ -203,18 +203,19 @@ extension FfiConverterRustBuffer { return value } -#if swift(>=5.8) - @_documentation(visibility: private) -#endif + #if swift(>=5.8) + @_documentation(visibility: private) + #endif public static func lower(_ value: SwiftType) -> RustBuffer { - var writer = createWriter() - write(value, into: &writer) - return RustBuffer(bytes: writer) + var writer = createWriter() + write(value, into: &writer) + return RustBuffer(bytes: writer) } } + // An error type for FFI errors. These errors occur at the UniFFI level, not // the library level. -fileprivate enum UniffiInternalError: LocalizedError { +private enum UniffiInternalError: LocalizedError { case bufferOverflow case incompleteData case unexpectedOptionalTag @@ -240,24 +241,24 @@ fileprivate enum UniffiInternalError: LocalizedError { } } -fileprivate extension NSLock { +private extension NSLock { func withLock(f: () throws -> T) rethrows -> T { - self.lock() + lock() defer { self.unlock() } return try f() } } -fileprivate let CALL_SUCCESS: Int8 = 0 -fileprivate let CALL_ERROR: Int8 = 1 -fileprivate let CALL_UNEXPECTED_ERROR: Int8 = 2 -fileprivate let CALL_CANCELLED: Int8 = 3 +private let CALL_SUCCESS: Int8 = 0 +private let CALL_ERROR: Int8 = 1 +private let CALL_UNEXPECTED_ERROR: Int8 = 2 +private let CALL_CANCELLED: Int8 = 3 -fileprivate extension RustCallStatus { +private extension RustCallStatus { init() { self.init( code: CALL_SUCCESS, - errorBuf: RustBuffer.init( + errorBuf: RustBuffer( capacity: 0, len: 0, data: nil @@ -273,7 +274,8 @@ private func rustCall(_ callback: (UnsafeMutablePointer) -> T private func rustCallWithError( _ errorHandler: @escaping (RustBuffer) throws -> E, - _ callback: (UnsafeMutablePointer) -> T) throws -> T { + _ callback: (UnsafeMutablePointer) -> T +) throws -> T { try makeRustCall(callback, errorHandler: errorHandler) } @@ -282,7 +284,7 @@ private func makeRustCall( errorHandler: ((RustBuffer) throws -> E)? ) throws -> T { uniffiEnsureInitialized() - var callStatus = RustCallStatus.init() + var callStatus = RustCallStatus() let returnedVal = callback(&callStatus) try uniffiCheckCallStatus(callStatus: callStatus, errorHandler: errorHandler) return returnedVal @@ -293,44 +295,44 @@ private func uniffiCheckCallStatus( errorHandler: ((RustBuffer) throws -> E)? ) throws { switch callStatus.code { - case CALL_SUCCESS: - return - - case CALL_ERROR: - if let errorHandler = errorHandler { - throw try errorHandler(callStatus.errorBuf) - } else { - callStatus.errorBuf.deallocate() - throw UniffiInternalError.unexpectedRustCallError - } + case CALL_SUCCESS: + return + + case CALL_ERROR: + if let errorHandler = errorHandler { + throw try errorHandler(callStatus.errorBuf) + } else { + callStatus.errorBuf.deallocate() + throw UniffiInternalError.unexpectedRustCallError + } - case CALL_UNEXPECTED_ERROR: - // When the rust code sees a panic, it tries to construct a RustBuffer - // with the message. But if that code panics, then it just sends back - // an empty buffer. - if callStatus.errorBuf.len > 0 { - throw UniffiInternalError.rustPanic(try FfiConverterString.lift(callStatus.errorBuf)) - } else { - callStatus.errorBuf.deallocate() - throw UniffiInternalError.rustPanic("Rust panic") - } + case CALL_UNEXPECTED_ERROR: + // When the rust code sees a panic, it tries to construct a RustBuffer + // with the message. But if that code panics, then it just sends back + // an empty buffer. + if callStatus.errorBuf.len > 0 { + throw try UniffiInternalError.rustPanic(FfiConverterString.lift(callStatus.errorBuf)) + } else { + callStatus.errorBuf.deallocate() + throw UniffiInternalError.rustPanic("Rust panic") + } - case CALL_CANCELLED: - fatalError("Cancellation not supported yet") + case CALL_CANCELLED: + fatalError("Cancellation not supported yet") - default: - throw UniffiInternalError.unexpectedRustCallStatusCode + default: + throw UniffiInternalError.unexpectedRustCallStatusCode } } private func uniffiTraitInterfaceCall( callStatus: UnsafeMutablePointer, makeCall: () throws -> T, - writeReturn: (T) -> () + writeReturn: (T) -> Void ) { do { try writeReturn(makeCall()) - } catch let error { + } catch { callStatus.pointee.code = CALL_UNEXPECTED_ERROR callStatus.pointee.errorBuf = FfiConverterString.lower(String(describing: error)) } @@ -339,7 +341,7 @@ private func uniffiTraitInterfaceCall( private func uniffiTraitInterfaceCallWithError( callStatus: UnsafeMutablePointer, makeCall: () throws -> T, - writeReturn: (T) -> (), + writeReturn: (T) -> Void, lowerError: (E) -> RustBuffer ) { do { @@ -352,7 +354,8 @@ private func uniffiTraitInterfaceCallWithError( callStatus.pointee.errorBuf = FfiConverterString.lower(String(describing: error)) } } -fileprivate class UniffiHandleMap { + +private class UniffiHandleMap { private var map: [UInt64: T] = [:] private let lock = NSLock() private var currentHandle: UInt64 = 1 @@ -366,7 +369,7 @@ fileprivate class UniffiHandleMap { } } - func get(handle: UInt64) throws -> T { + func get(handle: UInt64) throws -> T { try lock.withLock { guard let obj = map[handle] else { throw UniffiInternalError.unexpectedStaleHandle @@ -386,20 +389,16 @@ fileprivate class UniffiHandleMap { } var count: Int { - get { - map.count - } + map.count } } - // Public interface members begin here. - #if swift(>=5.8) -@_documentation(visibility: private) + @_documentation(visibility: private) #endif -fileprivate struct FfiConverterUInt32: FfiConverterPrimitive { +private struct FfiConverterUInt32: FfiConverterPrimitive { typealias FfiType = UInt32 typealias SwiftType = UInt32 @@ -413,9 +412,49 @@ fileprivate struct FfiConverterUInt32: FfiConverterPrimitive { } #if swift(>=5.8) -@_documentation(visibility: private) + @_documentation(visibility: private) +#endif +private struct FfiConverterInt64: FfiConverterPrimitive { + typealias FfiType = Int64 + typealias SwiftType = Int64 + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Int64 { + return try lift(readInt(&buf)) + } + + public static func write(_ value: Int64, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +private struct FfiConverterBool: FfiConverter { + typealias FfiType = Int8 + typealias SwiftType = Bool + + public static func lift(_ value: Int8) throws -> Bool { + return value != 0 + } + + public static func lower(_ value: Bool) -> Int8 { + return value ? 1 : 0 + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Bool { + return try lift(readInt(&buf)) + } + + public static func write(_ value: Bool, into buf: inout [UInt8]) { + writeInt(&buf, lower(value)) + } +} + +#if swift(>=5.8) + @_documentation(visibility: private) #endif -fileprivate struct FfiConverterString: FfiConverter { +private struct FfiConverterString: FfiConverter { typealias SwiftType = String typealias FfiType = RustBuffer @@ -443,7 +482,7 @@ fileprivate struct FfiConverterString: FfiConverter { public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> String { let len: Int32 = try readInt(&buf) - return String(bytes: try readBytes(&buf, count: Int(len)), encoding: String.Encoding.utf8)! + return try String(bytes: readBytes(&buf, count: Int(len)), encoding: String.Encoding.utf8)! } public static func write(_ value: String, into buf: inout [UInt8]) { @@ -452,31 +491,1566 @@ fileprivate struct FfiConverterString: FfiConverter { writeBytes(&buf, value.utf8) } } -public func hashcashMint(resource: String, bits: UInt32) -> String { - return try! FfiConverterString.lift(try! rustCall() { - uniffi_affine_mobile_native_fn_func_hashcash_mint( - FfiConverterString.lower(resource), - FfiConverterUInt32.lower(bits),$0 - ) -}) + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +private struct FfiConverterTimestamp: FfiConverterRustBuffer { + typealias SwiftType = Date + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Date { + let seconds: Int64 = try readInt(&buf) + let nanoseconds: UInt32 = try readInt(&buf) + if seconds >= 0 { + let delta = Double(seconds) + (Double(nanoseconds) / 1.0e9) + return Date(timeIntervalSince1970: delta) + } else { + let delta = Double(seconds) - (Double(nanoseconds) / 1.0e9) + return Date(timeIntervalSince1970: delta) + } + } + + public static func write(_ value: Date, into buf: inout [UInt8]) { + var delta = value.timeIntervalSince1970 + var sign: Int64 = 1 + if delta < 0 { + // The nanoseconds portion of the epoch offset must always be + // positive, to simplify the calculation we will use the absolute + // value of the offset. + sign = -1 + delta = -delta + } + if delta.rounded(.down) > Double(Int64.max) { + fatalError("Timestamp overflow, exceeds max bounds supported by Uniffi") + } + let seconds = Int64(delta) + let nanoseconds = UInt32((delta - Double(seconds)) * 1.0e9) + writeInt(&buf, sign * seconds) + writeInt(&buf, nanoseconds) + } } -private enum InitializationResult { - case ok - case contractVersionMismatch - case apiChecksumMismatch +public protocol DocStoragePoolProtocol: AnyObject { + func checkpoint(universalId: String) async throws + + func clearClocks(universalId: String) async throws + + func close(universalId: String) async throws + + /** + * Initialize the database and run migrations. + */ + func connect(universalId: String) async throws + + func deleteBlob(universalId: String, key: String, permanently: Bool) async throws + + func deleteDoc(universalId: String, docId: String) async throws + + func getBlob(universalId: String, key: String) async throws -> Blob? + + func getDocClock(universalId: String, docId: String) async throws -> DocClock? + + func getDocClocks(universalId: String, after: Date?) async throws -> [DocClock] + + func getDocSnapshot(universalId: String, docId: String) async throws -> DocRecord? + + func getDocUpdates(universalId: String, docId: String) async throws -> [DocUpdate] + + func getPeerPulledRemoteClock(universalId: String, peer: String, docId: String) async throws -> DocClock + + func getPeerPulledRemoteClocks(universalId: String, peer: String) async throws -> [DocClock] + + func getPeerPushedClocks(universalId: String, peer: String) async throws -> [DocClock] + + func getPeerRemoteClock(universalId: String, peer: String, docId: String) async throws -> DocClock + + func getPeerRemoteClocks(universalId: String, peer: String) async throws -> [DocClock] + + func isClosed(universalId: String) -> Bool + + func listBlobs(universalId: String) async throws -> [ListedBlob] + + func markUpdatesMerged(universalId: String, docId: String, updates: [Date]) async throws -> UInt32 + + func pushUpdate(universalId: String, docId: String, update: String) async throws -> Date + + func releaseBlobs(universalId: String) async throws + + func setBlob(universalId: String, blob: SetBlob) async throws + + func setDocSnapshot(universalId: String, snapshot: DocRecord) async throws -> Bool + + func setPeerPulledRemoteClock(universalId: String, peer: String, docId: String, clock: Date) async throws + + func setPeerPushedClock(universalId: String, peer: String, docId: String, clock: Date) async throws + + func setPeerRemoteClock(universalId: String, peer: String, docId: String, clock: Date) async throws + + func setSpaceId(universalId: String, spaceId: String) async throws + + func validate(universalId: String) async throws -> Bool } -// Use a global variable to perform the versioning checks. Swift ensures that -// the code inside is only computed once. -private var initializationResult: InitializationResult = { - // Get the bindings contract version from our ComponentInterface - let bindings_contract_version = 26 - // Get the scaffolding contract version by calling the into the dylib - let scaffolding_contract_version = ffi_affine_mobile_native_uniffi_contract_version() - if bindings_contract_version != scaffolding_contract_version { - return InitializationResult.contractVersionMismatch + +open class DocStoragePool: + DocStoragePoolProtocol +{ + fileprivate let pointer: UnsafeMutableRawPointer! + + /// Used to instantiate a [FFIObject] without an actual pointer, for fakes in tests, mostly. + #if swift(>=5.8) + @_documentation(visibility: private) + #endif + public struct NoPointer { + public init() {} + } + + // TODO: We'd like this to be `private` but for Swifty reasons, + // we can't implement `FfiConverter` without making this `required` and we can't + // make it `required` without making it `public`. + public required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + self.pointer = pointer + } + + // This constructor can be used to instantiate a fake object. + // - Parameter noPointer: Placeholder value so we can have a constructor separate from the default empty one that may be implemented for classes extending [FFIObject]. + // + // - Warning: + // Any object instantiated with this constructor cannot be passed to an actual Rust-backed object. Since there isn't a backing [Pointer] the FFI lower functions will crash. + #if swift(>=5.8) + @_documentation(visibility: private) + #endif + public init(noPointer _: NoPointer) { + pointer = nil + } + + #if swift(>=5.8) + @_documentation(visibility: private) + #endif + public func uniffiClonePointer() -> UnsafeMutableRawPointer { + return try! rustCall { uniffi_affine_mobile_native_fn_clone_docstoragepool(self.pointer, $0) } + } + + // No primary constructor declared for this class. + + deinit { + guard let pointer = pointer else { + return + } + + try! rustCall { uniffi_affine_mobile_native_fn_free_docstoragepool(pointer, $0) } + } + + open func checkpoint(universalId: String) async throws { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_affine_mobile_native_fn_method_docstoragepool_checkpoint( + self.uniffiClonePointer(), + FfiConverterString.lower(universalId) + ) + }, + pollFunc: ffi_affine_mobile_native_rust_future_poll_void, + completeFunc: ffi_affine_mobile_native_rust_future_complete_void, + freeFunc: ffi_affine_mobile_native_rust_future_free_void, + liftFunc: { $0 }, + errorHandler: FfiConverterTypeUniffiError.lift + ) + } + + open func clearClocks(universalId: String) async throws { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_affine_mobile_native_fn_method_docstoragepool_clear_clocks( + self.uniffiClonePointer(), + FfiConverterString.lower(universalId) + ) + }, + pollFunc: ffi_affine_mobile_native_rust_future_poll_void, + completeFunc: ffi_affine_mobile_native_rust_future_complete_void, + freeFunc: ffi_affine_mobile_native_rust_future_free_void, + liftFunc: { $0 }, + errorHandler: FfiConverterTypeUniffiError.lift + ) + } + + open func close(universalId: String) async throws { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_affine_mobile_native_fn_method_docstoragepool_close( + self.uniffiClonePointer(), + FfiConverterString.lower(universalId) + ) + }, + pollFunc: ffi_affine_mobile_native_rust_future_poll_void, + completeFunc: ffi_affine_mobile_native_rust_future_complete_void, + freeFunc: ffi_affine_mobile_native_rust_future_free_void, + liftFunc: { $0 }, + errorHandler: FfiConverterTypeUniffiError.lift + ) + } + + /** + * Initialize the database and run migrations. + */ + open func connect(universalId: String) async throws { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_affine_mobile_native_fn_method_docstoragepool_connect( + self.uniffiClonePointer(), + FfiConverterString.lower(universalId) + ) + }, + pollFunc: ffi_affine_mobile_native_rust_future_poll_void, + completeFunc: ffi_affine_mobile_native_rust_future_complete_void, + freeFunc: ffi_affine_mobile_native_rust_future_free_void, + liftFunc: { $0 }, + errorHandler: FfiConverterTypeUniffiError.lift + ) + } + + open func deleteBlob(universalId: String, key: String, permanently: Bool) async throws { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_affine_mobile_native_fn_method_docstoragepool_delete_blob( + self.uniffiClonePointer(), + FfiConverterString.lower(universalId), FfiConverterString.lower(key), FfiConverterBool.lower(permanently) + ) + }, + pollFunc: ffi_affine_mobile_native_rust_future_poll_void, + completeFunc: ffi_affine_mobile_native_rust_future_complete_void, + freeFunc: ffi_affine_mobile_native_rust_future_free_void, + liftFunc: { $0 }, + errorHandler: FfiConverterTypeUniffiError.lift + ) + } + + open func deleteDoc(universalId: String, docId: String) async throws { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_affine_mobile_native_fn_method_docstoragepool_delete_doc( + self.uniffiClonePointer(), + FfiConverterString.lower(universalId), FfiConverterString.lower(docId) + ) + }, + pollFunc: ffi_affine_mobile_native_rust_future_poll_void, + completeFunc: ffi_affine_mobile_native_rust_future_complete_void, + freeFunc: ffi_affine_mobile_native_rust_future_free_void, + liftFunc: { $0 }, + errorHandler: FfiConverterTypeUniffiError.lift + ) + } + + open func getBlob(universalId: String, key: String) async throws -> Blob? { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_affine_mobile_native_fn_method_docstoragepool_get_blob( + self.uniffiClonePointer(), + FfiConverterString.lower(universalId), FfiConverterString.lower(key) + ) + }, + pollFunc: ffi_affine_mobile_native_rust_future_poll_rust_buffer, + completeFunc: ffi_affine_mobile_native_rust_future_complete_rust_buffer, + freeFunc: ffi_affine_mobile_native_rust_future_free_rust_buffer, + liftFunc: FfiConverterOptionTypeBlob.lift, + errorHandler: FfiConverterTypeUniffiError.lift + ) + } + + open func getDocClock(universalId: String, docId: String) async throws -> DocClock? { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_affine_mobile_native_fn_method_docstoragepool_get_doc_clock( + self.uniffiClonePointer(), + FfiConverterString.lower(universalId), FfiConverterString.lower(docId) + ) + }, + pollFunc: ffi_affine_mobile_native_rust_future_poll_rust_buffer, + completeFunc: ffi_affine_mobile_native_rust_future_complete_rust_buffer, + freeFunc: ffi_affine_mobile_native_rust_future_free_rust_buffer, + liftFunc: FfiConverterOptionTypeDocClock.lift, + errorHandler: FfiConverterTypeUniffiError.lift + ) + } + + open func getDocClocks(universalId: String, after: Date?) async throws -> [DocClock] { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_affine_mobile_native_fn_method_docstoragepool_get_doc_clocks( + self.uniffiClonePointer(), + FfiConverterString.lower(universalId), FfiConverterOptionTimestamp.lower(after) + ) + }, + pollFunc: ffi_affine_mobile_native_rust_future_poll_rust_buffer, + completeFunc: ffi_affine_mobile_native_rust_future_complete_rust_buffer, + freeFunc: ffi_affine_mobile_native_rust_future_free_rust_buffer, + liftFunc: FfiConverterSequenceTypeDocClock.lift, + errorHandler: FfiConverterTypeUniffiError.lift + ) + } + + open func getDocSnapshot(universalId: String, docId: String) async throws -> DocRecord? { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_affine_mobile_native_fn_method_docstoragepool_get_doc_snapshot( + self.uniffiClonePointer(), + FfiConverterString.lower(universalId), FfiConverterString.lower(docId) + ) + }, + pollFunc: ffi_affine_mobile_native_rust_future_poll_rust_buffer, + completeFunc: ffi_affine_mobile_native_rust_future_complete_rust_buffer, + freeFunc: ffi_affine_mobile_native_rust_future_free_rust_buffer, + liftFunc: FfiConverterOptionTypeDocRecord.lift, + errorHandler: FfiConverterTypeUniffiError.lift + ) + } + + open func getDocUpdates(universalId: String, docId: String) async throws -> [DocUpdate] { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_affine_mobile_native_fn_method_docstoragepool_get_doc_updates( + self.uniffiClonePointer(), + FfiConverterString.lower(universalId), FfiConverterString.lower(docId) + ) + }, + pollFunc: ffi_affine_mobile_native_rust_future_poll_rust_buffer, + completeFunc: ffi_affine_mobile_native_rust_future_complete_rust_buffer, + freeFunc: ffi_affine_mobile_native_rust_future_free_rust_buffer, + liftFunc: FfiConverterSequenceTypeDocUpdate.lift, + errorHandler: FfiConverterTypeUniffiError.lift + ) + } + + open func getPeerPulledRemoteClock(universalId: String, peer: String, docId: String) async throws -> DocClock { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_affine_mobile_native_fn_method_docstoragepool_get_peer_pulled_remote_clock( + self.uniffiClonePointer(), + FfiConverterString.lower(universalId), FfiConverterString.lower(peer), FfiConverterString.lower(docId) + ) + }, + pollFunc: ffi_affine_mobile_native_rust_future_poll_rust_buffer, + completeFunc: ffi_affine_mobile_native_rust_future_complete_rust_buffer, + freeFunc: ffi_affine_mobile_native_rust_future_free_rust_buffer, + liftFunc: FfiConverterTypeDocClock.lift, + errorHandler: FfiConverterTypeUniffiError.lift + ) + } + + open func getPeerPulledRemoteClocks(universalId: String, peer: String) async throws -> [DocClock] { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_affine_mobile_native_fn_method_docstoragepool_get_peer_pulled_remote_clocks( + self.uniffiClonePointer(), + FfiConverterString.lower(universalId), FfiConverterString.lower(peer) + ) + }, + pollFunc: ffi_affine_mobile_native_rust_future_poll_rust_buffer, + completeFunc: ffi_affine_mobile_native_rust_future_complete_rust_buffer, + freeFunc: ffi_affine_mobile_native_rust_future_free_rust_buffer, + liftFunc: FfiConverterSequenceTypeDocClock.lift, + errorHandler: FfiConverterTypeUniffiError.lift + ) + } + + open func getPeerPushedClocks(universalId: String, peer: String) async throws -> [DocClock] { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_affine_mobile_native_fn_method_docstoragepool_get_peer_pushed_clocks( + self.uniffiClonePointer(), + FfiConverterString.lower(universalId), FfiConverterString.lower(peer) + ) + }, + pollFunc: ffi_affine_mobile_native_rust_future_poll_rust_buffer, + completeFunc: ffi_affine_mobile_native_rust_future_complete_rust_buffer, + freeFunc: ffi_affine_mobile_native_rust_future_free_rust_buffer, + liftFunc: FfiConverterSequenceTypeDocClock.lift, + errorHandler: FfiConverterTypeUniffiError.lift + ) + } + + open func getPeerRemoteClock(universalId: String, peer: String, docId: String) async throws -> DocClock { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_affine_mobile_native_fn_method_docstoragepool_get_peer_remote_clock( + self.uniffiClonePointer(), + FfiConverterString.lower(universalId), FfiConverterString.lower(peer), FfiConverterString.lower(docId) + ) + }, + pollFunc: ffi_affine_mobile_native_rust_future_poll_rust_buffer, + completeFunc: ffi_affine_mobile_native_rust_future_complete_rust_buffer, + freeFunc: ffi_affine_mobile_native_rust_future_free_rust_buffer, + liftFunc: FfiConverterTypeDocClock.lift, + errorHandler: FfiConverterTypeUniffiError.lift + ) + } + + open func getPeerRemoteClocks(universalId: String, peer: String) async throws -> [DocClock] { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_affine_mobile_native_fn_method_docstoragepool_get_peer_remote_clocks( + self.uniffiClonePointer(), + FfiConverterString.lower(universalId), FfiConverterString.lower(peer) + ) + }, + pollFunc: ffi_affine_mobile_native_rust_future_poll_rust_buffer, + completeFunc: ffi_affine_mobile_native_rust_future_complete_rust_buffer, + freeFunc: ffi_affine_mobile_native_rust_future_free_rust_buffer, + liftFunc: FfiConverterSequenceTypeDocClock.lift, + errorHandler: FfiConverterTypeUniffiError.lift + ) + } + + open func isClosed(universalId: String) -> Bool { + return try! FfiConverterBool.lift(try! rustCall { + uniffi_affine_mobile_native_fn_method_docstoragepool_is_closed(self.uniffiClonePointer(), + FfiConverterString.lower(universalId), $0) + }) + } + + open func listBlobs(universalId: String) async throws -> [ListedBlob] { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_affine_mobile_native_fn_method_docstoragepool_list_blobs( + self.uniffiClonePointer(), + FfiConverterString.lower(universalId) + ) + }, + pollFunc: ffi_affine_mobile_native_rust_future_poll_rust_buffer, + completeFunc: ffi_affine_mobile_native_rust_future_complete_rust_buffer, + freeFunc: ffi_affine_mobile_native_rust_future_free_rust_buffer, + liftFunc: FfiConverterSequenceTypeListedBlob.lift, + errorHandler: FfiConverterTypeUniffiError.lift + ) + } + + open func markUpdatesMerged(universalId: String, docId: String, updates: [Date]) async throws -> UInt32 { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_affine_mobile_native_fn_method_docstoragepool_mark_updates_merged( + self.uniffiClonePointer(), + FfiConverterString.lower(universalId), FfiConverterString.lower(docId), FfiConverterSequenceTimestamp.lower(updates) + ) + }, + pollFunc: ffi_affine_mobile_native_rust_future_poll_u32, + completeFunc: ffi_affine_mobile_native_rust_future_complete_u32, + freeFunc: ffi_affine_mobile_native_rust_future_free_u32, + liftFunc: FfiConverterUInt32.lift, + errorHandler: FfiConverterTypeUniffiError.lift + ) + } + + open func pushUpdate(universalId: String, docId: String, update: String) async throws -> Date { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_affine_mobile_native_fn_method_docstoragepool_push_update( + self.uniffiClonePointer(), + FfiConverterString.lower(universalId), FfiConverterString.lower(docId), FfiConverterString.lower(update) + ) + }, + pollFunc: ffi_affine_mobile_native_rust_future_poll_rust_buffer, + completeFunc: ffi_affine_mobile_native_rust_future_complete_rust_buffer, + freeFunc: ffi_affine_mobile_native_rust_future_free_rust_buffer, + liftFunc: FfiConverterTimestamp.lift, + errorHandler: FfiConverterTypeUniffiError.lift + ) + } + + open func releaseBlobs(universalId: String) async throws { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_affine_mobile_native_fn_method_docstoragepool_release_blobs( + self.uniffiClonePointer(), + FfiConverterString.lower(universalId) + ) + }, + pollFunc: ffi_affine_mobile_native_rust_future_poll_void, + completeFunc: ffi_affine_mobile_native_rust_future_complete_void, + freeFunc: ffi_affine_mobile_native_rust_future_free_void, + liftFunc: { $0 }, + errorHandler: FfiConverterTypeUniffiError.lift + ) + } + + open func setBlob(universalId: String, blob: SetBlob) async throws { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_affine_mobile_native_fn_method_docstoragepool_set_blob( + self.uniffiClonePointer(), + FfiConverterString.lower(universalId), FfiConverterTypeSetBlob.lower(blob) + ) + }, + pollFunc: ffi_affine_mobile_native_rust_future_poll_void, + completeFunc: ffi_affine_mobile_native_rust_future_complete_void, + freeFunc: ffi_affine_mobile_native_rust_future_free_void, + liftFunc: { $0 }, + errorHandler: FfiConverterTypeUniffiError.lift + ) + } + + open func setDocSnapshot(universalId: String, snapshot: DocRecord) async throws -> Bool { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_affine_mobile_native_fn_method_docstoragepool_set_doc_snapshot( + self.uniffiClonePointer(), + FfiConverterString.lower(universalId), FfiConverterTypeDocRecord.lower(snapshot) + ) + }, + pollFunc: ffi_affine_mobile_native_rust_future_poll_i8, + completeFunc: ffi_affine_mobile_native_rust_future_complete_i8, + freeFunc: ffi_affine_mobile_native_rust_future_free_i8, + liftFunc: FfiConverterBool.lift, + errorHandler: FfiConverterTypeUniffiError.lift + ) + } + + open func setPeerPulledRemoteClock(universalId: String, peer: String, docId: String, clock: Date) async throws { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_affine_mobile_native_fn_method_docstoragepool_set_peer_pulled_remote_clock( + self.uniffiClonePointer(), + FfiConverterString.lower(universalId), FfiConverterString.lower(peer), FfiConverterString.lower(docId), FfiConverterTimestamp.lower(clock) + ) + }, + pollFunc: ffi_affine_mobile_native_rust_future_poll_void, + completeFunc: ffi_affine_mobile_native_rust_future_complete_void, + freeFunc: ffi_affine_mobile_native_rust_future_free_void, + liftFunc: { $0 }, + errorHandler: FfiConverterTypeUniffiError.lift + ) + } + + open func setPeerPushedClock(universalId: String, peer: String, docId: String, clock: Date) async throws { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_affine_mobile_native_fn_method_docstoragepool_set_peer_pushed_clock( + self.uniffiClonePointer(), + FfiConverterString.lower(universalId), FfiConverterString.lower(peer), FfiConverterString.lower(docId), FfiConverterTimestamp.lower(clock) + ) + }, + pollFunc: ffi_affine_mobile_native_rust_future_poll_void, + completeFunc: ffi_affine_mobile_native_rust_future_complete_void, + freeFunc: ffi_affine_mobile_native_rust_future_free_void, + liftFunc: { $0 }, + errorHandler: FfiConverterTypeUniffiError.lift + ) + } + + open func setPeerRemoteClock(universalId: String, peer: String, docId: String, clock: Date) async throws { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_affine_mobile_native_fn_method_docstoragepool_set_peer_remote_clock( + self.uniffiClonePointer(), + FfiConverterString.lower(universalId), FfiConverterString.lower(peer), FfiConverterString.lower(docId), FfiConverterTimestamp.lower(clock) + ) + }, + pollFunc: ffi_affine_mobile_native_rust_future_poll_void, + completeFunc: ffi_affine_mobile_native_rust_future_complete_void, + freeFunc: ffi_affine_mobile_native_rust_future_free_void, + liftFunc: { $0 }, + errorHandler: FfiConverterTypeUniffiError.lift + ) + } + + open func setSpaceId(universalId: String, spaceId: String) async throws { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_affine_mobile_native_fn_method_docstoragepool_set_space_id( + self.uniffiClonePointer(), + FfiConverterString.lower(universalId), FfiConverterString.lower(spaceId) + ) + }, + pollFunc: ffi_affine_mobile_native_rust_future_poll_void, + completeFunc: ffi_affine_mobile_native_rust_future_complete_void, + freeFunc: ffi_affine_mobile_native_rust_future_free_void, + liftFunc: { $0 }, + errorHandler: FfiConverterTypeUniffiError.lift + ) + } + + open func validate(universalId: String) async throws -> Bool { + return + try await uniffiRustCallAsync( + rustFutureFunc: { + uniffi_affine_mobile_native_fn_method_docstoragepool_validate( + self.uniffiClonePointer(), + FfiConverterString.lower(universalId) + ) + }, + pollFunc: ffi_affine_mobile_native_rust_future_poll_i8, + completeFunc: ffi_affine_mobile_native_rust_future_complete_i8, + freeFunc: ffi_affine_mobile_native_rust_future_free_i8, + liftFunc: FfiConverterBool.lift, + errorHandler: FfiConverterTypeUniffiError.lift + ) + } +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +public struct FfiConverterTypeDocStoragePool: FfiConverter { + typealias FfiType = UnsafeMutableRawPointer + typealias SwiftType = DocStoragePool + + public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> DocStoragePool { + return DocStoragePool(unsafeFromRawPointer: pointer) + } + + public static func lower(_ value: DocStoragePool) -> UnsafeMutableRawPointer { + return value.uniffiClonePointer() + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> DocStoragePool { + let v: UInt64 = try readInt(&buf) + // The Rust code won't compile if a pointer won't fit in a UInt64. + // We have to go via `UInt` because that's the thing that's the size of a pointer. + let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: v)) + if ptr == nil { + throw UniffiInternalError.unexpectedNullPointer + } + return try lift(ptr!) + } + + public static func write(_ value: DocStoragePool, into buf: inout [UInt8]) { + // This fiddling is because `Int` is the thing that's the same size as a pointer. + // The Rust code won't compile if a pointer won't fit in a `UInt64`. + writeInt(&buf, UInt64(bitPattern: Int64(Int(bitPattern: lower(value))))) + } +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +public func FfiConverterTypeDocStoragePool_lift(_ pointer: UnsafeMutableRawPointer) throws -> DocStoragePool { + return try FfiConverterTypeDocStoragePool.lift(pointer) +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +public func FfiConverterTypeDocStoragePool_lower(_ value: DocStoragePool) -> UnsafeMutableRawPointer { + return FfiConverterTypeDocStoragePool.lower(value) +} + +public struct Blob { + public var key: String + public var data: String + public var mime: String + public var size: Int64 + public var createdAt: Date + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(key: String, data: String, mime: String, size: Int64, createdAt: Date) { + self.key = key + self.data = data + self.mime = mime + self.size = size + self.createdAt = createdAt + } +} + +extension Blob: Equatable, Hashable { + public static func == (lhs: Blob, rhs: Blob) -> Bool { + if lhs.key != rhs.key { + return false + } + if lhs.data != rhs.data { + return false + } + if lhs.mime != rhs.mime { + return false + } + if lhs.size != rhs.size { + return false + } + if lhs.createdAt != rhs.createdAt { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(key) + hasher.combine(data) + hasher.combine(mime) + hasher.combine(size) + hasher.combine(createdAt) + } +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +public struct FfiConverterTypeBlob: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Blob { + return + try Blob( + key: FfiConverterString.read(from: &buf), + data: FfiConverterString.read(from: &buf), + mime: FfiConverterString.read(from: &buf), + size: FfiConverterInt64.read(from: &buf), + createdAt: FfiConverterTimestamp.read(from: &buf) + ) + } + + public static func write(_ value: Blob, into buf: inout [UInt8]) { + FfiConverterString.write(value.key, into: &buf) + FfiConverterString.write(value.data, into: &buf) + FfiConverterString.write(value.mime, into: &buf) + FfiConverterInt64.write(value.size, into: &buf) + FfiConverterTimestamp.write(value.createdAt, into: &buf) + } +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +public func FfiConverterTypeBlob_lift(_ buf: RustBuffer) throws -> Blob { + return try FfiConverterTypeBlob.lift(buf) +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +public func FfiConverterTypeBlob_lower(_ value: Blob) -> RustBuffer { + return FfiConverterTypeBlob.lower(value) +} + +public struct DocClock { + public var docId: String + public var timestamp: Date + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(docId: String, timestamp: Date) { + self.docId = docId + self.timestamp = timestamp + } +} + +extension DocClock: Equatable, Hashable { + public static func == (lhs: DocClock, rhs: DocClock) -> Bool { + if lhs.docId != rhs.docId { + return false + } + if lhs.timestamp != rhs.timestamp { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(docId) + hasher.combine(timestamp) + } +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +public struct FfiConverterTypeDocClock: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> DocClock { + return + try DocClock( + docId: FfiConverterString.read(from: &buf), + timestamp: FfiConverterTimestamp.read(from: &buf) + ) + } + + public static func write(_ value: DocClock, into buf: inout [UInt8]) { + FfiConverterString.write(value.docId, into: &buf) + FfiConverterTimestamp.write(value.timestamp, into: &buf) + } +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +public func FfiConverterTypeDocClock_lift(_ buf: RustBuffer) throws -> DocClock { + return try FfiConverterTypeDocClock.lift(buf) +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +public func FfiConverterTypeDocClock_lower(_ value: DocClock) -> RustBuffer { + return FfiConverterTypeDocClock.lower(value) +} + +public struct DocRecord { + public var docId: String + public var data: String + public var timestamp: Date + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(docId: String, data: String, timestamp: Date) { + self.docId = docId + self.data = data + self.timestamp = timestamp + } +} + +extension DocRecord: Equatable, Hashable { + public static func == (lhs: DocRecord, rhs: DocRecord) -> Bool { + if lhs.docId != rhs.docId { + return false + } + if lhs.data != rhs.data { + return false + } + if lhs.timestamp != rhs.timestamp { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(docId) + hasher.combine(data) + hasher.combine(timestamp) + } +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +public struct FfiConverterTypeDocRecord: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> DocRecord { + return + try DocRecord( + docId: FfiConverterString.read(from: &buf), + data: FfiConverterString.read(from: &buf), + timestamp: FfiConverterTimestamp.read(from: &buf) + ) + } + + public static func write(_ value: DocRecord, into buf: inout [UInt8]) { + FfiConverterString.write(value.docId, into: &buf) + FfiConverterString.write(value.data, into: &buf) + FfiConverterTimestamp.write(value.timestamp, into: &buf) + } +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +public func FfiConverterTypeDocRecord_lift(_ buf: RustBuffer) throws -> DocRecord { + return try FfiConverterTypeDocRecord.lift(buf) +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +public func FfiConverterTypeDocRecord_lower(_ value: DocRecord) -> RustBuffer { + return FfiConverterTypeDocRecord.lower(value) +} + +public struct DocUpdate { + public var docId: String + public var createdAt: Date + public var data: String + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(docId: String, createdAt: Date, data: String) { + self.docId = docId + self.createdAt = createdAt + self.data = data + } +} + +extension DocUpdate: Equatable, Hashable { + public static func == (lhs: DocUpdate, rhs: DocUpdate) -> Bool { + if lhs.docId != rhs.docId { + return false + } + if lhs.createdAt != rhs.createdAt { + return false + } + if lhs.data != rhs.data { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(docId) + hasher.combine(createdAt) + hasher.combine(data) + } +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +public struct FfiConverterTypeDocUpdate: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> DocUpdate { + return + try DocUpdate( + docId: FfiConverterString.read(from: &buf), + createdAt: FfiConverterTimestamp.read(from: &buf), + data: FfiConverterString.read(from: &buf) + ) + } + + public static func write(_ value: DocUpdate, into buf: inout [UInt8]) { + FfiConverterString.write(value.docId, into: &buf) + FfiConverterTimestamp.write(value.createdAt, into: &buf) + FfiConverterString.write(value.data, into: &buf) + } +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +public func FfiConverterTypeDocUpdate_lift(_ buf: RustBuffer) throws -> DocUpdate { + return try FfiConverterTypeDocUpdate.lift(buf) +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +public func FfiConverterTypeDocUpdate_lower(_ value: DocUpdate) -> RustBuffer { + return FfiConverterTypeDocUpdate.lower(value) +} + +public struct ListedBlob { + public var key: String + public var size: Int64 + public var mime: String + public var createdAt: Date + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(key: String, size: Int64, mime: String, createdAt: Date) { + self.key = key + self.size = size + self.mime = mime + self.createdAt = createdAt + } +} + +extension ListedBlob: Equatable, Hashable { + public static func == (lhs: ListedBlob, rhs: ListedBlob) -> Bool { + if lhs.key != rhs.key { + return false + } + if lhs.size != rhs.size { + return false + } + if lhs.mime != rhs.mime { + return false + } + if lhs.createdAt != rhs.createdAt { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(key) + hasher.combine(size) + hasher.combine(mime) + hasher.combine(createdAt) + } +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +public struct FfiConverterTypeListedBlob: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ListedBlob { + return + try ListedBlob( + key: FfiConverterString.read(from: &buf), + size: FfiConverterInt64.read(from: &buf), + mime: FfiConverterString.read(from: &buf), + createdAt: FfiConverterTimestamp.read(from: &buf) + ) + } + + public static func write(_ value: ListedBlob, into buf: inout [UInt8]) { + FfiConverterString.write(value.key, into: &buf) + FfiConverterInt64.write(value.size, into: &buf) + FfiConverterString.write(value.mime, into: &buf) + FfiConverterTimestamp.write(value.createdAt, into: &buf) + } +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +public func FfiConverterTypeListedBlob_lift(_ buf: RustBuffer) throws -> ListedBlob { + return try FfiConverterTypeListedBlob.lift(buf) +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +public func FfiConverterTypeListedBlob_lower(_ value: ListedBlob) -> RustBuffer { + return FfiConverterTypeListedBlob.lower(value) +} + +public struct SetBlob { + public var key: String + public var data: String + public var mime: String + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(key: String, data: String, mime: String) { + self.key = key + self.data = data + self.mime = mime + } +} + +extension SetBlob: Equatable, Hashable { + public static func == (lhs: SetBlob, rhs: SetBlob) -> Bool { + if lhs.key != rhs.key { + return false + } + if lhs.data != rhs.data { + return false + } + if lhs.mime != rhs.mime { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(key) + hasher.combine(data) + hasher.combine(mime) + } +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +public struct FfiConverterTypeSetBlob: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SetBlob { + return + try SetBlob( + key: FfiConverterString.read(from: &buf), + data: FfiConverterString.read(from: &buf), + mime: FfiConverterString.read(from: &buf) + ) + } + + public static func write(_ value: SetBlob, into buf: inout [UInt8]) { + FfiConverterString.write(value.key, into: &buf) + FfiConverterString.write(value.data, into: &buf) + FfiConverterString.write(value.mime, into: &buf) + } +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +public func FfiConverterTypeSetBlob_lift(_ buf: RustBuffer) throws -> SetBlob { + return try FfiConverterTypeSetBlob.lift(buf) +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +public func FfiConverterTypeSetBlob_lower(_ value: SetBlob) -> RustBuffer { + return FfiConverterTypeSetBlob.lower(value) +} + +public enum UniffiError { + case GetUserDocumentDirectoryFailed + case CreateAffineDirFailed(String + ) + case EmptyDocStoragePath + case EmptySpaceId + case SqlxError(String + ) + case Base64DecodingError(String + ) + case InvalidUniversalId(String + ) + case InvalidSpaceType(String + ) + case ConcatSpaceDirFailed(String + ) +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +public struct FfiConverterTypeUniffiError: FfiConverterRustBuffer { + typealias SwiftType = UniffiError + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UniffiError { + let variant: Int32 = try readInt(&buf) + switch variant { + case 1: return .GetUserDocumentDirectoryFailed + case 2: return try .CreateAffineDirFailed( + FfiConverterString.read(from: &buf) + ) + case 3: return .EmptyDocStoragePath + case 4: return .EmptySpaceId + case 5: return try .SqlxError( + FfiConverterString.read(from: &buf) + ) + case 6: return try .Base64DecodingError( + FfiConverterString.read(from: &buf) + ) + case 7: return try .InvalidUniversalId( + FfiConverterString.read(from: &buf) + ) + case 8: return try .InvalidSpaceType( + FfiConverterString.read(from: &buf) + ) + case 9: return try .ConcatSpaceDirFailed( + FfiConverterString.read(from: &buf) + ) + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: UniffiError, into buf: inout [UInt8]) { + switch value { + case .GetUserDocumentDirectoryFailed: + writeInt(&buf, Int32(1)) + + case let .CreateAffineDirFailed(v1): + writeInt(&buf, Int32(2)) + FfiConverterString.write(v1, into: &buf) + + case .EmptyDocStoragePath: + writeInt(&buf, Int32(3)) + + case .EmptySpaceId: + writeInt(&buf, Int32(4)) + + case let .SqlxError(v1): + writeInt(&buf, Int32(5)) + FfiConverterString.write(v1, into: &buf) + + case let .Base64DecodingError(v1): + writeInt(&buf, Int32(6)) + FfiConverterString.write(v1, into: &buf) + + case let .InvalidUniversalId(v1): + writeInt(&buf, Int32(7)) + FfiConverterString.write(v1, into: &buf) + + case let .InvalidSpaceType(v1): + writeInt(&buf, Int32(8)) + FfiConverterString.write(v1, into: &buf) + + case let .ConcatSpaceDirFailed(v1): + writeInt(&buf, Int32(9)) + FfiConverterString.write(v1, into: &buf) + } + } +} + +extension UniffiError: Equatable, Hashable {} + +extension UniffiError: Foundation.LocalizedError { + public var errorDescription: String? { + String(reflecting: self) + } +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +private struct FfiConverterOptionTimestamp: FfiConverterRustBuffer { + typealias SwiftType = Date? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTimestamp.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTimestamp.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +private struct FfiConverterOptionTypeBlob: FfiConverterRustBuffer { + typealias SwiftType = Blob? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTypeBlob.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTypeBlob.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +private struct FfiConverterOptionTypeDocClock: FfiConverterRustBuffer { + typealias SwiftType = DocClock? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTypeDocClock.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTypeDocClock.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +private struct FfiConverterOptionTypeDocRecord: FfiConverterRustBuffer { + typealias SwiftType = DocRecord? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTypeDocRecord.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTypeDocRecord.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +private struct FfiConverterSequenceTimestamp: FfiConverterRustBuffer { + typealias SwiftType = [Date] + + public static func write(_ value: [Date], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterTimestamp.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [Date] { + let len: Int32 = try readInt(&buf) + var seq = [Date]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + try seq.append(FfiConverterTimestamp.read(from: &buf)) + } + return seq + } +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +private struct FfiConverterSequenceTypeDocClock: FfiConverterRustBuffer { + typealias SwiftType = [DocClock] + + public static func write(_ value: [DocClock], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterTypeDocClock.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [DocClock] { + let len: Int32 = try readInt(&buf) + var seq = [DocClock]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + try seq.append(FfiConverterTypeDocClock.read(from: &buf)) + } + return seq + } +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +private struct FfiConverterSequenceTypeDocUpdate: FfiConverterRustBuffer { + typealias SwiftType = [DocUpdate] + + public static func write(_ value: [DocUpdate], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterTypeDocUpdate.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [DocUpdate] { + let len: Int32 = try readInt(&buf) + var seq = [DocUpdate]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + try seq.append(FfiConverterTypeDocUpdate.read(from: &buf)) + } + return seq + } +} + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif +private struct FfiConverterSequenceTypeListedBlob: FfiConverterRustBuffer { + typealias SwiftType = [ListedBlob] + + public static func write(_ value: [ListedBlob], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterTypeListedBlob.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [ListedBlob] { + let len: Int32 = try readInt(&buf) + var seq = [ListedBlob]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + try seq.append(FfiConverterTypeListedBlob.read(from: &buf)) + } + return seq + } +} + +private let UNIFFI_RUST_FUTURE_POLL_READY: Int8 = 0 +private let UNIFFI_RUST_FUTURE_POLL_MAYBE_READY: Int8 = 1 + +private let uniffiContinuationHandleMap = UniffiHandleMap>() + +private func uniffiRustCallAsync( + rustFutureFunc: () -> UInt64, + pollFunc: (UInt64, @escaping UniffiRustFutureContinuationCallback, UInt64) -> Void, + completeFunc: (UInt64, UnsafeMutablePointer) -> F, + freeFunc: (UInt64) -> Void, + liftFunc: (F) throws -> T, + errorHandler: ((RustBuffer) throws -> Swift.Error)? +) async throws -> T { + // Make sure to call uniffiEnsureInitialized() since future creation doesn't have a + // RustCallStatus param, so doesn't use makeRustCall() + uniffiEnsureInitialized() + let rustFuture = rustFutureFunc() + defer { + freeFunc(rustFuture) + } + var pollResult: Int8 + repeat { + pollResult = await withUnsafeContinuation { + pollFunc( + rustFuture, + uniffiFutureContinuationCallback, + uniffiContinuationHandleMap.insert(obj: $0) + ) + } + } while pollResult != UNIFFI_RUST_FUTURE_POLL_READY + + return try liftFunc(makeRustCall( + { completeFunc(rustFuture, $0) }, + errorHandler: errorHandler + )) +} + +// Callback handlers for an async calls. These are invoked by Rust when the future is ready. They +// lift the return value or error and resume the suspended function. +private func uniffiFutureContinuationCallback(handle: UInt64, pollResult: Int8) { + if let continuation = try? uniffiContinuationHandleMap.remove(handle: handle) { + continuation.resume(returning: pollResult) + } else { + print("uniffiFutureContinuationCallback invalid handle") + } +} + +public func getDbPath(peer: String, spaceType: String, id: String) throws -> String { + return try FfiConverterString.lift(rustCallWithError(FfiConverterTypeUniffiError.lift) { + uniffi_affine_mobile_native_fn_func_get_db_path( + FfiConverterString.lower(peer), + FfiConverterString.lower(spaceType), + FfiConverterString.lower(id), $0 + ) + }) +} + +public func hashcashMint(resource: String, bits: UInt32) -> String { + return try! FfiConverterString.lift(try! rustCall { + uniffi_affine_mobile_native_fn_func_hashcash_mint( + FfiConverterString.lower(resource), + FfiConverterUInt32.lower(bits), $0 + ) + }) +} + +private enum InitializationResult { + case ok + case contractVersionMismatch + case apiChecksumMismatch +} + +// Use a global variable to perform the versioning checks. Swift ensures that +// the code inside is only computed once. +private var initializationResult: InitializationResult = { + // Get the bindings contract version from our ComponentInterface + let bindings_contract_version = 26 + // Get the scaffolding contract version by calling the into the dylib + let scaffolding_contract_version = ffi_affine_mobile_native_uniffi_contract_version() + if bindings_contract_version != scaffolding_contract_version { + return InitializationResult.contractVersionMismatch + } + if uniffi_affine_mobile_native_checksum_func_get_db_path() != 65350 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_affine_mobile_native_checksum_func_hashcash_mint() != 23633 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_affine_mobile_native_checksum_method_docstoragepool_checkpoint() != 36299 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_affine_mobile_native_checksum_method_docstoragepool_clear_clocks() != 51151 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_affine_mobile_native_checksum_method_docstoragepool_close() != 46846 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_affine_mobile_native_checksum_method_docstoragepool_connect() != 57961 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_affine_mobile_native_checksum_method_docstoragepool_delete_blob() != 53695 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_affine_mobile_native_checksum_method_docstoragepool_delete_doc() != 4005 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_affine_mobile_native_checksum_method_docstoragepool_get_blob() != 56927 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_affine_mobile_native_checksum_method_docstoragepool_get_doc_clock() != 48394 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_affine_mobile_native_checksum_method_docstoragepool_get_doc_clocks() != 23822 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_affine_mobile_native_checksum_method_docstoragepool_get_doc_snapshot() != 31220 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_affine_mobile_native_checksum_method_docstoragepool_get_doc_updates() != 65430 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_affine_mobile_native_checksum_method_docstoragepool_get_peer_pulled_remote_clock() != 40122 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_affine_mobile_native_checksum_method_docstoragepool_get_peer_pulled_remote_clocks() != 13441 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_affine_mobile_native_checksum_method_docstoragepool_get_peer_pushed_clocks() != 47148 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_affine_mobile_native_checksum_method_docstoragepool_get_peer_remote_clock() != 17458 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_affine_mobile_native_checksum_method_docstoragepool_get_peer_remote_clocks() != 14523 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_affine_mobile_native_checksum_method_docstoragepool_is_closed() != 40091 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_affine_mobile_native_checksum_method_docstoragepool_list_blobs() != 6777 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_affine_mobile_native_checksum_method_docstoragepool_mark_updates_merged() != 26982 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_affine_mobile_native_checksum_method_docstoragepool_push_update() != 54572 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_affine_mobile_native_checksum_method_docstoragepool_release_blobs() != 2203 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_affine_mobile_native_checksum_method_docstoragepool_set_blob() != 31398 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_affine_mobile_native_checksum_method_docstoragepool_set_doc_snapshot() != 5287 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_affine_mobile_native_checksum_method_docstoragepool_set_peer_pulled_remote_clock() != 40733 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_affine_mobile_native_checksum_method_docstoragepool_set_peer_pushed_clock() != 15697 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_affine_mobile_native_checksum_method_docstoragepool_set_peer_remote_clock() != 57108 { + return InitializationResult.apiChecksumMismatch + } + if uniffi_affine_mobile_native_checksum_method_docstoragepool_set_space_id() != 21955 { + return InitializationResult.apiChecksumMismatch } - if (uniffi_affine_mobile_native_checksum_func_hashcash_mint() != 23633) { + if uniffi_affine_mobile_native_checksum_method_docstoragepool_validate() != 17232 { return InitializationResult.apiChecksumMismatch } @@ -494,4 +2068,4 @@ private func uniffiEnsureInitialized() { } } -// swiftlint:enable all \ No newline at end of file +// swiftlint:enable all diff --git a/packages/frontend/apps/ios/App/App/uniffi/affine_mobile_nativeFFI.h b/packages/frontend/apps/ios/App/App/uniffi/affine_mobile_nativeFFI.h index 127e443db819d..05519cd029fd5 100644 --- a/packages/frontend/apps/ios/App/App/uniffi/affine_mobile_nativeFFI.h +++ b/packages/frontend/apps/ios/App/App/uniffi/affine_mobile_nativeFFI.h @@ -250,6 +250,161 @@ typedef struct UniffiForeignFutureStructVoid { typedef void (*UniffiForeignFutureCompleteVoid)(uint64_t, UniffiForeignFutureStructVoid ); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_CLONE_DOCSTORAGEPOOL +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_CLONE_DOCSTORAGEPOOL +void*_Nonnull uniffi_affine_mobile_native_fn_clone_docstoragepool(void*_Nonnull ptr, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_FREE_DOCSTORAGEPOOL +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_FREE_DOCSTORAGEPOOL +void uniffi_affine_mobile_native_fn_free_docstoragepool(void*_Nonnull ptr, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_CHECKPOINT +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_CHECKPOINT +uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_checkpoint(void*_Nonnull ptr, RustBuffer universal_id +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_CLEAR_CLOCKS +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_CLEAR_CLOCKS +uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_clear_clocks(void*_Nonnull ptr, RustBuffer universal_id +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_CLOSE +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_CLOSE +uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_close(void*_Nonnull ptr, RustBuffer universal_id +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_CONNECT +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_CONNECT +uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_connect(void*_Nonnull ptr, RustBuffer universal_id +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_DELETE_BLOB +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_DELETE_BLOB +uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_delete_blob(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer key, int8_t permanently +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_DELETE_DOC +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_DELETE_DOC +uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_delete_doc(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer doc_id +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_GET_BLOB +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_GET_BLOB +uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_get_blob(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer key +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_GET_DOC_CLOCK +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_GET_DOC_CLOCK +uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_get_doc_clock(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer doc_id +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_GET_DOC_CLOCKS +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_GET_DOC_CLOCKS +uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_get_doc_clocks(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer after +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_GET_DOC_SNAPSHOT +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_GET_DOC_SNAPSHOT +uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_get_doc_snapshot(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer doc_id +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_GET_DOC_UPDATES +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_GET_DOC_UPDATES +uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_get_doc_updates(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer doc_id +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_GET_PEER_PULLED_REMOTE_CLOCK +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_GET_PEER_PULLED_REMOTE_CLOCK +uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_get_peer_pulled_remote_clock(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer peer, RustBuffer doc_id +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_GET_PEER_PULLED_REMOTE_CLOCKS +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_GET_PEER_PULLED_REMOTE_CLOCKS +uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_get_peer_pulled_remote_clocks(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer peer +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_GET_PEER_PUSHED_CLOCKS +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_GET_PEER_PUSHED_CLOCKS +uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_get_peer_pushed_clocks(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer peer +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_GET_PEER_REMOTE_CLOCK +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_GET_PEER_REMOTE_CLOCK +uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_get_peer_remote_clock(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer peer, RustBuffer doc_id +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_GET_PEER_REMOTE_CLOCKS +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_GET_PEER_REMOTE_CLOCKS +uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_get_peer_remote_clocks(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer peer +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_IS_CLOSED +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_IS_CLOSED +int8_t uniffi_affine_mobile_native_fn_method_docstoragepool_is_closed(void*_Nonnull ptr, RustBuffer universal_id, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_LIST_BLOBS +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_LIST_BLOBS +uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_list_blobs(void*_Nonnull ptr, RustBuffer universal_id +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_MARK_UPDATES_MERGED +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_MARK_UPDATES_MERGED +uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_mark_updates_merged(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer doc_id, RustBuffer updates +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_PUSH_UPDATE +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_PUSH_UPDATE +uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_push_update(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer doc_id, RustBuffer update +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_RELEASE_BLOBS +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_RELEASE_BLOBS +uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_release_blobs(void*_Nonnull ptr, RustBuffer universal_id +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_SET_BLOB +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_SET_BLOB +uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_set_blob(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer blob +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_SET_DOC_SNAPSHOT +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_SET_DOC_SNAPSHOT +uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_set_doc_snapshot(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer snapshot +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_SET_PEER_PULLED_REMOTE_CLOCK +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_SET_PEER_PULLED_REMOTE_CLOCK +uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_set_peer_pulled_remote_clock(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer peer, RustBuffer doc_id, RustBuffer clock +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_SET_PEER_PUSHED_CLOCK +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_SET_PEER_PUSHED_CLOCK +uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_set_peer_pushed_clock(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer peer, RustBuffer doc_id, RustBuffer clock +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_SET_PEER_REMOTE_CLOCK +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_SET_PEER_REMOTE_CLOCK +uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_set_peer_remote_clock(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer peer, RustBuffer doc_id, RustBuffer clock +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_SET_SPACE_ID +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_SET_SPACE_ID +uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_set_space_id(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer space_id +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_VALIDATE +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_VALIDATE +uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_validate(void*_Nonnull ptr, RustBuffer universal_id +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_FUNC_GET_DB_PATH +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_FUNC_GET_DB_PATH +RustBuffer uniffi_affine_mobile_native_fn_func_get_db_path(RustBuffer peer, RustBuffer space_type, RustBuffer id, RustCallStatus *_Nonnull out_status +); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_FUNC_HASHCASH_MINT #define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_FUNC_HASHCASH_MINT @@ -534,12 +689,186 @@ void ffi_affine_mobile_native_rust_future_free_void(uint64_t handle #ifndef UNIFFI_FFIDEF_FFI_AFFINE_MOBILE_NATIVE_RUST_FUTURE_COMPLETE_VOID #define UNIFFI_FFIDEF_FFI_AFFINE_MOBILE_NATIVE_RUST_FUTURE_COMPLETE_VOID void ffi_affine_mobile_native_rust_future_complete_void(uint64_t handle, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_FUNC_GET_DB_PATH +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_FUNC_GET_DB_PATH +uint16_t uniffi_affine_mobile_native_checksum_func_get_db_path(void + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_FUNC_HASHCASH_MINT #define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_FUNC_HASHCASH_MINT uint16_t uniffi_affine_mobile_native_checksum_func_hashcash_mint(void +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_CHECKPOINT +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_CHECKPOINT +uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_checkpoint(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_CLEAR_CLOCKS +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_CLEAR_CLOCKS +uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_clear_clocks(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_CLOSE +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_CLOSE +uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_close(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_CONNECT +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_CONNECT +uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_connect(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_DELETE_BLOB +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_DELETE_BLOB +uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_delete_blob(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_DELETE_DOC +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_DELETE_DOC +uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_delete_doc(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_GET_BLOB +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_GET_BLOB +uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_get_blob(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_GET_DOC_CLOCK +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_GET_DOC_CLOCK +uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_get_doc_clock(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_GET_DOC_CLOCKS +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_GET_DOC_CLOCKS +uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_get_doc_clocks(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_GET_DOC_SNAPSHOT +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_GET_DOC_SNAPSHOT +uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_get_doc_snapshot(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_GET_DOC_UPDATES +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_GET_DOC_UPDATES +uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_get_doc_updates(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_GET_PEER_PULLED_REMOTE_CLOCK +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_GET_PEER_PULLED_REMOTE_CLOCK +uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_get_peer_pulled_remote_clock(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_GET_PEER_PULLED_REMOTE_CLOCKS +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_GET_PEER_PULLED_REMOTE_CLOCKS +uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_get_peer_pulled_remote_clocks(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_GET_PEER_PUSHED_CLOCKS +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_GET_PEER_PUSHED_CLOCKS +uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_get_peer_pushed_clocks(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_GET_PEER_REMOTE_CLOCK +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_GET_PEER_REMOTE_CLOCK +uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_get_peer_remote_clock(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_GET_PEER_REMOTE_CLOCKS +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_GET_PEER_REMOTE_CLOCKS +uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_get_peer_remote_clocks(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_IS_CLOSED +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_IS_CLOSED +uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_is_closed(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_LIST_BLOBS +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_LIST_BLOBS +uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_list_blobs(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_MARK_UPDATES_MERGED +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_MARK_UPDATES_MERGED +uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_mark_updates_merged(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_PUSH_UPDATE +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_PUSH_UPDATE +uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_push_update(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_RELEASE_BLOBS +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_RELEASE_BLOBS +uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_release_blobs(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_SET_BLOB +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_SET_BLOB +uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_set_blob(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_SET_DOC_SNAPSHOT +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_SET_DOC_SNAPSHOT +uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_set_doc_snapshot(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_SET_PEER_PULLED_REMOTE_CLOCK +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_SET_PEER_PULLED_REMOTE_CLOCK +uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_set_peer_pulled_remote_clock(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_SET_PEER_PUSHED_CLOCK +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_SET_PEER_PUSHED_CLOCK +uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_set_peer_pushed_clock(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_SET_PEER_REMOTE_CLOCK +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_SET_PEER_REMOTE_CLOCK +uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_set_peer_remote_clock(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_SET_SPACE_ID +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_SET_SPACE_ID +uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_set_space_id(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_VALIDATE +#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_VALIDATE +uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_validate(void + ); #endif #ifndef UNIFFI_FFIDEF_FFI_AFFINE_MOBILE_NATIVE_UNIFFI_CONTRACT_VERSION diff --git a/packages/frontend/apps/ios/App/xc-universal-binary.sh b/packages/frontend/apps/ios/App/xc-universal-binary.sh index 4c13990bd277a..b993008dc1045 100644 --- a/packages/frontend/apps/ios/App/xc-universal-binary.sh +++ b/packages/frontend/apps/ios/App/xc-universal-binary.sh @@ -71,4 +71,4 @@ for arch in $ARCHS; do esac done -$HOME/.cargo/bin/cargo run --bin uniffi-bindgen generate --library $SRCROOT/lib${FFI_TARGET}.a --language swift --out-dir $SRCROOT/../../ios/App/App/uniffi +$HOME/.cargo/bin/cargo run -p affine_mobile_native --bin uniffi-bindgen generate --library $SRCROOT/lib${FFI_TARGET}.a --language swift --out-dir $SRCROOT/../../ios/App/App/uniffi diff --git a/packages/frontend/apps/ios/package.json b/packages/frontend/apps/ios/package.json index 7212d11200a8d..b311a79a1c291 100644 --- a/packages/frontend/apps/ios/package.json +++ b/packages/frontend/apps/ios/package.json @@ -27,9 +27,11 @@ "next-themes": "^0.4.4", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-router-dom": "^6.28.0" + "react-router-dom": "^6.28.0", + "yjs": "13.6.18" }, "devDependencies": { + "@affine/native": "workspace:*", "@capacitor/cli": "^6.2.0", "@types/react": "^19.0.1", "@types/react-dom": "^19.0.2", diff --git a/packages/frontend/apps/ios/src/plugins/nbstore/blob.ts b/packages/frontend/apps/ios/src/plugins/nbstore/blob.ts new file mode 100644 index 0000000000000..c1e0641db9bf0 --- /dev/null +++ b/packages/frontend/apps/ios/src/plugins/nbstore/blob.ts @@ -0,0 +1,33 @@ +import { type BlobRecord, BlobStorageBase, share } from '@affine/nbstore'; + +import { NativeDBConnection } from './db'; + +export class SqliteBlobStorage extends BlobStorageBase { + override connection = share( + new NativeDBConnection(this.peer, this.spaceType, this.spaceId) + ); + + get db() { + return this.connection.inner; + } + + override async get(key: string) { + return this.db.getBlob(key); + } + + override async set(blob: BlobRecord) { + await this.db.setBlob(blob); + } + + override async delete(key: string, permanently: boolean) { + await this.db.deleteBlob(key, permanently); + } + + override async release() { + await this.db.releaseBlobs(); + } + + override async list() { + return this.db.listBlobs(); + } +} diff --git a/packages/frontend/apps/ios/src/plugins/nbstore/db.ts b/packages/frontend/apps/ios/src/plugins/nbstore/db.ts new file mode 100644 index 0000000000000..81121571db7d4 --- /dev/null +++ b/packages/frontend/apps/ios/src/plugins/nbstore/db.ts @@ -0,0 +1,60 @@ +import type { DocStorage } from '@affine/native'; +import { + AutoReconnectConnection, + isValidSpaceType, + type SpaceType, + universalId, +} from '@affine/nbstore'; + +import { NativeDocStorage, NbStoreDocStorage } from './plugin'; + +export class NativeDBConnection extends AutoReconnectConnection { + private readonly universalId: string; + + constructor( + private readonly peer: string, + private readonly type: SpaceType, + private readonly id: string + ) { + super(); + if (!isValidSpaceType(type)) { + throw new TypeError(`Invalid space type: ${type}`); + } + this.universalId = universalId({ + peer: peer, + type: type, + id: id, + }); + } + + async getDBPath() { + const { path } = await NbStoreDocStorage.getSpaceDBPath({ + peer: this.peer, + spaceType: this.type, + id: this.id, + }); + return path; + } + + override get shareId(): string { + return `sqlite:${this.peer}:${this.type}:${this.id}`; + } + + override async doConnect() { + const conn = new NativeDocStorage(this.universalId); + await conn.connect(); + console.info('[nbstore] connection established', this.shareId); + return conn; + } + + override doDisconnect(conn: NativeDocStorage) { + conn + .close() + .then(() => { + console.info('[nbstore] connection closed', this.shareId); + }) + .catch(err => { + console.error('[nbstore] connection close failed', this.shareId, err); + }); + } +} diff --git a/packages/frontend/apps/ios/src/plugins/nbstore/definitions.ts b/packages/frontend/apps/ios/src/plugins/nbstore/definitions.ts new file mode 100644 index 0000000000000..cac91534631c4 --- /dev/null +++ b/packages/frontend/apps/ios/src/plugins/nbstore/definitions.ts @@ -0,0 +1,144 @@ +export interface Blob { + key: string; + // base64 encoded data + data: string; + mime: string; + size: number; + createdAt: number; +} + +export interface SetBlob { + key: string; + // base64 encoded data + data: string; + mime: string; +} + +export interface ListedBlob { + key: string; + mime: string; + size: number; + createdAt: number; +} + +export interface DocClock { + docId: string; + timestamp: number; +} + +export interface NbStorePlugin { + getSpaceDBPath: (options: { + peer: string; + spaceType: string; + id: string; + }) => Promise<{ path: string }>; + create: (options: { id: string; path: string }) => Promise; + connect: (options: { id: string }) => Promise; + close: (options: { id: string }) => Promise; + isClosed: (options: { id: string }) => Promise<{ isClosed: boolean }>; + checkpoint: (options: { id: string }) => Promise; + validate: (options: { id: string }) => Promise<{ isValidate: boolean }>; + + setSpaceId: (options: { id: string; spaceId: string }) => Promise; + pushUpdate: (options: { + id: string; + docId: string; + data: string; + }) => Promise<{ timestamp: number }>; + getDocSnapshot: (options: { id: string; docId: string }) => Promise< + | { + docId: string; + // base64 encoded data + data: string; + timestamp: number; + } + | undefined + >; + setDocSnapshot: (options: { + id: string; + docId: string; + data: string; + }) => Promise<{ success: boolean }>; + getDocUpdates: (options: { id: string; docId: string }) => Promise< + { + docId: string; + createdAt: number; + // base64 encoded data + data: string; + }[] + >; + markUpdatesMerged: (options: { + id: string; + docId: string; + timestamps: number[]; + }) => Promise<{ count: number }>; + deleteDoc: (options: { id: string; docId: string }) => Promise; + getDocClocks: (options: { id: string; after: number }) => Promise< + { + docId: string; + timestamp: number; + }[] + >; + getDocClock: (options: { id: string; docId: string }) => Promise< + | { + docId: string; + timestamp: number; + } + | undefined + >; + getBlob: (options: { id: string; key: string }) => Promise; + setBlob: (options: { id: string } & SetBlob) => Promise; + deleteBlob: (options: { + id: string; + key: string; + permanently: boolean; + }) => Promise; + releaseBlobs: (options: { id: string }) => Promise; + listBlobs: (options: { id: string }) => Promise>; + getPeerRemoteClocks: (options: { + id: string; + peer: string; + }) => Promise>; + getPeerRemoteClock: (options: { + id: string; + peer: string; + docId: string; + }) => Promise; + setPeerRemoteClock: (options: { + id: string; + peer: string; + docId: string; + clock: number; + }) => Promise; + getPeerPushedClocks: (options: { + id: string; + peer: string; + }) => Promise>; + getPeerPushedClock: (options: { + id: string; + peer: string; + docId: string; + }) => Promise; + setPeerPushedClock: (options: { + id: string; + peer: string; + docId: string; + clock: number; + }) => Promise; + getPeerPulledRemoteClocks: (options: { + id: string; + peer: string; + }) => Promise>; + getPeerPulledRemoteClock: (options: { + id: string; + peer: string; + docId: string; + }) => Promise; + setPeerPulledRemoteClock: (options: { + id: string; + peer: string; + docId: string; + clock: number; + }) => Promise; + clearClocks: (options: { id: string }) => Promise; +} diff --git a/packages/frontend/apps/ios/src/plugins/nbstore/doc.ts b/packages/frontend/apps/ios/src/plugins/nbstore/doc.ts new file mode 100644 index 0000000000000..4078f50513d1d --- /dev/null +++ b/packages/frontend/apps/ios/src/plugins/nbstore/doc.ts @@ -0,0 +1,83 @@ +import { + type DocClocks, + type DocRecord, + DocStorageBase, + type DocUpdate, + share, +} from '@affine/nbstore'; + +import { NativeDBConnection } from './db'; + +export class SqliteDocStorage extends DocStorageBase { + override connection = share( + new NativeDBConnection(this.peer, this.spaceType, this.spaceId) + ); + + get db() { + return this.connection.inner; + } + + override async pushDocUpdate(update: DocUpdate) { + const timestamp = await this.db.pushUpdate(update.docId, update.bin); + + return { docId: update.docId, timestamp }; + } + + override async deleteDoc(docId: string) { + await this.db.deleteDoc(docId); + } + + override async getDocTimestamps(after?: Date) { + const clocks = await this.db.getDocClocks(after); + + return clocks.reduce((ret, cur) => { + ret[cur.docId] = cur.timestamp; + return ret; + }, {} as DocClocks); + } + + override async getDocTimestamp(docId: string) { + return this.db.getDocClock(docId); + } + + protected override async getDocSnapshot(docId: string) { + const snapshot = await this.db.getDocSnapshot(docId); + + if (!snapshot) { + return null; + } + + return { + docId, + bin: snapshot.data, + timestamp: snapshot.timestamp, + }; + } + + protected override async setDocSnapshot( + snapshot: DocRecord + ): Promise { + return this.db.setDocSnapshot({ + docId: snapshot.docId, + data: Buffer.from(snapshot.bin), + timestamp: new Date(snapshot.timestamp), + }); + } + + protected override async getDocUpdates(docId: string) { + return this.db.getDocUpdates(docId).then(updates => + updates.map(update => ({ + docId, + bin: update.data, + timestamp: update.createdAt, + })) + ); + } + + protected override markUpdatesMerged(docId: string, updates: DocRecord[]) { + return this.db.markUpdatesMerged( + docId, + updates.map(update => update.timestamp) + ); + } +} diff --git a/packages/frontend/apps/ios/src/plugins/nbstore/handlers.ts b/packages/frontend/apps/ios/src/plugins/nbstore/handlers.ts new file mode 100644 index 0000000000000..946cb79cf5041 --- /dev/null +++ b/packages/frontend/apps/ios/src/plugins/nbstore/handlers.ts @@ -0,0 +1,128 @@ +import { + type BlobRecord, + type DocClock, + type DocUpdate, +} from '@affine/nbstore'; + +import { ensureStorage, getStorage } from './storage'; + +export const nbstoreHandlers = { + connect: async (id: string) => { + await ensureStorage(id); + }, + + close: async (id: string) => { + const store = getStorage(id); + + if (store) { + store.disconnect(); + // The store may be shared with other tabs, so we don't delete it from cache + // the underlying connection will handle the close correctly + // STORE_CACHE.delete(`${spaceType}:${spaceId}`); + } + }, + + pushDocUpdate: async (id: string, update: DocUpdate) => { + const store = await ensureStorage(id); + return store.get('doc').pushDocUpdate(update); + }, + + getDoc: async (id: string, docId: string) => { + const store = await ensureStorage(id); + return store.get('doc').getDoc(docId); + }, + + deleteDoc: async (id: string, docId: string) => { + const store = await ensureStorage(id); + return store.get('doc').deleteDoc(docId); + }, + + getDocTimestamps: async (id: string, after?: Date) => { + const store = await ensureStorage(id); + return store.get('doc').getDocTimestamps(after); + }, + + getDocTimestamp: async (id: string, docId: string) => { + const store = await ensureStorage(id); + return store.get('doc').getDocTimestamp(docId); + }, + + setBlob: async (id: string, blob: BlobRecord) => { + const store = await ensureStorage(id); + return store.get('blob').set(blob); + }, + + getBlob: async (id: string, key: string) => { + const store = await ensureStorage(id); + return store.get('blob').get(key); + }, + + deleteBlob: async (id: string, key: string, permanently: boolean) => { + const store = await ensureStorage(id); + return store.get('blob').delete(key, permanently); + }, + + listBlobs: async (id: string) => { + const store = await ensureStorage(id); + return store.get('blob').list(); + }, + + releaseBlobs: async (id: string) => { + const store = await ensureStorage(id); + return store.get('blob').release(); + }, + + getPeerRemoteClocks: async (id: string, peer: string) => { + const store = await ensureStorage(id); + return store.get('sync').getPeerRemoteClocks(peer); + }, + + getPeerRemoteClock: async (id: string, peer: string, docId: string) => { + const store = await ensureStorage(id); + return store.get('sync').getPeerRemoteClock(peer, docId); + }, + + setPeerRemoteClock: async (id: string, peer: string, clock: DocClock) => { + const store = await ensureStorage(id); + return store.get('sync').setPeerRemoteClock(peer, clock); + }, + + getPeerPulledRemoteClocks: async (id: string, peer: string) => { + const store = await ensureStorage(id); + return store.get('sync').getPeerPulledRemoteClocks(peer); + }, + + getPeerPulledRemoteClock: async (id: string, peer: string, docId: string) => { + const store = await ensureStorage(id); + return store.get('sync').getPeerPulledRemoteClock(peer, docId); + }, + + setPeerPulledRemoteClock: async ( + id: string, + peer: string, + clock: DocClock + ) => { + const store = await ensureStorage(id); + return store.get('sync').setPeerPulledRemoteClock(peer, clock); + }, + + getPeerPushedClocks: async (id: string, peer: string) => { + const store = await ensureStorage(id); + return store.get('sync').getPeerPushedClocks(peer); + }, + + getPeerPushedClock: async (id: string, peer: string, docId: string) => { + const store = await ensureStorage(id); + return store.get('sync').getPeerPushedClock(peer, docId); + }, + + setPeerPushedClock: async (id: string, peer: string, clock: DocClock) => { + const store = await ensureStorage(id); + return store.get('sync').setPeerPushedClock(peer, clock); + }, + + clearClocks: async (id: string) => { + const store = await ensureStorage(id); + return store.get('sync').clearClocks(); + }, +}; diff --git a/packages/frontend/apps/ios/src/plugins/nbstore/index.ts b/packages/frontend/apps/ios/src/plugins/nbstore/index.ts new file mode 100644 index 0000000000000..6d17cae7d3acc --- /dev/null +++ b/packages/frontend/apps/ios/src/plugins/nbstore/index.ts @@ -0,0 +1,5 @@ +export * from './definitions'; +export { nbstoreHandlers } from './handlers'; +export { NbStoreDocStorage } from './plugin'; +export * from './storage'; +export { universalId } from '@affine/nbstore'; diff --git a/packages/frontend/apps/ios/src/plugins/nbstore/plugin.ts b/packages/frontend/apps/ios/src/plugins/nbstore/plugin.ts new file mode 100644 index 0000000000000..ae9ef3e8387a7 --- /dev/null +++ b/packages/frontend/apps/ios/src/plugins/nbstore/plugin.ts @@ -0,0 +1,312 @@ +import { + base64ToUint8Array, + uint8ArrayToBase64, +} from '@affine/core/modules/workspace-engine'; +import { + type Blob, + type DocClock, + type DocRecord, + type DocStorage, + type DocUpdate, + type ListedBlob, +} from '@affine/native'; +import { registerPlugin } from '@capacitor/core'; + +import type { NbStorePlugin } from './definitions'; + +export const NbStoreDocStorage = + registerPlugin('NbStoreDocStorage'); + +export interface SetBlob { + key: string; + data: Uint8Array; + mime: string; +} + +export class NativeDocStorage implements DocStorage { + constructor(private readonly universalId: string) {} + + /** Initialize the database and run migrations. */ + connect(): Promise { + return NbStoreDocStorage.connect({ + id: this.universalId, + }); + } + + close(): Promise { + return NbStoreDocStorage.close({ + id: this.universalId, + }); + } + + get isClosed(): Promise { + return NbStoreDocStorage.isClosed({ + id: this.universalId, + }).then(result => result.isClosed); + } + /** + * Flush the WAL file to the database file. + * See https://www.sqlite.org/pragma.html#pragma_wal_checkpoint:~:text=PRAGMA%20schema.wal_checkpoint%3B + */ + checkpoint(): Promise { + return NbStoreDocStorage.checkpoint({ + id: this.universalId, + }); + } + + validate(): Promise { + return NbStoreDocStorage.validate({ + id: this.universalId, + }).then(result => result.isValidate); + } + + setSpaceId(spaceId: string): Promise { + return NbStoreDocStorage.setSpaceId({ + id: this.universalId, + spaceId, + }); + } + + async pushUpdate(docId: string, update: Uint8Array): Promise { + return NbStoreDocStorage.pushUpdate({ + id: this.universalId, + docId, + data: await uint8ArrayToBase64(update), + }).then(result => new Date(result.timestamp)); + } + + getDocSnapshot(docId: string): Promise { + return NbStoreDocStorage.getDocSnapshot({ + id: this.universalId, + docId, + }).then(result => { + if (result) { + return { + ...result, + data: base64ToUint8Array(result.data), + timestamp: new Date(result.timestamp), + }; + } + return null; + }); + } + + async setDocSnapshot(snapshot: DocRecord): Promise { + return NbStoreDocStorage.setDocSnapshot({ + id: this.universalId, + docId: snapshot.docId, + data: await uint8ArrayToBase64(snapshot.data), + }).then(result => result.success); + } + + getDocUpdates(docId: string): Promise> { + return NbStoreDocStorage.getDocUpdates({ + id: this.universalId, + docId, + }).then(result => + result.map(update => ({ + ...update, + data: base64ToUint8Array(update.data), + createdAt: new Date(update.createdAt), + })) + ); + } + + markUpdatesMerged(docId: string, updates: Array): Promise { + return NbStoreDocStorage.markUpdatesMerged({ + id: this.universalId, + docId, + timestamps: updates.map(date => date.getTime()), + }).then(result => result.count); + } + + deleteDoc(docId: string): Promise { + return NbStoreDocStorage.deleteDoc({ + id: this.universalId, + docId, + }); + } + + getDocClocks(after: Date): Promise> { + return NbStoreDocStorage.getDocClocks({ + id: this.universalId, + after: after.getTime(), + }).then(result => + result.map(clock => ({ + ...clock, + timestamp: new Date(clock.timestamp), + })) + ); + } + + getDocClock(docId: string): Promise { + return NbStoreDocStorage.getDocClock({ + id: this.universalId, + docId, + }).then(result => { + if (result) { + return { + ...result, + timestamp: new Date(result.timestamp), + }; + } + return null; + }); + } + + getBlob(key: string): Promise { + return NbStoreDocStorage.getBlob({ + id: this.universalId, + key, + }).then(result => { + if (result) { + return { + ...result, + data: base64ToUint8Array(result.data), + createdAt: new Date(result.createdAt), + }; + } + return null; + }); + } + + async setBlob(blob: SetBlob): Promise { + return NbStoreDocStorage.setBlob({ + id: this.universalId, + key: blob.key, + data: await uint8ArrayToBase64(blob.data), + mime: blob.mime, + }); + } + + deleteBlob(key: string, permanently: boolean): Promise { + return NbStoreDocStorage.deleteBlob({ + id: this.universalId, + key, + permanently, + }); + } + + releaseBlobs(): Promise { + return NbStoreDocStorage.releaseBlobs({ + id: this.universalId, + }); + } + + async listBlobs(): Promise> { + return ( + await NbStoreDocStorage.listBlobs({ + id: this.universalId, + }) + ).map(blob => ({ + ...blob, + createdAt: new Date(blob.createdAt), + })); + } + + getPeerRemoteClocks(peer: string): Promise> { + return NbStoreDocStorage.getPeerRemoteClocks({ + id: this.universalId, + peer, + }).then(result => + result.map(clock => ({ + ...clock, + timestamp: new Date(clock.timestamp), + })) + ); + } + + getPeerRemoteClock(peer: string, docId: string): Promise { + return NbStoreDocStorage.getPeerRemoteClock({ + id: this.universalId, + peer, + docId, + }).then(result => ({ + ...result, + timestamp: new Date(result.timestamp), + })); + } + + setPeerRemoteClock(peer: string, docId: string, clock: Date): Promise { + return NbStoreDocStorage.setPeerRemoteClock({ + id: this.universalId, + peer, + docId, + clock: clock.getTime(), + }); + } + + getPeerPulledRemoteClocks(peer: string): Promise> { + return NbStoreDocStorage.getPeerPulledRemoteClocks({ + id: this.universalId, + peer, + }).then(result => + result.map(clock => ({ + ...clock, + timestamp: new Date(clock.timestamp), + })) + ); + } + + getPeerPulledRemoteClock(peer: string, docId: string): Promise { + return NbStoreDocStorage.getPeerPulledRemoteClock({ + id: this.universalId, + peer, + docId, + }).then(result => ({ + ...result, + timestamp: new Date(result.timestamp), + })); + } + + setPeerPulledRemoteClock( + peer: string, + docId: string, + clock: Date + ): Promise { + return NbStoreDocStorage.setPeerPulledRemoteClock({ + id: this.universalId, + peer, + docId, + clock: clock.getTime(), + }); + } + + getPeerPushedClocks(peer: string): Promise> { + return NbStoreDocStorage.getPeerPushedClocks({ + id: this.universalId, + peer, + }).then(result => + result.map(clock => ({ + ...clock, + timestamp: new Date(clock.timestamp), + })) + ); + } + + getPeerPushedClock(peer: string, docId: string): Promise { + return NbStoreDocStorage.getPeerPushedClock({ + id: this.universalId, + peer, + docId, + }).then(result => ({ + ...result, + timestamp: new Date(result.timestamp), + })); + } + + setPeerPushedClock(peer: string, docId: string, clock: Date): Promise { + return NbStoreDocStorage.setPeerPushedClock({ + id: this.universalId, + peer, + docId, + clock: clock.getTime(), + }); + } + + clearClocks(): Promise { + return NbStoreDocStorage.clearClocks({ + id: this.universalId, + }); + } +} diff --git a/packages/frontend/apps/ios/src/plugins/nbstore/storage.ts b/packages/frontend/apps/ios/src/plugins/nbstore/storage.ts new file mode 100644 index 0000000000000..5685743160f61 --- /dev/null +++ b/packages/frontend/apps/ios/src/plugins/nbstore/storage.ts @@ -0,0 +1,83 @@ +import { parseUniversalId, SpaceStorage } from '@affine/nbstore'; +import { applyUpdate, Doc as YDoc } from 'yjs'; + +import { SqliteBlobStorage } from './blob'; +import { NativeDBConnection } from './db'; +import { SqliteDocStorage } from './doc'; +import { SqliteSyncStorage } from './sync'; + +export class SqliteSpaceStorage extends SpaceStorage { + get connection() { + const docStore = this.get('doc'); + + if (!docStore) { + throw new Error('doc store not found'); + } + + const connection = docStore.connection; + + if (!(connection instanceof NativeDBConnection)) { + throw new Error('doc store connection is not a Sqlite connection'); + } + + return connection; + } + + async getDBPath() { + return this.connection.getDBPath(); + } + + async getWorkspaceName() { + const docStore = this.tryGet('doc'); + + if (!docStore) { + return null; + } + + const doc = await docStore.getDoc(docStore.spaceId); + if (!doc) { + return null; + } + + const ydoc = new YDoc(); + applyUpdate(ydoc, doc.bin); + return ydoc.getMap('meta').get('name') as string; + } + + async checkpoint() { + await this.connection.inner.checkpoint(); + } +} + +const STORE_CACHE = new Map(); + +export function getStorage(universalId: string) { + return STORE_CACHE.get(universalId); +} + +export async function ensureStorage(universalId: string) { + const { peer, type, id } = parseUniversalId(universalId); + let store = STORE_CACHE.get(universalId); + + if (!store) { + const opts = { + peer, + type, + id, + }; + + store = new SqliteSpaceStorage([ + new SqliteDocStorage(opts), + new SqliteBlobStorage(opts), + new SqliteSyncStorage(opts), + ]); + + store.connect(); + + await store.waitForConnected(); + + STORE_CACHE.set(universalId, store); + } + + return store; +} diff --git a/packages/frontend/apps/ios/src/plugins/nbstore/sync.ts b/packages/frontend/apps/ios/src/plugins/nbstore/sync.ts new file mode 100644 index 0000000000000..2942371b59be2 --- /dev/null +++ b/packages/frontend/apps/ios/src/plugins/nbstore/sync.ts @@ -0,0 +1,70 @@ +import { + BasicSyncStorage, + type DocClock, + type DocClocks, + share, +} from '@affine/nbstore'; + +import { NativeDBConnection } from './db'; + +export class SqliteSyncStorage extends BasicSyncStorage { + override connection = share( + new NativeDBConnection(this.peer, this.spaceType, this.spaceId) + ); + + get db() { + return this.connection.inner; + } + + override async getPeerRemoteClocks(peer: string) { + const records = await this.db.getPeerRemoteClocks(peer); + return records.reduce((clocks, { docId, timestamp }) => { + clocks[docId] = timestamp; + return clocks; + }, {} as DocClocks); + } + + override async getPeerRemoteClock(peer: string, docId: string) { + return this.db.getPeerRemoteClock(peer, docId); + } + + override async setPeerRemoteClock(peer: string, clock: DocClock) { + await this.db.setPeerRemoteClock(peer, clock.docId, clock.timestamp); + } + + override async getPeerPulledRemoteClock(peer: string, docId: string) { + return this.db.getPeerPulledRemoteClock(peer, docId); + } + + override async getPeerPulledRemoteClocks(peer: string) { + const records = await this.db.getPeerPulledRemoteClocks(peer); + return records.reduce((clocks, { docId, timestamp }) => { + clocks[docId] = timestamp; + return clocks; + }, {} as DocClocks); + } + + override async setPeerPulledRemoteClock(peer: string, clock: DocClock) { + await this.db.setPeerPulledRemoteClock(peer, clock.docId, clock.timestamp); + } + + override async getPeerPushedClocks(peer: string) { + const records = await this.db.getPeerPushedClocks(peer); + return records.reduce((clocks, { docId, timestamp }) => { + clocks[docId] = timestamp; + return clocks; + }, {} as DocClocks); + } + + override async getPeerPushedClock(peer: string, docId: string) { + return this.db.getPeerPushedClock(peer, docId); + } + + override async setPeerPushedClock(peer: string, clock: DocClock) { + await this.db.setPeerPushedClock(peer, clock.docId, clock.timestamp); + } + + override async clearClocks() { + await this.db.clearClocks(); + } +} diff --git a/packages/frontend/apps/ios/tsconfig.json b/packages/frontend/apps/ios/tsconfig.json index 0bb809226a8f7..3ff8bf63c786b 100644 --- a/packages/frontend/apps/ios/tsconfig.json +++ b/packages/frontend/apps/ios/tsconfig.json @@ -8,5 +8,9 @@ "rootDir": "./src" }, "include": ["./src"], - "references": [{ "path": "../../core" }] + "references": [ + { "path": "../../core" }, + { "path": "../../native" }, + { "path": "../../../common/nbstore" } + ] } diff --git a/packages/frontend/core/src/modules/workspace-engine/index.ts b/packages/frontend/core/src/modules/workspace-engine/index.ts index bde7581b45c44..aa634f354413e 100644 --- a/packages/frontend/core/src/modules/workspace-engine/index.ts +++ b/packages/frontend/core/src/modules/workspace-engine/index.ts @@ -16,6 +16,7 @@ import { import { WorkspaceEngineStorageProvider } from './providers/engine'; export { CloudBlobStorage } from './impls/engine/blob-cloud'; +export { base64ToUint8Array, uint8ArrayToBase64 } from './utils/base64'; export function configureBrowserWorkspaceFlavours(framework: Framework) { framework diff --git a/packages/frontend/mobile-native/Cargo.toml b/packages/frontend/mobile-native/Cargo.toml index 531e966aad6b7..d6e3d7d18fd1a 100644 --- a/packages/frontend/mobile-native/Cargo.toml +++ b/packages/frontend/mobile-native/Cargo.toml @@ -12,8 +12,22 @@ name = "uniffi-bindgen" path = "uniffi-bindgen.rs" [dependencies] -affine_common = { workspace = true } -uniffi = { version = "0.28", features = ["cli"] } +affine_common = { workspace = true } +affine_nbstore = { workspace = true, features = ["noop"] } +anyhow = { workspace = true } +base64-simd = { workspace = true } +chrono = { workspace = true } +dashmap = { workspace = true } +sqlx = { workspace = true } +thiserror = { workspace = true } +uniffi = { workspace = true, features = ["cli"] } + +[target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies] +objc2 = { workspace = true } +objc2-foundation = { workspace = true, features = ["NSArray", "NSFileManager", "NSPathUtilities", "NSString", "NSURL"] } + +[target.'cfg(not(any(target_os = "ios", target_os = "macos")))'.dependencies] +homedir = { workspace = true } [build-dependencies] -uniffi = { version = "0.28", features = ["build"] } +uniffi = { workspace = true, features = ["build"] } diff --git a/packages/frontend/mobile-native/src/error.rs b/packages/frontend/mobile-native/src/error.rs new file mode 100644 index 0000000000000..2974955754663 --- /dev/null +++ b/packages/frontend/mobile-native/src/error.rs @@ -0,0 +1,29 @@ +use thiserror::Error; + +#[derive(uniffi::Error, Error, Debug)] +pub enum UniffiError { + #[error("Get user document directory failed")] + GetUserDocumentDirectoryFailed, + #[error("Create affine dir failed: {0}")] + CreateAffineDirFailed(String), + #[error("Empty doc storage path")] + EmptyDocStoragePath, + #[error("Empty space id")] + EmptySpaceId, + #[error("Sqlx error: {0}")] + SqlxError(String), + #[error("Base64 decoding error: {0}")] + Base64DecodingError(String), + #[error("Invalid universal storage id: {0}. It should be in format of @peer($peer);@type($type);@id($id);")] + InvalidUniversalId(String), + #[error("Invalid space type: {0}")] + InvalidSpaceType(String), + #[error("Concat space dir failed: {0}")] + ConcatSpaceDirFailed(String), +} + +impl From for UniffiError { + fn from(err: sqlx::Error) -> Self { + UniffiError::SqlxError(err.to_string()) + } +} diff --git a/packages/frontend/mobile-native/src/lib.rs b/packages/frontend/mobile-native/src/lib.rs index 595c15cc3849b..44b32fff2c664 100644 --- a/packages/frontend/mobile-native/src/lib.rs +++ b/packages/frontend/mobile-native/src/lib.rs @@ -1,4 +1,15 @@ +use std::fmt::Display; +use std::str::FromStr; +use std::time::SystemTime; + use affine_common::hashcash::Stamp; +use affine_nbstore::storage; +use dashmap::{mapref::one::RefMut, DashMap, Entry}; + +use crate::error::UniffiError; + +mod error; +mod utils; uniffi::setup_scaffolding!("affine_mobile_native"); @@ -6,3 +17,867 @@ uniffi::setup_scaffolding!("affine_mobile_native"); pub fn hashcash_mint(resource: String, bits: u32) -> String { Stamp::mint(resource, Some(bits)).format() } + +#[derive(uniffi::Record)] +pub struct DocRecord { + pub doc_id: String, + // base64 encoded data + pub data: String, + pub timestamp: SystemTime, +} + +impl From for DocRecord { + fn from(record: affine_nbstore::DocRecord) -> Self { + Self { + doc_id: record.doc_id, + data: base64_simd::STANDARD.encode_to_string(&record.data), + timestamp: record.timestamp.and_utc().into(), + } + } +} + +impl TryFrom for affine_nbstore::DocRecord { + type Error = UniffiError; + + fn try_from(record: DocRecord) -> Result { + Ok(Self { + doc_id: record.doc_id, + data: base64_simd::STANDARD + .decode_to_vec(record.data) + .map_err(|e| UniffiError::Base64DecodingError(e.to_string()))?, + timestamp: chrono::DateTime::::from(record.timestamp).naive_utc(), + }) + } +} + +#[derive(uniffi::Record)] +pub struct DocUpdate { + pub doc_id: String, + pub created_at: SystemTime, + // base64 encoded data + pub data: String, +} + +impl From for DocUpdate { + fn from(update: affine_nbstore::DocUpdate) -> Self { + Self { + doc_id: update.doc_id, + created_at: update.created_at.and_utc().into(), + data: base64_simd::STANDARD.encode_to_string(&update.data), + } + } +} + +impl From for affine_nbstore::DocUpdate { + fn from(update: DocUpdate) -> Self { + Self { + doc_id: update.doc_id, + created_at: chrono::DateTime::::from(update.created_at).naive_utc(), + data: update.data.into(), + } + } +} + +#[derive(uniffi::Record)] +pub struct DocClock { + pub doc_id: String, + pub timestamp: SystemTime, +} + +impl From for DocClock { + fn from(clock: affine_nbstore::DocClock) -> Self { + Self { + doc_id: clock.doc_id, + timestamp: clock.timestamp.and_utc().into(), + } + } +} + +impl From for affine_nbstore::DocClock { + fn from(clock: DocClock) -> Self { + Self { + doc_id: clock.doc_id, + timestamp: chrono::DateTime::::from(clock.timestamp).naive_utc(), + } + } +} + +#[derive(uniffi::Record)] +pub struct Blob { + pub key: String, + // base64 encoded data + pub data: String, + pub mime: String, + pub size: i64, + pub created_at: SystemTime, +} + +impl From for Blob { + fn from(blob: affine_nbstore::Blob) -> Self { + Self { + key: blob.key, + data: base64_simd::STANDARD.encode_to_string(&blob.data), + mime: blob.mime, + size: blob.size, + created_at: blob.created_at.and_utc().into(), + } + } +} + +#[derive(uniffi::Record)] +pub struct SetBlob { + pub key: String, + // base64 encoded data + pub data: String, + pub mime: String, +} + +impl TryFrom for affine_nbstore::SetBlob { + type Error = UniffiError; + + fn try_from(blob: SetBlob) -> Result { + Ok(Self { + key: blob.key, + data: base64_simd::STANDARD + .decode_to_vec(blob.data) + .map_err(|e| UniffiError::Base64DecodingError(e.to_string()))?, + mime: blob.mime, + }) + } +} + +#[derive(uniffi::Record)] +pub struct ListedBlob { + pub key: String, + pub size: i64, + pub mime: String, + pub created_at: SystemTime, +} + +impl From for ListedBlob { + fn from(blob: affine_nbstore::ListedBlob) -> Self { + Self { + key: blob.key, + size: blob.size, + mime: blob.mime, + created_at: blob.created_at.and_utc().into(), + } + } +} + +#[derive(uniffi::Object)] +pub struct DocStoragePool { + inner: DashMap, +} + +impl DocStoragePool { + fn ensure_storage<'a>( + &'a self, + universal_id: &str, + ) -> Result, UniffiError> { + let entry = self.inner.entry(universal_id.to_string()); + + if let Entry::Occupied(storage) = entry { + return Ok(storage.into_ref()); + } + let options = parse_universal_id(entry.key())?; + let db_path = utils::get_db_path(&options)?; + let storage = DocStorage::new(db_path)?; + Ok(entry.or_insert(storage)) + } +} + +#[uniffi::export] +impl DocStoragePool { + /// Initialize the database and run migrations. + pub async fn connect(&self, universal_id: String) -> Result<(), UniffiError> { + let storage = self.ensure_storage(&universal_id)?; + Ok(storage.connect().await?) + } + + pub async fn close(&self, universal_id: String) -> Result<(), UniffiError> { + let storage = self.ensure_storage(&universal_id)?; + storage.close().await?; + self.inner.remove(&universal_id); + Ok(()) + } + + pub fn is_closed(&self, universal_id: String) -> bool { + let storage = self.ensure_storage(&universal_id).unwrap(); + storage.is_closed() + } + + pub async fn checkpoint(&self, universal_id: String) -> Result<(), UniffiError> { + let storage = self.ensure_storage(&universal_id)?; + Ok(storage.checkpoint().await?) + } + + pub async fn validate(&self, universal_id: String) -> Result { + let storage = self.ensure_storage(&universal_id)?; + Ok(storage.validate().await?) + } + + pub async fn set_space_id( + &self, + universal_id: String, + space_id: String, + ) -> Result<(), UniffiError> { + let storage = self.ensure_storage(&universal_id)?; + if space_id.is_empty() { + return Err(UniffiError::EmptySpaceId); + } + Ok(storage.set_space_id(space_id).await?) + } + + pub async fn push_update( + &self, + universal_id: String, + doc_id: String, + update: String, + ) -> Result { + let storage = self.ensure_storage(&universal_id)?; + Ok(storage.push_update(doc_id, update).await?) + } + + pub async fn get_doc_snapshot( + &self, + universal_id: String, + doc_id: String, + ) -> Result, UniffiError> { + let storage = self.ensure_storage(&universal_id)?; + Ok(storage.get_doc_snapshot(doc_id).await?) + } + + pub async fn set_doc_snapshot( + &self, + universal_id: String, + snapshot: DocRecord, + ) -> Result { + let storage = self.ensure_storage(&universal_id)?; + Ok(storage.set_doc_snapshot(snapshot).await?) + } + + pub async fn get_doc_updates( + &self, + universal_id: String, + doc_id: String, + ) -> Result, UniffiError> { + let storage = self.ensure_storage(&universal_id)?; + Ok(storage.get_doc_updates(doc_id).await?) + } + + pub async fn mark_updates_merged( + &self, + universal_id: String, + doc_id: String, + updates: Vec, + ) -> Result { + let storage = self.ensure_storage(&universal_id)?; + Ok(storage.mark_updates_merged(doc_id, updates).await?) + } + + pub async fn delete_doc(&self, universal_id: String, doc_id: String) -> Result<(), UniffiError> { + let storage = self.ensure_storage(&universal_id)?; + Ok(storage.delete_doc(doc_id).await?) + } + + pub async fn get_doc_clocks( + &self, + universal_id: String, + after: Option, + ) -> Result, UniffiError> { + let storage = self.ensure_storage(&universal_id)?; + Ok(storage.get_doc_clocks(after).await?) + } + + pub async fn get_doc_clock( + &self, + universal_id: String, + doc_id: String, + ) -> Result, UniffiError> { + let storage = self.ensure_storage(&universal_id)?; + Ok(storage.get_doc_clock(doc_id).await?) + } + + pub async fn get_blob( + &self, + universal_id: String, + key: String, + ) -> Result, UniffiError> { + let storage = self.ensure_storage(&universal_id)?; + Ok(storage.get_blob(key).await?) + } + + pub async fn set_blob(&self, universal_id: String, blob: SetBlob) -> Result<(), UniffiError> { + let storage = self.ensure_storage(&universal_id)?; + Ok(storage.set_blob(blob).await?) + } + + pub async fn delete_blob( + &self, + universal_id: String, + key: String, + permanently: bool, + ) -> Result<(), UniffiError> { + let storage = self.ensure_storage(&universal_id)?; + Ok(storage.delete_blob(key, permanently).await?) + } + + pub async fn release_blobs(&self, universal_id: String) -> Result<(), UniffiError> { + let storage = self.ensure_storage(&universal_id)?; + Ok(storage.release_blobs().await?) + } + + pub async fn list_blobs(&self, universal_id: String) -> Result, UniffiError> { + let storage = self.ensure_storage(&universal_id)?; + Ok(storage.list_blobs().await?) + } + + pub async fn get_peer_remote_clocks( + &self, + universal_id: String, + peer: String, + ) -> Result, UniffiError> { + let storage = self.ensure_storage(&universal_id)?; + Ok(storage.get_peer_remote_clocks(peer).await?) + } + + pub async fn get_peer_remote_clock( + &self, + universal_id: String, + peer: String, + doc_id: String, + ) -> Result { + let storage = self.ensure_storage(&universal_id)?; + Ok(storage.get_peer_remote_clock(peer, doc_id).await?) + } + + pub async fn set_peer_remote_clock( + &self, + universal_id: String, + peer: String, + doc_id: String, + clock: SystemTime, + ) -> Result<(), UniffiError> { + let storage = self.ensure_storage(&universal_id)?; + Ok(storage.set_peer_remote_clock(peer, doc_id, clock).await?) + } + + pub async fn get_peer_pulled_remote_clocks( + &self, + universal_id: String, + peer: String, + ) -> Result, UniffiError> { + let storage = self.ensure_storage(&universal_id)?; + Ok(storage.get_peer_pulled_remote_clocks(peer).await?) + } + + pub async fn get_peer_pulled_remote_clock( + &self, + universal_id: String, + peer: String, + doc_id: String, + ) -> Result { + let storage = self.ensure_storage(&universal_id)?; + Ok(storage.get_peer_pulled_remote_clock(peer, doc_id).await?) + } + + pub async fn set_peer_pulled_remote_clock( + &self, + universal_id: String, + peer: String, + doc_id: String, + clock: SystemTime, + ) -> Result<(), UniffiError> { + let storage = self.ensure_storage(&universal_id)?; + Ok( + storage + .set_peer_pulled_remote_clock(peer, doc_id, clock) + .await?, + ) + } + + pub async fn get_peer_pushed_clocks( + &self, + universal_id: String, + peer: String, + ) -> Result, UniffiError> { + let storage = self.ensure_storage(&universal_id)?; + Ok(storage.get_peer_pushed_clocks(peer).await?) + } + + pub async fn set_peer_pushed_clock( + &self, + universal_id: String, + peer: String, + doc_id: String, + clock: SystemTime, + ) -> Result<(), UniffiError> { + let storage = self.ensure_storage(&universal_id)?; + Ok(storage.set_peer_pushed_clock(peer, doc_id, clock).await?) + } + + pub async fn clear_clocks(&self, universal_id: String) -> Result<(), UniffiError> { + let storage = self.ensure_storage(&universal_id)?; + Ok(storage.clear_clocks().await?) + } +} + +pub struct DocStorage { + storage: storage::SqliteDocStorage, +} + +impl DocStorage { + pub fn new(path: String) -> Result { + if path.is_empty() { + return Err(UniffiError::EmptyDocStoragePath); + } + Ok(Self { + storage: storage::SqliteDocStorage::new(path), + }) + } + + /// Initialize the database and run migrations. + pub async fn connect(&self) -> Result<(), UniffiError> { + Ok(self.storage.connect().await?) + } + + pub async fn close(&self) -> Result<(), UniffiError> { + Ok(self.storage.close().await) + } + + pub fn is_closed(&self) -> bool { + self.storage.is_closed() + } + + pub async fn checkpoint(&self) -> Result<(), UniffiError> { + Ok(self.storage.checkpoint().await?) + } + + pub async fn validate(&self) -> Result { + Ok(self.storage.validate().await?) + } + + pub async fn set_space_id(&self, space_id: String) -> Result<(), UniffiError> { + if space_id.is_empty() { + return Err(UniffiError::EmptySpaceId); + } + Ok(self.storage.set_space_id(space_id).await?) + } + + pub async fn push_update( + &self, + doc_id: String, + update: String, + ) -> Result { + Ok( + self + .storage + .push_update( + doc_id, + base64_simd::STANDARD + .decode_to_vec(update) + .map_err(|e| UniffiError::Base64DecodingError(e.to_string()))?, + ) + .await? + .and_utc() + .into(), + ) + } + + pub async fn get_doc_snapshot(&self, doc_id: String) -> Result, UniffiError> { + Ok(self.storage.get_doc_snapshot(doc_id).await?.map(Into::into)) + } + + pub async fn set_doc_snapshot(&self, snapshot: DocRecord) -> Result { + Ok(self.storage.set_doc_snapshot(snapshot.try_into()?).await?) + } + + pub async fn get_doc_updates(&self, doc_id: String) -> Result, UniffiError> { + Ok( + self + .storage + .get_doc_updates(doc_id) + .await? + .into_iter() + .map(Into::into) + .collect(), + ) + } + + pub async fn mark_updates_merged( + &self, + doc_id: String, + updates: Vec, + ) -> Result { + Ok( + self + .storage + .mark_updates_merged( + doc_id, + updates + .into_iter() + .map(|t| chrono::DateTime::::from(t).naive_utc()) + .collect(), + ) + .await?, + ) + } + + pub async fn delete_doc(&self, doc_id: String) -> Result<(), UniffiError> { + Ok(self.storage.delete_doc(doc_id).await?) + } + + pub async fn get_doc_clocks( + &self, + after: Option, + ) -> Result, UniffiError> { + Ok( + self + .storage + .get_doc_clocks(after.map(|t| chrono::DateTime::::from(t).naive_utc())) + .await? + .into_iter() + .map(Into::into) + .collect(), + ) + } + + pub async fn get_doc_clock(&self, doc_id: String) -> Result, UniffiError> { + Ok(self.storage.get_doc_clock(doc_id).await?.map(Into::into)) + } + + pub async fn get_blob(&self, key: String) -> Result, UniffiError> { + Ok(self.storage.get_blob(key).await?.map(Into::into)) + } + + pub async fn set_blob(&self, blob: SetBlob) -> Result<(), UniffiError> { + Ok(self.storage.set_blob(blob.try_into()?).await?) + } + + pub async fn delete_blob(&self, key: String, permanently: bool) -> Result<(), UniffiError> { + Ok(self.storage.delete_blob(key, permanently).await?) + } + + pub async fn release_blobs(&self) -> Result<(), UniffiError> { + Ok(self.storage.release_blobs().await?) + } + + pub async fn list_blobs(&self) -> Result, UniffiError> { + Ok( + self + .storage + .list_blobs() + .await? + .into_iter() + .map(Into::into) + .collect(), + ) + } + + pub async fn get_peer_remote_clocks(&self, peer: String) -> Result, UniffiError> { + Ok( + self + .storage + .get_peer_remote_clocks(peer) + .await? + .into_iter() + .map(Into::into) + .collect(), + ) + } + + pub async fn get_peer_remote_clock( + &self, + peer: String, + doc_id: String, + ) -> Result { + Ok( + self + .storage + .get_peer_remote_clock(peer, doc_id) + .await? + .into(), + ) + } + + pub async fn set_peer_remote_clock( + &self, + peer: String, + doc_id: String, + clock: SystemTime, + ) -> Result<(), UniffiError> { + Ok( + self + .storage + .set_peer_remote_clock( + peer, + doc_id, + chrono::DateTime::::from(clock).naive_utc(), + ) + .await?, + ) + } + + pub async fn get_peer_pulled_remote_clocks( + &self, + peer: String, + ) -> Result, UniffiError> { + Ok( + self + .storage + .get_peer_pulled_remote_clocks(peer) + .await? + .into_iter() + .map(Into::into) + .collect(), + ) + } + + pub async fn get_peer_pulled_remote_clock( + &self, + peer: String, + doc_id: String, + ) -> Result { + Ok( + self + .storage + .get_peer_pulled_remote_clock(peer, doc_id) + .await? + .into(), + ) + } + + pub async fn set_peer_pulled_remote_clock( + &self, + peer: String, + doc_id: String, + clock: SystemTime, + ) -> Result<(), UniffiError> { + Ok( + self + .storage + .set_peer_pulled_remote_clock( + peer, + doc_id, + chrono::DateTime::::from(clock).naive_utc(), + ) + .await?, + ) + } + + pub async fn get_peer_pushed_clocks(&self, peer: String) -> Result, UniffiError> { + Ok( + self + .storage + .get_peer_pushed_clocks(peer) + .await? + .into_iter() + .map(Into::into) + .collect(), + ) + } + + pub async fn set_peer_pushed_clock( + &self, + peer: String, + doc_id: String, + clock: SystemTime, + ) -> Result<(), UniffiError> { + Ok( + self + .storage + .set_peer_pushed_clock( + peer, + doc_id, + chrono::DateTime::::from(clock).naive_utc(), + ) + .await?, + ) + } + + pub async fn clear_clocks(&self) -> Result<(), UniffiError> { + Ok(self.storage.clear_clocks().await?) + } +} + +#[uniffi::export] +pub fn get_db_path(peer: String, space_type: String, id: String) -> Result { + let options = StorageOptions { + peer, + space_type: SpaceType::from_str(&space_type)?, + id, + }; + utils::get_db_path(&options) +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] +pub enum SpaceType { + #[default] + Userspace, + Workspace, +} + +impl Display for SpaceType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SpaceType::Userspace => write!(f, "userspace"), + SpaceType::Workspace => write!(f, "workspace"), + } + } +} + +impl FromStr for SpaceType { + type Err = UniffiError; + + fn from_str(s: &str) -> Result { + Ok(match s { + "userspace" => Self::Userspace, + "workspace" => Self::Workspace, + _ => return Err(UniffiError::InvalidSpaceType(s.to_string())), + }) + } +} + +pub struct StorageOptions { + pub peer: String, + pub space_type: SpaceType, + pub id: String, +} + +pub fn parse_universal_id(id: &str) -> Result { + let mut result = StorageOptions { + peer: String::new(), + space_type: SpaceType::default(), + id: String::new(), + }; + + let mut key = String::new(); + let mut value = String::new(); + let mut is_in_value = false; + let mut chars = id.chars().peekable(); + + while let Some(ch) = chars.next() { + if is_in_value { + if ch == ')' && chars.peek() == Some(&';') { + // Store the collected value in the appropriate field + match key.as_str() { + "peer" => result.peer = value.clone(), + "type" => result.space_type = SpaceType::from_str(&value)?, + "id" => result.id = value.clone(), + _ => return Err(UniffiError::InvalidUniversalId(id.to_string())), + } + key.clear(); + value.clear(); + is_in_value = false; + chars.next(); // Skip the semicolon + continue; + } + value.push(ch); + continue; + } + + if ch == '@' { + // Find the position of next '(' + let mut temp_chars = chars.clone(); + let mut found_paren = false; + let mut key_chars = Vec::new(); + + while let Some(next_ch) = temp_chars.next() { + if next_ch == '(' { + found_paren = true; + break; + } + key_chars.push(next_ch); + } + + // Invalid format if no '(' found or it's immediately after '@' + if !found_paren || key_chars.is_empty() { + return Err(UniffiError::InvalidUniversalId(id.to_string())); + } + + key = key_chars.into_iter().collect(); + // Advance the original iterator to the position after the key + for _ in 0..key.len() + 1 { + chars.next(); + } + is_in_value = true; + } else { + return Err(UniffiError::InvalidUniversalId(id.to_string())); + } + } + + // Validate the parsed results + if result.peer.is_empty() || result.id.is_empty() { + return Err(UniffiError::InvalidUniversalId(id.to_string())); + } + + Ok(result) +} + +#[cfg(test)] +mod tests { + use super::*; + + // ... existing test functions ... + + #[test] + fn test_universal_id() { + let options = StorageOptions { + peer: "123".to_string(), + space_type: SpaceType::Workspace, + id: "456".to_string(), + }; + + let id = format!( + "@peer({});@type({});@id({});", + options.peer, options.space_type, options.id + ); + let result = parse_universal_id(&id).unwrap(); + + assert_eq!(result.peer, "123"); + assert_eq!(result.space_type, SpaceType::Workspace); + assert_eq!(result.id, "456"); + } + + #[test] + fn test_parse_universal_id_valid_cases() { + let testcases = vec![ + "@peer(123);@type(userspace);@id(456);", + "@peer(123);@type(workspace);@id(456);", + "@peer(https://app.affine.pro);@type(userspace);@id(hello:world);", + "@peer(@name);@type(userspace);@id(@id);", + "@peer(@peer(name);@type(userspace);@id(@id);", + ]; + + for id in testcases { + let result = parse_universal_id(id); + assert!(result.is_ok(), "Failed to parse: {}", id); + + let parsed = result.unwrap(); + assert!(!parsed.peer.is_empty()); + assert!(!parsed.id.is_empty()); + } + } + + #[test] + fn test_parse_universal_id_invalid_cases() { + let testcases = vec![ + // invalid space type + "@peer(123);@type(anyspace);@id(456);", + // invalid peer + "@peer(@peer(name););@type(userspace);@id(@id);", + ]; + + for id in testcases { + let result = parse_universal_id(id); + assert!(result.is_err(), "Should have failed to parse: {}", id); + + match result { + Err(UniffiError::InvalidUniversalId(_)) => (), + Err(UniffiError::InvalidSpaceType(_)) => (), + _ => panic!("Expected InvalidUniversalId error for: {}", id), + } + } + } +} diff --git a/packages/frontend/mobile-native/src/utils.rs b/packages/frontend/mobile-native/src/utils.rs new file mode 100644 index 0000000000000..bfe9078139972 --- /dev/null +++ b/packages/frontend/mobile-native/src/utils.rs @@ -0,0 +1,141 @@ +use std::fs; + +#[cfg(not(any(target_os = "ios", target_os = "macos")))] +use homedir::my_home; +#[cfg(any(target_os = "ios", target_os = "macos"))] +use objc2::rc::autoreleasepool; +#[cfg(any(target_os = "ios", target_os = "macos"))] +use objc2_foundation::{NSFileManager, NSSearchPathDirectory, NSSearchPathDomainMask, NSString}; + +use crate::{error::UniffiError, SpaceType, StorageOptions}; + +const DB_FILE_NAME: &str = "storage.db"; + +#[cfg(any(target_os = "ios", target_os = "macos"))] +pub(crate) fn get_db_path(options: &StorageOptions) -> Result { + let file_manager = unsafe { NSFileManager::defaultManager() }; + // equivalent to Swift: + // ```swift + // guard let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { + // return nil + // } + // ``` + let urls = unsafe { + file_manager.URLsForDirectory_inDomains( + NSSearchPathDirectory::NSDocumentDirectory, + NSSearchPathDomainMask::NSUserDomainMask, + ) + }; + let document_directory = urls + .first() + .ok_or(UniffiError::GetUserDocumentDirectoryFailed)?; + + let affine_dir = unsafe { + let spaces_dir = match options.space_type { + SpaceType::Userspace => "userspaces", + SpaceType::Workspace => "workspaces", + }; + let escaped_peer = escape_filename(&options.peer); + document_directory + .URLByAppendingPathComponent(&NSString::from_str(".affine")) + .and_then(|url| url.URLByAppendingPathComponent(&NSString::from_str(spaces_dir))) + .and_then(|url| url.URLByAppendingPathComponent(&NSString::from_str(&escaped_peer))) + .and_then(|url| url.URLByAppendingPathComponent(&NSString::from_str(&options.id))) + } + .ok_or(UniffiError::ConcatSpaceDirFailed(format!( + "{}:{}:{}", + options.peer, options.space_type, options.id + )))?; + let affine_dir_str = autoreleasepool(|pool| { + Ok::( + unsafe { affine_dir.path() } + .ok_or(UniffiError::GetUserDocumentDirectoryFailed)? + .as_str(pool) + .to_string(), + ) + })?; + + // Replicate Swift's appending ".affine" subdir, creating it if necessary + fs::create_dir_all(&affine_dir_str) + .map_err(|_| UniffiError::CreateAffineDirFailed(affine_dir_str.clone()))?; + + let db_path = autoreleasepool(|pool| { + let db_path = + unsafe { affine_dir.URLByAppendingPathComponent(&NSString::from_str(DB_FILE_NAME)) }.ok_or( + UniffiError::ConcatSpaceDirFailed(format!( + "{}:{}:{}/{DB_FILE_NAME}", + options.peer, options.space_type, options.id + )), + )?; + Ok::( + unsafe { db_path.path() } + .ok_or(UniffiError::GetUserDocumentDirectoryFailed)? + .as_str(pool) + .to_string(), + ) + })?; + + Ok(db_path) +} + +#[cfg(not(any(target_os = "ios", target_os = "macos")))] +pub(crate) fn get_db_path(options: &StorageOptions) -> Result { + let home_dir = my_home() + .map_err(|_| UniffiError::GetUserDocumentDirectoryFailed)? + .ok_or(UniffiError::GetUserDocumentDirectoryFailed)?; + let spaces_dir = match options.space_type { + SpaceType::Userspace => "userspaces", + SpaceType::Workspace => "workspaces", + }; + let escaped_peer = escape_filename(&options.peer); + let db_path = home_dir + .join(".affine") + .join(spaces_dir) + .join(&escaped_peer) + .join(&options.id); + fs::create_dir_all(&db_path) + .map_err(|_| UniffiError::CreateAffineDirFailed(db_path.to_string_lossy().to_string()))?; + db_path + .join(DB_FILE_NAME) + .to_str() + .map(|p| p.to_owned()) + .ok_or(UniffiError::GetUserDocumentDirectoryFailed) +} + +fn escape_filename(name: &str) -> String { + // First replace special chars with '_' + let with_underscores = name.replace(|c: char| "\\/!@#$%^&*()+~`\"':;,?<>|".contains(c), "_"); + + // Then collapse multiple '_' into single '_' + let mut result = String::with_capacity(with_underscores.len()); + let mut last_was_underscore = false; + + for c in with_underscores.chars() { + if c == '_' { + if !last_was_underscore { + result.push(c); + } + last_was_underscore = true; + } else { + result.push(c); + last_was_underscore = false; + } + } + + // Remove trailing underscore + result.trim_end_matches('_').to_string() +} + +#[cfg(all(test, any(target_os = "ios", target_os = "macos")))] +mod tests { + use super::*; + + #[test] + fn test_escape_filename() { + assert_eq!(escape_filename("hello@world"), "hello_world"); + assert_eq!(escape_filename("test!!file"), "test_file"); + assert_eq!(escape_filename("_test_"), "_test"); // Leading underscore preserved + assert_eq!(escape_filename("multi___under"), "multi_under"); + assert_eq!(escape_filename("path/to\\file"), "path_to_file"); + } +} diff --git a/packages/frontend/native/Cargo.toml b/packages/frontend/native/Cargo.toml index c30c76c88193f..f22a5e105af33 100644 --- a/packages/frontend/native/Cargo.toml +++ b/packages/frontend/native/Cargo.toml @@ -4,19 +4,19 @@ name = "affine_native" version = "0.0.0" [lib] -crate-type = ["rlib", "cdylib"] +crate-type = ["cdylib", "rlib"] [dependencies] -affine_common = { workspace = true } +affine_common = { workspace = true } +affine_nbstore = { path = "./nbstore" } affine_sqlite_v1 = { path = "./sqlite_v1" } -affine_nbstore = { path = "./nbstore" } -napi = { workspace = true } -napi-derive = { workspace = true } -once_cell = { workspace = true } -sqlx = { workspace = true, default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] } -tokio = { workspace = true, features = ["full"] } +napi = { workspace = true } +napi-derive = { workspace = true } +once_cell = { workspace = true } +sqlx = { workspace = true, default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] } +tokio = { workspace = true, features = ["full"] } [build-dependencies] -napi-build = { workspace = true } -sqlx = { workspace = true, default-features = false, features = ["chrono", "json", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] } -tokio = { workspace = true, features = ["full"] } +napi-build = { workspace = true } +sqlx = { workspace = true, default-features = false, features = ["chrono", "json", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] } +tokio = { workspace = true, features = ["full"] } diff --git a/packages/frontend/native/nbstore/Cargo.toml b/packages/frontend/native/nbstore/Cargo.toml index 99293cada0343..ef10e0f0ab0d6 100644 --- a/packages/frontend/native/nbstore/Cargo.toml +++ b/packages/frontend/native/nbstore/Cargo.toml @@ -4,7 +4,10 @@ name = "affine_nbstore" version = "0.0.0" [lib] -crate-type = ["rlib", "cdylib"] +crate-type = ["cdylib", "rlib"] + +[features] +noop = ["napi-derive/noop", "napi/noop"] [dependencies] affine_schema = { path = "../schema" } @@ -15,9 +18,12 @@ napi-derive = { workspace = true } sqlx = { workspace = true, default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] } tokio = { workspace = true, features = ["full"] } +[target.'cfg(any(target_os = "ios", target_os = "android"))'.dependencies] +uniffi = { workspace = true } + [build-dependencies] affine_schema = { path = "../schema" } -dotenvy = { workspace = true } +dotenvy = { workspace = true } napi-build = { workspace = true } sqlx = { workspace = true, default-features = false, features = ["chrono", "json", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] } -tokio = { workspace = true, features = ["full"] } \ No newline at end of file +tokio = { workspace = true, features = ["full"] } diff --git a/packages/frontend/native/nbstore/src/blob.rs b/packages/frontend/native/nbstore/src/blob.rs index 05e00d26e44c6..17996168ae96d 100644 --- a/packages/frontend/native/nbstore/src/blob.rs +++ b/packages/frontend/native/nbstore/src/blob.rs @@ -1,3 +1,5 @@ +use std::ops::Deref; + use super::{storage::SqliteDocStorage, Blob, ListedBlob, SetBlob}; type Result = std::result::Result; @@ -22,7 +24,7 @@ impl SqliteDocStorage { DO UPDATE SET data=$2, mime=$3, size=$4, deleted_at=NULL;"#, ) .bind(blob.key) - .bind(blob.data.as_ref()) + .bind(blob.data.deref()) .bind(blob.mime) .bind(blob.data.len() as i64) .execute(&self.pool) @@ -67,7 +69,6 @@ impl SqliteDocStorage { #[cfg(test)] mod tests { - use napi::bindgen_prelude::Uint8Array; use sqlx::Row; use super::*; @@ -87,7 +88,7 @@ mod tests { storage .set_blob(SetBlob { key: format!("test_{}", i), - data: Uint8Array::from(vec![0, 0]), + data: vec![0, 0].into(), mime: "text/plain".to_string(), }) .await @@ -127,7 +128,7 @@ mod tests { storage .set_blob(SetBlob { key: format!("test_{}", i), - data: Uint8Array::from(vec![0, 0]), + data: vec![0, 0].into(), mime: "text/plain".to_string(), }) .await @@ -175,7 +176,7 @@ mod tests { storage .set_blob(SetBlob { key: format!("test_{}", i), - data: Uint8Array::from(vec![0, 0]), + data: vec![0, 0].into(), mime: "text/plain".to_string(), }) .await diff --git a/packages/frontend/native/nbstore/src/doc.rs b/packages/frontend/native/nbstore/src/doc.rs index d44b972d0fa71..28088da6f1339 100644 --- a/packages/frontend/native/nbstore/src/doc.rs +++ b/packages/frontend/native/nbstore/src/doc.rs @@ -1,3 +1,5 @@ +use std::ops::Deref; + use chrono::NaiveDateTime; use sqlx::{QueryBuilder, Row}; @@ -110,7 +112,7 @@ impl SqliteDocStorage { WHERE updated_at <= $3;"#, ) .bind(snapshot.doc_id) - .bind(snapshot.data.as_ref()) + .bind(snapshot.data.deref()) .bind(snapshot.timestamp) .execute(&self.pool) .await?; @@ -206,7 +208,6 @@ impl SqliteDocStorage { #[cfg(test)] mod tests { use chrono::{DateTime, Utc}; - use napi::bindgen_prelude::Uint8Array; use super::*; @@ -252,7 +253,7 @@ mod tests { storage .set_doc_snapshot(DocRecord { doc_id: "test".to_string(), - data: Uint8Array::from(vec![0, 0]), + data: vec![0, 0].into(), timestamp: Utc::now().naive_utc(), }) .await @@ -331,7 +332,7 @@ mod tests { let snapshot = DocRecord { doc_id: "test".to_string(), - data: Uint8Array::from(vec![0, 0]), + data: vec![0, 0].into(), timestamp: Utc::now().naive_utc(), }; @@ -349,7 +350,7 @@ mod tests { let snapshot = DocRecord { doc_id: "test".to_string(), - data: Uint8Array::from(vec![0, 0]), + data: vec![0, 0].into(), timestamp: Utc::now().naive_utc(), }; @@ -362,7 +363,7 @@ mod tests { let snapshot = DocRecord { doc_id: "test".to_string(), - data: Uint8Array::from(vec![0, 1]), + data: vec![0, 1].into(), timestamp: DateTime::from_timestamp_millis(Utc::now().timestamp_millis() - 1000) .unwrap() .naive_utc(), diff --git a/packages/frontend/native/nbstore/src/lib.rs b/packages/frontend/native/nbstore/src/lib.rs index 4b6e088b3ab8d..ec4968a70f8f3 100644 --- a/packages/frontend/native/nbstore/src/lib.rs +++ b/packages/frontend/native/nbstore/src/lib.rs @@ -1,27 +1,47 @@ -mod blob; -mod doc; -mod storage; -mod sync; +pub mod blob; +pub mod doc; +pub mod storage; +pub mod sync; use chrono::NaiveDateTime; use napi::bindgen_prelude::*; use napi_derive::napi; -fn map_err(err: sqlx::Error) -> napi::Error { - napi::Error::from(anyhow::Error::from(err)) +#[cfg(feature = "noop")] +type Result = anyhow::Result; + +#[cfg(not(feature = "noop"))] +type Result = napi::Result; + +#[cfg(not(feature = "noop"))] +fn map_err(err: sqlx::Error) -> Error { + Error::from(anyhow::Error::from(err)) +} + +#[cfg(feature = "noop")] +fn map_err(err: sqlx::Error) -> anyhow::Error { + anyhow::Error::from(err) } +#[cfg(feature = "noop")] +pub type Data = Vec; + +#[cfg(not(feature = "noop"))] +pub type Data = Uint8Array; + #[napi(object)] pub struct DocUpdate { pub doc_id: String, pub created_at: NaiveDateTime, - pub data: Uint8Array, + #[napi(ts_type = "Uint8Array")] + pub data: Data, } #[napi(object)] pub struct DocRecord { pub doc_id: String, - pub data: Uint8Array, + #[napi(ts_type = "Uint8Array")] + pub data: Data, pub timestamp: NaiveDateTime, } @@ -35,14 +55,16 @@ pub struct DocClock { #[napi(object)] pub struct SetBlob { pub key: String, - pub data: Uint8Array, + #[napi(ts_type = "Uint8Array")] + pub data: Data, pub mime: String, } #[napi(object)] pub struct Blob { pub key: String, - pub data: Uint8Array, + #[napi(ts_type = "Uint8Array")] + pub data: Data, pub mime: String, pub size: i64, pub created_at: NaiveDateTime, @@ -64,7 +86,7 @@ pub struct DocStorage { #[napi] impl DocStorage { #[napi(constructor, async_runtime)] - pub fn new(path: String) -> napi::Result { + pub fn new(path: String) -> Result { Ok(Self { storage: storage::SqliteDocStorage::new(path), }) @@ -72,19 +94,19 @@ impl DocStorage { #[napi] /// Initialize the database and run migrations. - pub async fn connect(&self) -> napi::Result<()> { + pub async fn connect(&self) -> Result<()> { self.storage.connect().await.map_err(map_err) } #[napi] - pub async fn close(&self) -> napi::Result<()> { + pub async fn close(&self) -> Result<()> { self.storage.close().await; Ok(()) } #[napi(getter)] - pub async fn is_closed(&self) -> napi::Result { + pub async fn is_closed(&self) -> Result { Ok(self.storage.is_closed()) } @@ -93,26 +115,22 @@ impl DocStorage { * See https://www.sqlite.org/pragma.html#pragma_wal_checkpoint:~:text=PRAGMA%20schema.wal_checkpoint%3B */ #[napi] - pub async fn checkpoint(&self) -> napi::Result<()> { + pub async fn checkpoint(&self) -> Result<()> { self.storage.checkpoint().await.map_err(map_err) } #[napi] - pub async fn validate(&self) -> napi::Result { + pub async fn validate(&self) -> Result { self.storage.validate().await.map_err(map_err) } #[napi] - pub async fn set_space_id(&self, space_id: String) -> napi::Result<()> { + pub async fn set_space_id(&self, space_id: String) -> Result<()> { self.storage.set_space_id(space_id).await.map_err(map_err) } #[napi] - pub async fn push_update( - &self, - doc_id: String, - update: Uint8Array, - ) -> napi::Result { + pub async fn push_update(&self, doc_id: String, update: Uint8Array) -> Result { self .storage .push_update(doc_id, update) @@ -121,12 +139,12 @@ impl DocStorage { } #[napi] - pub async fn get_doc_snapshot(&self, doc_id: String) -> napi::Result> { + pub async fn get_doc_snapshot(&self, doc_id: String) -> Result> { self.storage.get_doc_snapshot(doc_id).await.map_err(map_err) } #[napi] - pub async fn set_doc_snapshot(&self, snapshot: DocRecord) -> napi::Result { + pub async fn set_doc_snapshot(&self, snapshot: DocRecord) -> Result { self .storage .set_doc_snapshot(snapshot) @@ -135,7 +153,7 @@ impl DocStorage { } #[napi] - pub async fn get_doc_updates(&self, doc_id: String) -> napi::Result> { + pub async fn get_doc_updates(&self, doc_id: String) -> Result> { self.storage.get_doc_updates(doc_id).await.map_err(map_err) } @@ -144,7 +162,7 @@ impl DocStorage { &self, doc_id: String, updates: Vec, - ) -> napi::Result { + ) -> Result { self .storage .mark_updates_merged(doc_id, updates) @@ -153,32 +171,32 @@ impl DocStorage { } #[napi] - pub async fn delete_doc(&self, doc_id: String) -> napi::Result<()> { + pub async fn delete_doc(&self, doc_id: String) -> Result<()> { self.storage.delete_doc(doc_id).await.map_err(map_err) } #[napi] - pub async fn get_doc_clocks(&self, after: Option) -> napi::Result> { + pub async fn get_doc_clocks(&self, after: Option) -> Result> { self.storage.get_doc_clocks(after).await.map_err(map_err) } #[napi] - pub async fn get_doc_clock(&self, doc_id: String) -> napi::Result> { + pub async fn get_doc_clock(&self, doc_id: String) -> Result> { self.storage.get_doc_clock(doc_id).await.map_err(map_err) } #[napi] - pub async fn get_blob(&self, key: String) -> napi::Result> { + pub async fn get_blob(&self, key: String) -> Result> { self.storage.get_blob(key).await.map_err(map_err) } #[napi] - pub async fn set_blob(&self, blob: SetBlob) -> napi::Result<()> { + pub async fn set_blob(&self, blob: SetBlob) -> Result<()> { self.storage.set_blob(blob).await.map_err(map_err) } #[napi] - pub async fn delete_blob(&self, key: String, permanently: bool) -> napi::Result<()> { + pub async fn delete_blob(&self, key: String, permanently: bool) -> Result<()> { self .storage .delete_blob(key, permanently) @@ -187,17 +205,17 @@ impl DocStorage { } #[napi] - pub async fn release_blobs(&self) -> napi::Result<()> { + pub async fn release_blobs(&self) -> Result<()> { self.storage.release_blobs().await.map_err(map_err) } #[napi] - pub async fn list_blobs(&self) -> napi::Result> { + pub async fn list_blobs(&self) -> Result> { self.storage.list_blobs().await.map_err(map_err) } #[napi] - pub async fn get_peer_remote_clocks(&self, peer: String) -> napi::Result> { + pub async fn get_peer_remote_clocks(&self, peer: String) -> Result> { self .storage .get_peer_remote_clocks(peer) @@ -206,11 +224,7 @@ impl DocStorage { } #[napi] - pub async fn get_peer_remote_clock( - &self, - peer: String, - doc_id: String, - ) -> napi::Result { + pub async fn get_peer_remote_clock(&self, peer: String, doc_id: String) -> Result { self .storage .get_peer_remote_clock(peer, doc_id) @@ -224,7 +238,7 @@ impl DocStorage { peer: String, doc_id: String, clock: NaiveDateTime, - ) -> napi::Result<()> { + ) -> Result<()> { self .storage .set_peer_remote_clock(peer, doc_id, clock) @@ -233,7 +247,7 @@ impl DocStorage { } #[napi] - pub async fn get_peer_pulled_remote_clocks(&self, peer: String) -> napi::Result> { + pub async fn get_peer_pulled_remote_clocks(&self, peer: String) -> Result> { self .storage .get_peer_pulled_remote_clocks(peer) @@ -246,7 +260,7 @@ impl DocStorage { &self, peer: String, doc_id: String, - ) -> napi::Result { + ) -> Result { self .storage .get_peer_pulled_remote_clock(peer, doc_id) @@ -260,7 +274,7 @@ impl DocStorage { peer: String, doc_id: String, clock: NaiveDateTime, - ) -> napi::Result<()> { + ) -> Result<()> { self .storage .set_peer_pulled_remote_clock(peer, doc_id, clock) @@ -269,7 +283,7 @@ impl DocStorage { } #[napi] - pub async fn get_peer_pushed_clocks(&self, peer: String) -> napi::Result> { + pub async fn get_peer_pushed_clocks(&self, peer: String) -> Result> { self .storage .get_peer_pushed_clocks(peer) @@ -278,11 +292,7 @@ impl DocStorage { } #[napi] - pub async fn get_peer_pushed_clock( - &self, - peer: String, - doc_id: String, - ) -> napi::Result { + pub async fn get_peer_pushed_clock(&self, peer: String, doc_id: String) -> Result { self .storage .get_peer_pushed_clock(peer, doc_id) @@ -296,7 +306,7 @@ impl DocStorage { peer: String, doc_id: String, clock: NaiveDateTime, - ) -> napi::Result<()> { + ) -> Result<()> { self .storage .set_peer_pushed_clock(peer, doc_id, clock) @@ -305,7 +315,7 @@ impl DocStorage { } #[napi] - pub async fn clear_clocks(&self) -> napi::Result<()> { + pub async fn clear_clocks(&self) -> Result<()> { self.storage.clear_clocks().await.map_err(map_err) } } diff --git a/packages/frontend/native/package.json b/packages/frontend/native/package.json index e0ebe3ddc7bcd..6c454e43bdba2 100644 --- a/packages/frontend/native/package.json +++ b/packages/frontend/native/package.json @@ -44,8 +44,8 @@ }, "scripts": { "artifacts": "napi artifacts", - "build": "napi build --platform --release --no-const-enum", - "build:debug": "napi build --platform", + "build": "napi build -p affine_native --platform --release --no-const-enum", + "build:debug": "napi build -p affine_native --platform", "universal": "napi universal", "test": "ava", "version": "napi version" diff --git a/packages/frontend/native/schema/Cargo.toml b/packages/frontend/native/schema/Cargo.toml index 79e53049dffe9..2180e23d44583 100644 --- a/packages/frontend/native/schema/Cargo.toml +++ b/packages/frontend/native/schema/Cargo.toml @@ -4,4 +4,4 @@ name = "affine_schema" version = "0.0.0" [dependencies] -sqlx = { workspace = true, default-features = false, features = ["migrate"] } \ No newline at end of file +sqlx = { workspace = true, default-features = false, features = ["migrate"] } diff --git a/packages/frontend/native/sqlite_v1/Cargo.toml b/packages/frontend/native/sqlite_v1/Cargo.toml index 3dde2e558c3bf..b6d1cd6555a8b 100644 --- a/packages/frontend/native/sqlite_v1/Cargo.toml +++ b/packages/frontend/native/sqlite_v1/Cargo.toml @@ -4,7 +4,7 @@ name = "affine_sqlite_v1" version = "0.0.0" [lib] -crate-type = ["rlib", "cdylib"] +crate-type = ["cdylib", "rlib"] [dependencies] affine_schema = { path = "../schema" } @@ -17,7 +17,7 @@ tokio = { workspace = true, features = ["full"] } [build-dependencies] affine_schema = { path = "../schema" } -dotenvy = { workspace = true } +dotenvy = { workspace = true } napi-build = { workspace = true } sqlx = { workspace = true, default-features = false, features = ["chrono", "json", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] } tokio = { workspace = true, features = ["full"] } diff --git a/tools/commitlint/.commitlintrc.json b/tools/commitlint/.commitlintrc.json index 35de033e6ff06..ac82f3ece66db 100644 --- a/tools/commitlint/.commitlintrc.json +++ b/tools/commitlint/.commitlintrc.json @@ -13,6 +13,7 @@ "mobile", "ios", "android", + "mobile-native", "docs", "component", "env", diff --git a/tools/utils/src/workspace.gen.ts b/tools/utils/src/workspace.gen.ts index abe5a3d27de6c..43e86284d3226 100644 --- a/tools/utils/src/workspace.gen.ts +++ b/tools/utils/src/workspace.gen.ts @@ -394,6 +394,7 @@ export const PackageList = [ 'packages/frontend/i18n', 'blocksuite/affine/all', 'packages/common/infra', + 'packages/frontend/native', ], }, { diff --git a/yarn.lock b/yarn.lock index 16d9dadbfb5ec..215e70a028cbb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -591,6 +591,7 @@ __metadata: "@affine/component": "workspace:*" "@affine/core": "workspace:*" "@affine/i18n": "workspace:*" + "@affine/native": "workspace:*" "@blocksuite/affine": "workspace:*" "@blocksuite/icons": "npm:2.1.75" "@capacitor/app": "npm:^6.0.2" @@ -610,6 +611,7 @@ __metadata: react-dom: "npm:^19.0.0" react-router-dom: "npm:^6.28.0" typescript: "npm:^5.7.2" + yjs: "npm:13.6.18" languageName: unknown linkType: soft