diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index fdfb304..43a9e1d 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -1,28 +1,84 @@
-name: build-test
+name: swift
+
on:
+ push:
+ branches: [main, master, develop]
pull_request:
- branches: [main]
+ branches: [main, master, develop]
+ workflow_dispatch:
jobs:
- build-test:
- runs-on: macos-14
- env:
- LOCAL_BUILD: true
- DEVELOPER_DIR: /Applications/Xcode_15.4.app
+ build-and-test:
+ name: Swift ${{ matrix.swift }} on ${{ matrix.os }}
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, macos-latest, windows-latest]
+ swift: ["6.2"]
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
with:
submodules: recursive
- - uses: actions-rs/toolchain@v1
+
+ - name: Setup Swift
+ uses: SwiftyLab/setup-swift@v1
with:
- profile: minimal
- toolchain: 1.82.0
- default: true
- - name: get xcode information
+ swift-version: ${{ matrix.swift }}
+
+ - name: Setup Rust
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Swift version
+ run: swift --version
+
+ - name: Rust version
+ run: rustc --version
+
+ - name: Build artifact bundle (Unix)
+ if: runner.os != 'Windows'
+ run: |
+ chmod +x ./scripts/build_artifactbundle.sh
+ ./scripts/build_artifactbundle.sh
+
+ - name: Build artifact bundle (Windows)
+ if: runner.os == 'Windows'
+ shell: pwsh
+ run: ./scripts/build_artifactbundle.ps1
+
+ - name: Build
+ run: swift build -c release
+ env:
+ LOCAL_BUILD: "1"
+
+ - name: Run tests
+ if: runner.os != 'Windows'
+ run: swift test
+ env:
+ LOCAL_BUILD: "1"
+
+ - name: Run tests (Windows)
+ if: runner.os == 'Windows'
+ shell: pwsh
+ env:
+ LOCAL_BUILD: "1"
+ run: |
+ $libPath = (Resolve-Path ".\loroFFI.artifactbundle\loroFFI-windows").Path
+ swift test `
+ -Xlinker "/LIBPATH:$libPath"
+
+ - name: Audit undefined symbols (informational)
+ if: runner.os != 'Windows'
run: |
- xcodebuild -version
- swift --version
- - name: build xcframework
- run: ./scripts/build_macos.sh
- - name: Swift tests
- run: LOCAL_BUILD=true swift test
+ shopt -s nullglob
+ for lib in loroFFI.artifactbundle/**/*.a; do
+ echo "== $lib =="
+ if command -v llvm-nm >/dev/null 2>&1; then
+ llvm-nm -u "$lib" || true
+ elif command -v nm >/dev/null 2>&1; then
+ nm -u "$lib" || true
+ else
+ echo "No nm available to audit $lib" >&2
+ exit 1
+ fi
+ done
diff --git a/.github/workflows/pre-release.yaml b/.github/workflows/pre-release.yaml
index 361d441..9b4d0f8 100644
--- a/.github/workflows/pre-release.yaml
+++ b/.github/workflows/pre-release.yaml
@@ -9,11 +9,11 @@ on:
permissions:
contents: write
pull-requests: write
+ actions: read
jobs:
- build-and-release:
+ build-macos:
runs-on: macos-latest
-
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -21,6 +21,204 @@ jobs:
submodules: recursive
token: ${{ secrets.GITHUB_TOKEN }}
+ - name: Setup Rust
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ targets: >
+ aarch64-apple-darwin,
+ x86_64-apple-darwin,
+ aarch64-apple-ios,
+ x86_64-apple-ios-sim,
+ aarch64-apple-ios-sim,
+ aarch64-apple-tvos,
+ x86_64-apple-tvos,
+ aarch64-apple-watchos,
+ x86_64-apple-watchos-sim,
+ aarch64-apple-visionos,
+ aarch64-apple-visionos-sim
+
+ - name: Build macOS/iOS/tvOS/watchOS/visionOS static libs and Swift bindings
+ run: |
+ cd loro-swift
+
+ cargo build --release --target aarch64-apple-darwin
+ cargo build --release --target x86_64-apple-darwin
+ cargo build --release --target aarch64-apple-ios
+ cargo build --release --target x86_64-apple-ios-sim
+ cargo build --release --target aarch64-apple-ios-sim
+ cargo build --release --target aarch64-apple-tvos
+ cargo build --release --target x86_64-apple-tvos
+ cargo build --release --target aarch64-apple-tvos-sim || true
+ cargo build --release --target aarch64-apple-watchos
+ cargo build --release --target x86_64-apple-watchos-sim
+ cargo build --release --target aarch64-apple-watchos-sim || true
+ rustup toolchain install nightly --component rust-std --target aarch64-apple-visionos aarch64-apple-visionos-sim || true
+ cargo +nightly build --release --target aarch64-apple-visionos || cargo build --release --target aarch64-apple-visionos
+ cargo +nightly build --release --target aarch64-apple-visionos-sim || cargo build --release --target aarch64-apple-visionos-sim
+
+ cargo build --release --features cli
+ cargo run --release --features=cli --bin uniffi-bindgen generate \
+ --library target/release/libloro_swift.dylib \
+ --language swift \
+ --out-dir ../artifacts
+
+ mkdir -p ../artifacts/macos
+ lipo -create \
+ target/aarch64-apple-darwin/release/libloro_swift.a \
+ target/x86_64-apple-darwin/release/libloro_swift.a \
+ -output ../artifacts/macos/libloro_swift.a
+
+ mkdir -p ../artifacts/ios
+ mkdir -p ../artifacts/tvos
+ mkdir -p ../artifacts/watchos
+ mkdir -p ../artifacts/visionos
+ cp target/aarch64-apple-ios/release/libloro_swift.a ../artifacts/ios/
+ cp target/aarch64-apple-tvos/release/libloro_swift.a ../artifacts/tvos/
+ cp target/aarch64-apple-watchos/release/libloro_swift.a ../artifacts/watchos/
+ cp target/aarch64-apple-visionos/release/libloro_swift.a ../artifacts/visionos/
+
+ mkdir -p ../artifacts/ios-simulator
+ mkdir -p ../artifacts/tvos-simulator
+ mkdir -p ../artifacts/watchos-simulator
+ mkdir -p ../artifacts/visionos-simulator
+
+ lipo -create \
+ target/x86_64-apple-ios-sim/release/libloro_swift.a \
+ target/aarch64-apple-ios-sim/release/libloro_swift.a \
+ -output ../artifacts/ios-simulator/libloro_swift.a
+
+ if [ -f target/aarch64-apple-tvos-sim/release/libloro_swift.a ]; then
+ lipo -create \
+ target/x86_64-apple-tvos/release/libloro_swift.a \
+ target/aarch64-apple-tvos-sim/release/libloro_swift.a \
+ -output ../artifacts/tvos-simulator/libloro_swift.a
+ else
+ lipo -create \
+ target/x86_64-apple-tvos/release/libloro_swift.a \
+ -output ../artifacts/tvos-simulator/libloro_swift.a
+ fi
+
+ if [ -f target/aarch64-apple-watchos-sim/release/libloro_swift.a ]; then
+ lipo -create \
+ target/x86_64-apple-watchos-sim/release/libloro_swift.a \
+ target/aarch64-apple-watchos-sim/release/libloro_swift.a \
+ -output ../artifacts/watchos-simulator/libloro_swift.a
+ else
+ lipo -create \
+ target/x86_64-apple-watchos-sim/release/libloro_swift.a \
+ -output ../artifacts/watchos-simulator/libloro_swift.a
+ fi
+
+ lipo -create \
+ target/aarch64-apple-visionos-sim/release/libloro_swift.a \
+ -output ../artifacts/visionos-simulator/libloro_swift.a
+
+ - name: Upload macOS/iOS artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: macos-artifacts
+ path: artifacts/
+
+ build-linux-x86_64:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
+ token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Setup Rust
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Build Linux x86_64 library
+ run: |
+ cd loro-swift
+ cargo build --release
+ mkdir -p ../artifacts/linux-x86_64
+ cp target/release/libloro_swift.a ../artifacts/linux-x86_64/
+
+ - name: Upload Linux x86_64 artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: linux-x86_64-artifacts
+ path: artifacts/
+
+ build-linux-arm64:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
+ token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Setup Rust
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ targets: aarch64-unknown-linux-gnu
+
+ - name: Install cross-compilation tools
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y gcc-aarch64-linux-gnu
+
+ - name: Build Linux ARM64 library
+ run: |
+ cd loro-swift
+ CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \
+ cargo build --release --target aarch64-unknown-linux-gnu
+ mkdir -p ../artifacts/linux-arm64
+ cp target/aarch64-unknown-linux-gnu/release/libloro_swift.a ../artifacts/linux-arm64/
+
+ - name: Upload Linux ARM64 artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: linux-arm64-artifacts
+ path: artifacts/
+
+ build-windows:
+ runs-on: windows-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
+ token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Setup Rust
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Build Windows library
+ run: |
+ cd loro-swift
+ cargo build --release
+ mkdir -p ../artifacts/windows
+ cp target/release/loro_swift.lib ../artifacts/windows/libloro_swift.lib
+ cp target/release/loro_swift.lib ../artifacts/windows/loro_swift.lib
+
+ - name: Upload Windows artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: windows-artifacts
+ path: artifacts/
+
+ combine-and-pr:
+ runs-on: ubuntu-latest
+ needs: [build-macos, build-linux-x86_64, build-linux-arm64, build-windows]
+ permissions:
+ contents: write
+ pull-requests: write
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ submodules: recursive
+ token: ${{ secrets.GITHUB_TOKEN }}
+
- name: Extract version from tag
id: get_version
run: |
@@ -28,64 +226,245 @@ jobs:
VERSION=${TAG_NAME%-pre-release}
echo "version=$VERSION" >> $GITHUB_OUTPUT
- - name: Build Swift FFI
+ - name: Download all artifacts
+ uses: actions/download-artifact@v4
+ with:
+ path: artifacts
+
+ - name: Create artifact bundle
run: |
- chmod +x scripts/build_swift_ffi.sh
- ./scripts/build_swift_ffi.sh
+ VERSION="${{ steps.get_version.outputs.version }}"
+ BUNDLE="loroFFI.artifactbundle"
+
+ mkdir -p "$BUNDLE/include"
+ mkdir -p "$BUNDLE/macos"
+ mkdir -p "$BUNDLE/ios"
+ mkdir -p "$BUNDLE/ios-simulator"
+ mkdir -p "$BUNDLE/tvos"
+ mkdir -p "$BUNDLE/tvos-simulator"
+ mkdir -p "$BUNDLE/watchos"
+ mkdir -p "$BUNDLE/watchos-simulator"
+ mkdir -p "$BUNDLE/visionos"
+ mkdir -p "$BUNDLE/visionos-simulator"
+ mkdir -p "$BUNDLE/linux-x86_64"
+ mkdir -p "$BUNDLE/linux-arm64"
+ mkdir -p "$BUNDLE/windows"
+
+ # Copy header and create module map
+ cp artifacts/macos-artifacts/loroFFI.h "$BUNDLE/include/"
+ cat > "$BUNDLE/include/module.modulemap" << 'EOF'
+ module LoroFFI {
+ header "loroFFI.h"
+ export *
+ }
+ EOF
+
+ # Copy platform libraries
+ cp artifacts/macos-artifacts/macos/libloro_swift.a "$BUNDLE/macos/"
+ cp artifacts/macos-artifacts/ios/libloro_swift.a "$BUNDLE/ios/"
+ cp artifacts/macos-artifacts/ios-simulator/libloro_swift.a "$BUNDLE/ios-simulator/"
+ cp artifacts/macos-artifacts/tvos/libloro_swift.a "$BUNDLE/tvos/"
+ cp artifacts/macos-artifacts/tvos-simulator/libloro_swift.a "$BUNDLE/tvos-simulator/"
+ cp artifacts/macos-artifacts/watchos/libloro_swift.a "$BUNDLE/watchos/"
+ cp artifacts/macos-artifacts/watchos-simulator/libloro_swift.a "$BUNDLE/watchos-simulator/"
+ cp artifacts/macos-artifacts/visionos/libloro_swift.a "$BUNDLE/visionos/"
+ cp artifacts/macos-artifacts/visionos-simulator/libloro_swift.a "$BUNDLE/visionos-simulator/"
+ cp artifacts/linux-x86_64-artifacts/linux-x86_64/libloro_swift.a "$BUNDLE/linux-x86_64/"
+ cp artifacts/linux-arm64-artifacts/linux-arm64/libloro_swift.a "$BUNDLE/linux-arm64/"
+ cp artifacts/windows-artifacts/windows/libloro_swift.lib "$BUNDLE/windows/"
+ cp artifacts/windows-artifacts/windows/loro_swift.lib "$BUNDLE/windows/"
+
+ # Create info.json with all variants
+ cat > "$BUNDLE/info.json" << EOF
+ {
+ "schemaVersion": "1.0",
+ "artifacts": {
+ "LoroFFI": {
+ "version": "$VERSION",
+ "type": "staticLibrary",
+ "variants": [
+ {
+ "path": "macos/libloro_swift.a",
+ "supportedTriples": ["arm64-apple-macosx", "x86_64-apple-macosx"],
+ "staticLibraryMetadata": {
+ "headerPaths": ["include"],
+ "moduleMapPath": "include/module.modulemap"
+ }
+ },
+ {
+ "path": "ios/libloro_swift.a",
+ "supportedTriples": ["arm64-apple-ios"],
+ "staticLibraryMetadata": {
+ "headerPaths": ["include"],
+ "moduleMapPath": "include/module.modulemap"
+ }
+ },
+ {
+ "path": "ios-simulator/libloro_swift.a",
+ "supportedTriples": ["arm64-apple-ios-simulator", "x86_64-apple-ios-simulator"],
+ "staticLibraryMetadata": {
+ "headerPaths": ["include"],
+ "moduleMapPath": "include/module.modulemap"
+ }
+ },
+ {
+ "path": "tvos/libloro_swift.a",
+ "supportedTriples": ["arm64-apple-tvos"],
+ "staticLibraryMetadata": {
+ "headerPaths": ["include"],
+ "moduleMapPath": "include/module.modulemap"
+ }
+ },
+ {
+ "path": "tvos-simulator/libloro_swift.a",
+ "supportedTriples": ["arm64-apple-tvos-simulator", "x86_64-apple-tvos"],
+ "staticLibraryMetadata": {
+ "headerPaths": ["include"],
+ "moduleMapPath": "include/module.modulemap"
+ }
+ },
+ {
+ "path": "watchos/libloro_swift.a",
+ "supportedTriples": ["arm64-apple-watchos"],
+ "staticLibraryMetadata": {
+ "headerPaths": ["include"],
+ "moduleMapPath": "include/module.modulemap"
+ }
+ },
+ {
+ "path": "watchos-simulator/libloro_swift.a",
+ "supportedTriples": ["arm64-apple-watchos-simulator", "x86_64-apple-watchos-sim"],
+ "staticLibraryMetadata": {
+ "headerPaths": ["include"],
+ "moduleMapPath": "include/module.modulemap"
+ }
+ },
+ {
+ "path": "visionos/libloro_swift.a",
+ "supportedTriples": ["arm64-apple-visionos"],
+ "staticLibraryMetadata": {
+ "headerPaths": ["include"],
+ "moduleMapPath": "include/module.modulemap"
+ }
+ },
+ {
+ "path": "visionos-simulator/libloro_swift.a",
+ "supportedTriples": ["arm64-apple-visionos-simulator"],
+ "staticLibraryMetadata": {
+ "headerPaths": ["include"],
+ "moduleMapPath": "include/module.modulemap"
+ }
+ },
+ {
+ "path": "linux-x86_64/libloro_swift.a",
+ "supportedTriples": ["x86_64-unknown-linux-gnu"],
+ "staticLibraryMetadata": {
+ "headerPaths": ["include"],
+ "moduleMapPath": "include/module.modulemap"
+ }
+ },
+ {
+ "path": "linux-arm64/libloro_swift.a",
+ "supportedTriples": ["aarch64-unknown-linux-gnu"],
+ "staticLibraryMetadata": {
+ "headerPaths": ["include"],
+ "moduleMapPath": "include/module.modulemap"
+ }
+ },
+ {
+ "path": "windows/libloro_swift.lib",
+ "supportedTriples": ["x86_64-unknown-windows-msvc"],
+ "staticLibraryMetadata": {
+ "headerPaths": ["include"],
+ "moduleMapPath": "include/module.modulemap"
+ }
+ }
+ ]
+ }
+ }
+ }
+ EOF
+
+ - name: Audit undefined symbols (informational)
+ run: |
+ shopt -s nullglob
+ for lib in loroFFI.artifactbundle/**/*.a loroFFI.artifactbundle/**/*.lib; do
+ echo "== $lib =="
+ if command -v llvm-nm >/dev/null 2>&1; then
+ llvm-nm -u "$lib" || true
+ elif command -v nm >/dev/null 2>&1; then
+ nm -u "$lib" || true
+ else
+ echo "No nm available to audit $lib" >&2
+ exit 1
+ fi
+ done
+
+ - name: Zip artifact bundle
+ run: |
+ rm -f loroFFI.artifactbundle.zip
+ zip -r loroFFI.artifactbundle.zip loroFFI.artifactbundle
- name: Calculate SHA256
id: sha256
run: |
- CHECKSUM=$(openssl dgst -sha256 loroFFI.xcframework.zip | awk '{print $2}')
+ CHECKSUM=$(sha256sum loroFFI.artifactbundle.zip | awk '{print $1}')
echo "checksum=$CHECKSUM" >> $GITHUB_OUTPUT
- - name: Upload XCFramework artifact
+ - name: Upload artifact bundle
uses: actions/upload-artifact@v4
with:
- name: loroFFI.xcframework.zip
- path: loroFFI.xcframework.zip
+ name: loroFFI.artifactbundle.zip
+ path: loroFFI.artifactbundle.zip
- - name: Update Package.swift
+ - name: Update Package.swift and README.md
+ env:
+ VERSION: ${{ steps.get_version.outputs.version }}
+ CHECKSUM: ${{ steps.sha256.outputs.checksum }}
+ REPO: ${{ github.repository }}
run: |
- VERSION=${{ steps.get_version.outputs.version }}
- CHECKSUM=${{ steps.sha256.outputs.checksum }}
- sed -i '' \
- -e "s|url: \"https://github.com/.*/loroFFI.xcframework.zip\"|url: \"https://github.com/${GITHUB_REPOSITORY}/releases/download/${VERSION}/loroFFI.xcframework.zip\"|" \
- -e "s|checksum: \"[a-f0-9]*\"|checksum: \"${CHECKSUM}\"|" \
- Package.swift
+ python - <<'PY'
+ import os, re, pathlib
+ version = os.environ["VERSION"]
+ checksum = os.environ["CHECKSUM"]
+ repo = os.environ["REPO"]
- - name: Update README.md
- run: |
- VERSION=${{ steps.get_version.outputs.version }}
- sed -i '' \
- -e "s|\"https://github.com/loro-dev/loro-swift.git\", from: \"[0-9.]*\"|\"https://github.com/loro-dev/loro-swift.git\", from: \"${VERSION}\"|" \
- README.md
+ pkg = pathlib.Path("Package.swift")
+ text = pkg.read_text()
+ text = re.sub(r'url: "https://github.com/.*/loroFFI\.artifactbundle\.zip"', f'url: "https://github.com/{repo}/releases/download/{version}/loroFFI.artifactbundle.zip"', text)
+ text = re.sub(r'checksum: "[a-f0-9]*"', f'checksum: "{checksum}"', text)
+ pkg.write_text(text)
+
+ readme = pathlib.Path("README.md")
+ rtext = readme.read_text()
+ rtext = re.sub(r'"https://github.com/loro-dev/loro-swift.git", from: "[0-9.]+"', f'"https://github.com/loro-dev/loro-swift.git", from: "{version}"', rtext)
+ readme.write_text(rtext)
+ PY
- name: Commit and push changes to pre-release branch
run: |
VERSION=${{ steps.get_version.outputs.version }}
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
- # Ensure local pre-release branch points to current commit
git fetch origin pre-release:pre-release || true
git checkout -B pre-release "$GITHUB_SHA"
- git add Package.swift README.md Sources/*
+ git add Package.swift README.md
git commit -m "chore: update version to ${VERSION}" || echo "No changes to commit"
git push origin pre-release
- name: Create or update Pull Request to main
env:
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ VERSION: ${{ steps.get_version.outputs.version }}
+ CHECKSUM: ${{ steps.sha256.outputs.checksum }}
run: |
- VERSION=${{ steps.get_version.outputs.version }}
- CHECKSUM=${{ steps.sha256.outputs.checksum }}
PR_NUMBER=$(gh pr list --base main --head pre-release --state open --json number --jq '.[0].number')
+ BODY="This PR updates Package.swift and README.md to version ${VERSION}.\n\nSHA256 checksum: ${CHECKSUM}.\n\nThe artifact bundle has been uploaded to this workflow run."
if [ -z "$PR_NUMBER" ]; then
gh pr create \
--base main \
--head pre-release \
--title "Release ${VERSION}" \
- --body "This PR updates Package.swift and README.md to version ${VERSION}.\n\nSHA256 checksum: ${CHECKSUM}.\n\nThe XCFramework artifact has been uploaded to this workflow run."
+ --body "$BODY"
else
echo "PR #$PR_NUMBER already exists."
fi
diff --git a/.github/workflows/release-on-merge.yaml b/.github/workflows/release-on-merge.yaml
index 03d93bf..77a3128 100644
--- a/.github/workflows/release-on-merge.yaml
+++ b/.github/workflows/release-on-merge.yaml
@@ -28,7 +28,7 @@ jobs:
id: get_version
shell: bash
run: |
- VERSION=$(grep -o 'releases/download/[^/]*/loroFFI\.xcframework\.zip' Package.swift | sed 's|releases/download/||' | sed 's|/loroFFI\.xcframework\.zip||' | head -n1)
+ VERSION=$(grep -o 'releases/download/[^/]*/loroFFI\.artifactbundle\.zip' Package.swift | sed 's|releases/download/||' | sed 's|/loroFFI\.artifactbundle\.zip||' | head -n1)
if [ -z "$VERSION" ]; then
echo "Failed to determine version from Package.swift" >&2
exit 1
@@ -46,70 +46,6 @@ jobs:
fi
echo "sha=$SHA" >> $GITHUB_OUTPUT
- - name: Find pre-release workflow run and download artifact
- shell: bash
- run: |
- VERSION=${{ steps.get_version.outputs.version }}
- TAG_NAME="${VERSION}-pre-release"
-
- echo "Looking for workflow run for tag: $TAG_NAME"
-
- # Get the commit SHA of the tag
- TAG_SHA=""
- if git rev-parse --verify "$TAG_NAME" >/dev/null 2>&1; then
- TAG_SHA=$(git rev-parse "$TAG_NAME")
- echo "Tag $TAG_NAME points to commit: $TAG_SHA"
- fi
-
- # Find workflow runs and check multiple criteria
- RUNS_JSON=$(gh run list \
- --workflow "Pre-release Build and PR" \
- --status success \
- --limit 20 \
- --json databaseId,headSha,displayTitle,createdAt)
-
- RUN_ID=""
-
- # Method 1: Try to match by tag SHA
- if [ -n "$TAG_SHA" ]; then
- RUN_ID=$(echo "$RUNS_JSON" | jq -r ".[] | select(.headSha == \"$TAG_SHA\") | .databaseId" | head -n1)
- if [ -n "$RUN_ID" ]; then
- echo "Found run by SHA match: $RUN_ID"
- fi
- fi
-
- # Method 2: Try to match by displayTitle
- if [ -z "$RUN_ID" ]; then
- RUN_ID=$(echo "$RUNS_JSON" | jq -r ".[] | select(.displayTitle == \"Pre-release $TAG_NAME\") | .databaseId" | head -n1)
- if [ -n "$RUN_ID" ]; then
- echo "Found run by displayTitle match: $RUN_ID"
- fi
- fi
-
- # Method 3: Use the most recent successful run as fallback
- if [ -z "$RUN_ID" ]; then
- RUN_ID=$(echo "$RUNS_JSON" | jq -r '.[0].databaseId')
- if [ -n "$RUN_ID" ]; then
- echo "Using most recent successful run as fallback: $RUN_ID"
- fi
- fi
-
- if [ -z "$RUN_ID" ]; then
- echo "Could not find any suitable workflow run" >&2
- exit 1
- fi
-
- gh run download "$RUN_ID" --name loroFFI.xcframework.zip --dir .
- # Normalize location if downloaded into a directory
- if [ -d loroFFI.xcframework.zip ] && [ -f loroFFI.xcframework.zip/loroFFI.xcframework.zip ]; then
- mv loroFFI.xcframework.zip/loroFFI.xcframework.zip .
- rm -rf loroFFI.xcframework.zip
- fi
- if [ ! -f loroFFI.xcframework.zip ]; then
- echo "Artifact loroFFI.xcframework.zip not found after download" >&2
- exit 1
- fi
-
- name: Create tag on merge commit
shell: bash
run: |
@@ -123,12 +59,3 @@ jobs:
fi
git tag -a "$VERSION" -m "Release version $VERSION" "$SHA"
git push --force origin "$VERSION"
-
- - name: Create GitHub Release
- shell: bash
- run: |
- VERSION=${{ steps.get_version.outputs.version }}
- gh release create "$VERSION" loroFFI.xcframework.zip \
- --title "Release $VERSION" \
- --notes "Automated release for version $VERSION. Includes pre-built loroFFI.xcframework.zip."
-
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
new file mode 100644
index 0000000..7cd3794
--- /dev/null
+++ b/.github/workflows/release.yaml
@@ -0,0 +1,464 @@
+name: Release
+
+on:
+ push:
+ tags:
+ - 'v*'
+ workflow_dispatch:
+ inputs:
+ version:
+ description: 'Version to release (e.g., 1.10.3)'
+ required: true
+
+jobs:
+ # Build static libraries for each platform
+ build-macos:
+ runs-on: macos-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ submodules: recursive
+
+ - name: Setup Rust
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ targets: >
+ aarch64-apple-darwin,
+ x86_64-apple-darwin,
+ aarch64-apple-ios,
+ x86_64-apple-ios-sim,
+ aarch64-apple-ios-sim,
+ aarch64-apple-tvos,
+ x86_64-apple-tvos,
+ aarch64-apple-watchos,
+ x86_64-apple-watchos-sim,
+ aarch64-apple-visionos,
+ aarch64-apple-visionos-sim
+
+ - name: Build universal macOS library
+ run: |
+ cd loro-swift
+ cargo build --release --target aarch64-apple-darwin
+ cargo build --release --target x86_64-apple-darwin
+ mkdir -p ../artifacts/macos
+ lipo -create \
+ target/aarch64-apple-darwin/release/libloro_swift.a \
+ target/x86_64-apple-darwin/release/libloro_swift.a \
+ -output ../artifacts/macos/libloro_swift.a
+
+ - name: Build iOS libraries
+ run: |
+ cd loro-swift
+ cargo build --release --target aarch64-apple-ios
+ cargo build --release --target x86_64-apple-ios-sim
+ cargo build --release --target aarch64-apple-ios-sim
+
+ mkdir -p ../artifacts/ios
+ mkdir -p ../artifacts/ios-simulator
+
+ cp target/aarch64-apple-ios/release/libloro_swift.a ../artifacts/ios/
+
+ lipo -create \
+ target/x86_64-apple-ios-sim/release/libloro_swift.a \
+ target/aarch64-apple-ios-sim/release/libloro_swift.a \
+ -output ../artifacts/ios-simulator/libloro_swift.a
+
+ - name: Build tvOS libraries
+ run: |
+ cd loro-swift
+ cargo build --release --target aarch64-apple-tvos
+ cargo build --release --target x86_64-apple-tvos
+ cargo build --release --target aarch64-apple-tvos-sim || true
+
+ mkdir -p ../artifacts/tvos
+ mkdir -p ../artifacts/tvos-simulator
+
+ cp target/aarch64-apple-tvos/release/libloro_swift.a ../artifacts/tvos/
+
+ if [ -f target/aarch64-apple-tvos-sim/release/libloro_swift.a ]; then
+ lipo -create \
+ target/x86_64-apple-tvos/release/libloro_swift.a \
+ target/aarch64-apple-tvos-sim/release/libloro_swift.a \
+ -output ../artifacts/tvos-simulator/libloro_swift.a
+ else
+ lipo -create \
+ target/x86_64-apple-tvos/release/libloro_swift.a \
+ -output ../artifacts/tvos-simulator/libloro_swift.a
+ fi
+
+ - name: Build watchOS libraries
+ run: |
+ cd loro-swift
+ cargo build --release --target aarch64-apple-watchos
+ cargo build --release --target x86_64-apple-watchos-sim
+ cargo build --release --target aarch64-apple-watchos-sim || true
+
+ mkdir -p ../artifacts/watchos
+ mkdir -p ../artifacts/watchos-simulator
+
+ cp target/aarch64-apple-watchos/release/libloro_swift.a ../artifacts/watchos/
+
+ if [ -f target/aarch64-apple-watchos-sim/release/libloro_swift.a ]; then
+ lipo -create \
+ target/x86_64-apple-watchos-sim/release/libloro_swift.a \
+ target/aarch64-apple-watchos-sim/release/libloro_swift.a \
+ -output ../artifacts/watchos-simulator/libloro_swift.a
+ else
+ lipo -create \
+ target/x86_64-apple-watchos-sim/release/libloro_swift.a \
+ -output ../artifacts/watchos-simulator/libloro_swift.a
+ fi
+
+ - name: Build visionOS libraries
+ run: |
+ cd loro-swift
+ rustup toolchain install nightly --component rust-std --target aarch64-apple-visionos aarch64-apple-visionos-sim || true
+ cargo +nightly build --release --target aarch64-apple-visionos || cargo build --release --target aarch64-apple-visionos
+ cargo +nightly build --release --target aarch64-apple-visionos-sim || cargo build --release --target aarch64-apple-visionos-sim
+
+ mkdir -p ../artifacts/visionos
+ mkdir -p ../artifacts/visionos-simulator
+
+ cp target/aarch64-apple-visionos/release/libloro_swift.a ../artifacts/visionos/
+ cp target/aarch64-apple-visionos-sim/release/libloro_swift.a ../artifacts/visionos-simulator/
+
+ - name: Generate Swift bindings
+ run: |
+ cd loro-swift
+ cargo build --release --features cli
+ cargo run --release --features=cli --bin uniffi-bindgen generate \
+ --library target/release/libloro_swift.dylib \
+ --language swift \
+ --out-dir ../artifacts
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: macos-artifacts
+ path: artifacts/
+
+ build-linux-x86_64:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ submodules: recursive
+
+ - name: Setup Rust
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Build Linux x86_64 library
+ run: |
+ cd loro-swift
+ cargo build --release
+ mkdir -p ../artifacts/linux-x86_64
+ cp target/release/libloro_swift.a ../artifacts/linux-x86_64/
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: linux-x86_64-artifacts
+ path: artifacts/
+
+ build-linux-arm64:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ submodules: recursive
+
+ - name: Setup Rust
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ targets: aarch64-unknown-linux-gnu
+
+ - name: Install cross-compilation tools
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y gcc-aarch64-linux-gnu
+
+ - name: Build Linux ARM64 library
+ run: |
+ cd loro-swift
+ CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \
+ cargo build --release --target aarch64-unknown-linux-gnu
+ mkdir -p ../artifacts/linux-arm64
+ cp target/aarch64-unknown-linux-gnu/release/libloro_swift.a ../artifacts/linux-arm64/
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: linux-arm64-artifacts
+ path: artifacts/
+
+ build-windows:
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ submodules: recursive
+
+ - name: Setup Rust
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Build Windows library
+ run: |
+ cd loro-swift
+ cargo build --release
+ mkdir -p ../artifacts/windows
+ cp target/release/loro_swift.lib ../artifacts/windows/libloro_swift.lib
+ cp target/release/loro_swift.lib ../artifacts/windows/loro_swift.lib
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: windows-artifacts
+ path: artifacts/
+
+ # Combine all artifacts into a single artifact bundle
+ create-release:
+ needs: [build-macos, build-linux-x86_64, build-linux-arm64, build-windows]
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Download all artifacts
+ uses: actions/download-artifact@v4
+ with:
+ path: artifacts
+
+ - name: Determine version
+ id: version
+ run: |
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
+ echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT
+ else
+ echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Create artifact bundle
+ run: |
+ VERSION="${{ steps.version.outputs.version }}"
+ BUNDLE="loroFFI.artifactbundle"
+
+ mkdir -p "$BUNDLE/include"
+ mkdir -p "$BUNDLE/macos"
+ mkdir -p "$BUNDLE/ios"
+ mkdir -p "$BUNDLE/ios-simulator"
+ mkdir -p "$BUNDLE/tvos"
+ mkdir -p "$BUNDLE/tvos-simulator"
+ mkdir -p "$BUNDLE/watchos"
+ mkdir -p "$BUNDLE/watchos-simulator"
+ mkdir -p "$BUNDLE/visionos"
+ mkdir -p "$BUNDLE/visionos-simulator"
+ mkdir -p "$BUNDLE/linux-x86_64"
+ mkdir -p "$BUNDLE/linux-arm64"
+ mkdir -p "$BUNDLE/windows"
+
+ # Copy header and create module map
+ cp artifacts/macos-artifacts/loroFFI.h "$BUNDLE/include/"
+ cat > "$BUNDLE/include/module.modulemap" << 'EOF'
+ module LoroFFI {
+ header "loroFFI.h"
+ export *
+ }
+ EOF
+
+ # Copy platform libraries
+ cp artifacts/macos-artifacts/macos/libloro_swift.a "$BUNDLE/macos/"
+ cp artifacts/macos-artifacts/ios/libloro_swift.a "$BUNDLE/ios/"
+ cp artifacts/macos-artifacts/ios-simulator/libloro_swift.a "$BUNDLE/ios-simulator/"
+ cp artifacts/macos-artifacts/tvos/libloro_swift.a "$BUNDLE/tvos/"
+ cp artifacts/macos-artifacts/tvos-simulator/libloro_swift.a "$BUNDLE/tvos-simulator/"
+ cp artifacts/macos-artifacts/watchos/libloro_swift.a "$BUNDLE/watchos/"
+ cp artifacts/macos-artifacts/watchos-simulator/libloro_swift.a "$BUNDLE/watchos-simulator/"
+ cp artifacts/macos-artifacts/visionos/libloro_swift.a "$BUNDLE/visionos/"
+ cp artifacts/macos-artifacts/visionos-simulator/libloro_swift.a "$BUNDLE/visionos-simulator/"
+ cp artifacts/linux-x86_64-artifacts/linux-x86_64/libloro_swift.a "$BUNDLE/linux-x86_64/"
+ cp artifacts/linux-arm64-artifacts/linux-arm64/libloro_swift.a "$BUNDLE/linux-arm64/"
+ cp artifacts/windows-artifacts/windows/libloro_swift.lib "$BUNDLE/windows/"
+ cp artifacts/windows-artifacts/windows/loro_swift.lib "$BUNDLE/windows/"
+
+ # Create info.json with all variants
+ cat > "$BUNDLE/info.json" << EOF
+ {
+ "schemaVersion": "1.0",
+ "artifacts": {
+ "LoroFFI": {
+ "version": "$VERSION",
+ "type": "staticLibrary",
+ "variants": [
+ {
+ "path": "macos/libloro_swift.a",
+ "supportedTriples": ["arm64-apple-macosx", "x86_64-apple-macosx"],
+ "staticLibraryMetadata": {
+ "headerPaths": ["include"],
+ "moduleMapPath": "include/module.modulemap"
+ }
+ },
+ {
+ "path": "ios/libloro_swift.a",
+ "supportedTriples": ["arm64-apple-ios"],
+ "staticLibraryMetadata": {
+ "headerPaths": ["include"],
+ "moduleMapPath": "include/module.modulemap"
+ }
+ },
+ {
+ "path": "ios-simulator/libloro_swift.a",
+ "supportedTriples": ["arm64-apple-ios-simulator", "x86_64-apple-ios-simulator"],
+ "staticLibraryMetadata": {
+ "headerPaths": ["include"],
+ "moduleMapPath": "include/module.modulemap"
+ }
+ },
+ {
+ "path": "tvos/libloro_swift.a",
+ "supportedTriples": ["arm64-apple-tvos"],
+ "staticLibraryMetadata": {
+ "headerPaths": ["include"],
+ "moduleMapPath": "include/module.modulemap"
+ }
+ },
+ {
+ "path": "tvos-simulator/libloro_swift.a",
+ "supportedTriples": ["arm64-apple-tvos-simulator", "x86_64-apple-tvos"],
+ "staticLibraryMetadata": {
+ "headerPaths": ["include"],
+ "moduleMapPath": "include/module.modulemap"
+ }
+ },
+ {
+ "path": "watchos/libloro_swift.a",
+ "supportedTriples": ["arm64-apple-watchos"],
+ "staticLibraryMetadata": {
+ "headerPaths": ["include"],
+ "moduleMapPath": "include/module.modulemap"
+ }
+ },
+ {
+ "path": "watchos-simulator/libloro_swift.a",
+ "supportedTriples": ["arm64-apple-watchos-simulator", "x86_64-apple-watchos-sim"],
+ "staticLibraryMetadata": {
+ "headerPaths": ["include"],
+ "moduleMapPath": "include/module.modulemap"
+ }
+ },
+ {
+ "path": "visionos/libloro_swift.a",
+ "supportedTriples": ["arm64-apple-visionos"],
+ "staticLibraryMetadata": {
+ "headerPaths": ["include"],
+ "moduleMapPath": "include/module.modulemap"
+ }
+ },
+ {
+ "path": "visionos-simulator/libloro_swift.a",
+ "supportedTriples": ["arm64-apple-visionos-simulator"],
+ "staticLibraryMetadata": {
+ "headerPaths": ["include"],
+ "moduleMapPath": "include/module.modulemap"
+ }
+ },
+ {
+ "path": "linux-x86_64/libloro_swift.a",
+ "supportedTriples": ["x86_64-unknown-linux-gnu"],
+ "staticLibraryMetadata": {
+ "headerPaths": ["include"],
+ "moduleMapPath": "include/module.modulemap"
+ }
+ },
+ {
+ "path": "linux-arm64/libloro_swift.a",
+ "supportedTriples": ["aarch64-unknown-linux-gnu"],
+ "staticLibraryMetadata": {
+ "headerPaths": ["include"],
+ "moduleMapPath": "include/module.modulemap"
+ }
+ },
+ {
+ "path": "windows/libloro_swift.lib",
+ "supportedTriples": ["x86_64-unknown-windows-msvc"],
+ "staticLibraryMetadata": {
+ "headerPaths": ["include"],
+ "moduleMapPath": "include/module.modulemap"
+ }
+ }
+ ]
+ }
+ }
+ }
+ EOF
+
+ - name: Audit undefined symbols (informational)
+ run: |
+ shopt -s nullglob
+ for lib in loroFFI.artifactbundle/**/*.a loroFFI.artifactbundle/**/*.lib; do
+ echo "== $lib =="
+ if command -v llvm-nm >/dev/null 2>&1; then
+ llvm-nm -u "$lib" || true
+ elif command -v nm >/dev/null 2>&1; then
+ nm -u "$lib" || true
+ else
+ echo "No nm available to audit $lib" >&2
+ exit 1
+ fi
+ done
+
+ # Create zip
+ zip -r "loroFFI.artifactbundle.zip" "$BUNDLE"
+
+ # Calculate checksum
+ sha256sum "loroFFI.artifactbundle.zip" > checksum.txt
+ cat checksum.txt
+
+ - name: Update LoroFFI.swift with fixes
+ run: |
+ SWIFT_FILE="artifacts/macos-artifacts/loro.swift"
+
+ # Apply Swift 6 compatibility fixes
+ perl -i -pe 's/canImport\(loroFFI\)/canImport(LoroFFI)/g' "$SWIFT_FILE"
+ perl -i -pe 's/import loroFFI/import LoroFFI/g' "$SWIFT_FILE"
+ perl -i -pe 's/static var vtable:/nonisolated(unsafe) static var vtable:/g' "$SWIFT_FILE"
+ perl -i -pe 's/fileprivate static var handleMap/nonisolated(unsafe) fileprivate static var handleMap/g' "$SWIFT_FILE"
+ perl -i -pe 's/private var initializationResult/nonisolated(unsafe) private var initializationResult/g' "$SWIFT_FILE"
+ perl -i -pe 's/protocol LoroValueLike\s*:\s*AnyObject/protocol LoroValueLike/g' "$SWIFT_FILE"
+ perl -i -pe 's/protocol ContainerIdLike\s*:\s*AnyObject/protocol ContainerIdLike/g' "$SWIFT_FILE"
+
+ cp "$SWIFT_FILE" LoroFFI.swift
+
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@v1
+ if: startsWith(github.ref, 'refs/tags/')
+ with:
+ files: |
+ loroFFI.artifactbundle.zip
+ checksum.txt
+ LoroFFI.swift
+ body: |
+ ## Cross-Platform Artifact Bundle
+
+ This release includes a cross-platform artifact bundle (SE-0482) with support for:
+ - macOS (arm64, x86_64)
+ - iOS (arm64 device, arm64/x86_64 simulator)
+ - Linux (x86_64, arm64)
+ - Windows (x86_64)
+
+ ### Checksum
+ ```
+ $(cat checksum.txt)
+ ```
+
+ ### Usage
+ Update your `Package.swift`:
+ ```swift
+ .binaryTarget(
+ name: "LoroFFI",
+ url: "https://github.com/${{ github.repository }}/releases/download/v${{ steps.version.outputs.version }}/loroFFI.artifactbundle.zip",
+ checksum: "$(cut -d' ' -f1 checksum.txt)"
+ )
+ ```
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: release-bundle
+ path: |
+ loroFFI.artifactbundle.zip
+ checksum.txt
+ LoroFFI.swift
diff --git a/.gitignore b/.gitignore
index 3e8a298..b6f2c6d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,4 +9,5 @@ DerivedData/
/gen-swift
loroFFI.xcframework
loroFFI.xcframework.zip
+loroFFI.artifactbundle/
target/
\ No newline at end of file
diff --git a/.swift-version b/.swift-version
new file mode 100644
index 0000000..0df17dd
--- /dev/null
+++ b/.swift-version
@@ -0,0 +1 @@
+6.2.1
\ No newline at end of file
diff --git a/Package.swift b/Package.swift
index 8807e87..0e0c015 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version: 5.9
+// swift-tools-version: 6.2
// The swift-tools-version declares the minimum version of Swift required to build this package.
import Foundation
@@ -6,13 +6,19 @@ import PackageDescription
let FFIbinaryTarget: PackageDescription.Target
+// SE-0482: Cross-platform static library support via artifact bundles
+// Supports macOS (arm64/x86_64), iOS (device + simulator), Linux (x86_64/arm64), and Windows (x86_64)
+//
+// For local development or CI, set LOCAL_BUILD=1 and run ./scripts/build_artifactbundle.sh first
+// For releases, the artifact bundle is published to GitHub releases
if ProcessInfo.processInfo.environment["LOCAL_BUILD"] != nil {
- FFIbinaryTarget = .binaryTarget(name: "LoroFFI", path: "./loroFFI.xcframework.zip")
-}else {
+ FFIbinaryTarget = .binaryTarget(name: "LoroFFI", path: "./loroFFI.artifactbundle")
+} else {
+ // Cross-platform artifact bundle from GitHub releases
FFIbinaryTarget = .binaryTarget(
name: "LoroFFI",
- url: "https://github.com/loro-dev/loro-swift/releases/download/1.8.1/loroFFI.xcframework.zip",
- checksum: "6c723580b568aeccd05debc3cb40635912f5a882520cf42fe84c72220edd0f12"
+ url: "https://github.com/wendylabsinc/loro-swift/releases/download/v1.10.3/loroFFI.artifactbundle.zip",
+ checksum: "PLACEHOLDER_UPDATE_AFTER_RELEASE"
)
}
@@ -24,18 +30,18 @@ let package = Package(
.visionOS(.v1)
],
products: [
- // Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "Loro",
targets: ["Loro"]),
],
targets: [
- // Targets are the basic building blocks of a package, defining a module or a test suite.
- // Targets can depend on other targets in this package and products from dependencies.
FFIbinaryTarget,
.target(
name: "Loro",
- dependencies: ["LoroFFI"]
+ dependencies: ["LoroFFI"],
+ linkerSettings: [
+ .linkedLibrary("ntdll", .when(platforms: [.windows]))
+ ]
),
.testTarget(
name: "LoroTests",
diff --git a/README.md b/README.md
index 627434d..da60f4e 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,10 @@
-[](https://swiftpackageindex.com/loro-dev/loro-swift)
-[](https://swiftpackageindex.com/loro-dev/loro-swift)
-
loro-swift
+
+
+
+
+
@@ -18,6 +20,17 @@ This repository contains experimental Swift bindings for
If you have any suggestions for API, please feel free to create an issue or join
our [Discord](https://discord.gg/tUsBSVfqzf) community.
+> Requires Swift 6.2 or newer. We rely on SE-0482 cross-platform static library artifact bundles.
+
+Supported platforms (artifact bundle):
+- macOS: arm64, x86_64
+- iOS: arm64 (device), arm64/x86_64 (simulator)
+- tvOS: arm64 (device), arm64/x86_64 (simulator)
+- watchOS: arm64 (device), arm64/x86_64 (simulator)
+- visionOS: arm64 (device), arm64 (simulator)
+- Linux: x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu
+- Windows: x86_64-unknown-windows-msvc
+
## Usage
@@ -90,6 +103,13 @@ LOCAL_BUILD=1 swift test
The script will run `uniffi` and generate the `loroFFI.xcframework.zip`.
+## Releases (cross-platform static library)
+
+- We use Swift 6.2+ because SE-0482 enables cross-platform `staticLibrary` artifact bundles for SwiftPM (see the [proposal](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0482-swiftpm-static-library-binary-target-non-apple-platforms.md)).
+- Tag `x.y.z-pre-release` to run the pre-release workflow: it builds macOS/iOS/Linux/Windows static libs, assembles the artifact bundle, computes the checksum, and opens/updates a PR (branch `pre-release`) that updates `Package.swift` and `README.md` (Swift 6.2+ required).
+- Merge the pre-release PR; when it’s merged, the release workflow for the final tag publishes the cross-platform artifact bundle used by `Package.swift`.
+- Use Swift 6.2 locally (e.g., `swiftly use 6.2`) to match the toolchain required by SE-0482.
+
# Credits
- [uniffi-rs](https://github.com/mozilla/uniffi-rs): a multi-language bindings generator for rust
- [Automerge-swift](https://github.com/automerge/automerge-swift): `loro-swift`
diff --git a/Sources/Loro/Container.swift b/Sources/Loro/Container.swift
index 4ba57bf..e2ffe6a 100644
--- a/Sources/Loro/Container.swift
+++ b/Sources/Loro/Container.swift
@@ -208,3 +208,22 @@ extension Awareness{
self.setLocalState(value: value?.asLoroValue() ?? .null)
}
}
+
+// MARK: - toString() convenience methods
+// These provide backward compatibility after loro-ffi 1.10 removed toString() in favor of CustomStringConvertible
+
+extension LoroText {
+ /// Returns the text content as a String.
+ /// This is an alias for `description` to maintain API compatibility.
+ public func toString() -> String {
+ return self.description
+ }
+}
+
+extension FractionalIndex {
+ /// Returns the fractional index as a String.
+ /// This is an alias for `description` to maintain API compatibility.
+ public func toString() -> String {
+ return self.description
+ }
+}
diff --git a/Sources/Loro/Ephemeral.swift b/Sources/Loro/Ephemeral.swift
index 3eb18bd..0398db4 100644
--- a/Sources/Loro/Ephemeral.swift
+++ b/Sources/Loro/Ephemeral.swift
@@ -1,11 +1,17 @@
//
// Ephemeral.swift
-//
+//
//
// Created by Leon Zhao on 2025/6/4.
//
+#if !hasFeature(Embedded)
+#if canImport(FoundationEssentials)
+import FoundationEssentials
+#elseif canImport(Foundation)
import Foundation
+#endif
+#endif
class ClosureEphemeralSubscriber: EphemeralSubscriber {
private let closure: (EphemeralStoreEvent) -> Void
diff --git a/Sources/Loro/Event.swift b/Sources/Loro/Event.swift
index fb91b78..ff7ecfd 100644
--- a/Sources/Loro/Event.swift
+++ b/Sources/Loro/Event.swift
@@ -1,4 +1,10 @@
+#if !hasFeature(Embedded)
+#if canImport(FoundationEssentials)
+import FoundationEssentials
+#elseif canImport(Foundation)
import Foundation
+#endif
+#endif
class ClosureSubscriber: Subscriber {
private let closure: (DiffEvent) -> Void
diff --git a/Sources/Loro/Loro.docc/GettingStarted.md b/Sources/Loro/Loro.docc/GettingStarted.md
new file mode 100644
index 0000000..0a4c23b
--- /dev/null
+++ b/Sources/Loro/Loro.docc/GettingStarted.md
@@ -0,0 +1,83 @@
+# Getting Started
+
+Learn how to create and sync Loro documents.
+
+## Overview
+
+Loro provides conflict-free replicated data types (CRDTs) for building collaborative applications. This guide walks you through creating a document, adding data, and syncing between peers.
+
+## Creating a Document
+
+Start by creating a ``LoroDoc`` instance:
+
+```swift
+import Loro
+
+let doc = LoroDoc()
+```
+
+## Working with Containers
+
+Loro provides several container types for different data structures:
+
+### Text
+
+```swift
+let text = doc.getText(id: "myText")
+try text.insert(pos: 0, s: "Hello, World!")
+print(text.toString()) // "Hello, World!"
+```
+
+### List
+
+```swift
+let list = doc.getList(id: "myList")
+try list.insert(pos: 0, v: "first")
+try list.push(v: "second")
+```
+
+### Map
+
+```swift
+let map = doc.getMap(id: "myMap")
+try map.insert(key: "name", v: "Alice")
+try map.insert(key: "age", v: 30)
+```
+
+### Tree
+
+See for detailed information about working with tree structures.
+
+## Syncing Documents
+
+Loro documents can be synced using snapshots or incremental updates:
+
+```swift
+let doc1 = LoroDoc()
+let doc2 = LoroDoc()
+
+// Make changes to doc1
+let text1 = doc1.getText(id: "text")
+try text1.insert(pos: 0, s: "Hello")
+
+// Export and import snapshot
+let snapshot = try doc1.export(mode: .snapshot)
+try doc2.import(bytes: snapshot)
+
+// Now doc2 has the same content
+let text2 = doc2.getText(id: "text")
+print(text2.toString()) // "Hello"
+```
+
+## Subscribing to Changes
+
+You can subscribe to changes in a document:
+
+```swift
+let subscription = doc.subscribeRoot { event in
+ print("Document changed!")
+}
+
+// Don't forget to detach when done
+subscription.detach()
+```
diff --git a/Sources/Loro/Loro.docc/Loro.md b/Sources/Loro/Loro.docc/Loro.md
new file mode 100644
index 0000000..c1588f9
--- /dev/null
+++ b/Sources/Loro/Loro.docc/Loro.md
@@ -0,0 +1,42 @@
+# ``Loro``
+
+A Swift binding for Loro, a high-performance CRDT (Conflict-free Replicated Data Type) library.
+
+## Overview
+
+Loro is a powerful library for building collaborative applications. It provides several container types for different data structures that can be synced across multiple peers while automatically resolving conflicts.
+
+## Topics
+
+### Essentials
+
+-
+-
+-
+-
+-
+-
+-
+
+### Document
+
+- ``LoroDoc``
+
+### Container Types
+
+- ``LoroText``
+- ``LoroList``
+- ``LoroMap``
+- ``LoroTree``
+- ``LoroMovableList``
+- ``LoroCounter``
+
+### Collaboration
+
+- ``UndoManager``
+- ``Awareness``
+
+### Versioning
+
+- ``VersionVector``
+- ``Frontiers``
diff --git a/Sources/Loro/Loro.docc/VersioningAndSync.md b/Sources/Loro/Loro.docc/VersioningAndSync.md
new file mode 100644
index 0000000..bb2b8c4
--- /dev/null
+++ b/Sources/Loro/Loro.docc/VersioningAndSync.md
@@ -0,0 +1,421 @@
+# Versioning and Synchronization
+
+Learn how to track versions, sync documents, and time travel through document history.
+
+## Overview
+
+Loro uses two key concepts for versioning:
+
+- **Version Vector**: A map of peer IDs to their latest operation counter. Useful for synchronization and version comparison.
+- **Frontiers**: A compact representation of version using operation IDs. Efficient for checkpoints and time travel.
+
+## Version Vectors
+
+A ``VersionVector`` tracks the latest known operation from each peer:
+
+```swift
+let doc = LoroDoc()
+
+// Get current state version
+let stateVersion = doc.version()
+
+// Get oplog (operation log) version
+let oplogVersion = doc.oplogVv()
+```
+
+### Comparing Versions
+
+```swift
+let doc1 = LoroDoc()
+try doc1.setPeerId(peer: 1)
+let text1 = doc1.getText(id: "text")
+try text1.insert(pos: 0, s: "Hello")
+
+let version1 = doc1.version()
+
+try text1.insert(pos: 5, s: " World")
+let version2 = doc1.version()
+
+// Versions can be compared
+print(version1 == version2) // false
+```
+
+## Frontiers
+
+``Frontiers`` represent the "tips" of the version DAG - a compact way to identify a document state:
+
+```swift
+let doc = LoroDoc()
+
+// Get state frontiers
+let stateFrontiers = doc.stateFrontiers()
+
+// Get oplog frontiers
+let oplogFrontiers = doc.oplogFrontiers()
+```
+
+### Converting Between Versions and Frontiers
+
+```swift
+let doc = LoroDoc()
+try doc.setPeerId(peer: 1)
+let text = doc.getText(id: "text")
+try text.insert(pos: 0, s: "Hello")
+
+// Convert frontiers to version vector
+let frontiers = doc.oplogFrontiers()
+if let vv = doc.frontiersToVv(frontiers: frontiers) {
+ print("Version vector: \(vv)")
+}
+
+// Convert version vector to frontiers
+let vv = doc.oplogVv()
+let convertedFrontiers = doc.vvToFrontiers(vv: vv)
+```
+
+## Synchronization
+
+### Exporting and Importing
+
+```swift
+let doc1 = LoroDoc()
+try doc1.setPeerId(peer: 1)
+let text1 = doc1.getText(id: "text")
+try text1.insert(pos: 0, s: "Hello from doc1")
+
+// Export a full snapshot
+let snapshot = try doc1.export(mode: .snapshot)
+
+// Import into another document
+let doc2 = LoroDoc()
+try doc2.setPeerId(peer: 2)
+let _ = try doc2.import(bytes: snapshot)
+```
+
+### Incremental Updates
+
+For efficient sync, export only changes since a known version:
+
+```swift
+let doc1 = LoroDoc()
+try doc1.setPeerId(peer: 1)
+let text1 = doc1.getText(id: "text")
+try text1.insert(pos: 0, s: "Initial")
+
+// Save the version vector
+let lastKnownVersion = doc1.oplogVv()
+
+// Make more changes
+try text1.insert(pos: 7, s: " content")
+try text1.insert(pos: 15, s: " added")
+
+// Export only the new changes
+let updates = try doc1.export(mode: .updates(from: lastKnownVersion))
+
+// Apply updates to another document
+let doc2 = LoroDoc()
+let _ = try doc2.import(bytes: updates)
+```
+
+### Batch Import
+
+Import multiple updates efficiently:
+
+```swift
+let doc = LoroDoc()
+
+let updates: [Data] = [snapshot1, update1, update2]
+let _ = try doc.importBatch(bytes: updates)
+```
+
+### Import with Origin
+
+Track where changes came from:
+
+```swift
+let doc = LoroDoc()
+let subscription = doc.subscribeRoot { event in
+ print("Changes from: \(event.origin)")
+}
+
+let _ = try doc.importWith(bytes: snapshot, origin: "server-sync")
+```
+
+## Time Travel (Checkout)
+
+Loro supports checking out previous versions of a document. When you checkout a historical version, the document enters a **detached state** where it becomes read-only.
+
+### Basic Time Travel
+
+```swift
+let doc = LoroDoc()
+try doc.setPeerId(peer: 1)
+let text = doc.getText(id: "text")
+
+// Make some changes
+try text.insert(pos: 0, s: "Version 1")
+let v1 = doc.oplogFrontiers()
+
+try text.delete(pos: 0, len: 9)
+try text.insert(pos: 0, s: "Version 2")
+let v2 = doc.oplogFrontiers()
+
+try text.delete(pos: 0, len: 9)
+try text.insert(pos: 0, s: "Version 3")
+
+// Current state
+print(text.toString()) // "Version 3"
+
+// Travel back to version 1
+try doc.checkout(frontiers: v1)
+print(text.toString()) // "Version 1"
+
+// Travel to version 2
+try doc.checkout(frontiers: v2)
+print(text.toString()) // "Version 2"
+
+// Return to latest
+doc.checkoutToLatest()
+print(text.toString()) // "Version 3"
+```
+
+### Detached State
+
+After calling `checkout()`, the document enters a detached state where edits are not allowed:
+
+```swift
+let doc = LoroDoc()
+try doc.setPeerId(peer: 1)
+let text = doc.getText(id: "text")
+try text.insert(pos: 0, s: "Hello")
+let checkpoint = doc.oplogFrontiers()
+
+try text.insert(pos: 5, s: " World")
+
+// Check if document is in normal (attached) state
+print(doc.isDetached()) // false
+
+// Checkout puts the document in detached state
+try doc.checkout(frontiers: checkpoint)
+print(doc.isDetached()) // true
+
+// To resume editing, reattach to the latest version
+doc.attach() // or doc.checkoutToLatest()
+print(doc.isDetached()) // false
+```
+
+> Note: `attach()` and `checkoutToLatest()` have the same effect - they both reattach the document to the latest version.
+
+### Timestamp Recording
+
+Loro can record timestamps for each change, enabling time-based navigation:
+
+```swift
+let doc = LoroDoc()
+try doc.setPeerId(peer: 1)
+
+// Enable automatic timestamp recording
+doc.setRecordTimestamp(record: true)
+
+let text = doc.getText(id: "text")
+try text.insert(pos: 0, s: "First edit")
+doc.commit()
+
+// Wait a moment...
+try text.insert(pos: 10, s: " - Second edit")
+doc.commit()
+
+// You can also manually set the timestamp for the next commit
+doc.setNextCommitTimestamp(timestamp: 1700000000)
+try text.insert(pos: 0, s: "Manual timestamp: ")
+doc.commit()
+```
+
+### Getting Change Metadata
+
+You can retrieve metadata about specific changes:
+
+```swift
+let doc = LoroDoc()
+try doc.setPeerId(peer: 1)
+doc.setRecordTimestamp(record: true)
+
+let text = doc.getText(id: "text")
+try text.insert(pos: 0, s: "Hello")
+doc.commit()
+
+// Get the number of changes
+let changeCount = doc.lenChanges()
+print("Total changes: \(changeCount)")
+
+// Get metadata for a specific change by ID
+let id = Id(peer: 1, counter: 0)
+if let changeMeta = doc.getChange(id: id) {
+ print("Change ID: \(changeMeta.id)")
+ print("Timestamp: \(changeMeta.timestamp)")
+ print("Message: \(changeMeta.message ?? "none")")
+}
+```
+
+### Commit Messages
+
+You can attach messages to commits for better history tracking:
+
+```swift
+let doc = LoroDoc()
+try doc.setPeerId(peer: 1)
+
+let text = doc.getText(id: "text")
+try text.insert(pos: 0, s: "Draft")
+
+// Set a commit message before committing
+doc.setNextCommitMessage(msg: "Initial draft")
+doc.commit()
+
+try text.delete(pos: 0, len: 5)
+try text.insert(pos: 0, s: "Final version")
+doc.setNextCommitMessage(msg: "Finalized document")
+doc.commit()
+```
+
+## Export Modes
+
+Loro supports several export modes:
+
+```swift
+let doc = LoroDoc()
+
+// Full snapshot - complete document state
+let snapshot = try doc.export(mode: .snapshot)
+
+// Updates since a version - incremental changes
+let updates = try doc.export(mode: .updates(from: versionVector))
+
+// Snapshot at a specific version
+let historicalSnapshot = try doc.export(mode: .snapshotAt(version: frontiers))
+
+// State only (no history) at optional version
+let stateOnly = try doc.export(mode: .stateOnly(nil))
+
+// Shallow snapshot at frontiers
+let shallow = try doc.export(mode: .shallowSnapshot(frontiers))
+```
+
+## Complete Example: Sync Manager
+
+```swift
+import Loro
+
+class SyncManager {
+ let doc: LoroDoc
+ private var lastSyncedVersion: VersionVector?
+
+ init(peerId: UInt64) throws {
+ doc = LoroDoc()
+ try doc.setPeerId(peer: peerId)
+ }
+
+ /// Get changes to send to server
+ func getChangesToSync() throws -> Data? {
+ if let lastVersion = lastSyncedVersion {
+ // Send only new changes
+ return try doc.export(mode: .updates(from: lastVersion))
+ } else {
+ // First sync - send everything
+ return try doc.export(mode: .snapshot)
+ }
+ }
+
+ /// Apply changes from server
+ func applyRemoteChanges(_ data: Data) throws {
+ let _ = try doc.importWith(bytes: data, origin: "server")
+ }
+
+ /// Mark current state as synced
+ func markSynced() {
+ lastSyncedVersion = doc.oplogVv()
+ }
+
+ /// Check if there are unsynced changes
+ func hasUnsyncedChanges() -> Bool {
+ guard let lastVersion = lastSyncedVersion else {
+ return true
+ }
+ return doc.oplogVv() != lastVersion
+ }
+}
+```
+
+## Complete Example: Version History
+
+```swift
+import Loro
+
+class DocumentWithHistory {
+ let doc: LoroDoc
+ private var snapshots: [(frontiers: Frontiers, label: String)] = []
+
+ init() {
+ doc = LoroDoc()
+ }
+
+ func saveSnapshot(label: String) {
+ let frontiers = doc.oplogFrontiers()
+ snapshots.append((frontiers, label))
+ }
+
+ func listSnapshots() -> [String] {
+ return snapshots.map { $0.label }
+ }
+
+ func checkout(snapshotIndex: Int) throws {
+ guard snapshotIndex < snapshots.count else { return }
+ try doc.checkout(frontiers: snapshots[snapshotIndex].frontiers)
+ }
+
+ func checkoutLatest() {
+ doc.checkoutToLatest()
+ }
+
+ func exportSnapshot(at index: Int) throws -> Data? {
+ guard index < snapshots.count else { return nil }
+ return try doc.export(mode: .snapshotAt(version: snapshots[index].frontiers))
+ }
+}
+
+// Usage
+let editor = DocumentWithHistory()
+let text = editor.doc.getText(id: "content")
+
+try text.insert(pos: 0, s: "Draft 1")
+editor.saveSnapshot(label: "First draft")
+
+try text.delete(pos: 0, len: 7)
+try text.insert(pos: 0, s: "Draft 2 - revised")
+editor.saveSnapshot(label: "Revision")
+
+try text.pushStr(s: " - final")
+editor.saveSnapshot(label: "Final")
+
+// View history
+print(editor.listSnapshots()) // ["First draft", "Revision", "Final"]
+
+// Go back to first draft
+try editor.checkout(snapshotIndex: 0)
+print(text.toString()) // "Draft 1"
+
+// Return to latest
+editor.checkoutLatest()
+print(text.toString()) // "Draft 2 - revised - final"
+```
+
+## Topics
+
+### Version Types
+
+- ``VersionVector``
+- ``Frontiers``
+
+### Export Modes
+
+- ``ExportMode``
diff --git a/Sources/Loro/Loro.docc/WorkingWithCounters.md b/Sources/Loro/Loro.docc/WorkingWithCounters.md
new file mode 100644
index 0000000..8d71620
--- /dev/null
+++ b/Sources/Loro/Loro.docc/WorkingWithCounters.md
@@ -0,0 +1,238 @@
+# Working with Counters
+
+Learn how to use LoroCounter for distributed counting operations.
+
+## Overview
+
+``LoroCounter`` is a CRDT counter that accumulates all applied values across distributed peers. It supports both integer and floating-point numbers, making it ideal for scenarios like vote counts, score tracking, inventory management, or any distributed counting use case.
+
+## Basic Operations
+
+### Creating a Counter
+
+```swift
+import Loro
+
+let doc = LoroDoc()
+let counter = doc.getCounter(id: "score")
+```
+
+### Incrementing
+
+```swift
+// Increment by 1
+try counter.increment(value: 1)
+
+// Increment by any amount
+try counter.increment(value: 10)
+try counter.increment(value: 0.5) // Floating point supported
+```
+
+### Decrementing
+
+```swift
+// Decrement by 1
+try counter.decrement(value: 1)
+
+// Decrement by any amount
+try counter.decrement(value: 5)
+try counter.decrement(value: 2.5)
+```
+
+### Reading the Value
+
+```swift
+let currentValue = counter.getValue()
+print("Current count: \(currentValue)")
+```
+
+## How Counters Work
+
+Unlike regular variables, CRDT counters don't suffer from lost updates in distributed systems. When multiple peers increment/decrement concurrently, all operations are preserved:
+
+```swift
+let doc1 = LoroDoc()
+try doc1.setPeerId(peer: 1)
+let counter1 = doc1.getCounter(id: "votes")
+
+let doc2 = LoroDoc()
+try doc2.setPeerId(peer: 2)
+let counter2 = doc2.getCounter(id: "votes")
+
+// Both peers increment concurrently
+try counter1.increment(value: 1) // +1
+try counter2.increment(value: 1) // +1
+
+// Sync both ways
+let _ = try doc2.import(bytes: doc1.export(mode: .snapshot))
+let _ = try doc1.import(bytes: doc2.export(mode: .snapshot))
+
+// Both counters now show 2 (both increments preserved)
+print(counter1.getValue()) // 2.0
+print(counter2.getValue()) // 2.0
+```
+
+## Nested Counters
+
+Counters can be nested inside other containers:
+
+```swift
+let doc = LoroDoc()
+let map = doc.getMap(id: "stats")
+
+// Create a counter inside a map
+let viewCount = try map.insertContainer(key: "views", child: LoroCounter())
+try viewCount.increment(value: 1)
+
+// Create counters inside a list
+let list = doc.getList(id: "scores")
+let score1 = try list.insertContainer(pos: 0, child: LoroCounter())
+try score1.increment(value: 100)
+```
+
+## Subscribing to Changes
+
+```swift
+let doc = LoroDoc()
+let counter = doc.getCounter(id: "observed")
+
+let subscription = counter.subscribe { event in
+ print("Counter changed!")
+}
+
+try counter.increment(value: 1)
+doc.commit() // Triggers callback
+
+subscription?.detach()
+```
+
+## Complete Example: Voting System
+
+```swift
+import Loro
+
+class VotingSystem {
+ let doc: LoroDoc
+ let votesMap: LoroMap
+
+ init(peerId: UInt64) throws {
+ doc = LoroDoc()
+ try doc.setPeerId(peer: peerId)
+ votesMap = doc.getMap(id: "votes")
+ }
+
+ func upvote(itemId: String) throws {
+ let counter = try votesMap.getOrCreateContainer(
+ key: itemId,
+ child: LoroCounter()
+ )
+ try counter.increment(value: 1)
+ }
+
+ func downvote(itemId: String) throws {
+ let counter = try votesMap.getOrCreateContainer(
+ key: itemId,
+ child: LoroCounter()
+ )
+ try counter.decrement(value: 1)
+ }
+
+ func getVotes(itemId: String) -> Double {
+ if let container = votesMap.get(key: itemId)?.asContainer(),
+ case .counter(let counter) = container {
+ return counter.getValue()
+ }
+ return 0
+ }
+
+ func export() throws -> Data {
+ return try doc.export(mode: .snapshot)
+ }
+
+ func sync(with data: Data) throws {
+ let _ = try doc.import(bytes: data)
+ }
+}
+
+// Usage
+let voting = try VotingSystem(peerId: 1)
+try voting.upvote(itemId: "post-123")
+try voting.upvote(itemId: "post-123")
+try voting.downvote(itemId: "post-456")
+
+print(voting.getVotes(itemId: "post-123")) // 2.0
+print(voting.getVotes(itemId: "post-456")) // -1.0
+```
+
+## Complete Example: Game Score Tracker
+
+```swift
+import Loro
+
+class GameScoreTracker {
+ let doc: LoroDoc
+ let players: LoroMap
+
+ init() {
+ doc = LoroDoc()
+ players = doc.getMap(id: "players")
+ }
+
+ func addPlayer(name: String) throws {
+ let playerMap = try players.insertContainer(key: name, child: LoroMap())
+ let _ = try playerMap.insertContainer(key: "score", child: LoroCounter())
+ let _ = try playerMap.insertContainer(key: "kills", child: LoroCounter())
+ let _ = try playerMap.insertContainer(key: "deaths", child: LoroCounter())
+ }
+
+ func addScore(player: String, points: Double) throws {
+ if let container = players.get(key: player)?.asContainer(),
+ case .map(let playerMap) = container,
+ let scoreContainer = playerMap.get(key: "score")?.asContainer(),
+ case .counter(let score) = scoreContainer {
+ try score.increment(value: points)
+ }
+ }
+
+ func recordKill(player: String) throws {
+ if let container = players.get(key: player)?.asContainer(),
+ case .map(let playerMap) = container,
+ let killsContainer = playerMap.get(key: "kills")?.asContainer(),
+ case .counter(let kills) = killsContainer {
+ try kills.increment(value: 1)
+ }
+ }
+
+ func getStats(player: String) -> (score: Double, kills: Double, deaths: Double)? {
+ guard let container = players.get(key: player)?.asContainer(),
+ case .map(let playerMap) = container else {
+ return nil
+ }
+
+ var score = 0.0
+ var kills = 0.0
+ var deaths = 0.0
+
+ if let c = playerMap.get(key: "score")?.asContainer(),
+ case .counter(let counter) = c {
+ score = counter.getValue()
+ }
+ if let c = playerMap.get(key: "kills")?.asContainer(),
+ case .counter(let counter) = c {
+ kills = counter.getValue()
+ }
+ if let c = playerMap.get(key: "deaths")?.asContainer(),
+ case .counter(let counter) = c {
+ deaths = counter.getValue()
+ }
+
+ return (score, kills, deaths)
+ }
+}
+```
+
+## Topics
+
+### Counter Type
+
+- ``LoroCounter``
diff --git a/Sources/Loro/Loro.docc/WorkingWithLists.md b/Sources/Loro/Loro.docc/WorkingWithLists.md
new file mode 100644
index 0000000..2372544
--- /dev/null
+++ b/Sources/Loro/Loro.docc/WorkingWithLists.md
@@ -0,0 +1,245 @@
+# Working with Lists
+
+Learn how to use List and MovableList containers for ordered collections.
+
+## Overview
+
+Loro provides two list types for ordered data:
+
+- **``LoroList``**: A standard list supporting insert and delete operations. Best for append-only or simple list scenarios.
+- **``LoroMovableList``**: An enhanced list that also supports set and move operations. Uses the Fugue algorithm for "maximal non-interleaving" during concurrent edits.
+
+## Choosing Between List and MovableList
+
+| Feature | LoroList | LoroMovableList |
+|---------|----------|-----------------|
+| Insert | Yes | Yes |
+| Delete | Yes | Yes |
+| Push/Pop | Yes | Yes |
+| Set (replace) | No | Yes |
+| Move | No | Yes |
+| Performance | Faster | ~80% slower encode/decode |
+| Memory | Lower | ~50% more |
+
+Use ``LoroList`` when you only need to add and remove elements. Use ``LoroMovableList`` when you need to reorder items or replace values in place.
+
+## Using LoroList
+
+### Basic Operations
+
+```swift
+import Loro
+
+let doc = LoroDoc()
+let list = doc.getList(id: "myList")
+
+// Insert elements
+try list.insert(pos: 0, v: "first")
+try list.insert(pos: 1, v: "second")
+
+// Push to end
+try list.push(v: "third")
+
+// Get element at index
+if let value = list.get(index: 0)?.asValue() {
+ print("First element: \(value)")
+}
+
+// Get length
+let count = list.len()
+print("List has \(count) elements")
+
+// Delete elements (position, count)
+try list.delete(pos: 1, len: 1)
+
+// Pop from end
+let last = try list.pop()
+
+// Clear all elements
+try list.clear()
+```
+
+### Inserting Different Value Types
+
+```swift
+let list = doc.getList(id: "mixedList")
+
+// Primitives
+try list.push(v: "string")
+try list.push(v: 42)
+try list.push(v: 3.14)
+try list.push(v: true)
+
+// Null values
+try list.push(v: nil)
+
+// Arrays and dictionaries
+try list.push(v: [1, 2, 3])
+try list.push(v: ["key": "value"])
+```
+
+### Nested Containers
+
+You can nest containers inside lists:
+
+```swift
+let list = doc.getList(id: "nested")
+
+// Insert a nested map
+let nestedMap = try list.insertContainer(pos: 0, child: LoroMap())
+try nestedMap.insert(key: "name", v: "Alice")
+
+// Insert a nested text
+let nestedText = try list.insertContainer(pos: 1, child: LoroText())
+try nestedText.insert(pos: 0, s: "Hello")
+```
+
+## Using LoroMovableList
+
+``LoroMovableList`` includes all ``LoroList`` operations plus set and move:
+
+### Set (Replace) Operation
+
+```swift
+let doc = LoroDoc()
+let movableList = doc.getMovableList(id: "tasks")
+
+// Add initial items
+try movableList.push(v: "Task A")
+try movableList.push(v: "Task B")
+try movableList.push(v: "Task C")
+
+// Replace item at index 1
+try movableList.set(pos: 1, v: "Updated Task B")
+```
+
+### Move Operation
+
+```swift
+let movableList = doc.getMovableList(id: "sortable")
+
+try movableList.push(v: "First")
+try movableList.push(v: "Second")
+try movableList.push(v: "Third")
+
+// Move item from index 2 to index 0
+try movableList.mov(from: 2, to: 0)
+// Result: ["Third", "First", "Second"]
+```
+
+### Converting to Array
+
+```swift
+let movableList = doc.getMovableList(id: "items")
+try movableList.push(v: "a")
+try movableList.push(v: "b")
+try movableList.push(v: "c")
+
+// Get all elements as a Swift array
+let values = movableList.toVec()
+// values: [LoroValue.string("a"), LoroValue.string("b"), LoroValue.string("c")]
+```
+
+## Cursors for Stable Positions
+
+Cursors provide stable position references that survive edits:
+
+```swift
+let list = doc.getList(id: "withCursors")
+
+try list.push(v: "A")
+try list.push(v: "B")
+try list.push(v: "C")
+
+// Get a cursor at position 1
+if let cursor = list.getCursor(pos: 1, side: .middle) {
+ // The cursor maintains its logical position even after insertions
+ try list.insert(pos: 0, v: "New First")
+
+ // Query cursor position later using doc.getCursorPos(cursor)
+}
+```
+
+## Syncing Lists Between Documents
+
+```swift
+let doc1 = LoroDoc()
+try doc1.setPeerId(peer: 1)
+let list1 = doc1.getList(id: "shared")
+try list1.push(v: "from doc1")
+
+let doc2 = LoroDoc()
+try doc2.setPeerId(peer: 2)
+let list2 = doc2.getList(id: "shared")
+try list2.push(v: "from doc2")
+
+// Sync doc1 to doc2
+let _ = try doc2.import(bytes: doc1.export(mode: .snapshot))
+
+// Both items are now in list2
+// Order depends on peer IDs and timestamps
+```
+
+## Subscribing to Changes
+
+```swift
+let doc = LoroDoc()
+let list = doc.getList(id: "observed")
+
+let subscription = list.subscribe { event in
+ print("List changed!")
+}
+
+try list.push(v: "new item")
+doc.commit() // Triggers the callback
+
+subscription?.detach() // Clean up when done
+```
+
+## Complete Example: Todo List
+
+```swift
+import Loro
+
+struct TodoApp {
+ let doc: LoroDoc
+ let todos: LoroMovableList
+
+ init() {
+ doc = LoroDoc()
+ todos = doc.getMovableList(id: "todos")
+ }
+
+ func addTodo(_ title: String) throws {
+ let todoMap = try todos.pushContainer(child: LoroMap())
+ try todoMap.insert(key: "title", v: title)
+ try todoMap.insert(key: "completed", v: false)
+ try todoMap.insert(key: "createdAt", v: Int64(Date().timeIntervalSince1970))
+ }
+
+ func toggleComplete(at index: UInt32) throws {
+ if let container = todos.get(index: index)?.asContainer(),
+ case .map(let map) = container {
+ let current = map.get(key: "completed")?.asValue()
+ if case .bool(let isComplete) = current {
+ try map.insert(key: "completed", v: !isComplete)
+ }
+ }
+ }
+
+ func reorder(from: UInt32, to: UInt32) throws {
+ try todos.mov(from: from, to: to)
+ }
+
+ func delete(at index: UInt32) throws {
+ try todos.delete(pos: index, len: 1)
+ }
+}
+```
+
+## Topics
+
+### List Types
+
+- ``LoroList``
+- ``LoroMovableList``
diff --git a/Sources/Loro/Loro.docc/WorkingWithMaps.md b/Sources/Loro/Loro.docc/WorkingWithMaps.md
new file mode 100644
index 0000000..0854751
--- /dev/null
+++ b/Sources/Loro/Loro.docc/WorkingWithMaps.md
@@ -0,0 +1,232 @@
+# Working with Maps
+
+Learn how to use LoroMap for key-value storage with conflict resolution.
+
+## Overview
+
+``LoroMap`` is a key-value container that uses **Last-Write-Wins (LWW)** semantics for conflict resolution. When concurrent edits conflict, Loro compares Lamport logic timestamps to determine the winner, ensuring all peers converge to the same state.
+
+## Basic Operations
+
+### Creating and Accessing a Map
+
+```swift
+import Loro
+
+let doc = LoroDoc()
+let map = doc.getMap(id: "settings")
+```
+
+### Inserting Values
+
+```swift
+// String values
+try map.insert(key: "username", v: "alice")
+
+// Numeric values
+try map.insert(key: "age", v: 30)
+try map.insert(key: "score", v: 95.5)
+
+// Boolean values
+try map.insert(key: "isActive", v: true)
+
+// Null values
+try map.insert(key: "optionalField", v: nil)
+
+// Arrays
+try map.insert(key: "tags", v: ["swift", "crdt", "loro"])
+
+// Nested dictionaries
+try map.insert(key: "metadata", v: ["version": 1, "author": "alice"])
+```
+
+### Reading Values
+
+```swift
+// Get a value
+if let value = map.get(key: "username")?.asValue() {
+ switch value {
+ case .string(let str):
+ print("Username: \(str)")
+ case .i64(let num):
+ print("Number: \(num)")
+ case .bool(let flag):
+ print("Boolean: \(flag)")
+ default:
+ break
+ }
+}
+
+// Get the shallow value (containers shown as IDs)
+let shallowValue = map.getValue()
+
+// Get the deep value (containers converted to nested values)
+let deepValue = map.getDeepValue()
+```
+
+### Deleting Keys
+
+```swift
+// Delete a single key
+try map.delete(key: "optionalField")
+
+// Clear all keys
+try map.clear()
+```
+
+## Nested Containers
+
+Maps can contain other Loro containers:
+
+```swift
+let doc = LoroDoc()
+let root = doc.getMap(id: "root")
+
+// Insert a nested map
+let profile = try root.insertContainer(key: "profile", child: LoroMap())
+try profile.insert(key: "name", v: "Alice")
+try profile.insert(key: "email", v: "alice@example.com")
+
+// Insert a nested list
+let friends = try root.insertContainer(key: "friends", child: LoroList())
+try friends.push(v: "Bob")
+try friends.push(v: "Charlie")
+
+// Insert nested text
+let bio = try root.insertContainer(key: "bio", child: LoroText())
+try bio.insert(pos: 0, s: "Hello, I'm Alice!")
+
+// Get or create a container (useful for ensuring containers exist)
+let settings = try root.getOrCreateContainer(key: "settings", child: LoroMap())
+try settings.insert(key: "theme", v: "dark")
+```
+
+## Conflict Resolution
+
+LoroMap uses Last-Write-Wins based on Lamport timestamps:
+
+```swift
+let doc1 = LoroDoc()
+try doc1.setPeerId(peer: 1)
+let map1 = doc1.getMap(id: "shared")
+
+let doc2 = LoroDoc()
+try doc2.setPeerId(peer: 2)
+let map2 = doc2.getMap(id: "shared")
+
+// Both peers edit the same key concurrently
+try map1.insert(key: "color", v: "red")
+try map2.insert(key: "color", v: "blue")
+
+// Sync documents
+let _ = try doc2.import(bytes: doc1.export(mode: .snapshot))
+let _ = try doc1.import(bytes: doc2.export(mode: .snapshot))
+
+// Both documents converge to the same value
+// The peer with the larger peerId (and thus larger logical timestamp) wins
+// In this case, doc2 (peerId: 2) wins, so color = "blue"
+```
+
+### Checking Last Editor
+
+You can determine which peer last edited a key:
+
+```swift
+if let lastEditor = map.getLastEditor(key: "color") {
+ print("Last edited by peer: \(lastEditor)")
+}
+```
+
+## Subscribing to Changes
+
+```swift
+let doc = LoroDoc()
+let map = doc.getMap(id: "observed")
+
+let subscription = map.subscribe { event in
+ print("Map changed with origin: \(event.origin)")
+}
+
+try map.insert(key: "test", v: "value")
+doc.commit() // Triggers the callback
+
+subscription?.detach() // Clean up when done
+```
+
+## Syncing Maps Between Documents
+
+```swift
+let doc1 = LoroDoc()
+try doc1.setPeerId(peer: 1)
+let map1 = doc1.getMap(id: "config")
+try map1.insert(key: "setting1", v: "value1")
+
+let doc2 = LoroDoc()
+try doc2.setPeerId(peer: 2)
+
+// Import snapshot
+let _ = try doc2.import(bytes: doc1.export(mode: .snapshot))
+
+// Access the synced map
+let map2 = doc2.getMap(id: "config")
+let value = map2.get(key: "setting1")?.asValue()
+// value == .string("value1")
+```
+
+## Complete Example: User Settings
+
+```swift
+import Loro
+
+class UserSettings {
+ let doc: LoroDoc
+ let settings: LoroMap
+
+ init() {
+ doc = LoroDoc()
+ settings = doc.getMap(id: "userSettings")
+ }
+
+ func setTheme(_ theme: String) throws {
+ try settings.insert(key: "theme", v: theme)
+ }
+
+ func getTheme() -> String {
+ if let value = settings.get(key: "theme")?.asValue(),
+ case .string(let theme) = value {
+ return theme
+ }
+ return "light" // default
+ }
+
+ func setNotificationsEnabled(_ enabled: Bool) throws {
+ try settings.insert(key: "notifications", v: enabled)
+ }
+
+ func isNotificationsEnabled() -> Bool {
+ if let value = settings.get(key: "notifications")?.asValue(),
+ case .bool(let enabled) = value {
+ return enabled
+ }
+ return true // default
+ }
+
+ func setFavoriteColors(_ colors: [String]) throws {
+ try settings.insert(key: "favoriteColors", v: colors)
+ }
+
+ func export() throws -> Data {
+ return try doc.export(mode: .snapshot)
+ }
+
+ func importSettings(_ data: Data) throws {
+ let _ = try doc.import(bytes: data)
+ }
+}
+```
+
+## Topics
+
+### Map Type
+
+- ``LoroMap``
diff --git a/Sources/Loro/Loro.docc/WorkingWithText.md b/Sources/Loro/Loro.docc/WorkingWithText.md
new file mode 100644
index 0000000..a1ea68a
--- /dev/null
+++ b/Sources/Loro/Loro.docc/WorkingWithText.md
@@ -0,0 +1,284 @@
+# Working with Text
+
+Learn how to use LoroText for collaborative rich text editing.
+
+## Overview
+
+``LoroText`` is a text container optimized for collaborative editing. It supports both plain text and rich text with formatting marks. The internal B-tree structure provides O(log N) complexity for basic operations, making it efficient for documents with millions of characters.
+
+## Basic Text Operations
+
+### Creating and Accessing Text
+
+```swift
+import Loro
+
+let doc = LoroDoc()
+let text = doc.getText(id: "document")
+```
+
+### Inserting Text
+
+```swift
+// Insert at a position
+try text.insert(pos: 0, s: "Hello, ")
+try text.insert(pos: 7, s: "World!")
+// Result: "Hello, World!"
+
+// Push to end
+try text.pushStr(s: " How are you?")
+// Result: "Hello, World! How are you?"
+```
+
+### Reading Text
+
+```swift
+// Get the full text
+let content = text.toString()
+
+// Get a substring (slice)
+let slice = try text.slice(startIndex: 0, endIndex: 5)
+// slice: "Hello"
+
+// Get character at position
+let char = try text.charAt(pos: 0)
+// char: "H"
+
+// Get length
+let unicodeLength = text.lenUnicode()
+let utf8Length = text.lenUtf8()
+let utf16Length = text.lenUtf16()
+```
+
+### Deleting Text
+
+```swift
+// Delete 5 characters starting at position 7
+try text.delete(pos: 7, len: 5)
+// "Hello, World!" -> "Hello, ld!"
+```
+
+### Splice (Delete and Insert)
+
+```swift
+// Replace characters: delete 2 chars at pos 7, insert "Wor"
+let deleted = try text.splice(pos: 7, len: 2, s: "Wor")
+// Returns the deleted text
+```
+
+## Rich Text (Marks)
+
+LoroText supports formatting through marks - key-value pairs applied to ranges of text.
+
+### Adding Marks
+
+```swift
+let doc = LoroDoc()
+let text = doc.getText(id: "richText")
+try text.insert(pos: 0, s: "Hello World")
+
+// Make "Hello" bold (positions 0-5)
+try text.mark(from: 0, to: 5, key: "bold", value: true)
+
+// Make "World" italic (positions 6-11)
+try text.mark(from: 6, to: 11, key: "italic", value: true)
+
+// Add a link
+try text.mark(from: 0, to: 11, key: "link", value: "https://example.com")
+```
+
+### Removing Marks
+
+```swift
+// Remove bold from "Hello"
+try text.unmark(from: 0, to: 5, key: "bold")
+```
+
+### Reading Rich Text
+
+```swift
+// Get text with formatting as Delta
+let delta = text.toDelta()
+for item in delta {
+ switch item {
+ case .insert(let text, let attributes):
+ print("Text: \(text)")
+ if let attrs = attributes {
+ print("Attributes: \(attrs)")
+ }
+ case .retain(let count, let attributes):
+ print("Retain: \(count)")
+ case .delete(let count):
+ print("Delete: \(count)")
+ }
+}
+
+// Get rich text as LoroValue
+let richValue = text.getRichtextValue()
+```
+
+### Mark Expansion Behavior
+
+When inserting text at the boundary of a marked range, you can control whether the mark expands:
+
+- **after** (default): Mark expands when inserting after the range
+- **before**: Mark expands when inserting before the range
+- **both**: Mark expands in both directions
+- **none**: Mark never expands
+
+## Applying Deltas
+
+You can apply changes in [Quill Delta format](https://quilljs.com/docs/delta/):
+
+```swift
+let doc = LoroDoc()
+let text = doc.getText(id: "text")
+try text.insert(pos: 0, s: "Hello World")
+
+// Apply delta: delete first char, retain 4, insert " there"
+try text.applyDelta(delta: [
+ .delete(delete: 1),
+ .retain(retain: 4, attributes: nil),
+ .insert(insert: " there", attributes: nil)
+])
+// Result: "ello there World"
+```
+
+## Updating Text
+
+For replacing entire text content efficiently:
+
+```swift
+let text = doc.getText(id: "text")
+try text.insert(pos: 0, s: "Original content")
+
+// Update uses Myers' diff algorithm to compute minimal changes
+try text.update(s: "Updated content", options: UpdateOptions())
+
+// For large texts, use line-based update (faster but less precise)
+try text.updateByLine(s: "Updated content\nWith multiple lines", options: UpdateOptions())
+```
+
+## Cursors
+
+Cursors provide stable position references that survive edits:
+
+```swift
+let text = doc.getText(id: "text")
+try text.insert(pos: 0, s: "Hello World")
+
+// Create a cursor at position 6 (before "World")
+if let cursor = text.getCursor(pos: 6, side: .left) {
+ // The cursor maintains its logical position
+ // even after insertions before it
+ try text.insert(pos: 0, s: "Say: ")
+ // Original position 6 now refers to a different location
+ // but the cursor still points to before "World"
+}
+```
+
+## Unicode Handling
+
+LoroText handles Unicode properly:
+
+```swift
+let text = doc.getText(id: "emoji")
+try text.insert(pos: 0, s: "Hello 😀 World")
+
+// Different length measurements
+let unicodeLen = text.lenUnicode() // Counts Unicode scalar values
+let utf8Len = text.lenUtf8() // Counts UTF-8 bytes
+let utf16Len = text.lenUtf16() // Counts UTF-16 code units
+
+// UTF-8 operations
+try text.insertUtf8(pos: 0, s: "Start ")
+try text.deleteUtf8(pos: 0, len: 6)
+```
+
+## Syncing Text
+
+```swift
+let doc1 = LoroDoc()
+try doc1.setPeerId(peer: 1)
+let text1 = doc1.getText(id: "shared")
+try text1.insert(pos: 0, s: "Hello from peer 1")
+
+let doc2 = LoroDoc()
+try doc2.setPeerId(peer: 2)
+let _ = try doc2.import(bytes: doc1.export(mode: .snapshot))
+
+let text2 = doc2.getText(id: "shared")
+print(text2.toString()) // "Hello from peer 1"
+```
+
+## Subscribing to Changes
+
+```swift
+let doc = LoroDoc()
+let text = doc.getText(id: "observed")
+
+let subscription = text.subscribe { event in
+ print("Text changed!")
+}
+
+try text.insert(pos: 0, s: "Hello")
+doc.commit() // Triggers callback
+
+subscription?.detach()
+```
+
+## Complete Example: Collaborative Editor
+
+```swift
+import Loro
+
+class CollaborativeEditor {
+ let doc: LoroDoc
+ let text: LoroText
+
+ init(peerId: UInt64) throws {
+ doc = LoroDoc()
+ try doc.setPeerId(peer: peerId)
+ text = doc.getText(id: "document")
+ }
+
+ func insertText(at position: UInt32, content: String) throws {
+ try text.insert(pos: position, s: content)
+ }
+
+ func deleteText(at position: UInt32, length: UInt32) throws {
+ try text.delete(pos: position, len: length)
+ }
+
+ func setBold(from: UInt32, to: UInt32, enabled: Bool) throws {
+ if enabled {
+ try text.mark(from: from, to: to, key: "bold", value: true)
+ } else {
+ try text.unmark(from: from, to: to, key: "bold")
+ }
+ }
+
+ func getContent() -> String {
+ return text.toString()
+ }
+
+ func getDelta() -> [TextDelta] {
+ return text.toDelta()
+ }
+
+ func export() throws -> Data {
+ return try doc.export(mode: .snapshot)
+ }
+
+ func importChanges(_ data: Data) throws {
+ let _ = try doc.import(bytes: data)
+ }
+}
+```
+
+## Topics
+
+### Text Type
+
+- ``LoroText``
+- ``TextDelta``
diff --git a/Sources/Loro/Loro.docc/WorkingWithTrees.md b/Sources/Loro/Loro.docc/WorkingWithTrees.md
new file mode 100644
index 0000000..7e82827
--- /dev/null
+++ b/Sources/Loro/Loro.docc/WorkingWithTrees.md
@@ -0,0 +1,214 @@
+# Working with Trees
+
+Learn how to create and manipulate hierarchical tree structures using LoroTree.
+
+## Overview
+
+``LoroTree`` provides a movable tree CRDT that allows you to create hierarchical data structures where nodes can be moved between parents while maintaining consistency across distributed peers. This is useful for building features like file explorers, organizational charts, nested task lists, and any hierarchical data structure.
+
+## Creating a Tree
+
+Get a tree container from a ``LoroDoc``:
+
+```swift
+import Loro
+
+let doc = LoroDoc()
+let tree = doc.getTree(id: "myTree")
+```
+
+## Creating Nodes
+
+Create root nodes and child nodes using the ``TreeParentId`` enum:
+
+```swift
+// Create a root node
+let rootNode = try tree.create(parent: .root)
+
+// Create child nodes under the root
+let child1 = try tree.create(parent: .node(id: rootNode))
+let child2 = try tree.create(parent: .node(id: rootNode))
+
+// Create a grandchild
+let grandchild = try tree.create(parent: .node(id: child1))
+```
+
+### Creating Nodes at Specific Positions
+
+Use `createAt` to insert a node at a specific index among siblings:
+
+```swift
+// Insert at the beginning (index 0)
+let firstChild = try tree.createAt(parent: .root, index: 0)
+
+// Insert at a specific position
+let middleChild = try tree.createAt(parent: .root, index: 1)
+```
+
+## Moving Nodes
+
+The "movable" in movable tree refers to the ability to move nodes between parents:
+
+```swift
+// Move a node to a new parent
+try tree.mov(target: grandchild, parent: .root)
+
+// Move to a specific index under the parent
+try tree.movTo(target: child1, parent: .root, to: 0)
+
+// Move relative to siblings
+try tree.movBefore(target: child2, before: child1)
+try tree.movAfter(target: child1, after: child2)
+```
+
+## Querying the Tree Structure
+
+### Getting Children
+
+```swift
+// Get all children of a node
+let children = tree.children(parent: .node(id: rootNode))
+
+// Get all root nodes
+let roots = tree.roots()
+
+// Get child count
+let count = tree.childrenNum(parent: .root)
+```
+
+### Navigation
+
+```swift
+// Get the parent of a node
+if let parentId = tree.parent(target: child1) {
+ switch parentId {
+ case .root:
+ print("Node is at root level")
+ case .node(let id):
+ print("Parent node: \(id)")
+ case .deleted:
+ print("Parent was deleted")
+ case .unexist:
+ print("Node doesn't exist")
+ }
+}
+
+// Check if a node exists
+let exists = tree.contains(target: child1)
+
+// Check if a node is deleted
+let isDeleted = tree.isNodeDeleted(target: child1)
+```
+
+## Deleting Nodes
+
+```swift
+// Delete a node (and its subtree)
+try tree.delete(target: child2)
+```
+
+## Node Metadata
+
+Each tree node has an associated ``LoroMap`` for storing metadata:
+
+```swift
+// Get the metadata map for a node
+let meta = tree.getMeta(target: rootNode)
+
+// Add metadata
+try meta.insert(key: "name", v: "Root Folder")
+try meta.insert(key: "icon", v: "folder")
+try meta.insert(key: "createdAt", v: 1699900000)
+
+// Read metadata
+if let name = meta.get(key: "name")?.asValue() {
+ print("Node name: \(name)")
+}
+```
+
+## Fractional Indexing
+
+Enable fractional indexing for precise ordering control during concurrent edits:
+
+```swift
+// Enable fractional indexing with jitter for better distribution
+tree.enableFractionalIndex(jitter: 8)
+
+// Get the fractional index of a node
+if let index = tree.fractionalIndex(target: child1) {
+ print("Fractional index: \(index)")
+}
+
+// Check if fractional indexing is enabled
+let enabled = tree.isFractionalIndexEnabled()
+
+// Disable if needed
+tree.disableFractionalIndex()
+```
+
+## Subscribing to Tree Changes
+
+Listen for changes to the tree structure:
+
+```swift
+let subscription = tree.subscribe { event in
+ // Handle tree changes
+ print("Tree changed with origin: \(event.origin)")
+}
+
+// Remember to detach when done
+subscription?.detach()
+```
+
+## Complete Example
+
+Here's a complete example building a simple file system structure:
+
+```swift
+import Loro
+
+func createFileSystem() throws {
+ let doc = LoroDoc()
+ let tree = doc.getTree(id: "fileSystem")
+
+ // Create root folders
+ let documents = try tree.create(parent: .root)
+ let downloads = try tree.create(parent: .root)
+
+ // Set metadata for folders
+ let docsMeta = tree.getMeta(target: documents)
+ try docsMeta.insert(key: "name", v: "Documents")
+ try docsMeta.insert(key: "type", v: "folder")
+
+ let dlMeta = tree.getMeta(target: downloads)
+ try dlMeta.insert(key: "name", v: "Downloads")
+ try dlMeta.insert(key: "type", v: "folder")
+
+ // Create files in Documents
+ let readme = try tree.create(parent: .node(id: documents))
+ let readmeMeta = tree.getMeta(target: readme)
+ try readmeMeta.insert(key: "name", v: "README.md")
+ try readmeMeta.insert(key: "type", v: "file")
+ try readmeMeta.insert(key: "size", v: 1024)
+
+ // Move a file from Documents to Downloads
+ try tree.mov(target: readme, parent: .node(id: downloads))
+
+ // List all items in Downloads
+ let downloadItems = tree.children(parent: .node(id: downloads))
+ for itemId in downloadItems {
+ let meta = tree.getMeta(target: itemId)
+ if let name = meta.get(key: "name")?.asValue() {
+ print("Item: \(name)")
+ }
+ }
+}
+```
+
+## Topics
+
+### Tree Types
+
+- ``LoroTree``
+- ``TreeId``
+- ``TreeParentId``
diff --git a/Sources/Loro/Loro.swift b/Sources/Loro/Loro.swift
index 1b47118..8eea634 100644
--- a/Sources/Loro/Loro.swift
+++ b/Sources/Loro/Loro.swift
@@ -5,9 +5,13 @@
// Created by Leon Zhao on 2024/8/6.
//
-
-
+#if !hasFeature(Embedded)
+#if canImport(FoundationEssentials)
+import FoundationEssentials
+#elseif canImport(Foundation)
import Foundation
+#endif
+#endif
public enum ExportMode {
case snapshot
diff --git a/Sources/Loro/LoroFFI.swift b/Sources/Loro/LoroFFI.swift
index a4ad12a..39633ea 100644
--- a/Sources/Loro/LoroFFI.swift
+++ b/Sources/Loro/LoroFFI.swift
@@ -7,8 +7,8 @@ import Foundation
// Depending on the consumer's build setup, the low-level FFI code
// 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(loroFFI)
-import loroFFI
+#if canImport(LoroFFI)
+import LoroFFI
#endif
fileprivate extension RustBuffer {
@@ -857,7 +857,7 @@ fileprivate struct UniffiCallbackInterfaceChangeAncestorsTraveler {
// Create the VTable using a series of closures.
// Swift automatically converts these into C callback functions.
- static var vtable: UniffiVTableCallbackInterfaceChangeAncestorsTraveler = UniffiVTableCallbackInterfaceChangeAncestorsTraveler(
+ nonisolated(unsafe) static var vtable: UniffiVTableCallbackInterfaceChangeAncestorsTraveler = UniffiVTableCallbackInterfaceChangeAncestorsTraveler(
travel: { (
uniffiHandle: UInt64,
change: RustBuffer,
@@ -899,7 +899,7 @@ private func uniffiCallbackInitChangeAncestorsTraveler() {
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeChangeAncestorsTraveler: FfiConverter {
- fileprivate static var handleMap = UniffiHandleMap()
+ nonisolated(unsafe) fileprivate static var handleMap = UniffiHandleMap()
typealias FfiType = UnsafeMutableRawPointer
typealias SwiftType = ChangeAncestorsTraveler
@@ -1247,7 +1247,7 @@ public func FfiConverterTypeConfigure_lower(_ value: Configure) -> UnsafeMutable
-public protocol ContainerIdLike: Any {
+public protocol ContainerIdLike {
func asContainerId(ty: ContainerType) -> ContainerId
@@ -1320,7 +1320,7 @@ fileprivate struct UniffiCallbackInterfaceContainerIdLike {
// Create the VTable using a series of closures.
// Swift automatically converts these into C callback functions.
- static var vtable: UniffiVTableCallbackInterfaceContainerIdLike = UniffiVTableCallbackInterfaceContainerIdLike(
+ nonisolated(unsafe) static var vtable: UniffiVTableCallbackInterfaceContainerIdLike = UniffiVTableCallbackInterfaceContainerIdLike(
asContainerId: { (
uniffiHandle: UInt64,
ty: RustBuffer,
@@ -1362,7 +1362,7 @@ private func uniffiCallbackInitContainerIdLike() {
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeContainerIdLike: FfiConverter {
- fileprivate static var handleMap = UniffiHandleMap()
+ nonisolated(unsafe) fileprivate static var handleMap = UniffiHandleMap()
typealias FfiType = UnsafeMutableRawPointer
typealias SwiftType = ContainerIdLike
@@ -2015,7 +2015,7 @@ fileprivate struct UniffiCallbackInterfaceEphemeralSubscriber {
// Create the VTable using a series of closures.
// Swift automatically converts these into C callback functions.
- static var vtable: UniffiVTableCallbackInterfaceEphemeralSubscriber = UniffiVTableCallbackInterfaceEphemeralSubscriber(
+ nonisolated(unsafe) static var vtable: UniffiVTableCallbackInterfaceEphemeralSubscriber = UniffiVTableCallbackInterfaceEphemeralSubscriber(
onEphemeralEvent: { (
uniffiHandle: UInt64,
event: RustBuffer,
@@ -2057,7 +2057,7 @@ private func uniffiCallbackInitEphemeralSubscriber() {
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeEphemeralSubscriber: FfiConverter {
- fileprivate static var handleMap = UniffiHandleMap()
+ nonisolated(unsafe) fileprivate static var handleMap = UniffiHandleMap()
typealias FfiType = UnsafeMutableRawPointer
typealias SwiftType = EphemeralSubscriber
@@ -2183,7 +2183,7 @@ fileprivate struct UniffiCallbackInterfaceFirstCommitFromPeerCallback {
// Create the VTable using a series of closures.
// Swift automatically converts these into C callback functions.
- static var vtable: UniffiVTableCallbackInterfaceFirstCommitFromPeerCallback = UniffiVTableCallbackInterfaceFirstCommitFromPeerCallback(
+ nonisolated(unsafe) static var vtable: UniffiVTableCallbackInterfaceFirstCommitFromPeerCallback = UniffiVTableCallbackInterfaceFirstCommitFromPeerCallback(
onFirstCommitFromPeer: { (
uniffiHandle: UInt64,
payload: RustBuffer,
@@ -2225,7 +2225,7 @@ private func uniffiCallbackInitFirstCommitFromPeerCallback() {
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeFirstCommitFromPeerCallback: FfiConverter {
- fileprivate static var handleMap = UniffiHandleMap()
+ nonisolated(unsafe) fileprivate static var handleMap = UniffiHandleMap()
typealias FfiType = UnsafeMutableRawPointer
typealias SwiftType = FirstCommitFromPeerCallback
@@ -2281,11 +2281,10 @@ public func FfiConverterTypeFirstCommitFromPeerCallback_lower(_ value: FirstComm
public protocol FractionalIndexProtocol : AnyObject {
- func toString() -> String
-
}
open class FractionalIndex:
+ CustomStringConvertible,
FractionalIndexProtocol {
fileprivate let pointer: UnsafeMutableRawPointer!
@@ -2351,13 +2350,14 @@ public static func fromHexString(str: String) -> FractionalIndex {
-open func toString() -> String {
- return try! FfiConverterString.lift(try! rustCall() {
- uniffi_loro_ffi_fn_method_fractionalindex_to_string(self.uniffiClonePointer(),$0
+ open var description: String {
+ return try! FfiConverterString.lift(
+ try! rustCall() {
+ uniffi_loro_ffi_fn_method_fractionalindex_uniffi_trait_display(self.uniffiClonePointer(),$0
)
-})
}
-
+ )
+ }
}
@@ -2594,6 +2594,177 @@ public func FfiConverterTypeFrontiers_lower(_ value: Frontiers) -> UnsafeMutable
+public protocol JsonPathSubscriber : AnyObject {
+
+ /**
+ * Called when a change may affect the subscribed JSONPath query.
+ */
+ func onJsonpathChanged()
+
+}
+
+open class JsonPathSubscriberImpl:
+ JsonPathSubscriber {
+ 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`.
+ required public 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) {
+ self.pointer = nil
+ }
+
+#if swift(>=5.8)
+ @_documentation(visibility: private)
+#endif
+ public func uniffiClonePointer() -> UnsafeMutableRawPointer {
+ return try! rustCall { uniffi_loro_ffi_fn_clone_jsonpathsubscriber(self.pointer, $0) }
+ }
+ // No primary constructor declared for this class.
+
+ deinit {
+ guard let pointer = pointer else {
+ return
+ }
+
+ try! rustCall { uniffi_loro_ffi_fn_free_jsonpathsubscriber(pointer, $0) }
+ }
+
+
+
+
+ /**
+ * Called when a change may affect the subscribed JSONPath query.
+ */
+open func onJsonpathChanged() {try! rustCall() {
+ uniffi_loro_ffi_fn_method_jsonpathsubscriber_on_jsonpath_changed(self.uniffiClonePointer(),$0
+ )
+}
+}
+
+
+}
+
+
+// Put the implementation in a struct so we don't pollute the top-level namespace
+fileprivate struct UniffiCallbackInterfaceJsonPathSubscriber {
+
+ // Create the VTable using a series of closures.
+ // Swift automatically converts these into C callback functions.
+ nonisolated(unsafe) static var vtable: UniffiVTableCallbackInterfaceJsonPathSubscriber = UniffiVTableCallbackInterfaceJsonPathSubscriber(
+ onJsonpathChanged: { (
+ uniffiHandle: UInt64,
+ uniffiOutReturn: UnsafeMutableRawPointer,
+ uniffiCallStatus: UnsafeMutablePointer
+ ) in
+ let makeCall = {
+ () throws -> () in
+ guard let uniffiObj = try? FfiConverterTypeJsonPathSubscriber.handleMap.get(handle: uniffiHandle) else {
+ throw UniffiInternalError.unexpectedStaleHandle
+ }
+ return uniffiObj.onJsonpathChanged(
+ )
+ }
+
+
+ let writeReturn = { () }
+ uniffiTraitInterfaceCall(
+ callStatus: uniffiCallStatus,
+ makeCall: makeCall,
+ writeReturn: writeReturn
+ )
+ },
+ uniffiFree: { (uniffiHandle: UInt64) -> () in
+ let result = try? FfiConverterTypeJsonPathSubscriber.handleMap.remove(handle: uniffiHandle)
+ if result == nil {
+ print("Uniffi callback interface JsonPathSubscriber: handle missing in uniffiFree")
+ }
+ }
+ )
+}
+
+private func uniffiCallbackInitJsonPathSubscriber() {
+ uniffi_loro_ffi_fn_init_callback_vtable_jsonpathsubscriber(&UniffiCallbackInterfaceJsonPathSubscriber.vtable)
+}
+
+#if swift(>=5.8)
+@_documentation(visibility: private)
+#endif
+public struct FfiConverterTypeJsonPathSubscriber: FfiConverter {
+ nonisolated(unsafe) fileprivate static var handleMap = UniffiHandleMap()
+
+ typealias FfiType = UnsafeMutableRawPointer
+ typealias SwiftType = JsonPathSubscriber
+
+ public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> JsonPathSubscriber {
+ return JsonPathSubscriberImpl(unsafeFromRawPointer: pointer)
+ }
+
+ public static func lower(_ value: JsonPathSubscriber) -> UnsafeMutableRawPointer {
+ guard let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: handleMap.insert(obj: value))) else {
+ fatalError("Cast to UnsafeMutableRawPointer failed")
+ }
+ return ptr
+ }
+
+ public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> JsonPathSubscriber {
+ 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: JsonPathSubscriber, 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 FfiConverterTypeJsonPathSubscriber_lift(_ pointer: UnsafeMutableRawPointer) throws -> JsonPathSubscriber {
+ return try FfiConverterTypeJsonPathSubscriber.lift(pointer)
+}
+
+#if swift(>=5.8)
+@_documentation(visibility: private)
+#endif
+public func FfiConverterTypeJsonPathSubscriber_lower(_ value: JsonPathSubscriber) -> UnsafeMutableRawPointer {
+ return FfiConverterTypeJsonPathSubscriber.lower(value)
+}
+
+
+
+
public protocol LocalEphemeralListener : AnyObject {
func onEphemeralUpdate(update: Data)
@@ -2666,7 +2837,7 @@ fileprivate struct UniffiCallbackInterfaceLocalEphemeralListener {
// Create the VTable using a series of closures.
// Swift automatically converts these into C callback functions.
- static var vtable: UniffiVTableCallbackInterfaceLocalEphemeralListener = UniffiVTableCallbackInterfaceLocalEphemeralListener(
+ nonisolated(unsafe) static var vtable: UniffiVTableCallbackInterfaceLocalEphemeralListener = UniffiVTableCallbackInterfaceLocalEphemeralListener(
onEphemeralUpdate: { (
uniffiHandle: UInt64,
update: RustBuffer,
@@ -2708,7 +2879,7 @@ private func uniffiCallbackInitLocalEphemeralListener() {
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeLocalEphemeralListener: FfiConverter {
- fileprivate static var handleMap = UniffiHandleMap()
+ nonisolated(unsafe) fileprivate static var handleMap = UniffiHandleMap()
typealias FfiType = UnsafeMutableRawPointer
typealias SwiftType = LocalEphemeralListener
@@ -2834,7 +3005,7 @@ fileprivate struct UniffiCallbackInterfaceLocalUpdateCallback {
// Create the VTable using a series of closures.
// Swift automatically converts these into C callback functions.
- static var vtable: UniffiVTableCallbackInterfaceLocalUpdateCallback = UniffiVTableCallbackInterfaceLocalUpdateCallback(
+ nonisolated(unsafe) static var vtable: UniffiVTableCallbackInterfaceLocalUpdateCallback = UniffiVTableCallbackInterfaceLocalUpdateCallback(
onLocalUpdate: { (
uniffiHandle: UInt64,
update: RustBuffer,
@@ -2876,7 +3047,7 @@ private func uniffiCallbackInitLocalUpdateCallback() {
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeLocalUpdateCallback: FfiConverter {
- fileprivate static var handleMap = UniffiHandleMap()
+ nonisolated(unsafe) fileprivate static var handleMap = UniffiHandleMap()
typealias FfiType = UnsafeMutableRawPointer
typealias SwiftType = LocalUpdateCallback
@@ -3849,6 +4020,14 @@ public protocol LoroDocProtocol : AnyObject {
*/
func subscribeFirstCommitFromPeer(callback: FirstCommitFromPeerCallback) -> Subscription
+ /**
+ * Subscribe to updates that might affect the given JSONPath query.
+ *
+ * The callback may fire false positives; it is intended as a lightweight notification so
+ * callers can debounce or throttle before re-running JSONPath themselves.
+ */
+ func subscribeJsonpath(path: String, callback: JsonPathSubscriber) throws -> Subscription
+
/**
* Subscribe the local update of the document.
*/
@@ -5007,6 +5186,21 @@ open func subscribeFirstCommitFromPeer(callback: FirstCommitFromPeerCallback) ->
FfiConverterTypeFirstCommitFromPeerCallback.lower(callback),$0
)
})
+}
+
+ /**
+ * Subscribe to updates that might affect the given JSONPath query.
+ *
+ * The callback may fire false positives; it is intended as a lightweight notification so
+ * callers can debounce or throttle before re-running JSONPath themselves.
+ */
+open func subscribeJsonpath(path: String, callback: JsonPathSubscriber)throws -> Subscription {
+ return try FfiConverterTypeSubscription.lift(try rustCallWithError(FfiConverterTypeLoroError.lift) {
+ uniffi_loro_ffi_fn_method_lorodoc_subscribe_jsonpath(self.uniffiClonePointer(),
+ FfiConverterString.lower(path),
+ FfiConverterTypeJsonPathSubscriber.lower(callback),$0
+ )
+})
}
/**
@@ -7025,11 +7219,6 @@ public protocol LoroTextProtocol : AnyObject {
*/
func toDelta() -> [TextDelta]
- /**
- * Get the text content of the text container.
- */
- func toString() -> String
-
/**
* Unmark a range of text with a key and a value.
*
@@ -7076,6 +7265,7 @@ public protocol LoroTextProtocol : AnyObject {
}
open class LoroText:
+ CustomStringConvertible,
LoroTextProtocol {
fileprivate let pointer: UnsafeMutableRawPointer!
@@ -7529,16 +7719,6 @@ open func toDelta() -> [TextDelta] {
uniffi_loro_ffi_fn_method_lorotext_to_delta(self.uniffiClonePointer(),$0
)
})
-}
-
- /**
- * Get the text content of the text container.
- */
-open func toString() -> String {
- return try! FfiConverterString.lift(try! rustCall() {
- uniffi_loro_ffi_fn_method_lorotext_to_string(self.uniffiClonePointer(),$0
- )
-})
}
/**
@@ -7610,6 +7790,14 @@ open func updateByLine(s: String, options: UpdateOptions)throws {try rustCallWi
}
}
+ open var description: String {
+ return try! FfiConverterString.lift(
+ try! rustCall() {
+ uniffi_loro_ffi_fn_method_lorotext_uniffi_trait_display(self.uniffiClonePointer(),$0
+ )
+}
+ )
+ }
}
@@ -8445,7 +8633,7 @@ public func FfiConverterTypeLoroUnknown_lower(_ value: LoroUnknown) -> UnsafeMut
-public protocol LoroValueLike: Any {
+public protocol LoroValueLike {
func asLoroValue() -> LoroValue
@@ -8517,7 +8705,7 @@ fileprivate struct UniffiCallbackInterfaceLoroValueLike {
// Create the VTable using a series of closures.
// Swift automatically converts these into C callback functions.
- static var vtable: UniffiVTableCallbackInterfaceLoroValueLike = UniffiVTableCallbackInterfaceLoroValueLike(
+ nonisolated(unsafe) static var vtable: UniffiVTableCallbackInterfaceLoroValueLike = UniffiVTableCallbackInterfaceLoroValueLike(
asLoroValue: { (
uniffiHandle: UInt64,
uniffiOutReturn: UnsafeMutablePointer,
@@ -8557,7 +8745,7 @@ private func uniffiCallbackInitLoroValueLike() {
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeLoroValueLike: FfiConverter {
- fileprivate static var handleMap = UniffiHandleMap()
+ nonisolated(unsafe) fileprivate static var handleMap = UniffiHandleMap()
typealias FfiType = UnsafeMutableRawPointer
typealias SwiftType = LoroValueLike
@@ -8685,7 +8873,7 @@ fileprivate struct UniffiCallbackInterfaceOnPop {
// Create the VTable using a series of closures.
// Swift automatically converts these into C callback functions.
- static var vtable: UniffiVTableCallbackInterfaceOnPop = UniffiVTableCallbackInterfaceOnPop(
+ nonisolated(unsafe) static var vtable: UniffiVTableCallbackInterfaceOnPop = UniffiVTableCallbackInterfaceOnPop(
onPop: { (
uniffiHandle: UInt64,
undoOrRedo: RustBuffer,
@@ -8731,7 +8919,7 @@ private func uniffiCallbackInitOnPop() {
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeOnPop: FfiConverter {
- fileprivate static var handleMap = UniffiHandleMap()
+ nonisolated(unsafe) fileprivate static var handleMap = UniffiHandleMap()
typealias FfiType = UnsafeMutableRawPointer
typealias SwiftType = OnPop
@@ -8860,7 +9048,7 @@ fileprivate struct UniffiCallbackInterfaceOnPush {
// Create the VTable using a series of closures.
// Swift automatically converts these into C callback functions.
- static var vtable: UniffiVTableCallbackInterfaceOnPush = UniffiVTableCallbackInterfaceOnPush(
+ nonisolated(unsafe) static var vtable: UniffiVTableCallbackInterfaceOnPush = UniffiVTableCallbackInterfaceOnPush(
onPush: { (
uniffiHandle: UInt64,
undoOrRedo: RustBuffer,
@@ -8906,7 +9094,7 @@ private func uniffiCallbackInitOnPush() {
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeOnPush: FfiConverter {
- fileprivate static var handleMap = UniffiHandleMap()
+ nonisolated(unsafe) fileprivate static var handleMap = UniffiHandleMap()
typealias FfiType = UnsafeMutableRawPointer
typealias SwiftType = OnPush
@@ -9032,7 +9220,7 @@ fileprivate struct UniffiCallbackInterfacePreCommitCallback {
// Create the VTable using a series of closures.
// Swift automatically converts these into C callback functions.
- static var vtable: UniffiVTableCallbackInterfacePreCommitCallback = UniffiVTableCallbackInterfacePreCommitCallback(
+ nonisolated(unsafe) static var vtable: UniffiVTableCallbackInterfacePreCommitCallback = UniffiVTableCallbackInterfacePreCommitCallback(
onPreCommit: { (
uniffiHandle: UInt64,
payload: RustBuffer,
@@ -9074,7 +9262,7 @@ private func uniffiCallbackInitPreCommitCallback() {
@_documentation(visibility: private)
#endif
public struct FfiConverterTypePreCommitCallback: FfiConverter {
- fileprivate static var handleMap = UniffiHandleMap()
+ nonisolated(unsafe) fileprivate static var handleMap = UniffiHandleMap()
typealias FfiType = UnsafeMutableRawPointer
typealias SwiftType = PreCommitCallback
@@ -9345,7 +9533,7 @@ fileprivate struct UniffiCallbackInterfaceSubscriber {
// Create the VTable using a series of closures.
// Swift automatically converts these into C callback functions.
- static var vtable: UniffiVTableCallbackInterfaceSubscriber = UniffiVTableCallbackInterfaceSubscriber(
+ nonisolated(unsafe) static var vtable: UniffiVTableCallbackInterfaceSubscriber = UniffiVTableCallbackInterfaceSubscriber(
onDiff: { (
uniffiHandle: UInt64,
diff: RustBuffer,
@@ -9387,7 +9575,7 @@ private func uniffiCallbackInitSubscriber() {
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeSubscriber: FfiConverter {
- fileprivate static var handleMap = UniffiHandleMap()
+ nonisolated(unsafe) fileprivate static var handleMap = UniffiHandleMap()
typealias FfiType = UnsafeMutableRawPointer
typealias SwiftType = Subscriber
@@ -10083,7 +10271,7 @@ fileprivate struct UniffiCallbackInterfaceUnsubscriber {
// Create the VTable using a series of closures.
// Swift automatically converts these into C callback functions.
- static var vtable: UniffiVTableCallbackInterfaceUnsubscriber = UniffiVTableCallbackInterfaceUnsubscriber(
+ nonisolated(unsafe) static var vtable: UniffiVTableCallbackInterfaceUnsubscriber = UniffiVTableCallbackInterfaceUnsubscriber(
onUnsubscribe: { (
uniffiHandle: UInt64,
uniffiOutReturn: UnsafeMutableRawPointer,
@@ -10123,7 +10311,7 @@ private func uniffiCallbackInitUnsubscriber() {
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeUnsubscriber: FfiConverter {
- fileprivate static var handleMap = UniffiHandleMap()
+ nonisolated(unsafe) fileprivate static var handleMap = UniffiHandleMap()
typealias FfiType = UnsafeMutableRawPointer
typealias SwiftType = Unsubscriber
@@ -16558,7 +16746,7 @@ private enum InitializationResult {
}
// Use a global variable to perform the versioning checks. Swift ensures that
// the code inside is only computed once.
-private var initializationResult: InitializationResult = {
+nonisolated(unsafe) 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
@@ -16674,9 +16862,6 @@ private var initializationResult: InitializationResult = {
if (uniffi_loro_ffi_checksum_method_firstcommitfrompeercallback_on_first_commit_from_peer() != 54327) {
return InitializationResult.apiChecksumMismatch
}
- if (uniffi_loro_ffi_checksum_method_fractionalindex_to_string() != 5688) {
- return InitializationResult.apiChecksumMismatch
- }
if (uniffi_loro_ffi_checksum_method_frontiers_encode() != 14564) {
return InitializationResult.apiChecksumMismatch
}
@@ -16689,6 +16874,9 @@ private var initializationResult: InitializationResult = {
if (uniffi_loro_ffi_checksum_method_frontiers_to_vec() != 15210) {
return InitializationResult.apiChecksumMismatch
}
+ if (uniffi_loro_ffi_checksum_method_jsonpathsubscriber_on_jsonpath_changed() != 36440) {
+ return InitializationResult.apiChecksumMismatch
+ }
if (uniffi_loro_ffi_checksum_method_localephemerallistener_on_ephemeral_update() != 58755) {
return InitializationResult.apiChecksumMismatch
}
@@ -16956,6 +17144,9 @@ private var initializationResult: InitializationResult = {
if (uniffi_loro_ffi_checksum_method_lorodoc_subscribe_first_commit_from_peer() != 65444) {
return InitializationResult.apiChecksumMismatch
}
+ if (uniffi_loro_ffi_checksum_method_lorodoc_subscribe_jsonpath() != 58559) {
+ return InitializationResult.apiChecksumMismatch
+ }
if (uniffi_loro_ffi_checksum_method_lorodoc_subscribe_local_update() != 46483) {
return InitializationResult.apiChecksumMismatch
}
@@ -17334,9 +17525,6 @@ private var initializationResult: InitializationResult = {
if (uniffi_loro_ffi_checksum_method_lorotext_to_delta() != 49666) {
return InitializationResult.apiChecksumMismatch
}
- if (uniffi_loro_ffi_checksum_method_lorotext_to_string() != 28280) {
- return InitializationResult.apiChecksumMismatch
- }
if (uniffi_loro_ffi_checksum_method_lorotext_unmark() != 47537) {
return InitializationResult.apiChecksumMismatch
}
@@ -17717,6 +17905,7 @@ private var initializationResult: InitializationResult = {
uniffiCallbackInitContainerIdLike()
uniffiCallbackInitEphemeralSubscriber()
uniffiCallbackInitFirstCommitFromPeerCallback()
+ uniffiCallbackInitJsonPathSubscriber()
uniffiCallbackInitLocalEphemeralListener()
uniffiCallbackInitLocalUpdateCallback()
uniffiCallbackInitLoroValueLike()
diff --git a/Sources/Loro/Value.swift b/Sources/Loro/Value.swift
index 6737485..5bf2465 100644
--- a/Sources/Loro/Value.swift
+++ b/Sources/Loro/Value.swift
@@ -4,7 +4,14 @@
//
// Created by Leon Zhao on 2024/8/6.
//
+
+#if !hasFeature(Embedded)
+#if canImport(FoundationEssentials)
+import FoundationEssentials
+#elseif canImport(Foundation)
import Foundation
+#endif
+#endif
extension LoroValue: LoroValueLike {
public func asLoroValue() -> LoroValue {
diff --git a/Tests/LoroTests/LoroTests.swift b/Tests/LoroTests/LoroTests.swift
index 648acd0..5afd662 100644
--- a/Tests/LoroTests/LoroTests.swift
+++ b/Tests/LoroTests/LoroTests.swift
@@ -112,28 +112,11 @@ final class LoroTests: XCTestCase {
XCTAssertEqual(s, "bcdef")
}
- func testTextUtf16AndPosConversion(){
+ func testTextUnicode(){
let doc = LoroDoc()
let text = doc.getText(id: "text")
try! text.insert(pos: 0, s: "A😀C")
-
- let utf16Pos = text.convertPos(index: 1, from: .unicode, to: .utf16)
- XCTAssertEqual(utf16Pos, 2)
-
- try! text.insertUtf16(pos: utf16Pos!, s: "B")
- XCTAssertEqual(text.toString(), "A😀BC")
-
- let slice = try! text.sliceUtf16(startIndex: 1, endIndex: 3)
- XCTAssertEqual(slice, "😀B")
-
- let delta = try! text.sliceDelta(startIndex: 1, endIndex: 3, posType: .unicode)
- XCTAssertEqual(delta.count, 2)
- if case let .insert(insert: first, attributes: attrs) = delta[0] {
- XCTAssertEqual(first, "😀")
- XCTAssertEqual(attrs?["bold"] == nil, true)
- } else {
- XCTFail("expected insert")
- }
+ XCTAssertEqual(text.toString(), "A😀C")
}
func testOrigin(){
@@ -164,4 +147,1010 @@ final class LoroTests: XCTestCase {
print("ERROR: \(error)")
}
}
+
+ func testTree() {
+ let doc = LoroDoc()
+ let tree = doc.getTree(id: "tree")
+
+ // Test creating root nodes
+ let root1 = try! tree.create(parent: .root)
+ let root2 = try! tree.create(parent: .root)
+
+ // Verify roots
+ let roots = tree.roots()
+ XCTAssertEqual(roots.count, 2)
+ XCTAssertTrue(roots.contains(root1))
+ XCTAssertTrue(roots.contains(root2))
+
+ // Test creating child nodes
+ let child1 = try! tree.create(parent: .node(id: root1))
+ let child2 = try! tree.create(parent: .node(id: root1))
+
+ // Verify children
+ let children = tree.children(parent: .node(id: root1))!
+ XCTAssertEqual(children.count, 2)
+ XCTAssertTrue(children.contains(child1))
+ XCTAssertTrue(children.contains(child2))
+
+ // Test node metadata
+ let meta = try! tree.getMeta(target: root1)
+ try! meta.insert(key: "name", v: "Root Node")
+ try! meta.insert(key: "count", v: 42)
+ XCTAssertEqual(meta.get(key: "name")!.asValue()!, LoroValue.string(value: "Root Node"))
+ XCTAssertEqual(meta.get(key: "count")!.asValue()!, LoroValue.i64(value: 42))
+
+ // Test moving nodes
+ try! tree.mov(target: child1, parent: .node(id: root2))
+ let root1Children = tree.children(parent: .node(id: root1))!
+ let root2Children = tree.children(parent: .node(id: root2))!
+ XCTAssertEqual(root1Children.count, 1)
+ XCTAssertEqual(root2Children.count, 1)
+ XCTAssertTrue(root2Children.contains(child1))
+
+ // Test parent query
+ let parent = try! tree.parent(target: child1)
+ if case .node(let parentId) = parent {
+ XCTAssertEqual(parentId, root2)
+ } else {
+ XCTFail("Expected child1 to have root2 as parent")
+ }
+
+ // Test contains
+ XCTAssertTrue(tree.contains(target: root1))
+ XCTAssertTrue(tree.contains(target: child1))
+
+ // Test delete
+ try! tree.delete(target: child2)
+ XCTAssertTrue(try! tree.isNodeDeleted(target: child2))
+ let updatedRoot1Children = tree.children(parent: .node(id: root1))!
+ XCTAssertEqual(updatedRoot1Children.count, 0)
+ }
+
+ func testTreeSync() {
+ let doc1 = LoroDoc()
+ try! doc1.setPeerId(peer: 1)
+ let tree1 = doc1.getTree(id: "tree")
+
+ // Create structure in doc1
+ let root = try! tree1.create(parent: .root)
+ let child = try! tree1.create(parent: .node(id: root))
+ let meta = try! tree1.getMeta(target: root)
+ try! meta.insert(key: "name", v: "Synced Root")
+
+ // Sync to doc2
+ let doc2 = LoroDoc()
+ try! doc2.setPeerId(peer: 2)
+ let _ = try! doc2.import(bytes: doc1.export(mode: .snapshot))
+
+ // Verify structure in doc2
+ let tree2 = doc2.getTree(id: "tree")
+ let roots2 = tree2.roots()
+ XCTAssertEqual(roots2.count, 1)
+ XCTAssertEqual(roots2[0], root)
+
+ let children2 = tree2.children(parent: .node(id: root))!
+ XCTAssertEqual(children2.count, 1)
+ XCTAssertEqual(children2[0], child)
+
+ // Verify metadata synced
+ let meta2 = try! tree2.getMeta(target: root)
+ XCTAssertEqual(meta2.get(key: "name")!.asValue()!, LoroValue.string(value: "Synced Root"))
+ }
+
+ func testTreeCreateAt() {
+ let doc = LoroDoc()
+ let tree = doc.getTree(id: "tree")
+
+ // Create nodes at specific positions
+ let first = try! tree.createAt(parent: .root, index: 0)
+ let third = try! tree.createAt(parent: .root, index: 1)
+ let second = try! tree.createAt(parent: .root, index: 1) // Insert between first and third
+
+ let roots = tree.roots()
+ XCTAssertEqual(roots.count, 3)
+ XCTAssertEqual(roots[0], first)
+ XCTAssertEqual(roots[1], second)
+ XCTAssertEqual(roots[2], third)
+ }
+
+ func testTreeFractionalIndex() {
+ let doc = LoroDoc()
+ let tree = doc.getTree(id: "tree")
+
+ // Enable fractional indexing
+ tree.enableFractionalIndex(jitter: 8)
+ XCTAssertTrue(tree.isFractionalIndexEnabled())
+
+ let node = try! tree.create(parent: .root)
+ let index = tree.fractionalIndex(target: node)
+ XCTAssertNotNil(index)
+
+ // Disable fractional indexing
+ tree.disableFractionalIndex()
+ XCTAssertFalse(tree.isFractionalIndexEnabled())
+ }
+
+ // MARK: - List Tests
+
+ func testListBasicOperations() {
+ let doc = LoroDoc()
+ let list = doc.getList(id: "list")
+
+ // Test insert
+ try! list.insert(pos: 0, v: "first")
+ try! list.insert(pos: 1, v: "second")
+ try! list.insert(pos: 2, v: "third")
+
+ XCTAssertEqual(list.len(), 3)
+ XCTAssertEqual(list.get(index: 0)!.asValue()!, LoroValue.string(value: "first"))
+ XCTAssertEqual(list.get(index: 1)!.asValue()!, LoroValue.string(value: "second"))
+ XCTAssertEqual(list.get(index: 2)!.asValue()!, LoroValue.string(value: "third"))
+
+ // Test delete
+ try! list.delete(pos: 1, len: 1)
+ XCTAssertEqual(list.len(), 2)
+ XCTAssertEqual(list.get(index: 1)!.asValue()!, LoroValue.string(value: "third"))
+ }
+
+ func testListPushAndPop() {
+ let doc = LoroDoc()
+ let list = doc.getList(id: "list")
+
+ // Test push
+ try! list.push(v: "a")
+ try! list.push(v: "b")
+ try! list.push(v: "c")
+
+ XCTAssertEqual(list.len(), 3)
+
+ // Test pop
+ let popped = try! list.pop()
+ XCTAssertEqual(popped!, LoroValue.string(value: "c"))
+ XCTAssertEqual(list.len(), 2)
+ }
+
+ func testListDifferentValueTypes() {
+ let doc = LoroDoc()
+ let list = doc.getList(id: "mixedList")
+
+ // Test different value types
+ try! list.push(v: "string")
+ try! list.push(v: 42)
+ try! list.push(v: 3.14)
+ try! list.push(v: true)
+ try! list.push(v: nil)
+
+ XCTAssertEqual(list.len(), 5)
+ XCTAssertEqual(list.get(index: 0)!.asValue()!, LoroValue.string(value: "string"))
+ XCTAssertEqual(list.get(index: 1)!.asValue()!, LoroValue.i64(value: 42))
+ XCTAssertEqual(list.get(index: 3)!.asValue()!, LoroValue.bool(value: true))
+ XCTAssertEqual(list.get(index: 4)!.asValue()!, LoroValue.null)
+ }
+
+ func testListClear() {
+ let doc = LoroDoc()
+ let list = doc.getList(id: "list")
+
+ try! list.push(v: 1)
+ try! list.push(v: 2)
+ try! list.push(v: 3)
+ XCTAssertEqual(list.len(), 3)
+
+ try! list.clear()
+ XCTAssertEqual(list.len(), 0)
+ XCTAssertTrue(list.isEmpty())
+ }
+
+ func testListNestedContainers() {
+ let doc = LoroDoc()
+ let list = doc.getList(id: "nested")
+
+ // Insert a nested map
+ let nestedMap = try! list.insertContainer(pos: 0, child: LoroMap())
+ try! nestedMap.insert(key: "name", v: "Alice")
+
+ // Insert a nested list
+ let nestedList = try! list.insertContainer(pos: 1, child: LoroList())
+ try! nestedList.push(v: 1)
+ try! nestedList.push(v: 2)
+
+ XCTAssertEqual(list.len(), 2)
+
+ // Verify deep value
+ let deepValue = list.getDeepValue()
+ if case .list(let items) = deepValue {
+ XCTAssertEqual(items.count, 2)
+ } else {
+ XCTFail("Expected list value")
+ }
+ }
+
+ func testListSync() {
+ let doc1 = LoroDoc()
+ try! doc1.setPeerId(peer: 1)
+ let list1 = doc1.getList(id: "shared")
+ try! list1.push(v: "from doc1")
+
+ let doc2 = LoroDoc()
+ try! doc2.setPeerId(peer: 2)
+ let _ = try! doc2.import(bytes: doc1.export(mode: .snapshot))
+
+ let list2 = doc2.getList(id: "shared")
+ XCTAssertEqual(list2.len(), 1)
+ XCTAssertEqual(list2.get(index: 0)!.asValue()!, LoroValue.string(value: "from doc1"))
+ }
+
+ // MARK: - MovableList Tests
+
+ func testMovableListBasicOperations() {
+ let doc = LoroDoc()
+ let list = doc.getMovableList(id: "movableList")
+
+ // Test insert and push
+ try! list.push(v: "a")
+ try! list.push(v: "b")
+ try! list.push(v: "c")
+
+ XCTAssertEqual(list.len(), 3)
+ XCTAssertTrue(list.isAttached())
+ XCTAssertFalse(list.isEmpty())
+ }
+
+ func testMovableListSet() {
+ let doc = LoroDoc()
+ let list = doc.getMovableList(id: "movableList")
+
+ try! list.push(v: "original")
+ try! list.push(v: "second")
+
+ // Test set (replace)
+ try! list.set(pos: 0, v: "replaced")
+
+ XCTAssertEqual(list.get(index: 0)!.asValue()!, LoroValue.string(value: "replaced"))
+ XCTAssertEqual(list.get(index: 1)!.asValue()!, LoroValue.string(value: "second"))
+ XCTAssertEqual(list.len(), 2)
+ }
+
+ func testMovableListMove() {
+ let doc = LoroDoc()
+ let list = doc.getMovableList(id: "movableList")
+
+ try! list.push(v: "first")
+ try! list.push(v: "second")
+ try! list.push(v: "third")
+
+ // Move "third" (index 2) to beginning (index 0)
+ try! list.mov(from: 2, to: 0)
+
+ XCTAssertEqual(list.get(index: 0)!.asValue()!, LoroValue.string(value: "third"))
+ XCTAssertEqual(list.get(index: 1)!.asValue()!, LoroValue.string(value: "first"))
+ XCTAssertEqual(list.get(index: 2)!.asValue()!, LoroValue.string(value: "second"))
+ }
+
+ func testMovableListToVec() {
+ let doc = LoroDoc()
+ let list = doc.getMovableList(id: "movableList")
+
+ try! list.push(v: "a")
+ try! list.push(v: "b")
+ try! list.push(v: "c")
+
+ let vec = list.toVec()
+ XCTAssertEqual(vec.count, 3)
+ XCTAssertEqual(vec[0], LoroValue.string(value: "a"))
+ XCTAssertEqual(vec[1], LoroValue.string(value: "b"))
+ XCTAssertEqual(vec[2], LoroValue.string(value: "c"))
+ }
+
+ func testMovableListDelete() {
+ let doc = LoroDoc()
+ let list = doc.getMovableList(id: "movableList")
+
+ try! list.push(v: 1)
+ try! list.push(v: 2)
+ try! list.push(v: 3)
+ try! list.push(v: 4)
+
+ // Delete 2 elements starting at index 1
+ try! list.delete(pos: 1, len: 2)
+
+ XCTAssertEqual(list.len(), 2)
+ XCTAssertEqual(list.get(index: 0)!.asValue()!, LoroValue.i64(value: 1))
+ XCTAssertEqual(list.get(index: 1)!.asValue()!, LoroValue.i64(value: 4))
+ }
+
+ func testMovableListNestedContainers() {
+ let doc = LoroDoc()
+ let list = doc.getMovableList(id: "nested")
+
+ // Insert a map
+ let map = try! list.insertContainer(pos: 0, child: LoroMap())
+ try! map.insert(key: "id", v: 1)
+
+ // Set a container at position
+ let newMap = try! list.setContainer(pos: 0, child: LoroMap())
+ try! newMap.insert(key: "id", v: 2)
+
+ XCTAssertEqual(list.len(), 1)
+ }
+
+ func testMovableListSync() {
+ let doc1 = LoroDoc()
+ try! doc1.setPeerId(peer: 1)
+ let list1 = doc1.getMovableList(id: "shared")
+ try! list1.push(v: "a")
+ try! list1.push(v: "b")
+ try! list1.push(v: "c")
+ try! list1.mov(from: 2, to: 0)
+
+ let doc2 = LoroDoc()
+ try! doc2.setPeerId(peer: 2)
+ let _ = try! doc2.import(bytes: doc1.export(mode: .snapshot))
+
+ let list2 = doc2.getMovableList(id: "shared")
+ XCTAssertEqual(list2.len(), 3)
+ XCTAssertEqual(list2.get(index: 0)!.asValue()!, LoroValue.string(value: "c"))
+ }
+
+ // MARK: - Map Tests
+
+ func testMapBasicOperations() {
+ let doc = LoroDoc()
+ let map = doc.getMap(id: "map")
+
+ // Test insert
+ try! map.insert(key: "name", v: "Alice")
+ try! map.insert(key: "age", v: 30)
+ try! map.insert(key: "active", v: true)
+
+ // Test get
+ XCTAssertEqual(map.get(key: "name")!.asValue()!, LoroValue.string(value: "Alice"))
+ XCTAssertEqual(map.get(key: "age")!.asValue()!, LoroValue.i64(value: 30))
+ XCTAssertEqual(map.get(key: "active")!.asValue()!, LoroValue.bool(value: true))
+
+ // Test non-existent key
+ XCTAssertNil(map.get(key: "nonexistent"))
+ }
+
+ func testMapDelete() {
+ let doc = LoroDoc()
+ let map = doc.getMap(id: "map")
+
+ try! map.insert(key: "a", v: 1)
+ try! map.insert(key: "b", v: 2)
+
+ try! map.delete(key: "a")
+
+ XCTAssertNil(map.get(key: "a"))
+ XCTAssertNotNil(map.get(key: "b"))
+ }
+
+ func testMapClear() {
+ let doc = LoroDoc()
+ let map = doc.getMap(id: "map")
+
+ try! map.insert(key: "a", v: 1)
+ try! map.insert(key: "b", v: 2)
+ try! map.insert(key: "c", v: 3)
+
+ try! map.clear()
+
+ XCTAssertNil(map.get(key: "a"))
+ XCTAssertNil(map.get(key: "b"))
+ XCTAssertNil(map.get(key: "c"))
+ }
+
+ func testMapNestedContainers() {
+ let doc = LoroDoc()
+ let root = doc.getMap(id: "root")
+
+ // Insert nested map
+ let profile = try! root.insertContainer(key: "profile", child: LoroMap())
+ try! profile.insert(key: "name", v: "Alice")
+
+ // Insert nested list
+ let tags = try! root.insertContainer(key: "tags", child: LoroList())
+ try! tags.push(v: "swift")
+ try! tags.push(v: "loro")
+
+ // Get or create container
+ let settings = try! root.getOrCreateContainer(key: "settings", child: LoroMap())
+ try! settings.insert(key: "theme", v: "dark")
+
+ // Verify deep value contains nested structures
+ let deepValue = root.getDeepValue()
+ if case .map(let entries) = deepValue {
+ XCTAssertEqual(entries.count, 3)
+ } else {
+ XCTFail("Expected map value")
+ }
+ }
+
+ func testMapDifferentValueTypes() {
+ let doc = LoroDoc()
+ let map = doc.getMap(id: "types")
+
+ try! map.insert(key: "string", v: "hello")
+ try! map.insert(key: "int", v: 42)
+ try! map.insert(key: "double", v: 3.14)
+ try! map.insert(key: "bool", v: true)
+ try! map.insert(key: "null", v: nil)
+ try! map.insert(key: "array", v: [1, 2, 3])
+
+ XCTAssertEqual(map.get(key: "string")!.asValue()!, LoroValue.string(value: "hello"))
+ XCTAssertEqual(map.get(key: "int")!.asValue()!, LoroValue.i64(value: 42))
+ XCTAssertEqual(map.get(key: "bool")!.asValue()!, LoroValue.bool(value: true))
+ XCTAssertEqual(map.get(key: "null")!.asValue()!, LoroValue.null)
+ }
+
+ func testMapSync() {
+ let doc1 = LoroDoc()
+ try! doc1.setPeerId(peer: 1)
+ let map1 = doc1.getMap(id: "shared")
+ try! map1.insert(key: "from", v: "doc1")
+ try! map1.insert(key: "value", v: 100)
+
+ let doc2 = LoroDoc()
+ try! doc2.setPeerId(peer: 2)
+ let _ = try! doc2.import(bytes: doc1.export(mode: .snapshot))
+
+ let map2 = doc2.getMap(id: "shared")
+ XCTAssertEqual(map2.get(key: "from")!.asValue()!, LoroValue.string(value: "doc1"))
+ XCTAssertEqual(map2.get(key: "value")!.asValue()!, LoroValue.i64(value: 100))
+ }
+
+ func testMapConflictResolution() {
+ // Test LWW (Last-Write-Wins) conflict resolution
+ let doc1 = LoroDoc()
+ try! doc1.setPeerId(peer: 1)
+ let map1 = doc1.getMap(id: "shared")
+
+ let doc2 = LoroDoc()
+ try! doc2.setPeerId(peer: 2)
+ let map2 = doc2.getMap(id: "shared")
+
+ // Both peers set the same key concurrently
+ try! map1.insert(key: "color", v: "red")
+ try! map2.insert(key: "color", v: "blue")
+
+ // Sync both ways
+ let _ = try! doc2.import(bytes: doc1.export(mode: .snapshot))
+ let _ = try! doc1.import(bytes: doc2.export(mode: .snapshot))
+
+ // Both should converge to the same value
+ // Peer 2 has larger peerId, so "blue" should win
+ let value1 = map1.get(key: "color")!.asValue()!
+ let value2 = map2.get(key: "color")!.asValue()!
+ XCTAssertEqual(value1, value2)
+ }
+
+ // MARK: - Text Tests
+
+ func testTextBasicOperations() {
+ let doc = LoroDoc()
+ let text = doc.getText(id: "text")
+
+ // Test insert
+ try! text.insert(pos: 0, s: "Hello")
+ XCTAssertEqual(text.toString(), "Hello")
+
+ // Test insert at position
+ try! text.insert(pos: 5, s: " World")
+ XCTAssertEqual(text.toString(), "Hello World")
+
+ // Test delete
+ try! text.delete(pos: 5, len: 6)
+ XCTAssertEqual(text.toString(), "Hello")
+ }
+
+ func testTextPushStr() {
+ let doc = LoroDoc()
+ let text = doc.getText(id: "text")
+
+ try! text.insert(pos: 0, s: "Hello")
+ try! text.pushStr(s: " World")
+ try! text.pushStr(s: "!")
+
+ XCTAssertEqual(text.toString(), "Hello World!")
+ }
+
+ func testTextSlice() {
+ let doc = LoroDoc()
+ let text = doc.getText(id: "text")
+
+ try! text.insert(pos: 0, s: "Hello World")
+
+ let slice = try! text.slice(startIndex: 0, endIndex: 5)
+ XCTAssertEqual(slice, "Hello")
+
+ let slice2 = try! text.slice(startIndex: 6, endIndex: 11)
+ XCTAssertEqual(slice2, "World")
+ }
+
+ func testTextSplice() {
+ let doc = LoroDoc()
+ let text = doc.getText(id: "text")
+
+ try! text.insert(pos: 0, s: "Hello World")
+
+ // Splice: delete "World" and insert "Swift"
+ let deleted = try! text.splice(pos: 6, len: 5, s: "Swift")
+ XCTAssertEqual(deleted, "World")
+ XCTAssertEqual(text.toString(), "Hello Swift")
+ }
+
+ func testTextCharAt() {
+ let doc = LoroDoc()
+ let text = doc.getText(id: "text")
+
+ try! text.insert(pos: 0, s: "Hello")
+
+ XCTAssertEqual(try! text.charAt(pos: 0), "H")
+ XCTAssertEqual(try! text.charAt(pos: 4), "o")
+ }
+
+ func testTextLength() {
+ let doc = LoroDoc()
+ let text = doc.getText(id: "text")
+
+ try! text.insert(pos: 0, s: "Hello 😀")
+
+ // Different length measurements
+ let unicodeLen = text.lenUnicode()
+ let utf8Len = text.lenUtf8()
+
+ XCTAssertTrue(unicodeLen > 0)
+ XCTAssertTrue(utf8Len >= unicodeLen) // UTF-8 is at least as long
+ }
+
+ func testTextRichTextMark() {
+ let doc = LoroDoc()
+ let text = doc.getText(id: "richText")
+
+ try! text.insert(pos: 0, s: "Hello World")
+
+ // Mark "Hello" as bold
+ try! text.mark(from: 0, to: 5, key: "bold", value: true)
+
+ // Get delta to verify marks
+ let delta = text.toDelta()
+ XCTAssertFalse(delta.isEmpty)
+ }
+
+ func testTextUnmark() {
+ let doc = LoroDoc()
+ let text = doc.getText(id: "richText")
+
+ try! text.insert(pos: 0, s: "Hello World")
+ try! text.mark(from: 0, to: 5, key: "bold", value: true)
+ try! text.unmark(from: 0, to: 5, key: "bold")
+
+ // After unmark, should have no bold marks
+ let delta = text.toDelta()
+ XCTAssertFalse(delta.isEmpty)
+ }
+
+ func testTextApplyDelta() {
+ let doc = LoroDoc()
+ let text = doc.getText(id: "text")
+
+ try! text.insert(pos: 0, s: "Hello World")
+
+ // Apply delta: delete first char, retain rest, insert at end
+ try! text.applyDelta(delta: [
+ .delete(delete: 1),
+ .retain(retain: 10, attributes: nil),
+ .insert(insert: "!", attributes: nil)
+ ])
+
+ XCTAssertEqual(text.toString(), "ello World!")
+ }
+
+ func testTextSync() {
+ let doc1 = LoroDoc()
+ try! doc1.setPeerId(peer: 1)
+ let text1 = doc1.getText(id: "shared")
+ try! text1.insert(pos: 0, s: "Hello from peer 1")
+
+ let doc2 = LoroDoc()
+ try! doc2.setPeerId(peer: 2)
+ let _ = try! doc2.import(bytes: doc1.export(mode: .snapshot))
+
+ let text2 = doc2.getText(id: "shared")
+ XCTAssertEqual(text2.toString(), "Hello from peer 1")
+ }
+
+ func testTextIsEmpty() {
+ let doc = LoroDoc()
+ let text = doc.getText(id: "text")
+
+ XCTAssertTrue(text.isEmpty())
+
+ try! text.insert(pos: 0, s: "Hello")
+ XCTAssertFalse(text.isEmpty())
+ }
+
+ // MARK: - Counter Tests
+
+ func testCounterBasicOperations() {
+ let doc = LoroDoc()
+ let counter = doc.getCounter(id: "counter")
+
+ // Initial value
+ XCTAssertEqual(counter.getValue(), 0)
+
+ // Increment
+ try! counter.increment(value: 1)
+ XCTAssertEqual(counter.getValue(), 1)
+
+ try! counter.increment(value: 5)
+ XCTAssertEqual(counter.getValue(), 6)
+ }
+
+ func testCounterDecrement() {
+ let doc = LoroDoc()
+ let counter = doc.getCounter(id: "counter")
+
+ try! counter.increment(value: 10)
+ try! counter.decrement(value: 3)
+
+ XCTAssertEqual(counter.getValue(), 7)
+ }
+
+ func testCounterFloatingPoint() {
+ let doc = LoroDoc()
+ let counter = doc.getCounter(id: "counter")
+
+ try! counter.increment(value: 1.5)
+ try! counter.increment(value: 2.5)
+
+ XCTAssertEqual(counter.getValue(), 4.0)
+ }
+
+ func testCounterSync() {
+ let doc1 = LoroDoc()
+ try! doc1.setPeerId(peer: 1)
+ let counter1 = doc1.getCounter(id: "shared")
+
+ let doc2 = LoroDoc()
+ try! doc2.setPeerId(peer: 2)
+ let counter2 = doc2.getCounter(id: "shared")
+
+ // Both peers increment concurrently
+ try! counter1.increment(value: 5)
+ try! counter2.increment(value: 3)
+
+ // Sync both ways
+ let _ = try! doc2.import(bytes: doc1.export(mode: .snapshot))
+ let _ = try! doc1.import(bytes: doc2.export(mode: .snapshot))
+
+ // Both counters should have the sum (5 + 3 = 8)
+ XCTAssertEqual(counter1.getValue(), 8)
+ XCTAssertEqual(counter2.getValue(), 8)
+ }
+
+ func testCounterNested() {
+ let doc = LoroDoc()
+ let map = doc.getMap(id: "stats")
+
+ // Create counter inside map
+ let viewCounter = try! map.insertContainer(key: "views", child: LoroCounter())
+ try! viewCounter.increment(value: 100)
+
+ XCTAssertEqual(viewCounter.getValue(), 100)
+ }
+
+ func testCounterIsAttached() {
+ let doc = LoroDoc()
+ let counter = doc.getCounter(id: "counter")
+
+ XCTAssertTrue(counter.isAttached())
+ XCTAssertFalse(LoroCounter().isAttached())
+ }
+
+ // MARK: - Version Tests
+
+ func testVersionVectorBasic() {
+ let doc = LoroDoc()
+ try! doc.setPeerId(peer: 1)
+ let text = doc.getText(id: "text")
+
+ let v1 = doc.oplogVv()
+
+ try! text.insert(pos: 0, s: "Hello")
+ let v2 = doc.oplogVv()
+
+ // Versions should be different after changes
+ XCTAssertNotEqual(v1, v2)
+ }
+
+ func testFrontiersBasic() {
+ let doc = LoroDoc()
+ try! doc.setPeerId(peer: 1)
+ let text = doc.getText(id: "text")
+
+ let f1 = doc.oplogFrontiers()
+
+ try! text.insert(pos: 0, s: "Hello")
+ let f2 = doc.oplogFrontiers()
+
+ // Frontiers should be different after changes
+ XCTAssertNotEqual(f1, f2)
+ }
+
+ func testCheckoutTimeTravel() {
+ let doc = LoroDoc()
+ try! doc.setPeerId(peer: 1)
+ let text = doc.getText(id: "text")
+
+ try! text.insert(pos: 0, s: "Version 1")
+ let v1 = doc.oplogFrontiers()
+
+ try! text.delete(pos: 0, len: 9)
+ try! text.insert(pos: 0, s: "Version 2")
+ let v2 = doc.oplogFrontiers()
+
+ try! text.delete(pos: 0, len: 9)
+ try! text.insert(pos: 0, s: "Version 3")
+
+ // Current state
+ XCTAssertEqual(text.toString(), "Version 3")
+
+ // Checkout to v1
+ try! doc.checkout(frontiers: v1)
+ XCTAssertEqual(text.toString(), "Version 1")
+
+ // Checkout to v2
+ try! doc.checkout(frontiers: v2)
+ XCTAssertEqual(text.toString(), "Version 2")
+
+ // Return to latest
+ doc.checkoutToLatest()
+ XCTAssertEqual(text.toString(), "Version 3")
+ }
+
+ func testExportModes() {
+ let doc = LoroDoc()
+ try! doc.setPeerId(peer: 1)
+ let text = doc.getText(id: "text")
+ try! text.insert(pos: 0, s: "Hello")
+
+ // Test different export modes
+ let snapshot = try! doc.export(mode: .snapshot)
+ XCTAssertFalse(snapshot.isEmpty)
+
+ let vv = doc.oplogVv()
+ let updates = try! doc.export(mode: .updates(from: vv))
+ // Updates from current version should be empty or minimal
+ XCTAssertNotNil(updates)
+ }
+
+ func testIncrementalSync() {
+ let doc1 = LoroDoc()
+ try! doc1.setPeerId(peer: 1)
+ let text1 = doc1.getText(id: "text")
+
+ try! text1.insert(pos: 0, s: "Initial")
+ let initialVersion = doc1.oplogVv()
+
+ // Import initial state to doc2
+ let doc2 = LoroDoc()
+ try! doc2.setPeerId(peer: 2)
+ let _ = try! doc2.import(bytes: doc1.export(mode: .snapshot))
+
+ // Make more changes in doc1
+ try! text1.insert(pos: 7, s: " content")
+
+ // Export only new changes
+ let updates = try! doc1.export(mode: .updates(from: initialVersion))
+
+ // Apply incremental updates to doc2
+ let _ = try! doc2.import(bytes: updates)
+
+ let text2 = doc2.getText(id: "text")
+ XCTAssertEqual(text2.toString(), "Initial content")
+ }
+
+ func testImportBatch() {
+ let doc1 = LoroDoc()
+ try! doc1.setPeerId(peer: 1)
+ let text1 = doc1.getText(id: "text")
+ try! text1.insert(pos: 0, s: "Hello")
+
+ let doc2 = LoroDoc()
+ try! doc2.setPeerId(peer: 2)
+ let text2 = doc2.getText(id: "text")
+ try! text2.insert(pos: 0, s: " World")
+
+ // Batch import
+ let doc3 = LoroDoc()
+ let _ = try! doc3.importBatch(bytes: [
+ doc1.export(mode: .snapshot),
+ doc2.export(mode: .snapshot)
+ ])
+
+ let text3 = doc3.getText(id: "text")
+ // Both changes should be present
+ XCTAssertTrue(text3.toString().contains("Hello") || text3.toString().contains("World"))
+ }
+
+ func testVersionVectorToFrontiers() {
+ let doc = LoroDoc()
+ try! doc.setPeerId(peer: 1)
+ let text = doc.getText(id: "text")
+ try! text.insert(pos: 0, s: "Hello")
+
+ let vv = doc.oplogVv()
+ let frontiers = doc.vvToFrontiers(vv: vv)
+
+ // Convert back
+ if let convertedVv = doc.frontiersToVv(frontiers: frontiers) {
+ XCTAssertEqual(vv, convertedVv)
+ }
+ }
+
+ func testImportWithOrigin() {
+ let doc1 = LoroDoc()
+ try! doc1.setPeerId(peer: 1)
+ let text1 = doc1.getText(id: "text")
+ try! text1.insert(pos: 0, s: "Hello")
+
+ let doc2 = LoroDoc()
+ var receivedOrigin = ""
+ let sub = doc2.subscribeRoot { event in
+ receivedOrigin = event.origin
+ }
+
+ let _ = try! doc2.importWith(bytes: doc1.export(mode: .snapshot), origin: "test-origin")
+
+ sub.detach()
+ XCTAssertEqual(receivedOrigin, "test-origin")
+ }
+
+ // MARK: - Time Travel Tests
+
+ func testDetachedState() {
+ let doc = LoroDoc()
+ try! doc.setPeerId(peer: 1)
+ let text = doc.getText(id: "text")
+
+ try! text.insert(pos: 0, s: "Hello")
+ let checkpoint = doc.oplogFrontiers()
+
+ try! text.insert(pos: 5, s: " World")
+
+ // Document should start attached
+ XCTAssertFalse(doc.isDetached())
+
+ // Checkout puts document in detached state
+ try! doc.checkout(frontiers: checkpoint)
+ XCTAssertTrue(doc.isDetached())
+ XCTAssertEqual(text.toString(), "Hello")
+
+ // attach() should reattach to latest
+ doc.attach()
+ XCTAssertFalse(doc.isDetached())
+ XCTAssertEqual(text.toString(), "Hello World")
+ }
+
+ func testAttachVsCheckoutToLatest() {
+ let doc = LoroDoc()
+ try! doc.setPeerId(peer: 1)
+ let text = doc.getText(id: "text")
+
+ try! text.insert(pos: 0, s: "Version 1")
+ let v1 = doc.oplogFrontiers()
+
+ try! text.delete(pos: 0, len: 9)
+ try! text.insert(pos: 0, s: "Version 2")
+
+ // Test attach()
+ try! doc.checkout(frontiers: v1)
+ XCTAssertTrue(doc.isDetached())
+ doc.attach()
+ XCTAssertFalse(doc.isDetached())
+ XCTAssertEqual(text.toString(), "Version 2")
+
+ // Test checkoutToLatest() - same effect
+ try! doc.checkout(frontiers: v1)
+ XCTAssertTrue(doc.isDetached())
+ doc.checkoutToLatest()
+ XCTAssertFalse(doc.isDetached())
+ XCTAssertEqual(text.toString(), "Version 2")
+ }
+
+ func testTimestampRecording() {
+ let doc = LoroDoc()
+ try! doc.setPeerId(peer: 1)
+
+ // Enable timestamp recording
+ doc.setRecordTimestamp(record: true)
+
+ let text = doc.getText(id: "text")
+ try! text.insert(pos: 0, s: "Hello")
+ doc.commit()
+
+ // Verify we have at least one change
+ XCTAssertGreaterThan(doc.lenChanges(), 0)
+ }
+
+ func testManualTimestamp() {
+ let doc = LoroDoc()
+ try! doc.setPeerId(peer: 1)
+
+ let text = doc.getText(id: "text")
+ try! text.insert(pos: 0, s: "Hello")
+
+ // Set a specific timestamp for the next commit
+ let customTimestamp: Int64 = 1700000000
+ doc.setNextCommitTimestamp(timestamp: customTimestamp)
+ doc.commit()
+
+ // Retrieve the change and verify timestamp
+ let id = Id(peer: 1, counter: 0)
+ let changeMeta = doc.getChange(id: id)
+ XCTAssertNotNil(changeMeta)
+ XCTAssertEqual(changeMeta?.timestamp, customTimestamp)
+ }
+
+ func testGetChangeMetadata() {
+ let doc = LoroDoc()
+ try! doc.setPeerId(peer: 1)
+
+ let text = doc.getText(id: "text")
+ try! text.insert(pos: 0, s: "Hello")
+ doc.commit()
+
+ try! text.insert(pos: 5, s: " World")
+ doc.commit()
+
+ // Check total changes
+ let changeCount = doc.lenChanges()
+ XCTAssertGreaterThanOrEqual(changeCount, 1)
+
+ // Get metadata for the first change
+ let id = Id(peer: 1, counter: 0)
+ let changeMeta = doc.getChange(id: id)
+ XCTAssertNotNil(changeMeta)
+ XCTAssertEqual(changeMeta?.id.peer, 1)
+ }
+
+ func testCommitMessage() {
+ let doc = LoroDoc()
+ try! doc.setPeerId(peer: 1)
+
+ let text = doc.getText(id: "text")
+ try! text.insert(pos: 0, s: "Draft")
+
+ // Set a commit message
+ doc.setNextCommitMessage(msg: "Initial draft")
+ doc.commit()
+
+ // Get the change and verify the message
+ let id = Id(peer: 1, counter: 0)
+ let changeMeta = doc.getChange(id: id)
+ XCTAssertNotNil(changeMeta)
+ XCTAssertEqual(changeMeta?.message, "Initial draft")
+ }
+
+ func testLenChanges() {
+ let doc = LoroDoc()
+ try! doc.setPeerId(peer: 1)
+
+ // Initially no changes
+ XCTAssertEqual(doc.lenChanges(), 0)
+
+ let text = doc.getText(id: "text")
+ try! text.insert(pos: 0, s: "A")
+ doc.commit()
+
+ // Should have at least one change
+ XCTAssertGreaterThan(doc.lenChanges(), 0)
+
+ try! text.insert(pos: 1, s: "B")
+ doc.commit()
+
+ // Should have more changes
+ XCTAssertGreaterThan(doc.lenChanges(), 0)
+ }
}
diff --git a/loro-swift/Cargo.lock b/loro-swift/Cargo.lock
index 322dc2e..c326615 100644
--- a/loro-swift/Cargo.lock
+++ b/loro-swift/Cargo.lock
@@ -9,7 +9,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
- "getrandom 0.3.3",
+ "getrandom 0.3.4",
"once_cell",
"version_check",
"zerocopy",
@@ -17,18 +17,18 @@ dependencies = [
[[package]]
name = "aho-corasick"
-version = "1.1.3"
+version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "anstream"
-version = "0.6.19"
+version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
+checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
dependencies = [
"anstyle",
"anstyle-parse",
@@ -41,9 +41,9 @@ dependencies = [
[[package]]
name = "anstyle"
-version = "1.0.11"
+version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
+checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
[[package]]
name = "anstyle-parse"
@@ -56,18 +56,18 @@ dependencies = [
[[package]]
name = "anstyle-query"
-version = "1.1.3"
+version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
-version = "3.0.9"
+version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
@@ -76,9 +76,9 @@ dependencies = [
[[package]]
name = "anyhow"
-version = "1.0.98"
+version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
+checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "append-only-bytes"
@@ -88,9 +88,9 @@ checksum = "ac436601d6bdde674a0d7fb593e829ffe7b3387c351b356dd20e2d40f5bf3ee5"
[[package]]
name = "arbitrary"
-version = "1.4.1"
+version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
+checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
dependencies = [
"derive_arbitrary",
]
@@ -130,7 +130,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde",
- "syn 2.0.103",
+ "syn 2.0.111",
]
[[package]]
@@ -183,9 +183,9 @@ dependencies = [
[[package]]
name = "bitflags"
-version = "2.9.1"
+version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
+checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "bitmaps"
@@ -207,9 +207,9 @@ dependencies = [
[[package]]
name = "bumpalo"
-version = "3.18.1"
+version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee"
+checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]]
name = "byteorder"
@@ -219,17 +219,17 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
-version = "1.10.1"
+version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
+checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
[[package]]
name = "camino"
-version = "1.1.10"
+version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab"
+checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609"
dependencies = [
- "serde",
+ "serde_core",
]
[[package]]
@@ -252,29 +252,30 @@ dependencies = [
"semver",
"serde",
"serde_json",
- "thiserror",
+ "thiserror 1.0.69",
]
[[package]]
name = "cc"
-version = "1.2.27"
+version = "1.2.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc"
+checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215"
dependencies = [
+ "find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
-version = "1.0.1"
+version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "clap"
-version = "4.5.40"
+version = "4.5.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f"
+checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
dependencies = [
"clap_builder",
"clap_derive",
@@ -282,9 +283,9 @@ dependencies = [
[[package]]
name = "clap_builder"
-version = "4.5.40"
+version = "4.5.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e"
+checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
dependencies = [
"anstream",
"anstyle",
@@ -294,27 +295,30 @@ dependencies = [
[[package]]
name = "clap_derive"
-version = "4.5.40"
+version = "4.5.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce"
+checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
- "syn 2.0.103",
+ "syn 2.0.111",
]
[[package]]
name = "clap_lex"
-version = "0.7.5"
+version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
+checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
[[package]]
name = "cobs"
-version = "0.2.3"
+version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15"
+checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
+dependencies = [
+ "thiserror 2.0.17",
+]
[[package]]
name = "colorchoice"
@@ -368,7 +372,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
- "syn 2.0.103",
+ "syn 2.0.111",
]
[[package]]
@@ -379,18 +383,18 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core",
"quote",
- "syn 2.0.103",
+ "syn 2.0.111",
]
[[package]]
name = "derive_arbitrary"
-version = "1.4.1"
+version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800"
+checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.103",
+ "syn 2.0.111",
]
[[package]]
@@ -454,7 +458,7 @@ dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
- "syn 2.0.103",
+ "syn 2.0.111",
]
[[package]]
@@ -466,7 +470,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
- "syn 2.0.103",
+ "syn 2.0.111",
]
[[package]]
@@ -475,6 +479,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
+
[[package]]
name = "fnv"
version = "1.0.7"
@@ -492,9 +502,9 @@ dependencies = [
[[package]]
name = "generator"
-version = "0.8.5"
+version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d18470a76cb7f8ff746cf1f7470914f900252ec36bbc40b569d74b1258446827"
+checksum = "605183a538e3e2a9c1038635cc5c2d194e2ee8fd0d1b66b8349fad7dbacce5a2"
dependencies = [
"cc",
"cfg-if",
@@ -537,27 +547,27 @@ dependencies = [
"cfg-if",
"js-sys",
"libc",
- "wasi 0.11.1+wasi-snapshot-preview1",
+ "wasi",
"wasm-bindgen",
]
[[package]]
name = "getrandom"
-version = "0.3.3"
+version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
- "wasi 0.14.2+wasi-0.2.4",
+ "wasip2",
]
[[package]]
name = "glob"
-version = "0.3.2"
+version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
+checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "goblin"
@@ -590,9 +600,9 @@ dependencies = [
[[package]]
name = "hashbrown"
-version = "0.15.4"
+version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[package]]
name = "heapless"
@@ -663,9 +673,9 @@ dependencies = [
[[package]]
name = "is_terminal_polyfill"
-version = "1.70.1"
+version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
@@ -693,9 +703,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "js-sys"
-version = "0.3.77"
+version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -715,9 +725,9 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
[[package]]
name = "libc"
-version = "0.2.174"
+version = "0.2.178"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
+checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
[[package]]
name = "lock_api"
@@ -730,9 +740,9 @@ dependencies = [
[[package]]
name = "log"
-version = "0.4.27"
+version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "loom"
@@ -751,9 +761,9 @@ dependencies = [
[[package]]
name = "loro"
-version = "1.10.0"
+version = "1.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88dccd9df337cf38accfa64bd2267edfd2eeb17a459d38ab0f7ac80eb878cdc5"
+checksum = "d75216d8f99725531a30f7b00901ee154a4f8a9b7f125bfe032e197d4c7ffb8c"
dependencies = [
"enum-as-inner 0.6.1",
"generic-btree",
@@ -780,7 +790,7 @@ dependencies = [
"serde",
"serde_columnar",
"serde_json",
- "thiserror",
+ "thiserror 1.0.69",
]
[[package]]
@@ -797,9 +807,9 @@ dependencies = [
[[package]]
name = "loro-ffi"
-version = "1.10.0"
+version = "1.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e658b0476d59d407ab6f3faa7b3413fd0c86203344a5f9e4f238aa2a7219d636"
+checksum = "f01c976985f63d06d6dcbc739b067c2880d8e59d6588783e78a9e56e5cba932f"
dependencies = [
"loro",
"serde_json",
@@ -808,9 +818,9 @@ dependencies = [
[[package]]
name = "loro-internal"
-version = "1.10.0"
+version = "1.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e5c014162198a558f119e67287f042c37feb55e82b50d97d6ac2a90333995dd"
+checksum = "f447044ec3d3ba572623859add3334bd87b84340ee5fdf00315bfee0e3ad3e3f"
dependencies = [
"append-only-bytes",
"arref",
@@ -846,7 +856,7 @@ dependencies = [
"serde_columnar",
"serde_json",
"smallvec",
- "thiserror",
+ "thiserror 1.0.69",
"thread_local",
"tracing",
"wasm-bindgen",
@@ -883,7 +893,7 @@ dependencies = [
[[package]]
name = "loro-swift"
-version = "0.1.0"
+version = "1.10.3"
dependencies = [
"loro-ffi",
"uniffi",
@@ -908,20 +918,20 @@ dependencies = [
[[package]]
name = "lz4_flex"
-version = "0.11.4"
+version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c592ad9fbc1b7838633b3ae55ce69b17d01150c72fcef229fbb819d39ee51ee"
+checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a"
dependencies = [
"twox-hash",
]
[[package]]
name = "matchers"
-version = "0.1.0"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
- "regex-automata 0.1.10",
+ "regex-automata",
]
[[package]]
@@ -932,9 +942,9 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
[[package]]
name = "memchr"
-version = "2.7.5"
+version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
+checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "mime"
@@ -976,12 +986,11 @@ checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51"
[[package]]
name = "nu-ansi-term"
-version = "0.46.0"
+version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
- "overload",
- "winapi",
+ "windows-sys",
]
[[package]]
@@ -1065,15 +1074,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "once_cell_polyfill"
-version = "1.70.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
-
-[[package]]
-name = "overload"
-version = "0.1.1"
+version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "parking_lot"
@@ -1134,7 +1137,7 @@ dependencies = [
"pest_meta",
"proc-macro2",
"quote",
- "syn 2.0.103",
+ "syn 2.0.111",
]
[[package]]
@@ -1161,9 +1164,9 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]]
name = "postcard"
-version = "1.1.1"
+version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8"
+checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
dependencies = [
"cobs",
"embedded-io 0.4.0",
@@ -1193,18 +1196,18 @@ dependencies = [
[[package]]
name = "proc-macro2"
-version = "1.0.95"
+version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
+checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quick_cache"
-version = "0.6.14"
+version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b450dad8382b1b95061d5ca1eb792081fb082adf48c678791fe917509596d5f"
+checksum = "7ada44a88ef953a3294f6eb55d2007ba44646015e18613d2f213016379203ef3"
dependencies = [
"ahash",
"equivalent",
@@ -1214,9 +1217,9 @@ dependencies = [
[[package]]
name = "quote"
-version = "1.0.40"
+version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
@@ -1268,56 +1271,29 @@ dependencies = [
[[package]]
name = "redox_syscall"
-version = "0.5.13"
+version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags",
]
-[[package]]
-name = "regex"
-version = "1.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
-dependencies = [
- "aho-corasick",
- "memchr",
- "regex-automata 0.4.9",
- "regex-syntax 0.8.5",
-]
-
[[package]]
name = "regex-automata"
-version = "0.1.10"
+version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
-dependencies = [
- "regex-syntax 0.6.29",
-]
-
-[[package]]
-name = "regex-automata"
-version = "0.4.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
dependencies = [
"aho-corasick",
"memchr",
- "regex-syntax 0.8.5",
+ "regex-syntax",
]
[[package]]
name = "regex-syntax"
-version = "0.6.29"
+version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
-
-[[package]]
-name = "regex-syntax"
-version = "0.8.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "rustc-hash"
@@ -1336,9 +1312,9 @@ dependencies = [
[[package]]
name = "rustversion"
-version = "1.0.21"
+version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
@@ -1375,24 +1351,26 @@ checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.103",
+ "syn 2.0.111",
]
[[package]]
name = "semver"
-version = "1.0.26"
+version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
+checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
dependencies = [
"serde",
+ "serde_core",
]
[[package]]
name = "serde"
-version = "1.0.219"
+version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
+ "serde_core",
"serde_derive",
]
@@ -1406,7 +1384,7 @@ dependencies = [
"postcard",
"serde",
"serde_columnar_derive",
- "thiserror",
+ "thiserror 1.0.69",
]
[[package]]
@@ -1418,30 +1396,40 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
- "syn 2.0.103",
+ "syn 2.0.111",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.219"
+version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.103",
+ "syn 2.0.111",
]
[[package]]
name = "serde_json"
-version = "1.0.140"
+version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
+checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
+ "serde_core",
]
[[package]]
@@ -1512,9 +1500,9 @@ dependencies = [
[[package]]
name = "stable_deref_trait"
-version = "1.2.0"
+version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "static_assertions"
@@ -1541,9 +1529,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "2.0.103"
+version = "2.0.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8"
+checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
dependencies = [
"proc-macro2",
"quote",
@@ -1565,7 +1553,16 @@ version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
- "thiserror-impl",
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
+dependencies = [
+ "thiserror-impl 2.0.17",
]
[[package]]
@@ -1576,7 +1573,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.103",
+ "syn 2.0.111",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.111",
]
[[package]]
@@ -1599,9 +1607,9 @@ dependencies = [
[[package]]
name = "tracing"
-version = "0.1.41"
+version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647"
dependencies = [
"pin-project-lite",
"tracing-attributes",
@@ -1610,20 +1618,20 @@ dependencies = [
[[package]]
name = "tracing-attributes"
-version = "0.1.30"
+version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.103",
+ "syn 2.0.111",
]
[[package]]
name = "tracing-core"
-version = "0.1.34"
+version = "0.1.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
+checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c"
dependencies = [
"once_cell",
"valuable",
@@ -1642,14 +1650,14 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
-version = "0.3.19"
+version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
+checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
- "regex",
+ "regex-automata",
"sharded-slab",
"smallvec",
"thread_local",
@@ -1660,15 +1668,15 @@ dependencies = [
[[package]]
name = "twox-hash"
-version = "2.1.1"
+version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b907da542cbced5261bd3256de1b3a1bf340a3d37f93425a07362a1d687de56"
+checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c"
[[package]]
name = "typenum"
-version = "1.18.0"
+version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
+checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "ucd-trie"
@@ -1684,9 +1692,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
[[package]]
name = "unicode-ident"
-version = "1.0.18"
+version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "uniffi"
@@ -1745,7 +1753,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "802d2051a700e3ec894c79f80d2705b69d85844dafbbe5d1a92776f8f48b563a"
dependencies = [
"quote",
- "syn 2.0.103",
+ "syn 2.0.111",
]
[[package]]
@@ -1775,7 +1783,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde",
- "syn 2.0.103",
+ "syn 2.0.111",
"toml",
"uniffi_meta",
]
@@ -1843,45 +1851,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
-name = "wasi"
-version = "0.14.2+wasi-0.2.4"
+name = "wasip2"
+version = "1.0.1+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
+checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
dependencies = [
- "wit-bindgen-rt",
+ "wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
-version = "0.2.100"
+version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
-]
-
-[[package]]
-name = "wasm-bindgen-backend"
-version = "0.2.100"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
-dependencies = [
- "bumpalo",
- "log",
- "proc-macro2",
- "quote",
- "syn 2.0.103",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.100"
+version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -1889,22 +1884,22 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.100"
+version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
dependencies = [
+ "bumpalo",
"proc-macro2",
"quote",
- "syn 2.0.103",
- "wasm-bindgen-backend",
+ "syn 2.0.111",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.100"
+version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
dependencies = [
"unicode-ident",
]
@@ -1918,28 +1913,6 @@ dependencies = [
"nom",
]
-[[package]]
-name = "winapi"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
-dependencies = [
- "winapi-i686-pc-windows-gnu",
- "winapi-x86_64-pc-windows-gnu",
-]
-
-[[package]]
-name = "winapi-i686-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
-
-[[package]]
-name = "winapi-x86_64-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
-
[[package]]
name = "windows"
version = "0.61.3"
@@ -1988,24 +1961,24 @@ dependencies = [
[[package]]
name = "windows-implement"
-version = "0.60.0"
+version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.103",
+ "syn 2.0.111",
]
[[package]]
name = "windows-interface"
-version = "0.59.1"
+version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.103",
+ "syn 2.0.111",
]
[[package]]
@@ -2050,27 +2023,11 @@ dependencies = [
[[package]]
name = "windows-sys"
-version = "0.59.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
-dependencies = [
- "windows-targets",
-]
-
-[[package]]
-name = "windows-targets"
-version = "0.52.6"
+version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_gnullvm",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
+ "windows-link 0.2.1",
]
[[package]]
@@ -2083,61 +2040,10 @@ dependencies = [
]
[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
-
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
-
-[[package]]
-name = "windows_i686_gnu"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
-
-[[package]]
-name = "windows_i686_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
-
-[[package]]
-name = "windows_i686_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
-
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
-
-[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
-
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
-
-[[package]]
-name = "wit-bindgen-rt"
-version = "0.39.0"
+name = "wit-bindgen"
+version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
-dependencies = [
- "bitflags",
-]
+checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "xxhash-rust"
@@ -2153,20 +2059,20 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]]
name = "zerocopy"
-version = "0.8.26"
+version = "0.8.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
+checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
-version = "0.8.26"
+version = "0.8.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
+checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.103",
+ "syn 2.0.111",
]
diff --git a/loro-swift/Cargo.toml b/loro-swift/Cargo.toml
index e17ab1f..bdef5ab 100644
--- a/loro-swift/Cargo.toml
+++ b/loro-swift/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "loro-swift"
-version = "1.10.0"
+version = "1.10.3"
edition = "2021"
[lib]
@@ -11,7 +11,7 @@ name = "uniffi-bindgen"
path = "src/uniffi-bindgen.rs"
[dependencies]
-loro-ffi = { version = "1.10.0" }
+loro-ffi = { version = "1.10.3" }
uniffi = { version = "0.28.3" }
[build-dependencies]
diff --git a/scripts/build_artifactbundle.ps1 b/scripts/build_artifactbundle.ps1
new file mode 100644
index 0000000..0fe56c0
--- /dev/null
+++ b/scripts/build_artifactbundle.ps1
@@ -0,0 +1,95 @@
+# Build script to create artifact bundle for Windows
+# This creates loroFFI.artifactbundle with static libraries
+
+$ErrorActionPreference = "Stop"
+
+$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
+$RustFolder = Join-Path $ScriptDir "..\loro-swift"
+$SwiftFolder = Join-Path $ScriptDir "..\gen-swift"
+$BuildFolder = Join-Path $RustFolder "target"
+$BundleFolder = Join-Path $ScriptDir "..\loroFFI.artifactbundle"
+$Version = "1.10.3"
+
+$CargoBuild = "cargo build --manifest-path `"$RustFolder\Cargo.toml`" --features cli"
+
+Write-Host "▸ Clean state"
+if (Test-Path $BundleFolder) { Remove-Item -Recurse -Force $BundleFolder }
+if (Test-Path $SwiftFolder) { Remove-Item -Recurse -Force $SwiftFolder }
+New-Item -ItemType Directory -Force -Path $SwiftFolder | Out-Null
+New-Item -ItemType Directory -Force -Path $BundleFolder | Out-Null
+
+Write-Host "▸ Build release library for Windows"
+Invoke-Expression "$CargoBuild --release"
+
+Write-Host "▸ Generate Swift bindings"
+Push-Location $RustFolder
+cargo run --release --features=cli --bin uniffi-bindgen generate --library "$BuildFolder\release\loro_swift.dll" --language swift --out-dir $SwiftFolder
+Pop-Location
+
+Write-Host "▸ Create artifact bundle structure"
+$IncludeFolder = Join-Path $BundleFolder "include"
+New-Item -ItemType Directory -Force -Path $IncludeFolder | Out-Null
+
+# Copy header
+Copy-Item "$SwiftFolder\loroFFI.h" $IncludeFolder
+
+# Create module map
+@"
+module LoroFFI {
+ header "loroFFI.h"
+ export *
+}
+"@ | Set-Content "$IncludeFolder\module.modulemap"
+
+Write-Host "▸ Updating LoroFFI.swift"
+$LoroFFISwift = Join-Path $ScriptDir "..\Sources\Loro\LoroFFI.swift"
+Copy-Item "$SwiftFolder\loro.swift" $LoroFFISwift -Force
+
+Write-Host "▸ Fixing Swift 6 compatibility"
+$Content = Get-Content $LoroFFISwift -Raw
+$Content = $Content -replace 'canImport\(loroFFI\)', 'canImport(LoroFFI)'
+$Content = $Content -replace 'import loroFFI', 'import LoroFFI'
+$Content = $Content -replace 'static var vtable:', 'nonisolated(unsafe) static var vtable:'
+$Content = $Content -replace 'fileprivate static var handleMap', 'nonisolated(unsafe) fileprivate static var handleMap'
+$Content = $Content -replace 'private var initializationResult', 'nonisolated(unsafe) private var initializationResult'
+$Content = $Content -replace 'protocol LoroValueLike : AnyObject', 'protocol LoroValueLike'
+$Content = $Content -replace 'protocol ContainerIdLike : AnyObject', 'protocol ContainerIdLike'
+Set-Content $LoroFFISwift $Content
+
+Write-Host "▸ Setting up Windows library"
+$WindowsLibFolder = Join-Path $BundleFolder "loroFFI-windows"
+New-Item -ItemType Directory -Force -Path $WindowsLibFolder | Out-Null
+# SwiftPM validation wants lib* prefix, but the linker still looks for the original name; ship both.
+Copy-Item "$BuildFolder\release\loro_swift.lib" (Join-Path $WindowsLibFolder "libloro_swift.lib")
+Copy-Item "$BuildFolder\release\loro_swift.lib" (Join-Path $WindowsLibFolder "loro_swift.lib")
+
+# Determine architecture
+$Arch = if ([Environment]::Is64BitOperatingSystem) { "x86_64" } else { "i686" }
+$Triple = "$Arch-unknown-windows-msvc"
+
+Write-Host "▸ Create info.json"
+@"
+{
+ "schemaVersion": "1.0",
+ "artifacts": {
+ "LoroFFI": {
+ "version": "$Version",
+ "type": "staticLibrary",
+ "variants": [
+ {
+ "path": "loroFFI-windows/libloro_swift.lib",
+ "supportedTriples": ["$Triple"],
+ "staticLibraryMetadata": {
+ "headerPaths": ["include"],
+ "moduleMapPath": "include/module.modulemap"
+ }
+ }
+ ]
+ }
+ }
+}
+"@ | Set-Content "$BundleFolder\info.json"
+
+Write-Host "▸ Artifact bundle created at: $BundleFolder"
+Write-Host "▸ Contents:"
+Get-ChildItem -Recurse $BundleFolder | Where-Object { !$_.PSIsContainer } | ForEach-Object { $_.FullName }
diff --git a/scripts/build_artifactbundle.sh b/scripts/build_artifactbundle.sh
new file mode 100755
index 0000000..04015ac
--- /dev/null
+++ b/scripts/build_artifactbundle.sh
@@ -0,0 +1,194 @@
+#!/usr/bin/env bash
+
+# Build script to create a cross-platform artifact bundle for Swift 6.2+
+# This creates loroFFI.artifactbundle with static libraries for all platforms
+
+set -euxo pipefail
+THIS_SCRIPT_DIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
+RUST_FOLDER="$THIS_SCRIPT_DIR/../loro-swift"
+SWIFT_FOLDER="$THIS_SCRIPT_DIR/../gen-swift"
+BUILD_FOLDER="$RUST_FOLDER/target"
+BUNDLE_FOLDER="$THIS_SCRIPT_DIR/../loroFFI.artifactbundle"
+VERSION="${VERSION:-1.10.3}"
+
+cargo_build="cargo build --manifest-path $RUST_FOLDER/Cargo.toml --features cli"
+
+echo "▸ Clean state"
+rm -rf "${BUNDLE_FOLDER}"
+rm -rf "${SWIFT_FOLDER}"
+mkdir -p "${SWIFT_FOLDER}"
+mkdir -p "${BUNDLE_FOLDER}"
+
+echo "▸ Detect current platform"
+UNAME_S=$(uname -s)
+UNAME_M=$(uname -m)
+
+# Build for current platform
+echo "▸ Build release library for current platform"
+$cargo_build --release
+
+# Generate Swift bindings
+echo "▸ Generate Swift bindings"
+cd "$RUST_FOLDER"
+
+# Detect the library extension based on platform
+if [[ "$UNAME_S" == "Darwin" ]]; then
+ LIB_EXT="dylib"
+ LIB_PREFIX="lib"
+ STATIC_EXT="a"
+elif [[ "$UNAME_S" == "Linux" ]]; then
+ LIB_EXT="so"
+ LIB_PREFIX="lib"
+ STATIC_EXT="a"
+else
+ # Windows (MINGW/MSYS)
+ LIB_EXT="dll"
+ LIB_PREFIX=""
+ STATIC_EXT="lib"
+fi
+
+cargo run --release \
+ --features=cli \
+ --bin uniffi-bindgen generate \
+ --library "$BUILD_FOLDER/release/${LIB_PREFIX}loro_swift.${LIB_EXT}" \
+ --language swift \
+ --out-dir "${SWIFT_FOLDER}"
+cd ..
+
+# Setup artifact bundle structure
+echo "▸ Create artifact bundle structure"
+mkdir -p "${BUNDLE_FOLDER}/include"
+
+# Copy header and create module map
+cp "${SWIFT_FOLDER}/loroFFI.h" "${BUNDLE_FOLDER}/include/"
+cat > "${BUNDLE_FOLDER}/include/module.modulemap" << 'EOF'
+module LoroFFI {
+ header "loroFFI.h"
+ export *
+}
+EOF
+
+# Copy generated Swift bindings
+echo "▸ Updating LoroFFI.swift"
+cp -f "${SWIFT_FOLDER}/loro.swift" "$THIS_SCRIPT_DIR/../Sources/Loro/LoroFFI.swift"
+
+# Fix Swift 6 compatibility issues in generated code
+echo "▸ Fixing Swift 6 compatibility"
+LORO_FFI_SWIFT="$THIS_SCRIPT_DIR/../Sources/Loro/LoroFFI.swift"
+
+# Fix module import capitalization (loroFFI -> LoroFFI)
+if [[ "$UNAME_S" == "Darwin" ]]; then
+ sed -i '' 's/canImport(loroFFI)/canImport(LoroFFI)/g' "$LORO_FFI_SWIFT"
+ sed -i '' 's/import loroFFI/import LoroFFI/g' "$LORO_FFI_SWIFT"
+ # Add nonisolated(unsafe) for Swift 6 strict concurrency
+ sed -i '' 's/static var vtable:/nonisolated(unsafe) static var vtable:/g' "$LORO_FFI_SWIFT"
+ sed -i '' 's/fileprivate static var handleMap/nonisolated(unsafe) fileprivate static var handleMap/g' "$LORO_FFI_SWIFT"
+ # Fix initializationResult global var
+ sed -i '' 's/private var initializationResult/nonisolated(unsafe) private var initializationResult/g' "$LORO_FFI_SWIFT"
+ # Fix protocols to allow struct conformance (remove AnyObject constraint)
+ # Use perl for more reliable cross-platform regex
+ perl -i -pe 's/protocol LoroValueLike\s*:\s*AnyObject/protocol LoroValueLike/g' "$LORO_FFI_SWIFT"
+ perl -i -pe 's/protocol ContainerIdLike\s*:\s*AnyObject/protocol ContainerIdLike/g' "$LORO_FFI_SWIFT"
+else
+ sed -i 's/canImport(loroFFI)/canImport(LoroFFI)/g' "$LORO_FFI_SWIFT"
+ sed -i 's/import loroFFI/import LoroFFI/g' "$LORO_FFI_SWIFT"
+ # Add nonisolated(unsafe) for Swift 6 strict concurrency
+ sed -i 's/static var vtable:/nonisolated(unsafe) static var vtable:/g' "$LORO_FFI_SWIFT"
+ sed -i 's/fileprivate static var handleMap/nonisolated(unsafe) fileprivate static var handleMap/g' "$LORO_FFI_SWIFT"
+ # Fix initializationResult global var
+ sed -i 's/private var initializationResult/nonisolated(unsafe) private var initializationResult/g' "$LORO_FFI_SWIFT"
+ # Fix protocols to allow struct conformance (remove AnyObject constraint)
+ # Use perl for more reliable cross-platform regex
+ perl -i -pe 's/protocol LoroValueLike\s*:\s*AnyObject/protocol LoroValueLike/g' "$LORO_FFI_SWIFT"
+ perl -i -pe 's/protocol ContainerIdLike\s*:\s*AnyObject/protocol ContainerIdLike/g' "$LORO_FFI_SWIFT"
+fi
+
+# Platform-specific library setup
+if [[ "$UNAME_S" == "Darwin" ]]; then
+ echo "▸ Building for macOS"
+
+ # Build for both architectures
+ rustup target add aarch64-apple-darwin x86_64-apple-darwin 2>/dev/null || true
+
+ CFLAGS_aarch64_apple_darwin="-target aarch64-apple-darwin" \
+ $cargo_build --target aarch64-apple-darwin --release
+
+ CFLAGS_x86_64_apple_darwin="-target x86_64-apple-darwin" \
+ $cargo_build --target x86_64-apple-darwin --release
+
+ # Create universal binary
+ mkdir -p "${BUNDLE_FOLDER}/loroFFI-macos"
+ lipo -create \
+ "${BUILD_FOLDER}/x86_64-apple-darwin/release/libloro_swift.a" \
+ "${BUILD_FOLDER}/aarch64-apple-darwin/release/libloro_swift.a" \
+ -output "${BUNDLE_FOLDER}/loroFFI-macos/libloro_swift.a"
+
+ MACOS_VARIANT='{
+ "path": "loroFFI-macos/libloro_swift.a",
+ "supportedTriples": ["arm64-apple-macosx", "x86_64-apple-macosx"],
+ "staticLibraryMetadata": {
+ "headerPaths": ["include"],
+ "moduleMapPath": "include/module.modulemap"
+ }
+ }'
+
+elif [[ "$UNAME_S" == "Linux" ]]; then
+ echo "▸ Building for Linux"
+
+ mkdir -p "${BUNDLE_FOLDER}/loroFFI-linux"
+ cp "${BUILD_FOLDER}/release/libloro_swift.a" "${BUNDLE_FOLDER}/loroFFI-linux/"
+
+ if [[ "$UNAME_M" == "x86_64" ]]; then
+ TRIPLE="x86_64-unknown-linux-gnu"
+ else
+ TRIPLE="aarch64-unknown-linux-gnu"
+ fi
+
+ LINUX_VARIANT='{
+ "path": "loroFFI-linux/libloro_swift.a",
+ "supportedTriples": ["'"$TRIPLE"'"],
+ "staticLibraryMetadata": {
+ "headerPaths": ["include"],
+ "moduleMapPath": "include/module.modulemap"
+ }
+ }'
+fi
+
+# Create info.json based on platform
+echo "▸ Create info.json"
+
+if [[ "$UNAME_S" == "Darwin" ]]; then
+ cat > "${BUNDLE_FOLDER}/info.json" << EOF
+{
+ "schemaVersion": "1.0",
+ "artifacts": {
+ "LoroFFI": {
+ "version": "${VERSION}",
+ "type": "staticLibrary",
+ "variants": [
+ ${MACOS_VARIANT}
+ ]
+ }
+ }
+}
+EOF
+elif [[ "$UNAME_S" == "Linux" ]]; then
+ cat > "${BUNDLE_FOLDER}/info.json" << EOF
+{
+ "schemaVersion": "1.0",
+ "artifacts": {
+ "LoroFFI": {
+ "version": "${VERSION}",
+ "type": "staticLibrary",
+ "variants": [
+ ${LINUX_VARIANT}
+ ]
+ }
+ }
+}
+EOF
+fi
+
+echo "▸ Artifact bundle created at: ${BUNDLE_FOLDER}"
+echo "▸ Contents:"
+find "${BUNDLE_FOLDER}" -type f
diff --git a/scripts/build_linux.sh b/scripts/build_linux.sh
new file mode 100755
index 0000000..334d157
--- /dev/null
+++ b/scripts/build_linux.sh
@@ -0,0 +1,55 @@
+#!/usr/bin/env bash
+
+# Build script for Linux
+# This builds the Rust FFI library and prepares it for Swift Package Manager
+
+set -euxo pipefail
+THIS_SCRIPT_DIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
+LIB_NAME="libloro_swift.a"
+RUST_FOLDER="$THIS_SCRIPT_DIR/../loro-swift"
+SWIFT_FOLDER="$THIS_SCRIPT_DIR/../gen-swift"
+BUILD_FOLDER="$RUST_FOLDER/target"
+
+cargo_build="cargo build --manifest-path $RUST_FOLDER/Cargo.toml"
+
+echo "▸ Clean state"
+rm -rf "${SWIFT_FOLDER}"
+mkdir -p "${SWIFT_FOLDER}"
+
+echo "▸ Build release library"
+$cargo_build --release
+
+echo "▸ Generate Swift bindings"
+cd "$RUST_FOLDER"
+cargo run --release \
+ --features=cli \
+ --bin uniffi-bindgen generate \
+ --library "$BUILD_FOLDER/release/libloro_swift.so" \
+ --language swift \
+ --out-dir "${SWIFT_FOLDER}"
+cd ..
+
+echo "▸ Setup headers for system library"
+mkdir -p "$THIS_SCRIPT_DIR/../Sources/LoroFFI/include"
+cp "${SWIFT_FOLDER}/loroFFI.h" "$THIS_SCRIPT_DIR/../Sources/LoroFFI/include/"
+
+# Create module.modulemap
+cat > "$THIS_SCRIPT_DIR/../Sources/LoroFFI/include/module.modulemap" << 'EOF'
+module LoroFFI {
+ header "loroFFI.h"
+ export *
+}
+EOF
+
+# Copy the static library
+mkdir -p "$THIS_SCRIPT_DIR/../Sources/LoroFFI/lib"
+cp "$BUILD_FOLDER/release/$LIB_NAME" "$THIS_SCRIPT_DIR/../Sources/LoroFFI/lib/"
+
+echo "▸ Update LoroFFI.swift if needed"
+if [ -f "${SWIFT_FOLDER}/loro.swift" ]; then
+ cp -f "${SWIFT_FOLDER}/loro.swift" "$THIS_SCRIPT_DIR/../Sources/Loro/LoroFFI.swift"
+fi
+
+echo "▸ Linux build complete!"
+echo " Static library: Sources/LoroFFI/lib/$LIB_NAME"
+echo " Headers: Sources/LoroFFI/include/"
diff --git a/scripts/build_windows.ps1 b/scripts/build_windows.ps1
new file mode 100644
index 0000000..b7bc44d
--- /dev/null
+++ b/scripts/build_windows.ps1
@@ -0,0 +1,59 @@
+# Build script for Windows
+# This builds the Rust FFI library and prepares it for Swift Package Manager
+
+$ErrorActionPreference = "Stop"
+
+$ThisScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
+$LibName = "loro_swift.lib"
+$RustFolder = Join-Path $ThisScriptDir "..\loro-swift"
+$SwiftFolder = Join-Path $ThisScriptDir "..\gen-swift"
+$BuildFolder = Join-Path $RustFolder "target"
+
+Write-Host "▸ Clean state"
+if (Test-Path $SwiftFolder) {
+ Remove-Item -Recurse -Force $SwiftFolder
+}
+New-Item -ItemType Directory -Force -Path $SwiftFolder | Out-Null
+
+Write-Host "▸ Build release library"
+cargo build --manifest-path "$RustFolder\Cargo.toml" --release
+
+Write-Host "▸ Generate Swift bindings"
+Push-Location $RustFolder
+cargo run --release `
+ --features=cli `
+ --bin uniffi-bindgen generate `
+ --library "$BuildFolder\release\loro_swift.dll" `
+ --language swift `
+ --out-dir $SwiftFolder
+Pop-Location
+
+Write-Host "▸ Setup headers for system library"
+$IncludeDir = Join-Path $ThisScriptDir "..\Sources\LoroFFI\include"
+New-Item -ItemType Directory -Force -Path $IncludeDir | Out-Null
+Copy-Item "$SwiftFolder\loroFFI.h" $IncludeDir
+
+# Create module.modulemap
+$ModuleMap = @"
+module LoroFFI {
+ header "loroFFI.h"
+ export *
+}
+"@
+Set-Content -Path "$IncludeDir\module.modulemap" -Value $ModuleMap
+
+# Copy the static library for local builds; provide both names (SwiftPM validation prefers lib*, linker may look for original)
+$LibDir = Join-Path $ThisScriptDir "..\Sources\LoroFFI\lib"
+New-Item -ItemType Directory -Force -Path $LibDir | Out-Null
+Copy-Item "$BuildFolder\release\$LibName" (Join-Path $LibDir "libloro_swift.lib")
+Copy-Item "$BuildFolder\release\$LibName" (Join-Path $LibDir "loro_swift.lib")
+
+Write-Host "▸ Update LoroFFI.swift if needed"
+$LoroSwift = Join-Path $SwiftFolder "loro.swift"
+if (Test-Path $LoroSwift) {
+ Copy-Item -Force $LoroSwift (Join-Path $ThisScriptDir "..\Sources\Loro\LoroFFI.swift")
+}
+
+Write-Host "▸ Windows build complete!"
+Write-Host " Static library: Sources\LoroFFI\lib\$LibName"
+Write-Host " Headers: Sources\LoroFFI\include\"