diff --git a/.github/workflows/client-app.yml b/.github/workflows/client-app.yml index 628dfe619f..278e0be421 100644 --- a/.github/workflows/client-app.yml +++ b/.github/workflows/client-app.yml @@ -69,6 +69,14 @@ jobs: - name: Setup Node.js uses: ./.github/actions/setup-node + - name: install Rust stable + uses: dtolnay/rust-toolchain@stable + + - name: Rust cache + uses: swatinem/rust-cache@v2 + with: + workspaces: './packages/octobase-node -> target' + - uses: actions/download-artifact@v3 with: name: before-make-web-static @@ -79,6 +87,13 @@ jobs: name: before-make-electron-dist path: apps/electron/dist + - name: build octobase-node + run: yarn build:octobase-node + working-directory: apps/electron + + - name: move octobase Binary + run: cp ./packages/octobase-node/octobase.*.node ./apps/electron/dist/layers/main/ + - name: make build run: yarn make-macos-x64 working-directory: apps/electron @@ -102,6 +117,17 @@ jobs: - name: Setup Node.js uses: ./.github/actions/setup-node + - name: install Rust stable + uses: dtolnay/rust-toolchain@stable + + - name: add arm64 target + run: rustup target add aarch64-apple-darwin + + - name: Rust cache + uses: swatinem/rust-cache@v2 + with: + workspaces: './packages/octobase-node -> target' + - uses: actions/download-artifact@v3 with: name: before-make-web-static @@ -112,6 +138,13 @@ jobs: name: before-make-electron-dist path: apps/electron/dist + - name: build octobase-node + run: yarn build:octobase-node + working-directory: apps/electron + + - name: move octobase Binary + run: cp ./packages/octobase-node/octobase.*.node ./apps/electron/dist/layers/main/ + - name: make build run: yarn make-macos-arm64 working-directory: apps/electron @@ -135,6 +168,9 @@ jobs: - name: Setup Node.js uses: ./.github/actions/setup-node + - name: install Rust stable + uses: dtolnay/rust-toolchain@stable + - uses: actions/download-artifact@v3 with: name: before-make-web-static @@ -145,6 +181,18 @@ jobs: name: before-make-electron-dist path: apps/electron/dist + - name: move octobase Binary + run: cp ./packages/octobase-node/octobase.*.node ./apps/electron/dist/layers/main/ + + - name: Rust cache + uses: swatinem/rust-cache@v2 + with: + workspaces: './packages/octobase-node -> target' + + - name: build octobase-node + run: yarn build:octobase-node + working-directory: apps/electron + - name: make build run: yarn make-windows-x64 working-directory: apps/electron diff --git a/apps/electron/.yarnrc.yml b/apps/electron/.yarnrc.yml deleted file mode 100644 index 9938b962f9..0000000000 --- a/apps/electron/.yarnrc.yml +++ /dev/null @@ -1,2 +0,0 @@ -yarnPath: .yarn/releases/yarn-3.4.1.cjs -nodeLinker: node-modules diff --git a/apps/electron/README.md b/apps/electron/README.md index 59df01327e..e836f6a8e8 100644 --- a/apps/electron/README.md +++ b/apps/electron/README.md @@ -9,10 +9,13 @@ See https://github.com/electron/forge/issues/2633 ``` # in project root, start web app at :8080 -pnpm dev +yarn dev + +# build octobase-node +yarn workspace @affine/octobase-node build # in /apps/electron, start electron app -pnpm dev +yarn dev ``` ## Credits diff --git a/apps/electron/layers/main/src/app-state/index.ts b/apps/electron/layers/main/src/app-state/index.ts new file mode 100644 index 0000000000..c2228cb7ef --- /dev/null +++ b/apps/electron/layers/main/src/app-state/index.ts @@ -0,0 +1,21 @@ +import * as os from 'node:os'; +import path from 'node:path'; + +import { Storage } from '@affine/octobase-node'; +import { ipcMain } from 'electron'; +import fs from 'fs-extra'; + +const AFFINE_ROOT = path.join(os.homedir(), '.affine'); + +fs.ensureDirSync(AFFINE_ROOT); + +// todo: rethink this +export const appState = { + storage: new Storage(path.join(AFFINE_ROOT, 'test.db')), +}; + +export const registerHandlers = () => { + ipcMain.handle('workspaceSync', async (_, id) => { + return appState.storage.sync(id, ''); + }); +}; diff --git a/apps/electron/layers/main/src/index.ts b/apps/electron/layers/main/src/index.ts index 450eb3c5c5..f2cd54efe6 100644 --- a/apps/electron/layers/main/src/index.ts +++ b/apps/electron/layers/main/src/index.ts @@ -2,6 +2,7 @@ import './security-restrictions'; import { app } from 'electron'; +import { registerHandlers } from './app-state'; import { restoreOrCreateWindow } from './main-window'; import { registerProtocol } from './protocol'; @@ -41,6 +42,7 @@ app.on('activate', restoreOrCreateWindow); app .whenReady() .then(registerProtocol) + .then(registerHandlers) .then(restoreOrCreateWindow) .catch(e => console.error('Failed create window:', e)); diff --git a/apps/electron/layers/preload/src/index.ts b/apps/electron/layers/preload/src/index.ts index aa07f05aad..cda2c3915d 100644 --- a/apps/electron/layers/preload/src/index.ts +++ b/apps/electron/layers/preload/src/index.ts @@ -2,7 +2,7 @@ * @module preload */ -import { contextBridge } from 'electron'; +import { contextBridge, ipcRenderer } from 'electron'; import { sha256sum } from './sha256sum'; @@ -31,3 +31,11 @@ contextBridge.exposeInMainWorld('yerba', { version: 0.1 }); * window.nodeCrypto('data') */ contextBridge.exposeInMainWorld('nodeCrypto', { sha256sum }); + +contextBridge.exposeInMainWorld('apis', { + workspaceSync: (id: string) => ipcRenderer.invoke('workspaceSync', id), +}); + +contextBridge.exposeInMainWorld('appInfo', { + electron: 1, +}); diff --git a/apps/electron/package.json b/apps/electron/package.json index d140303bab..fd82f1833f 100644 --- a/apps/electron/package.json +++ b/apps/electron/package.json @@ -14,6 +14,7 @@ "make-macos-arm64": "electron-forge make --platform=darwin --arch=arm64", "make-macos-x64": "electron-forge make --platform=darwin --arch=x64", "make-windows-x64": "electron-forge make --platform=win32 --arch=x64", + "build:octobase-node": "yarn workspace @affine/octobase-node build", "postinstall": "ELECTRON_RUN_AS_NODE=1 electron scripts/update-electron-vendors.mjs" }, "config": { @@ -21,6 +22,7 @@ }, "main": "./dist/layers/main/index.js", "devDependencies": { + "@affine/octobase-node": "workspace:*", "@electron-forge/cli": "^6.0.5", "@electron-forge/core": "^6.0.5", "@electron-forge/core-utils": "^6.0.5", @@ -37,6 +39,7 @@ }, "dependencies": { "cross-env": "7.0.3", - "electron-window-state": "^5.0.3" + "electron-window-state": "^5.0.3", + "fs-extra": "^11.1.1" } } diff --git a/apps/electron/scripts/before-make.mjs b/apps/electron/scripts/before-make.mjs index b24bac398f..ca6446b26e 100644 --- a/apps/electron/scripts/before-make.mjs +++ b/apps/electron/scripts/before-make.mjs @@ -27,18 +27,18 @@ console.log('build with following dir', { // step 0: clean up await cleanup(); -console.log('Clean up done'); +echo('Clean up done'); // step 1: build web (nextjs) dist cd(repoRootDir); -await $`pnpm i -r`; -await $`pnpm build`; -await $`pnpm export`; +await $`yarn add`; +await $`yarn build`; +await $`yarn export`; await fs.move(affineWebOutDir, publicAffineOutDir, { overwrite: true }); // step 2: build electron resources await buildLayers(); -console.log('Build layers done'); +echo('Build layers done'); /// -------- /// -------- diff --git a/packages/octobase-node/.gitignore b/packages/octobase-node/.gitignore new file mode 100644 index 0000000000..fe393f513b --- /dev/null +++ b/packages/octobase-node/.gitignore @@ -0,0 +1,197 @@ +# Created by https://www.toptal.com/developers/gitignore/api/node +# Edit at https://www.toptal.com/developers/gitignore?templates=node + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# End of https://www.toptal.com/developers/gitignore/api/node + +# Created by https://www.toptal.com/developers/gitignore/api/macos +# Edit at https://www.toptal.com/developers/gitignore?templates=macos + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +# End of https://www.toptal.com/developers/gitignore/api/macos + +# Created by https://www.toptal.com/developers/gitignore/api/windows +# Edit at https://www.toptal.com/developers/gitignore?templates=windows + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/windows + +#Added by cargo + +/target +Cargo.lock + +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +*.node diff --git a/packages/octobase-node/Cargo.toml b/packages/octobase-node/Cargo.toml new file mode 100644 index 0000000000..338d63e4f2 --- /dev/null +++ b/packages/octobase-node/Cargo.toml @@ -0,0 +1,29 @@ +[package] +edition = "2021" +name = "affine_octobase" +version = "0.0.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix +napi = { version = "2.11.1", default-features = false, features = ["napi4", "tokio_rt"] } +napi-derive = "2.11.0" +jwst = { git = "https://github.com/toeverything/OctoBase", rev = "b701935", package = "jwst" } +jwst-storage = { git = "https://github.com/toeverything/OctoBase", rev = "b701935", package = "jwst-storage", features = [ "sqlite"] } +cloud-database = { git = "https://github.com/toeverything/OctoBase", rev = "b701935", package = "cloud-database", features = [ "sqlite"] } +jwst-rpc = { git = "https://github.com/toeverything/OctoBase", rev = "b701935", package = "jwst-rpc" } +lib0 = "0.16.3" +tokio = "1.24.2" +yrs = "0.16.3" +bytes = "1.3.0" +futures = "^0.3.25" + +[build-dependencies] +napi-build = "2.0.1" + +[patch.crates-io] +rust-embed = { git = "https://github.com/pyrossh/rust-embed", rev = "7c0fc42" } +lib0 = { git = "https://github.com/toeverything/y-crdt", rev = "a3f7263" } +yrs = { git = "https://github.com/toeverything/y-crdt", rev = "a3f7263" } diff --git a/packages/octobase-node/build.rs b/packages/octobase-node/build.rs new file mode 100644 index 0000000000..1f866b6a3c --- /dev/null +++ b/packages/octobase-node/build.rs @@ -0,0 +1,5 @@ +extern crate napi_build; + +fn main() { + napi_build::setup(); +} diff --git a/packages/octobase-node/index.d.ts b/packages/octobase-node/index.d.ts new file mode 100644 index 0000000000..bcf7cb51d9 --- /dev/null +++ b/packages/octobase-node/index.d.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ + +/* auto-generated by NAPI-RS */ + +export class Storage { + constructor(path: string); + error(): string | null; + getBlob(workspaceId: string | undefined | null, id: string): Promise; + connect(workspaceId: string, remote: string): Workspace | null; + sync(workspaceId: string, remote: string): Workspace; +} +export class Workspace { + constructor(id: string); + id(): string; + clientId(): number; + search(query: string): string; + getSearchIndex(): Array; + setSearchIndex(fields: Array): boolean; +} diff --git a/packages/octobase-node/index.js b/packages/octobase-node/index.js new file mode 100644 index 0000000000..d6fedda9eb --- /dev/null +++ b/packages/octobase-node/index.js @@ -0,0 +1,269 @@ +/* tslint:disable */ +/* eslint-disable */ +/* prettier-ignore */ + +/* auto-generated by NAPI-RS */ + +const { existsSync, readFileSync } = require('fs') +const { join } = require('path'); + +const { platform, arch } = process; + +let nativeBinding = null; +let localFileExisted = false; +let loadError = null; + +function isMusl() { + // For Node 10 + if (!process.report || typeof process.report.getReport !== 'function') { + try { + const lddPath = require('child_process') + .execSync('which ldd') + .toString() + .trim(); + return readFileSync(lddPath, 'utf8').includes('musl'); + } catch (e) { + return true; + } + } else { + const { glibcVersionRuntime } = process.report.getReport().header; + return !glibcVersionRuntime; + } +} + +switch (platform) { + case 'android': + switch (arch) { + case 'arm64': + localFileExisted = existsSync( + join(__dirname, 'octobase.android-arm64.node') + ); + try { + if (localFileExisted) { + nativeBinding = require('./octobase.android-arm64.node'); + } else { + nativeBinding = require('@affine/octobase-node-android-arm64'); + } + } catch (e) { + loadError = e; + } + break; + case 'arm': + localFileExisted = existsSync( + join(__dirname, 'octobase.android-arm-eabi.node') + ); + try { + if (localFileExisted) { + nativeBinding = require('./octobase.android-arm-eabi.node'); + } else { + nativeBinding = require('@affine/octobase-node-android-arm-eabi'); + } + } catch (e) { + loadError = e; + } + break; + default: + throw new Error(`Unsupported architecture on Android ${arch}`); + } + break; + case 'win32': + switch (arch) { + case 'x64': + localFileExisted = existsSync( + join(__dirname, 'octobase.win32-x64-msvc.node') + ); + try { + if (localFileExisted) { + nativeBinding = require('./octobase.win32-x64-msvc.node'); + } else { + nativeBinding = require('@affine/octobase-node-win32-x64-msvc'); + } + } catch (e) { + loadError = e; + } + break; + case 'ia32': + localFileExisted = existsSync( + join(__dirname, 'octobase.win32-ia32-msvc.node') + ); + try { + if (localFileExisted) { + nativeBinding = require('./octobase.win32-ia32-msvc.node'); + } else { + nativeBinding = require('@affine/octobase-node-win32-ia32-msvc'); + } + } catch (e) { + loadError = e; + } + break; + case 'arm64': + localFileExisted = existsSync( + join(__dirname, 'octobase.win32-arm64-msvc.node') + ); + try { + if (localFileExisted) { + nativeBinding = require('./octobase.win32-arm64-msvc.node'); + } else { + nativeBinding = require('@affine/octobase-node-win32-arm64-msvc'); + } + } catch (e) { + loadError = e; + } + break; + default: + throw new Error(`Unsupported architecture on Windows: ${arch}`); + } + break; + case 'darwin': + localFileExisted = existsSync( + join(__dirname, 'octobase.darwin-universal.node') + ); + try { + if (localFileExisted) { + nativeBinding = require('./octobase.darwin-universal.node'); + } else { + nativeBinding = require('@affine/octobase-node-darwin-universal'); + } + break; + } catch {} + switch (arch) { + case 'x64': + localFileExisted = existsSync( + join(__dirname, 'octobase.darwin-x64.node') + ); + try { + if (localFileExisted) { + nativeBinding = require('./octobase.darwin-x64.node'); + } else { + nativeBinding = require('@affine/octobase-node-darwin-x64'); + } + } catch (e) { + loadError = e; + } + break; + case 'arm64': + localFileExisted = existsSync( + join(__dirname, 'octobase.darwin-arm64.node') + ); + try { + if (localFileExisted) { + nativeBinding = require('./octobase.darwin-arm64.node'); + } else { + nativeBinding = require('@affine/octobase-node-darwin-arm64'); + } + } catch (e) { + loadError = e; + } + break; + default: + throw new Error(`Unsupported architecture on macOS: ${arch}`); + } + break; + case 'freebsd': + if (arch !== 'x64') { + throw new Error(`Unsupported architecture on FreeBSD: ${arch}`); + } + localFileExisted = existsSync(join(__dirname, 'octobase.freebsd-x64.node')); + try { + if (localFileExisted) { + nativeBinding = require('./octobase.freebsd-x64.node'); + } else { + nativeBinding = require('@affine/octobase-node-freebsd-x64'); + } + } catch (e) { + loadError = e; + } + break; + case 'linux': + switch (arch) { + case 'x64': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'octobase.linux-x64-musl.node') + ); + try { + if (localFileExisted) { + nativeBinding = require('./octobase.linux-x64-musl.node'); + } else { + nativeBinding = require('@affine/octobase-node-linux-x64-musl'); + } + } catch (e) { + loadError = e; + } + } else { + localFileExisted = existsSync( + join(__dirname, 'octobase.linux-x64-gnu.node') + ); + try { + if (localFileExisted) { + nativeBinding = require('./octobase.linux-x64-gnu.node'); + } else { + nativeBinding = require('@affine/octobase-node-linux-x64-gnu'); + } + } catch (e) { + loadError = e; + } + } + break; + case 'arm64': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'octobase.linux-arm64-musl.node') + ); + try { + if (localFileExisted) { + nativeBinding = require('./octobase.linux-arm64-musl.node'); + } else { + nativeBinding = require('@affine/octobase-node-linux-arm64-musl'); + } + } catch (e) { + loadError = e; + } + } else { + localFileExisted = existsSync( + join(__dirname, 'octobase.linux-arm64-gnu.node') + ); + try { + if (localFileExisted) { + nativeBinding = require('./octobase.linux-arm64-gnu.node'); + } else { + nativeBinding = require('@affine/octobase-node-linux-arm64-gnu'); + } + } catch (e) { + loadError = e; + } + } + break; + case 'arm': + localFileExisted = existsSync( + join(__dirname, 'octobase.linux-arm-gnueabihf.node') + ); + try { + if (localFileExisted) { + nativeBinding = require('./octobase.linux-arm-gnueabihf.node'); + } else { + nativeBinding = require('@affine/octobase-node-linux-arm-gnueabihf'); + } + } catch (e) { + loadError = e; + } + break; + default: + throw new Error(`Unsupported architecture on Linux: ${arch}`); + } + break; + default: + throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`); +} + +if (!nativeBinding) { + if (loadError) { + throw loadError; + } + throw new Error(`Failed to load native binding`); +} + +const { Storage, Workspace } = nativeBinding; + +module.exports.Storage = Storage; +module.exports.Workspace = Workspace; diff --git a/packages/octobase-node/package.json b/packages/octobase-node/package.json new file mode 100644 index 0000000000..2d04363cd4 --- /dev/null +++ b/packages/octobase-node/package.json @@ -0,0 +1,29 @@ +{ + "name": "@affine/octobase-node", + "version": "0.0.0", + "main": "index.js", + "types": "index.d.ts", + "napi": { + "name": "octobase", + "triples": { + "additional": [ + "aarch64-apple-darwin" + ] + } + }, + "license": "MIT", + "devDependencies": { + "@napi-rs/cli": "^2.14.8", + "@types/node": "^18.15.5" + }, + "engines": { + "node": ">= 10" + }, + "scripts": { + "artifacts": "napi artifacts", + "build": "napi build --platform --release", + "build:debug": "napi build --platform", + "universal": "napi universal", + "version": "napi version" + } +} diff --git a/packages/octobase-node/src/block.rs b/packages/octobase-node/src/block.rs new file mode 100644 index 0000000000..3e251c6a93 --- /dev/null +++ b/packages/octobase-node/src/block.rs @@ -0,0 +1,216 @@ +use super::DynamicValue; +use jwst::{Block as JwstBlock, Workspace}; +use lib0::any::Any; + +#[napi()] +pub struct Block { + pub workspace: Workspace, + pub block: JwstBlock, +} + +#[napi()] +impl Block { + #[napi(constructor)] + pub fn new(workspace: Workspace, block: JwstBlock) -> Self { + Self { workspace, block } + } + + #[napi] + pub fn get(&self, key: String) -> Option { + self.workspace + .with_trx(|trx| self.block.get(&trx.trx, &key).map(DynamicValue::new)) + } + + #[napi] + pub fn children(&self) -> Vec { + self.workspace.with_trx(|trx| self.block.children(&trx.trx)) + } + + #[napi] + pub fn push_children(&self, block: &Block) { + self.workspace + .with_trx(|mut trx| self.block.push_children(&mut trx.trx, &block.block)); + } + + #[napi] + pub fn insert_children_at(&self, block: &Block, pos: u32) { + self.workspace.with_trx(|mut trx| { + self.block + .insert_children_at(&mut trx.trx, &block.block, pos) + }); + } + + #[napi] + pub fn insert_children_before(&self, block: &Block, reference: &str) { + self.workspace.with_trx(|mut trx| { + self.block + .insert_children_before(&mut trx.trx, &block.block, reference) + }); + } + + #[napi] + pub fn insert_children_after(&self, block: &Block, reference: &str) { + self.workspace.with_trx(|mut trx| { + self.block + .insert_children_after(&mut trx.trx, &block.block, reference) + }); + } + + #[napi] + pub fn remove_children(&self, block: &Block) { + self.workspace + .with_trx(|mut trx| self.block.remove_children(&mut trx.trx, &block.block)); + } + + #[napi] + pub fn exists_children(&self, block_id: &str) -> i32 { + self.workspace + .with_trx(|trx| self.block.exists_children(&trx.trx, block_id)) + .map(|i| i as i32) + .unwrap_or(-1) + } + + #[napi] + pub fn parent(&self) -> String { + self.workspace + .with_trx(|trx| self.block.parent(&trx.trx).unwrap()) + } + + #[napi] + pub fn updated(&self) -> u64 { + self.workspace.with_trx(|trx| self.block.updated(&trx.trx)) + } + + #[napi] + pub fn id(&self) -> String { + self.block.block_id() + } + + #[napi] + pub fn flavor(&self) -> String { + self.workspace.with_trx(|trx| self.block.flavor(&trx.trx)) + } + + #[napi] + pub fn version(&self) -> String { + self.workspace.with_trx(|trx| { + let [major, minor] = self.block.version(&trx.trx); + format!("{major}.{minor}") + }) + } + + #[napi] + pub fn created(&self) -> u64 { + self.workspace.with_trx(|trx| self.block.created(&trx.trx)) + } + + #[napi] + pub fn set_bool(&self, key: String, value: bool) { + self.workspace + .with_trx(|mut trx| self.block.set(&mut trx.trx, &key, value)); + } + + #[napi] + pub fn set_string(&self, key: String, value: String) { + self.workspace + .with_trx(|mut trx| self.block.set(&mut trx.trx, &key, value)); + } + + #[napi] + pub fn set_float(&self, key: String, value: f64) { + self.workspace + .with_trx(|mut trx| self.block.set(&mut trx.trx, &key, value)); + } + + #[napi] + pub fn set_integer(&self, key: String, value: i64) { + self.workspace + .with_trx(|mut trx| self.block.set(&mut trx.trx, &key, value)); + } + + #[napi] + pub fn set_null(&self, key: String) { + self.workspace + .with_trx(|mut trx| self.block.set(&mut trx.trx, &key, Any::Null)); + } + + #[napi] + pub fn is_bool(&self, key: String) -> bool { + self.workspace.with_trx(|trx| { + self.block + .get(&trx.trx, &key) + .map(|a| matches!(a, Any::Bool(_))) + .unwrap_or(false) + }) + } + + #[napi] + pub fn is_string(&self, key: String) -> bool { + self.workspace.with_trx(|trx| { + self.block + .get(&trx.trx, &key) + .map(|a| matches!(a, Any::String(_))) + .unwrap_or(false) + }) + } + + #[napi] + pub fn is_float(&self, key: String) -> bool { + self.workspace.with_trx(|trx| { + self.block + .get(&trx.trx, &key) + .map(|a| matches!(a, Any::Number(_))) + .unwrap_or(false) + }) + } + + #[napi] + pub fn is_integer(&self, key: String) -> bool { + self.workspace.with_trx(|trx| { + self.block + .get(&trx.trx, &key) + .map(|a| matches!(a, Any::BigInt(_))) + .unwrap_or(false) + }) + } + + #[napi] + pub fn get_bool(&self, key: String) -> Option { + self.workspace.with_trx(|trx| { + self.block.get(&trx.trx, &key).and_then(|a| match a { + Any::Bool(i) => Some(i.into()), + _ => None, + }) + }) + } + + #[napi] + pub fn get_string(&self, key: String) -> Option { + self.workspace.with_trx(|trx| { + self.block.get(&trx.trx, &key).and_then(|a| match a { + Any::String(i) => Some(i.into()), + _ => None, + }) + }) + } + + #[napi] + pub fn get_float(&self, key: String) -> Option { + self.workspace.with_trx(|trx| { + self.block.get(&trx.trx, &key).and_then(|a| match a { + Any::Number(i) => Some(i), + _ => None, + }) + }) + } + + #[napi] + pub fn get_integer(&self, key: String) -> Option { + self.workspace.with_trx(|trx| { + self.block.get(&trx.trx, &key).and_then(|a| match a { + Any::BigInt(i) => Some(i), + _ => None, + }) + }) + } +} diff --git a/packages/octobase-node/src/dynamic_value.rs b/packages/octobase-node/src/dynamic_value.rs new file mode 100644 index 0000000000..37b5e62f35 --- /dev/null +++ b/packages/octobase-node/src/dynamic_value.rs @@ -0,0 +1,68 @@ +use lib0::any::Any; +use std::collections::HashMap; + +pub type DynamicValueMap = HashMap; + +pub struct DynamicValue { + any: Any, +} + +impl DynamicValue { + pub fn new(any: Any) -> Self { + Self { any } + } + + pub fn as_bool(&self) -> Option { + match self.any { + Any::Bool(value) => Some(value), + _ => None, + } + } + + pub fn as_number(&self) -> Option { + match self.any { + Any::Number(value) => Some(value), + _ => None, + } + } + + pub fn as_int(&self) -> Option { + match self.any { + Any::BigInt(value) => Some(value), + _ => None, + } + } + + pub fn as_string(&self) -> Option { + match &self.any { + Any::String(value) => Some(value.to_string()), + _ => None, + } + } + + pub fn as_buffer(&self) -> Option> { + match &self.any { + Any::Buffer(value) => Some(value.to_vec()), + _ => None, + } + } + + pub fn as_array(&self) -> Option> { + match &self.any { + Any::Array(value) => Some(value.iter().map(|a| DynamicValue::new(a.clone())).collect()), + _ => None, + } + } + + pub fn as_map(&self) -> Option> { + match &self.any { + Any::Map(value) => Some( + value + .iter() + .map(|(key, value)| (key.clone(), DynamicValue::new(value.clone()))) + .collect(), + ), + _ => None, + } + } +} diff --git a/packages/octobase-node/src/lib.rs b/packages/octobase-node/src/lib.rs new file mode 100644 index 0000000000..a3c4b9e6aa --- /dev/null +++ b/packages/octobase-node/src/lib.rs @@ -0,0 +1,12 @@ +// mod block; +mod dynamic_value; +mod storage; +mod workspace; + +// pub use block::Block; +pub use dynamic_value::{DynamicValue, DynamicValueMap}; +pub use storage::Storage; +pub use workspace::Workspace; + +#[macro_use] +extern crate napi_derive; diff --git a/packages/octobase-node/src/storage.rs b/packages/octobase-node/src/storage.rs new file mode 100644 index 0000000000..d8233c04e2 --- /dev/null +++ b/packages/octobase-node/src/storage.rs @@ -0,0 +1,125 @@ +use crate::Workspace; +use jwst::{error, info, BlobStorage, DocStorage}; +use jwst_rpc::start_client; +use jwst_storage::JwstStorage as AutoStorage; +use std::sync::Arc; +use tokio::{runtime::Runtime, sync::RwLock}; +use napi::bindgen_prelude::*; +use napi::{Error, Result, Status}; + +#[napi] +#[derive(Clone)] +pub struct Storage { + pub(crate) storage: Option>>, + pub(crate) error: Option, +} + +#[napi] +impl Storage { + #[napi(constructor)] + pub fn new(path: String) -> Self { + let rt = Runtime::new().unwrap(); + + // FIXME: do not use block_on + match rt.block_on(AutoStorage::new(&format!("sqlite:{path}?mode=rwc"))) { + Ok(pool) => Self { + storage: Some(Arc::new(RwLock::new(pool))), + error: None, + }, + Err(e) => Self { + storage: None, + error: Some(e.to_string()), + }, + } + } + + #[napi] + pub fn error(&self) -> Option { + self.error.clone() + } + + #[napi] + pub async fn get_blob(&self, workspace_id: Option, id: String) -> Result { + if let Some(storage) = &self.storage { + let storage_handle = storage.read().await; + let blobs = storage_handle.blobs(); + + let blob = blobs.get_blob(workspace_id.clone(), id.clone(), None).await.map_err(|e| { + Error::new( + Status::GenericFailure, + format!( + "Failed to get blob file {}/{} from storage, error: {}", + workspace_id.clone().unwrap_or_default().to_string(), + id, + e + ), + ) + })?; + + Ok(blob.into()) + } else { + return Err(Error::new( + Status::GenericFailure, + "Storage is not connected", + )); + } + } + + #[napi] + pub fn connect(&mut self, workspace_id: String, remote: String) -> Option { + match self.sync(workspace_id, remote) { + Ok(workspace) => Some(workspace), + Err(e) => { + error!("Failed to connect to workspace: {}", e); + self.error = Some(e.to_string()); + None + } + } + } + + #[napi] + pub fn sync(&self, workspace_id: String, remote: String) -> Result { + if let Some(storage) = &self.storage { + let rt = Runtime::new().unwrap(); + + // FIXME: do not use block_on + let mut workspace = rt + .block_on(async move { + let storage = storage.read().await; + + start_client(&storage, workspace_id, remote).await + }) + .map_err(|e| Error::new(Status::GenericFailure, e.to_string()))?; + + let (sub, workspace) = { + let id = workspace.id(); + let storage = self.storage.clone(); + let sub = workspace.observe(move |_, e| { + let id = id.clone(); + if let Some(storage) = storage.clone() { + let rt = Runtime::new().unwrap(); + info!("update: {:?}", &e.update); + if let Err(e) = rt.block_on(async move { + let storage = storage.write().await; + storage.docs().write_update(id, &e.update).await + }) { + error!("Failed to write update to storage: {}", e); + } + } + }); + + (sub, workspace) + }; + + Ok(Workspace { + workspace, + _sub: sub, + }) + } else { + Err(Error::new( + Status::GenericFailure, + "Storage is not connected", + )) + } + } +} diff --git a/packages/octobase-node/src/workspace.rs b/packages/octobase-node/src/workspace.rs new file mode 100644 index 0000000000..e0db27249d --- /dev/null +++ b/packages/octobase-node/src/workspace.rs @@ -0,0 +1,84 @@ +// use super::Block; +use jwst::Workspace as JwstWorkspace; +use yrs::UpdateSubscription; + + +#[napi()] +pub struct Workspace { + pub(crate) workspace: JwstWorkspace, + pub(crate) _sub: Option, +} + +#[napi()] +impl Workspace { + #[napi(constructor)] + pub fn new(id: String) -> Self { + Self { + workspace: JwstWorkspace::new(id), + _sub: None, + } + } + + #[napi] + pub fn id(&self) -> String { + self.workspace.id() + } + + #[napi] + pub fn client_id(&self) -> i64 { + self.workspace.client_id() as i64 + } + + // #[napi] + // pub fn get(&self, block_id: String) -> Option { + // let workspace = self.workspace.clone(); + // self.workspace.with_trx(|mut trx| { + // let block = trx + // .get_blocks() + // .get(&trx.trx, &block_id) + // .map(|b| Block::new(workspace, b)); + // drop(trx); + // block + // }) + // } + + // #[napi] + // pub fn create(&self, block_id: String, flavor: String) -> Block { + // let workspace = self.workspace.clone(); + // self.workspace.with_trx(|mut trx| { + // let block = Block::new( + // workspace, + // trx.get_blocks().create(&mut trx.trx, block_id, flavor), + // ); + // drop(trx); + // block + // }) + // } + + #[napi] + pub fn search(&self, query: String) -> String { + self.workspace.search_result(query) + } + + // #[napi] + // pub fn get_blocks_by_flavour(&self, flavour: &str) -> Vec { + // self.workspace + // .with_trx(|mut trx| trx.get_blocks().get_blocks_by_flavour(&trx.trx, flavour)) + // .iter() + // .map(|block| Block { + // workspace: self.workspace.clone(), + // block: block.clone(), + // }) + // .collect() + // } + + #[napi] + pub fn get_search_index(&self) -> Vec { + self.workspace.metadata().search_index + } + + #[napi] + pub fn set_search_index(&self, fields: Vec) -> bool { + self.workspace.set_search_index(fields) + } +} diff --git a/yarn.lock b/yarn.lock index c2b699a630..c028cbae52 100644 --- a/yarn.lock +++ b/yarn.lock @@ -148,6 +148,7 @@ __metadata: version: 0.0.0-use.local resolution: "@affine/electron@workspace:apps/electron" dependencies: + "@affine/octobase-node": "workspace:*" "@electron-forge/cli": ^6.0.5 "@electron-forge/core": ^6.0.5 "@electron-forge/core-utils": ^6.0.5 @@ -162,6 +163,7 @@ __metadata: electron: 23.1.4 electron-window-state: ^5.0.3 esbuild: ^0.17.12 + fs-extra: ^11.1.1 zx: ^7.2.1 languageName: unknown linkType: soft @@ -197,6 +199,15 @@ __metadata: languageName: unknown linkType: soft +"@affine/octobase-node@workspace:*, @affine/octobase-node@workspace:packages/octobase-node": + version: 0.0.0-use.local + resolution: "@affine/octobase-node@workspace:packages/octobase-node" + dependencies: + "@napi-rs/cli": ^2.14.8 + "@types/node": ^18.15.5 + languageName: unknown + linkType: soft + "@affine/templates@workspace:*, @affine/templates@workspace:packages/templates": version: 0.0.0-use.local resolution: "@affine/templates@workspace:packages/templates" @@ -4106,6 +4117,15 @@ __metadata: languageName: node linkType: hard +"@napi-rs/cli@npm:^2.14.8": + version: 2.15.2 + resolution: "@napi-rs/cli@npm:2.15.2" + bin: + napi: scripts/index.js + checksum: dedcbd339f634e4a4c442febbfaee7c0fd907f3b8082c1f3ddba18b0e02ae2f0e4bff75688763669db5fb1a0f5ab26a7200cb6c01fe8825effd0a70a44bcbbc7 + languageName: node + linkType: hard + "@ndelangen/get-tarball@npm:^3.0.7": version: 3.0.7 resolution: "@ndelangen/get-tarball@npm:3.0.7" @@ -12154,7 +12174,7 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:^11.1.0": +"fs-extra@npm:^11.1.0, fs-extra@npm:^11.1.1": version: 11.1.1 resolution: "fs-extra@npm:11.1.1" dependencies: