diff --git a/apps/android/app/src/main/java/com/example/jwst_demo/MainActivity.kt b/apps/android/app/src/main/java/com/example/jwst_demo/MainActivity.kt index 0b5a42fe..62715036 100644 --- a/apps/android/app/src/main/java/com/example/jwst_demo/MainActivity.kt +++ b/apps/android/app/src/main/java/com/example/jwst_demo/MainActivity.kt @@ -11,9 +11,25 @@ import java.io.File import java.util.* import com.toeverything.jwst.Workspace import kotlin.jvm.optionals.getOrNull +import kotlin.random.Random fun Optional.unwrap(): T? = orElse(null) +fun String.hexStringToByteArray(): ByteArray { + return this.chunked(2) + .map { it.toInt(16).toByte() } + .toByteArray() +} + +fun getStaticWorkspace(): String { + return "010895E2C0E01D0027010A73706163653A6D6574610570616765730027010C73706163653A626C6F636B73047465737401280095E2C0E01D010B7379733A666C61766F757201770474657374270095E2C0E01D010C7379733A6368696C6472656E00280095E2C0E01D010B7379733A63726561746564017B4278B38B757B900028010D73706163653A757064617465640474657374017B4278B38B757B9000280095E2C0E01D010970726F703A74657374017703616263A895E2C0E01D05017B4278B38B757B90000195E2C0E01D010501" +} + +fun getRandomId(): String { + val chars = "abcdefghijklmnopqrstuvwxyz0123456789" + return (1..8).map { chars[Random.nextInt(chars.length)] }.joinToString("") +} + class MainActivity : AppCompatActivity() { @RequiresApi(Build.VERSION_CODES.TIRAMISU) @@ -24,6 +40,10 @@ class MainActivity : AppCompatActivity() { val database = File(filesDir, "jwst.db") val storage = Storage(database.absolutePath, "ws://10.0.2.2:3000/collaboration", "debug") + storage.initWorkspace(getRandomId(), getStaticWorkspace().hexStringToByteArray()) + val text = storage.getWorkspace("test1").get().get("test").get().get("test").get() + Log.i("jwst", "text: $text") + storage.getWorkspace("test").unwrap()?.let { workspace -> setupWorkspace(workspace) @@ -60,7 +80,6 @@ class MainActivity : AppCompatActivity() { Log.i("jwst", searchResult3) while (true) { - Log.i("jwst", " getting root") workspace.get("root").unwrap()?.let { block -> block.get("test").ifPresent { value -> diff --git a/apps/android/build.gradle b/apps/android/build.gradle index 018efaf7..a2003fd0 100644 --- a/apps/android/build.gradle +++ b/apps/android/build.gradle @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '8.1.0' apply false - id 'com.android.library' version '8.1.0' apply false + id 'com.android.application' version '8.1.2' apply false + id 'com.android.library' version '8.1.2' apply false id 'org.jetbrains.kotlin.android' version '1.8.10' apply false } diff --git a/apps/swift/OctoBaseSwift/RustXcframework.xcframework/Info.plist b/apps/swift/OctoBaseSwift/RustXcframework.xcframework/Info.plist index 75a6e1fa..1809eda0 100644 --- a/apps/swift/OctoBaseSwift/RustXcframework.xcframework/Info.plist +++ b/apps/swift/OctoBaseSwift/RustXcframework.xcframework/Info.plist @@ -5,10 +5,12 @@ AvailableLibraries + BinaryPath + liboctobase.a HeadersPath Headers LibraryIdentifier - ios-arm64-simulator + macos-arm64 LibraryPath liboctobase.a SupportedArchitectures @@ -16,11 +18,11 @@ arm64 SupportedPlatform - ios - SupportedPlatformVariant - simulator + macos + BinaryPath + liboctobase.a HeadersPath Headers LibraryIdentifier @@ -35,10 +37,12 @@ ios + BinaryPath + liboctobase.a HeadersPath Headers LibraryIdentifier - macos-arm64 + ios-arm64-simulator LibraryPath liboctobase.a SupportedArchitectures @@ -46,7 +50,9 @@ arm64 SupportedPlatform - macos + ios + SupportedPlatformVariant + simulator CFBundlePackageType diff --git a/apps/swift/OctoBaseSwift/RustXcframework.xcframework/ios-arm64-simulator/Headers/jwst-swift.h b/apps/swift/OctoBaseSwift/RustXcframework.xcframework/ios-arm64-simulator/Headers/jwst-swift.h index 7fa5b3b6..98c97d3a 100644 --- a/apps/swift/OctoBaseSwift/RustXcframework.xcframework/ios-arm64-simulator/Headers/jwst-swift.h +++ b/apps/swift/OctoBaseSwift/RustXcframework.xcframework/ios-arm64-simulator/Headers/jwst-swift.h @@ -122,6 +122,8 @@ bool __swift_bridge__$Storage$is_connected(void* self); bool __swift_bridge__$Storage$is_finished(void* self); bool __swift_bridge__$Storage$is_error(void* self); void* __swift_bridge__$Storage$get_sync_state(void* self); +bool __swift_bridge__$Storage$init(void* self, void* workspace_id, void* data); +void* __swift_bridge__$Storage$export(void* self, void* workspace_id); void* __swift_bridge__$Storage$connect(void* self, void* workspace_id, void* remote); void* __swift_bridge__$Storage$get_last_synced(void* self); diff --git a/apps/swift/OctoBaseSwift/RustXcframework.xcframework/ios-arm64/Headers/jwst-swift.h b/apps/swift/OctoBaseSwift/RustXcframework.xcframework/ios-arm64/Headers/jwst-swift.h index 7fa5b3b6..98c97d3a 100644 --- a/apps/swift/OctoBaseSwift/RustXcframework.xcframework/ios-arm64/Headers/jwst-swift.h +++ b/apps/swift/OctoBaseSwift/RustXcframework.xcframework/ios-arm64/Headers/jwst-swift.h @@ -122,6 +122,8 @@ bool __swift_bridge__$Storage$is_connected(void* self); bool __swift_bridge__$Storage$is_finished(void* self); bool __swift_bridge__$Storage$is_error(void* self); void* __swift_bridge__$Storage$get_sync_state(void* self); +bool __swift_bridge__$Storage$init(void* self, void* workspace_id, void* data); +void* __swift_bridge__$Storage$export(void* self, void* workspace_id); void* __swift_bridge__$Storage$connect(void* self, void* workspace_id, void* remote); void* __swift_bridge__$Storage$get_last_synced(void* self); diff --git a/apps/swift/OctoBaseSwift/RustXcframework.xcframework/macos-arm64/Headers/jwst-swift.h b/apps/swift/OctoBaseSwift/RustXcframework.xcframework/macos-arm64/Headers/jwst-swift.h index 7fa5b3b6..98c97d3a 100644 --- a/apps/swift/OctoBaseSwift/RustXcframework.xcframework/macos-arm64/Headers/jwst-swift.h +++ b/apps/swift/OctoBaseSwift/RustXcframework.xcframework/macos-arm64/Headers/jwst-swift.h @@ -122,6 +122,8 @@ bool __swift_bridge__$Storage$is_connected(void* self); bool __swift_bridge__$Storage$is_finished(void* self); bool __swift_bridge__$Storage$is_error(void* self); void* __swift_bridge__$Storage$get_sync_state(void* self); +bool __swift_bridge__$Storage$init(void* self, void* workspace_id, void* data); +void* __swift_bridge__$Storage$export(void* self, void* workspace_id); void* __swift_bridge__$Storage$connect(void* self, void* workspace_id, void* remote); void* __swift_bridge__$Storage$get_last_synced(void* self); diff --git a/apps/swift/OctoBaseSwift/Sources/OctoBase/jwst-swift.swift b/apps/swift/OctoBaseSwift/Sources/OctoBase/jwst-swift.swift index 19d3b783..98decd0d 100644 --- a/apps/swift/OctoBaseSwift/Sources/OctoBase/jwst-swift.swift +++ b/apps/swift/OctoBaseSwift/Sources/OctoBase/jwst-swift.swift @@ -578,6 +578,14 @@ public class StorageRefMut: StorageRef { } } extension StorageRefMut { + public func init(_ workspace_id: GenericIntoRustString, _ data: RustVec) -> Bool { + __swift_bridge__$Storage$init(ptr, { let rustString = workspace_id.intoRustString(); rustString.isOwned = false; return rustString.ptr }(), { let val = data; val.isOwned = false; return val.ptr }()) + } + + public func export(_ workspace_id: GenericIntoRustString) -> Optional> { + { let val = __swift_bridge__$Storage$export(ptr, { let rustString = workspace_id.intoRustString(); rustString.isOwned = false; return rustString.ptr }()); if val != nil { return RustVec(ptr: val!) } else { return nil } }() + } + public func connect(_ workspace_id: GenericIntoRustString, _ remote: GenericIntoRustString) -> Optional { { let val = __swift_bridge__$Storage$connect(ptr, { let rustString = workspace_id.intoRustString(); rustString.isOwned = false; return rustString.ptr }(), { let rustString = remote.intoRustString(); rustString.isOwned = false; return rustString.ptr }()); if val != nil { return Workspace(ptr: val!) } else { return nil } }() } diff --git a/libs/jwst-binding/jwst-jni/android/build.gradle b/libs/jwst-binding/jwst-jni/android/build.gradle index 8b9805ec..3e664058 100644 --- a/libs/jwst-binding/jwst-jni/android/build.gradle +++ b/libs/jwst-binding/jwst-jni/android/build.gradle @@ -117,7 +117,7 @@ publishing { release(MavenPublication) { groupId = 'com.toeverything' artifactId = 'octobase' - version = '0.2.6' + version = '0.2.7' afterEvaluate { from components.release diff --git a/libs/jwst-binding/jwst-jni/android/src/main/java/com/toeverything/jwst/JWST.kt b/libs/jwst-binding/jwst-jni/android/src/main/java/com/toeverything/jwst/JWST.kt index 4124fba2..3a9ee62f 100644 --- a/libs/jwst-binding/jwst-jni/android/src/main/java/com/toeverything/jwst/JWST.kt +++ b/libs/jwst-binding/jwst-jni/android/src/main/java/com/toeverything/jwst/JWST.kt @@ -182,6 +182,26 @@ class Storage constructor(path: String, private val remote: String = "", private val error get() = this.storage.error() + fun initWorkspace(id: String, data: ByteArray): Result { + val success = this.storage.init(id,data) + return if (success) { + Result.success(Unit) + } else { + val error = this.storage.error().orElse("Unknown error") + Result.failure(Exception(error)) + } + } + + fun exportWorkspace(id: String): Result { + val data = this.storage.export(id) + return if (data.isNotEmpty()) { + Result.success(data) + } else { + val error = this.storage.error().orElse("Unknown error") + Result.failure(Exception(error)) + } + } + fun getWorkspace(id: String): Optional { return this.storage.connect(id, this.remote.takeIf { it.isNotEmpty() }?.let { "$it/$id" } ?: "").map { Workspace(it) } } diff --git a/libs/jwst-binding/jwst-jni/android/src/main/java/com/toeverything/jwst/lib/JwstStorage.java b/libs/jwst-binding/jwst-jni/android/src/main/java/com/toeverything/jwst/lib/JwstStorage.java index e529c69b..e849c03d 100644 --- a/libs/jwst-binding/jwst-jni/android/src/main/java/com/toeverything/jwst/lib/JwstStorage.java +++ b/libs/jwst-binding/jwst-jni/android/src/main/java/com/toeverything/jwst/lib/JwstStorage.java @@ -58,6 +58,20 @@ public final boolean is_error() { } private static native @NonNull String do_get_sync_state(long self); + public final boolean init(@NonNull String workspace_id, @NonNull byte [] data) { + boolean ret = do_init(mNativeObj, workspace_id, data); + + return ret; + } + private static native boolean do_init(long self, @NonNull String workspace_id, byte [] data); + + public final byte [] export(@NonNull String workspace_id) { + byte [] ret = do_export(mNativeObj, workspace_id); + + return ret; + } + private static native byte [] do_export(long self, @NonNull String workspace_id); + public final @NonNull java.util.Optional connect(@NonNull String workspace_id, @NonNull String remote) { long ret = do_connect(mNativeObj, workspace_id, remote); java.util.Optional convRet; diff --git a/libs/jwst-binding/jwst-jni/build.rs b/libs/jwst-binding/jwst-jni/build.rs index d1484f96..2c0d81dd 100644 --- a/libs/jwst-binding/jwst-jni/build.rs +++ b/libs/jwst-binding/jwst-jni/build.rs @@ -19,6 +19,32 @@ fn main() { .chain(["use jni_sys::*;"].iter()) .chain( [ + r#"foreign_typemap!( + ($p:r_type) Vec => jbyteArray { + let slice = &($p)[..]; + let slice = unsafe { std::mem::transmute::<&[u8], &[i8]>(slice) }; + let raw = JavaByteArray::from_slice_to_raw(slice, env); + $out = raw; + }; + ($p:f_type) => "jbyteArray"; +); + +foreign_typemap!( + ($p:r_type) Vec => jbyteArray { + let slice = &($p)[..]; + let slice = unsafe { std::mem::transmute::<&[u8], &[i8]>(slice) }; + let raw = JavaByteArray::from_slice_to_raw(slice, env); + $out = raw; + }; + ($p:f_type) => "jbyteArray"; + ($p:r_type) &'a [u8] <= jbyteArray { + let arr = JavaByteArray::new(env, $p); + let slice = arr.to_slice(); + let slice = unsafe { std::mem::transmute::<&[i8], &[u8]>(slice) }; + $out = slice; + }; + ($p:f_type) <= "jbyteArray"; +);"#, r#"foreign_class!( class JwstStorage { self_type JwstStorage; @@ -30,6 +56,8 @@ fn main() { fn JwstStorage::is_finished(&self) -> bool; fn JwstStorage::is_error(&self) -> bool; fn JwstStorage::get_sync_state(&self) -> String; + fn JwstStorage::init(&mut self, workspace_id: String, data: &[u8]) -> bool; alias init; + fn JwstStorage::export(&mut self, workspace_id: String) -> Vec; alias export; fn JwstStorage::connect(&mut self, workspace_id: String, remote: String) -> Option; alias connect; fn JwstStorage::get_last_synced(&self) ->Vec; } @@ -82,7 +110,7 @@ foreign_class!( .join("\n"); fs::write(&in_temp, &template).unwrap(); - let template_changed = fs::read_to_string(in_src).unwrap() != template; + let template_changed = fs::read_to_string(&in_src).unwrap() != template; if template_changed || !in_temp.with_extension("").exists() || !jni_dir.exists() { // delete the lib folder then create it again to prevent obsolete files @@ -104,6 +132,7 @@ foreign_class!( != fs::read_to_string(in_temp.with_extension("")).unwrap() { fs::copy(in_temp.with_extension("out"), in_temp.with_extension("")).unwrap(); + fs::copy(in_temp.with_extension("out"), in_src).unwrap(); } } } diff --git a/libs/jwst-binding/jwst-jni/src/java_glue.rs.in b/libs/jwst-binding/jwst-jni/src/java_glue.rs.in index cd9a3add..ce1ecea0 100644 --- a/libs/jwst-binding/jwst-jni/src/java_glue.rs.in +++ b/libs/jwst-binding/jwst-jni/src/java_glue.rs.in @@ -2,6 +2,32 @@ use crate::*; use jni_sys::*; +foreign_typemap!( + ($p:r_type) Vec => jbyteArray { + let slice = &($p)[..]; + let slice = unsafe { std::mem::transmute::<&[u8], &[i8]>(slice) }; + let raw = JavaByteArray::from_slice_to_raw(slice, env); + $out = raw; + }; + ($p:f_type) => "jbyteArray"; +); + +foreign_typemap!( + ($p:r_type) Vec => jbyteArray { + let slice = &($p)[..]; + let slice = unsafe { std::mem::transmute::<&[u8], &[i8]>(slice) }; + let raw = JavaByteArray::from_slice_to_raw(slice, env); + $out = raw; + }; + ($p:f_type) => "jbyteArray"; + ($p:r_type) &'a [u8] <= jbyteArray { + let arr = JavaByteArray::new(env, $p); + let slice = arr.to_slice(); + let slice = unsafe { std::mem::transmute::<&[i8], &[u8]>(slice) }; + $out = slice; + }; + ($p:f_type) <= "jbyteArray"; +); foreign_class!( class JwstStorage { self_type JwstStorage; @@ -13,6 +39,8 @@ foreign_class!( fn JwstStorage::is_finished(&self) -> bool; fn JwstStorage::is_error(&self) -> bool; fn JwstStorage::get_sync_state(&self) -> String; + fn JwstStorage::init(&mut self, workspace_id: String, data: &[u8]) -> bool; alias init; + fn JwstStorage::export(&mut self, workspace_id: String) -> Vec; alias export; fn JwstStorage::connect(&mut self, workspace_id: String, remote: String) -> Option; alias connect; fn JwstStorage::get_last_synced(&self) ->Vec; } diff --git a/libs/jwst-binding/jwst-jni/src/storage.rs b/libs/jwst-binding/jwst-jni/src/storage.rs index 33a203ee..c183dc6c 100644 --- a/libs/jwst-binding/jwst-jni/src/storage.rs +++ b/libs/jwst-binding/jwst-jni/src/storage.rs @@ -91,6 +91,54 @@ impl JwstStorage { } } + pub fn init(&mut self, workspace_id: String, data: &[u8]) -> bool { + match self.init_workspace(workspace_id, data) { + Ok(_) => true, + Err(e) => { + let error = format!("Failed to init workspace: {:?}", e); + error!("{}", error); + self.error = Some(error); + false + } + } + } + + fn init_workspace(&self, workspace_id: String, data: &[u8]) -> JwstStorageResult { + let rt = Arc::new( + Builder::new_multi_thread() + .worker_threads(1) + .enable_all() + .thread_name("jwst-jni-init") + .build() + .map_err(JwstStorageError::SyncThread)?, + ); + rt.block_on(self.storage.init_workspace(workspace_id, data.to_vec())) + } + + pub fn export(&mut self, workspace_id: String) -> Vec { + match self.export_workspace(workspace_id) { + Ok(data) => data, + Err(e) => { + let error = format!("Failed to export workspace: {:?}", e); + error!("{}", error); + self.error = Some(error); + vec![] + } + } + } + + fn export_workspace(&self, workspace_id: String) -> JwstStorageResult> { + let rt = Arc::new( + Builder::new_multi_thread() + .worker_threads(1) + .enable_all() + .thread_name("jwst-jni-export") + .build() + .map_err(JwstStorageError::SyncThread)?, + ); + rt.block_on(self.storage.export_workspace(workspace_id)) + } + pub fn connect(&mut self, workspace_id: String, remote: String) -> Option { match self.sync(workspace_id, remote) { Ok(workspace) => Some(workspace), diff --git a/libs/jwst-binding/jwst-swift/src/lib.rs b/libs/jwst-binding/jwst-swift/src/lib.rs index 8bd239c8..5872026a 100644 --- a/libs/jwst-binding/jwst-swift/src/lib.rs +++ b/libs/jwst-binding/jwst-swift/src/lib.rs @@ -134,6 +134,10 @@ mod ffi { fn get_sync_state(self: &Storage) -> String; + fn init(self: &mut Storage, workspace_id: String, data: Vec) -> bool; + + fn export(self: &mut Storage, workspace_id: String) -> Option>; + fn connect(self: &mut Storage, workspace_id: String, remote: String) -> Option; fn get_last_synced(self: &Storage) -> Vec; diff --git a/libs/jwst-binding/jwst-swift/src/storage.rs b/libs/jwst-binding/jwst-swift/src/storage.rs index 62e537ee..450fc1ae 100644 --- a/libs/jwst-binding/jwst-swift/src/storage.rs +++ b/libs/jwst-binding/jwst-swift/src/storage.rs @@ -88,6 +88,54 @@ impl Storage { } } + pub fn init(&mut self, workspace_id: String, data: Vec) -> bool { + match self.init_workspace(workspace_id, data) { + Ok(_) => true, + Err(e) => { + let error = format!("Failed to init workspace: {:?}", e); + error!("{}", error); + self.error = Some(error); + false + } + } + } + + fn init_workspace(&self, workspace_id: String, data: Vec) -> JwstStorageResult { + let rt = Arc::new( + Builder::new_multi_thread() + .worker_threads(1) + .enable_all() + .thread_name("jwst-swift-init") + .build() + .map_err(JwstStorageError::SyncThread)?, + ); + rt.block_on(self.storage.init_workspace(workspace_id, data)) + } + + pub fn export(&mut self, workspace_id: String) -> Option> { + match self.export_workspace(workspace_id) { + Ok(data) => Some(data), + Err(e) => { + let error = format!("Failed to export workspace: {:?}", e); + error!("{}", error); + self.error = Some(error); + None + } + } + } + + fn export_workspace(&self, workspace_id: String) -> JwstStorageResult> { + let rt = Arc::new( + Builder::new_multi_thread() + .worker_threads(1) + .enable_all() + .thread_name("jwst-swift-export") + .build() + .map_err(JwstStorageError::SyncThread)?, + ); + rt.block_on(self.storage.export_workspace(workspace_id)) + } + pub fn connect(&mut self, workspace_id: String, remote: String) -> Option { match self.sync(workspace_id, remote) { Ok(workspace) => Some(workspace), diff --git a/libs/jwst-codec/src/doc/common/range.rs b/libs/jwst-codec/src/doc/common/range.rs index 4391b3b8..0c295599 100644 --- a/libs/jwst-codec/src/doc/common/range.rs +++ b/libs/jwst-codec/src/doc/common/range.rs @@ -36,6 +36,84 @@ impl OrderRange { } } + pub fn contains(&self, clock: u64) -> bool { + match self { + OrderRange::Range(range) => range.contains(&clock), + OrderRange::Fragment(ranges) => ranges.iter().any(|r| r.contains(&clock)), + } + } + + fn check_range_covered(old_vec: &[Range], new_vec: &[Range]) -> bool { + let mut old_iter = old_vec.iter(); + let mut next_old = old_iter.next(); + let mut new_iter = new_vec.iter().peekable(); + let mut next_new = new_iter.next(); + 'new_loop: while let Some(new_range) = next_new { + while let Some(old_range) = next_old { + if old_range.start < new_range.start || old_range.end > new_range.end { + if new_iter.peek().is_some() { + next_new = new_iter.next(); + continue 'new_loop; + } else { + return false; + } + } + next_old = old_iter.next(); + if let Some(next_old) = &next_old { + if next_old.start > new_range.end { + continue; + } + } + } + next_new = new_iter.next(); + } + true + } + + /// diff_range returns the difference between the old range and the new + /// range. current range must be covered by the new range + pub fn diff_range(&self, new_range: &OrderRange) -> Vec> { + let old_vec = self.clone().into_iter().collect::>(); + let new_vec = new_range.clone().into_iter().collect::>(); + + if !Self::check_range_covered(&old_vec, &new_vec) { + return Vec::new(); + } + + let mut diffs = Vec::new(); + let mut old_idx = 0; + + for new_range in &new_vec { + let mut overlap_ranges = Vec::new(); + while old_idx < old_vec.len() && old_vec[old_idx].start <= new_range.end { + overlap_ranges.push(old_vec[old_idx].clone()); + old_idx += 1; + } + + if overlap_ranges.is_empty() { + diffs.push(new_range.clone()); + } else { + let mut last_end = overlap_ranges[0].start; + if last_end > new_range.start { + diffs.push(new_range.start..last_end); + } + + for overlap in &overlap_ranges { + if overlap.start > last_end { + diffs.push(last_end..overlap.start); + } + last_end = overlap.end; + } + + if new_range.end > last_end { + diffs.push(last_end..new_range.end); + } + } + } + + diffs + } + pub fn extends(&mut self, list: T) where T: Into>>, @@ -294,6 +372,60 @@ mod tests { assert_eq!(range, OrderRange::Fragment(vec![(0..10), (10..50), (20..30)])); } + #[test] + fn test_range_covered() { + assert_eq!(OrderRange::check_range_covered(&[0..1], &[2..3]), false); + assert_eq!(OrderRange::check_range_covered(&[0..1], &[0..3]), true); + assert_eq!(OrderRange::check_range_covered(&[0..1], &[1..3]), false); + assert_eq!(OrderRange::check_range_covered(&[0..1], &[0..3]), true); + assert_eq!(OrderRange::check_range_covered(&[1..2], &[0..3]), true); + assert_eq!(OrderRange::check_range_covered(&[1..2, 2..3], &[0..3]), true); + assert_eq!(OrderRange::check_range_covered(&[1..2, 2..3, 3..4], &[0..3]), false); + assert_eq!(OrderRange::check_range_covered(&[0..1, 2..3], &[0..2, 2..4]), true); + assert_eq!( + OrderRange::check_range_covered(&[0..1, 2..3, 3..4], &[0..2, 2..4]), + true + ); + } + + #[test] + fn test_range_diff() { + { + let old = OrderRange::Range(0..1); + let new = OrderRange::Range(2..3); + let ranges = old.diff_range(&new); + assert_eq!(ranges, vec![]); + } + + { + let old = OrderRange::Range(0..10); + let new = OrderRange::Range(0..11); + let ranges = old.diff_range(&new); + assert_eq!(ranges, vec![(10..11)]); + } + + { + let old: OrderRange = vec![(0..10), (20..30)].into(); + let new: OrderRange = vec![(0..15), (20..30)].into(); + let ranges = old.diff_range(&new); + assert_eq!(ranges, vec![(10..15)]); + } + + { + let old: OrderRange = vec![(0..3), (5..7), (8..10), (16..18), (21..23)].into(); + let new: OrderRange = vec![(0..12), (15..23)].into(); + let ranges = old.diff_range(&new); + assert_eq!(ranges, vec![(3..5), (7..8), (10..12), (15..16), (18..21)]); + } + + { + let old: OrderRange = vec![(1..6), (8..12)].into(); + let new: OrderRange = vec![(0..12), (15..23), (24..28)].into(); + let ranges = old.diff_range(&new); + assert_eq!(ranges, vec![(0..1), (6..8), (15..23), (24..28)]); + } + } + #[test] fn test_range_merge() { let mut range: OrderRange = (0..10).into(); diff --git a/libs/jwst-codec/src/doc/history.rs b/libs/jwst-codec/src/doc/history.rs index 73963cbd..6d583db4 100644 --- a/libs/jwst-codec/src/doc/history.rs +++ b/libs/jwst-codec/src/doc/history.rs @@ -70,6 +70,37 @@ impl StoreHistory { self.parse_items(store_items) } + pub fn parse_delete_sets( + &self, + old_sets: &HashMap, + new_sets: &HashMap, + ) -> Vec { + let store = self.store.read().unwrap(); + let deleted_items = new_sets + .iter() + .filter_map(|(id, new_range)| { + // diff range if old range exists, or use new range + let range = old_sets + .get(id) + .map(|r| r.diff_range(new_range).into()) + .unwrap_or(new_range.clone()); + (!range.is_empty()).then_some((id, range)) + }) + .filter_map(|(client, range)| { + // check items contains in deleted range + store.items.get(client).map(move |items| { + items + .iter() + .filter(move |i| range.contains(i.clock())) + .filter_map(|i| i.as_item().get().cloned()) + }) + }) + .flatten() + .collect(); + + self.parse_deleted_items(deleted_items) + } + pub fn parse_store(&self, options: HistoryOptions) -> Vec { let store_items = { let client = options @@ -118,6 +149,23 @@ impl StoreHistory { id: item.id.to_string(), parent: Self::parse_path(item, &parents), content: Value::from(&item.content).to_string(), + action: HistoryAction::Update, + }) + } + + histories + } + + fn parse_deleted_items(&self, deleted_items: Vec) -> Vec { + let parents = self.parents.read().unwrap(); + let mut histories = vec![]; + + for item in deleted_items { + histories.push(History { + id: item.id.to_string(), + parent: Self::parse_path(&item, &parents), + content: Value::from(&item.content).to_string(), + action: HistoryAction::Delete, }) } @@ -190,11 +238,19 @@ impl StoreHistory { } } +#[derive(Debug, PartialEq)] +pub enum HistoryAction { + Insert, + Update, + Delete, +} + #[derive(Debug, PartialEq)] pub struct History { pub id: String, pub parent: Vec, pub content: String, + pub action: HistoryAction, } pub(crate) struct SortedNodes<'a> { diff --git a/libs/jwst-codec/src/doc/publisher.rs b/libs/jwst-codec/src/doc/publisher.rs index 3e1d1387..5713c84b 100644 --- a/libs/jwst-codec/src/doc/publisher.rs +++ b/libs/jwst-codec/src/doc/publisher.rs @@ -52,6 +52,7 @@ impl DocPublisher { debug!("start observing"); let thread = spawn(move || { let mut last_update = store.read().unwrap().get_state_vector(); + let mut last_deletes = store.read().unwrap().delete_set.clone(); loop { sleep(Duration::from_millis(OBSERVE_INTERVAL)); if !observing.load(Ordering::Acquire) { @@ -67,20 +68,31 @@ impl DocPublisher { let store = store.read().unwrap(); let update = store.get_state_vector(); - if update != last_update { + let deletes = store.delete_set.clone(); + if update != last_update || deletes != last_deletes { trace!( "update: {:?}, last_update: {:?}, {:?}", update, last_update, current().id(), ); + trace!( + "deletes: {:?}, last_deletes: {:?}, {:?}", + deletes, + last_deletes, + current().id(), + ); history.resolve_with_store(&store); let (binary, history) = match store.diff_state_vector(&last_update, false) { Ok(update) => { drop(store); - let history = history.parse_update(&update); + let history = history + .parse_update(&update) + .into_iter() + .chain(history.parse_delete_sets(&last_deletes, &deletes)) + .collect::>(); let mut encoder = RawEncoder::default(); if let Err(e) = update.write(&mut encoder) { @@ -96,6 +108,7 @@ impl DocPublisher { }; last_update = update; + last_deletes = deletes; for cb in subscribers.iter() { use std::panic::{catch_unwind, AssertUnwindSafe}; diff --git a/libs/jwst-storage/src/storage/mod.rs b/libs/jwst-storage/src/storage/mod.rs index 181a2606..f1c75e05 100644 --- a/libs/jwst-storage/src/storage/mod.rs +++ b/libs/jwst-storage/src/storage/mod.rs @@ -166,6 +166,20 @@ impl JwstStorage { } } + pub async fn export_workspace(&self, workspace_id: S) -> JwstStorageResult> + where + S: AsRef, + { + let workspace_id = workspace_id.as_ref(); + info!("export_workspace: {}", workspace_id); + if let Some(doc) = self.docs.get_doc(workspace_id.into()).await? { + doc.encode_update_v1() + .map_err(|e| JwstStorageError::Crud(format!("failed to export workspace {workspace_id}: {e}"))) + } else { + Err(JwstStorageError::WorkspaceNotFound(workspace_id.into())) + } + } + pub async fn full_migrate(&self, workspace_id: String, update: Option>, force: bool) -> bool { let mut map = self.last_migrate.lock().await; let ts = map.entry(workspace_id.clone()).or_insert(Instant::now());