mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
feat(y-octo): import y-octo monorepo (#11750)
This commit is contained in:
2
packages/common/y-octo/node/.gitignore
vendored
Normal file
2
packages/common/y-octo/node/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*.node
|
||||
.coverage
|
||||
20
packages/common/y-octo/node/Cargo.toml
Normal file
20
packages/common/y-octo/node/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
authors = ["DarkSky <darksky2048@gmail.com>"]
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "y-octo-node"
|
||||
repository = "https://github.com/toeverything/y-octo"
|
||||
version = "0.0.1"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
napi = { workspace = true, features = ["anyhow", "napi4"] }
|
||||
napi-derive = { workspace = true }
|
||||
y-octo = { workspace = true, features = ["large_refs"] }
|
||||
|
||||
[build-dependencies]
|
||||
napi-build = { workspace = true }
|
||||
3
packages/common/y-octo/node/build.rs
Normal file
3
packages/common/y-octo/node/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
napi_build::setup();
|
||||
}
|
||||
48
packages/common/y-octo/node/index.d.ts
vendored
Normal file
48
packages/common/y-octo/node/index.d.ts
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
/* auto-generated by NAPI-RS */
|
||||
/* eslint-disable */
|
||||
export declare class Doc {
|
||||
constructor(clientId?: number | undefined | null)
|
||||
get clientId(): number
|
||||
get guid(): string
|
||||
get keys(): Array<string>
|
||||
getOrCreateArray(key: string): YArray
|
||||
getOrCreateText(key: string): YText
|
||||
getOrCreateMap(key: string): YMap
|
||||
createArray(): YArray
|
||||
createText(): YText
|
||||
createMap(): YMap
|
||||
applyUpdate(update: Uint8Array): void
|
||||
encodeStateAsUpdateV1(state?: Uint8Array | undefined | null): Uint8Array
|
||||
gc(): void
|
||||
onUpdate(callback: (result: Uint8Array) => void): void
|
||||
}
|
||||
|
||||
export declare class YArray {
|
||||
constructor()
|
||||
get length(): number
|
||||
get isEmpty(): boolean
|
||||
get<T = unknown>(index: number): T
|
||||
insert(index: number, value: YArray | YMap | YText | boolean | number | string | Record<string, any> | null | undefined): void
|
||||
remove(index: number, len: number): void
|
||||
toJson(): JsArray
|
||||
}
|
||||
|
||||
export declare class YMap {
|
||||
constructor()
|
||||
get length(): number
|
||||
get isEmpty(): boolean
|
||||
get<T = unknown>(key: string): T
|
||||
set(key: string, value: YArray | YMap | YText | boolean | number | string | Record<string, any> | null | undefined): void
|
||||
remove(key: string): void
|
||||
toJson(): object
|
||||
}
|
||||
|
||||
export declare class YText {
|
||||
constructor()
|
||||
get len(): number
|
||||
get isEmpty(): boolean
|
||||
insert(index: number, str: string): void
|
||||
remove(index: number, len: number): void
|
||||
get length(): number
|
||||
toString(): string
|
||||
}
|
||||
377
packages/common/y-octo/node/index.js
Normal file
377
packages/common/y-octo/node/index.js
Normal file
@@ -0,0 +1,377 @@
|
||||
// prettier-ignore
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
/* auto-generated by NAPI-RS */
|
||||
|
||||
const { createRequire } = require('node:module')
|
||||
require = createRequire(__filename);
|
||||
|
||||
const { readFileSync } = require('node:fs');
|
||||
let nativeBinding = null;
|
||||
const loadErrors = [];
|
||||
|
||||
const isMusl = () => {
|
||||
let musl = false;
|
||||
if (process.platform === 'linux') {
|
||||
musl = isMuslFromFilesystem();
|
||||
if (musl === null) {
|
||||
musl = isMuslFromReport();
|
||||
}
|
||||
if (musl === null) {
|
||||
musl = isMuslFromChildProcess();
|
||||
}
|
||||
}
|
||||
return musl;
|
||||
};
|
||||
|
||||
const isFileMusl = f => f.includes('libc.musl-') || f.includes('ld-musl-');
|
||||
|
||||
const isMuslFromFilesystem = () => {
|
||||
try {
|
||||
return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const isMuslFromReport = () => {
|
||||
let report = null;
|
||||
if (typeof process.report?.getReport === 'function') {
|
||||
process.report.excludeNetwork = true;
|
||||
report = process.report.getReport();
|
||||
}
|
||||
if (!report) {
|
||||
return null;
|
||||
}
|
||||
if (report.header && report.header.glibcVersionRuntime) {
|
||||
return false;
|
||||
}
|
||||
if (Array.isArray(report.sharedObjects)) {
|
||||
if (report.sharedObjects.some(isFileMusl)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const isMuslFromChildProcess = () => {
|
||||
try {
|
||||
return require('child_process')
|
||||
.execSync('ldd --version', { encoding: 'utf8' })
|
||||
.includes('musl');
|
||||
} catch (e) {
|
||||
// If we reach this case, we don't know if the system is musl or not, so is better to just fallback to false
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
function requireNative() {
|
||||
if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) {
|
||||
try {
|
||||
nativeBinding = require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH);
|
||||
} catch (err) {
|
||||
loadErrors.push(err);
|
||||
}
|
||||
} else if (process.platform === 'android') {
|
||||
if (process.arch === 'arm64') {
|
||||
try {
|
||||
return require('./y-octo.android-arm64.node');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
try {
|
||||
return require('@y-octo/node-android-arm64');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
} else if (process.arch === 'arm') {
|
||||
try {
|
||||
return require('./y-octo.android-arm-eabi.node');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
try {
|
||||
return require('@y-octo/node-android-arm-eabi');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(
|
||||
new Error(`Unsupported architecture on Android ${process.arch}`)
|
||||
);
|
||||
}
|
||||
} else if (process.platform === 'win32') {
|
||||
if (process.arch === 'x64') {
|
||||
try {
|
||||
return require('./y-octo.win32-x64-msvc.node');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
try {
|
||||
return require('@y-octo/node-win32-x64-msvc');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
} else if (process.arch === 'ia32') {
|
||||
try {
|
||||
return require('./y-octo.win32-ia32-msvc.node');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
try {
|
||||
return require('@y-octo/node-win32-ia32-msvc');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
} else if (process.arch === 'arm64') {
|
||||
try {
|
||||
return require('./y-octo.win32-arm64-msvc.node');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
try {
|
||||
return require('@y-octo/node-win32-arm64-msvc');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(
|
||||
new Error(`Unsupported architecture on Windows: ${process.arch}`)
|
||||
);
|
||||
}
|
||||
} else if (process.platform === 'darwin') {
|
||||
try {
|
||||
return require('./y-octo.darwin-universal.node');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
try {
|
||||
return require('@y-octo/node-darwin-universal');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
|
||||
if (process.arch === 'x64') {
|
||||
try {
|
||||
return require('./y-octo.darwin-x64.node');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
try {
|
||||
return require('@y-octo/node-darwin-x64');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
} else if (process.arch === 'arm64') {
|
||||
try {
|
||||
return require('./y-octo.darwin-arm64.node');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
try {
|
||||
return require('@y-octo/node-darwin-arm64');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(
|
||||
new Error(`Unsupported architecture on macOS: ${process.arch}`)
|
||||
);
|
||||
}
|
||||
} else if (process.platform === 'freebsd') {
|
||||
if (process.arch === 'x64') {
|
||||
try {
|
||||
return require('./y-octo.freebsd-x64.node');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
try {
|
||||
return require('@y-octo/node-freebsd-x64');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
} else if (process.arch === 'arm64') {
|
||||
try {
|
||||
return require('./y-octo.freebsd-arm64.node');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
try {
|
||||
return require('@y-octo/node-freebsd-arm64');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(
|
||||
new Error(`Unsupported architecture on FreeBSD: ${process.arch}`)
|
||||
);
|
||||
}
|
||||
} else if (process.platform === 'linux') {
|
||||
if (process.arch === 'x64') {
|
||||
if (isMusl()) {
|
||||
try {
|
||||
return require('./y-octo.linux-x64-musl.node');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
try {
|
||||
return require('@y-octo/node-linux-x64-musl');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./y-octo.linux-x64-gnu.node');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
try {
|
||||
return require('@y-octo/node-linux-x64-gnu');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'arm64') {
|
||||
if (isMusl()) {
|
||||
try {
|
||||
return require('./y-octo.linux-arm64-musl.node');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
try {
|
||||
return require('@y-octo/node-linux-arm64-musl');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./y-octo.linux-arm64-gnu.node');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
try {
|
||||
return require('@y-octo/node-linux-arm64-gnu');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'arm') {
|
||||
if (isMusl()) {
|
||||
try {
|
||||
return require('./y-octo.linux-arm-musleabihf.node');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
try {
|
||||
return require('@y-octo/node-linux-arm-musleabihf');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./y-octo.linux-arm-gnueabihf.node');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
try {
|
||||
return require('@y-octo/node-linux-arm-gnueabihf');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'riscv64') {
|
||||
if (isMusl()) {
|
||||
try {
|
||||
return require('./y-octo.linux-riscv64-musl.node');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
try {
|
||||
return require('@y-octo/node-linux-riscv64-musl');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./y-octo.linux-riscv64-gnu.node');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
try {
|
||||
return require('@y-octo/node-linux-riscv64-gnu');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'ppc64') {
|
||||
try {
|
||||
return require('./y-octo.linux-ppc64-gnu.node');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
try {
|
||||
return require('@y-octo/node-linux-ppc64-gnu');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
} else if (process.arch === 's390x') {
|
||||
try {
|
||||
return require('./y-octo.linux-s390x-gnu.node');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
try {
|
||||
return require('@y-octo/node-linux-s390x-gnu');
|
||||
} catch (e) {
|
||||
loadErrors.push(e);
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(
|
||||
new Error(`Unsupported architecture on Linux: ${process.arch}`)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(
|
||||
new Error(
|
||||
`Unsupported OS: ${process.platform}, architecture: ${process.arch}`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
nativeBinding = requireNative();
|
||||
|
||||
if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) {
|
||||
try {
|
||||
nativeBinding = require('./y-octo.wasi.cjs');
|
||||
} catch (err) {
|
||||
if (process.env.NAPI_RS_FORCE_WASI) {
|
||||
loadErrors.push(err);
|
||||
}
|
||||
}
|
||||
if (!nativeBinding) {
|
||||
try {
|
||||
nativeBinding = require('@y-octo/node-wasm32-wasi');
|
||||
} catch (err) {
|
||||
if (process.env.NAPI_RS_FORCE_WASI) {
|
||||
loadErrors.push(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!nativeBinding) {
|
||||
if (loadErrors.length > 0) {
|
||||
// TODO Link to documentation with potential fixes
|
||||
// - The package owner could build/publish bindings for this arch
|
||||
// - The user may need to bundle the correct files
|
||||
// - The user may need to re-install node_modules to get new packages
|
||||
throw new Error('Failed to load native binding', { cause: loadErrors });
|
||||
}
|
||||
throw new Error(`Failed to load native binding`);
|
||||
}
|
||||
|
||||
module.exports.Doc = nativeBinding.Doc;
|
||||
module.exports.YArray = nativeBinding.YArray;
|
||||
module.exports.YMap = nativeBinding.YMap;
|
||||
module.exports.YText = nativeBinding.YText;
|
||||
72
packages/common/y-octo/node/package.json
Normal file
72
packages/common/y-octo/node/package.json
Normal file
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"name": "@y-octo/node",
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"napi": {
|
||||
"binaryName": "y-octo",
|
||||
"targets": [
|
||||
"x86_64-unknown-linux-gnu",
|
||||
"x86_64-apple-darwin",
|
||||
"x86_64-pc-windows-msvc",
|
||||
"aarch64-apple-darwin",
|
||||
"aarch64-pc-windows-msvc",
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"x86_64-unknown-linux-musl",
|
||||
"aarch64-unknown-linux-musl"
|
||||
],
|
||||
"ts": {
|
||||
"constEnum": false
|
||||
}
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "3.0.0-alpha.77",
|
||||
"@types/node": "^22.14.1",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"c8": "^10.1.3",
|
||||
"prompts": "^2.4.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.8.3",
|
||||
"yjs": "^13.6.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"scripts": {
|
||||
"artifacts": "napi artifacts",
|
||||
"build": "napi build --platform --release --no-const-enum",
|
||||
"build:debug": "napi build --platform --no-const-enum",
|
||||
"universal": "napi universal",
|
||||
"test": "NODE_NO_WARNINGS=1 node ./scripts/run-test.mts all",
|
||||
"test:watch": "yarn exec ts-node-esm ./scripts/run-test.ts all --watch",
|
||||
"test:coverage": "NODE_OPTIONS=\"--loader ts-node/esm\" c8 node ./scripts/run-test.mts all",
|
||||
"version": "napi version"
|
||||
},
|
||||
"version": "0.0.1",
|
||||
"sharedConfig": {
|
||||
"nodeArgs": [
|
||||
"--loader",
|
||||
"ts-node/esm",
|
||||
"--es-module-specifier-resolution=node"
|
||||
],
|
||||
"env": {
|
||||
"TS_NODE_TRANSPILE_ONLY": "1",
|
||||
"TS_NODE_PROJECT": "./tsconfig.json",
|
||||
"NODE_ENV": "development",
|
||||
"DEBUG": "napi:*"
|
||||
}
|
||||
},
|
||||
"c8": {
|
||||
"reporter": [
|
||||
"text",
|
||||
"lcov"
|
||||
],
|
||||
"report-dir": ".coverage",
|
||||
"exclude": [
|
||||
"scripts",
|
||||
"node_modules",
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
78
packages/common/y-octo/node/scripts/run-test.mts
Executable file
78
packages/common/y-octo/node/scripts/run-test.mts
Executable file
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env ts-node-esm
|
||||
import { resolve } from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { readdir } from 'node:fs/promises';
|
||||
import * as process from 'node:process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import prompts from 'prompts';
|
||||
|
||||
import pkg from '../package.json' with { type: 'json' };
|
||||
const root = fileURLToPath(new URL('..', import.meta.url));
|
||||
const testDir = resolve(root, 'tests');
|
||||
const files = await readdir(testDir);
|
||||
|
||||
const watchMode = process.argv.includes('--watch');
|
||||
|
||||
const sharedArgs = [
|
||||
...pkg.sharedConfig.nodeArgs,
|
||||
'--test',
|
||||
watchMode ? '--watch' : '',
|
||||
];
|
||||
|
||||
const env = {
|
||||
...pkg.sharedConfig.env,
|
||||
PATH: process.env.PATH,
|
||||
NODE_ENV: 'test',
|
||||
NODE_NO_WARNINGS: '1',
|
||||
};
|
||||
|
||||
if (process.argv[2] === 'all') {
|
||||
const cp = spawn(
|
||||
'node',
|
||||
[...sharedArgs, ...files.map(f => resolve(testDir, f))],
|
||||
{
|
||||
cwd: root,
|
||||
env,
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
}
|
||||
);
|
||||
cp.on('exit', code => {
|
||||
process.exit(code ?? 0);
|
||||
});
|
||||
} else {
|
||||
const result = await prompts([
|
||||
{
|
||||
type: 'select',
|
||||
name: 'file',
|
||||
message: 'Select a file to run',
|
||||
choices: files.map(file => ({
|
||||
title: file,
|
||||
value: file,
|
||||
})),
|
||||
initial: 1,
|
||||
},
|
||||
]);
|
||||
|
||||
const target = resolve(testDir, result.file);
|
||||
|
||||
const cp = spawn(
|
||||
'node',
|
||||
[
|
||||
...sharedArgs,
|
||||
'--test-reporter=spec',
|
||||
'--test-reporter-destination=stdout',
|
||||
target,
|
||||
],
|
||||
{
|
||||
cwd: root,
|
||||
env,
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
}
|
||||
);
|
||||
cp.on('exit', code => {
|
||||
process.exit(code ?? 0);
|
||||
});
|
||||
}
|
||||
160
packages/common/y-octo/node/src/array.rs
Normal file
160
packages/common/y-octo/node/src/array.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
use napi::{bindgen_prelude::Array as JsArray, Env, JsUnknown, ValueType};
|
||||
use y_octo::{Any, Array, Value};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[napi]
|
||||
pub struct YArray {
|
||||
pub(crate) array: Array,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl YArray {
|
||||
#[allow(clippy::new_without_default)]
|
||||
#[napi(constructor)]
|
||||
pub fn new() -> Self {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub(crate) fn inner_new(array: Array) -> Self {
|
||||
Self { array }
|
||||
}
|
||||
|
||||
#[napi(getter)]
|
||||
pub fn length(&self) -> i64 {
|
||||
self.array.len() as i64
|
||||
}
|
||||
|
||||
#[napi(getter)]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.array.is_empty()
|
||||
}
|
||||
|
||||
#[napi(ts_generic_types = "T = unknown", ts_return_type = "T")]
|
||||
pub fn get(&self, env: Env, index: i64) -> Result<MixedYType> {
|
||||
if let Some(value) = self.array.get(index as u64) {
|
||||
match value {
|
||||
Value::Any(any) => get_js_unknown_from_any(env, any).map(MixedYType::D),
|
||||
Value::Array(array) => Ok(MixedYType::A(YArray::inner_new(array))),
|
||||
Value::Map(map) => Ok(MixedYType::B(YMap::inner_new(map))),
|
||||
Value::Text(text) => Ok(MixedYType::C(YText::inner_new(text))),
|
||||
_ => env.get_null().map(|v| v.into_unknown()).map(MixedYType::D),
|
||||
}
|
||||
.map_err(anyhow::Error::from)
|
||||
} else {
|
||||
Ok(MixedYType::D(env.get_null()?.into_unknown()))
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(
|
||||
ts_args_type = "index: number, value: YArray | YMap | YText | boolean | number | string | \
|
||||
Record<string, any> | null | undefined"
|
||||
)]
|
||||
pub fn insert(&mut self, index: i64, value: MixedRefYType) -> Result<()> {
|
||||
match value {
|
||||
MixedRefYType::A(array) => self
|
||||
.array
|
||||
.insert(index as u64, array.array.clone())
|
||||
.map_err(anyhow::Error::from),
|
||||
MixedRefYType::B(map) => self
|
||||
.array
|
||||
.insert(index as u64, map.map.clone())
|
||||
.map_err(anyhow::Error::from),
|
||||
MixedRefYType::C(text) => self
|
||||
.array
|
||||
.insert(index as u64, text.text.clone())
|
||||
.map_err(anyhow::Error::from),
|
||||
MixedRefYType::D(unknown) => match unknown.get_type() {
|
||||
Ok(value_type) => match value_type {
|
||||
ValueType::Undefined | ValueType::Null => self
|
||||
.array
|
||||
.insert(index as u64, Any::Null)
|
||||
.map_err(anyhow::Error::from),
|
||||
ValueType::Boolean => match unknown.coerce_to_bool().and_then(|v| v.get_value()) {
|
||||
Ok(boolean) => self
|
||||
.array
|
||||
.insert(index as u64, boolean)
|
||||
.map_err(anyhow::Error::from),
|
||||
Err(e) => Err(anyhow::Error::new(e).context("Failed to coerce value to boolean")),
|
||||
},
|
||||
ValueType::Number => match unknown.coerce_to_number().and_then(|v| v.get_double()) {
|
||||
Ok(number) => self
|
||||
.array
|
||||
.insert(index as u64, number)
|
||||
.map_err(anyhow::Error::from),
|
||||
Err(e) => Err(anyhow::Error::new(e).context("Failed to coerce value to number")),
|
||||
},
|
||||
ValueType::String => {
|
||||
match unknown
|
||||
.coerce_to_string()
|
||||
.and_then(|v| v.into_utf8())
|
||||
.and_then(|s| s.as_str().map(|s| s.to_string()))
|
||||
{
|
||||
Ok(string) => self
|
||||
.array
|
||||
.insert(index as u64, string)
|
||||
.map_err(anyhow::Error::from),
|
||||
Err(e) => Err(anyhow::Error::new(e).context("Failed to coerce value to string")),
|
||||
}
|
||||
}
|
||||
ValueType::Object => match unknown
|
||||
.coerce_to_object()
|
||||
.and_then(|o| o.get_array_length().map(|l| (o, l)))
|
||||
{
|
||||
Ok((object, length)) => {
|
||||
for i in 0..length {
|
||||
if let Ok(any) = object
|
||||
.get_element::<JsUnknown>(i)
|
||||
.and_then(get_any_from_js_unknown)
|
||||
{
|
||||
self
|
||||
.array
|
||||
.insert(index as u64 + i as u64, Value::Any(any))
|
||||
.map_err(anyhow::Error::from)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(anyhow::Error::new(e).context("Failed to coerce value to object")),
|
||||
},
|
||||
ValueType::BigInt => Err(anyhow::Error::msg("BigInt values are not supported")),
|
||||
ValueType::Symbol => Err(anyhow::Error::msg("Symbol values are not supported")),
|
||||
ValueType::Function => Err(anyhow::Error::msg("Function values are not supported")),
|
||||
ValueType::External => Err(anyhow::Error::msg("External values are not supported")),
|
||||
ValueType::Unknown => Err(anyhow::Error::msg("Unknown values are not supported")),
|
||||
},
|
||||
Err(e) => Err(anyhow::Error::from(e)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn remove(&mut self, index: i64, len: i64) -> Result<()> {
|
||||
self
|
||||
.array
|
||||
.remove(index as u64, len as u64)
|
||||
.map_err(anyhow::Error::from)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn to_json(&self, env: Env) -> Result<JsArray> {
|
||||
let mut js_array = env.create_array(0)?;
|
||||
for value in self.array.iter() {
|
||||
js_array.insert(get_js_unknown_from_value(env, value)?)?;
|
||||
}
|
||||
Ok(js_array)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_array_init() {
|
||||
let doc = Doc::new(None);
|
||||
let array = doc.get_or_create_array("array".into()).unwrap();
|
||||
assert_eq!(array.length(), 0);
|
||||
}
|
||||
}
|
||||
176
packages/common/y-octo/node/src/doc.rs
Normal file
176
packages/common/y-octo/node/src/doc.rs
Normal file
@@ -0,0 +1,176 @@
|
||||
use napi::{
|
||||
bindgen_prelude::{Buffer, Uint8Array},
|
||||
threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
|
||||
};
|
||||
use y_octo::{CrdtRead, Doc as YDoc, History, RawDecoder, StateVector};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[napi]
|
||||
pub struct Doc {
|
||||
doc: YDoc,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl Doc {
|
||||
#[napi(constructor)]
|
||||
pub fn new(client_id: Option<i64>) -> Self {
|
||||
Self {
|
||||
doc: if let Some(client_id) = client_id {
|
||||
YDoc::with_client(client_id as u64)
|
||||
} else {
|
||||
YDoc::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(getter)]
|
||||
pub fn client_id(&self) -> i64 {
|
||||
self.doc.client() as i64
|
||||
}
|
||||
|
||||
#[napi(getter)]
|
||||
pub fn guid(&self) -> &str {
|
||||
self.doc.guid()
|
||||
}
|
||||
|
||||
#[napi(getter)]
|
||||
pub fn keys(&self) -> Vec<String> {
|
||||
self.doc.keys()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_or_create_array(&self, key: String) -> Result<YArray> {
|
||||
self
|
||||
.doc
|
||||
.get_or_create_array(key)
|
||||
.map(YArray::inner_new)
|
||||
.map_err(anyhow::Error::from)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_or_create_text(&self, key: String) -> Result<YText> {
|
||||
self
|
||||
.doc
|
||||
.get_or_create_text(key)
|
||||
.map(YText::inner_new)
|
||||
.map_err(anyhow::Error::from)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_or_create_map(&self, key: String) -> Result<YMap> {
|
||||
self
|
||||
.doc
|
||||
.get_or_create_map(key)
|
||||
.map(YMap::inner_new)
|
||||
.map_err(anyhow::Error::from)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn create_array(&self) -> Result<YArray> {
|
||||
self
|
||||
.doc
|
||||
.create_array()
|
||||
.map(YArray::inner_new)
|
||||
.map_err(anyhow::Error::from)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn create_text(&self) -> Result<YText> {
|
||||
self
|
||||
.doc
|
||||
.create_text()
|
||||
.map(YText::inner_new)
|
||||
.map_err(anyhow::Error::from)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn create_map(&self) -> Result<YMap> {
|
||||
self
|
||||
.doc
|
||||
.create_map()
|
||||
.map(YMap::inner_new)
|
||||
.map_err(anyhow::Error::from)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn apply_update(&mut self, update: &[u8]) -> Result<()> {
|
||||
self.doc.apply_update_from_binary_v1(update)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn encode_state_as_update_v1(&self, state: Option<&[u8]>) -> Result<Uint8Array> {
|
||||
let result = match state {
|
||||
Some(state) => {
|
||||
let mut decoder = RawDecoder::new(state);
|
||||
let state = StateVector::read(&mut decoder)?;
|
||||
self.doc.encode_state_as_update_v1(&state)
|
||||
}
|
||||
None => self.doc.encode_update_v1(),
|
||||
};
|
||||
|
||||
result.map(|v| v.into()).map_err(anyhow::Error::from)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn gc(&self) -> Result<()> {
|
||||
self.doc.gc().map_err(anyhow::Error::from)
|
||||
}
|
||||
|
||||
#[napi(ts_args_type = "callback: (result: Uint8Array) => void")]
|
||||
pub fn on_update(&mut self, callback: ThreadsafeFunction<Buffer>) -> Result<()> {
|
||||
let callback = move |update: &[u8], _h: &[History]| {
|
||||
callback.call(
|
||||
Ok(update.to_vec().into()),
|
||||
ThreadsafeFunctionCallMode::Blocking,
|
||||
);
|
||||
};
|
||||
self.doc.subscribe(Box::new(callback));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_doc_client() {
|
||||
let client_id = 1;
|
||||
let doc = Doc::new(Some(client_id));
|
||||
assert_eq!(doc.client_id(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_doc_guid() {
|
||||
let doc = Doc::new(None);
|
||||
assert_eq!(doc.guid().len(), 21);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_array() {
|
||||
let doc = Doc::new(None);
|
||||
let array = doc.get_or_create_array("array".into()).unwrap();
|
||||
assert_eq!(array.length(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_text() {
|
||||
let doc = Doc::new(None);
|
||||
let text = doc.get_or_create_text("text".into()).unwrap();
|
||||
assert_eq!(text.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keys() {
|
||||
let doc = Doc::new(None);
|
||||
doc.get_or_create_array("array".into()).unwrap();
|
||||
doc.get_or_create_text("text".into()).unwrap();
|
||||
doc.get_or_create_map("map".into()).unwrap();
|
||||
let mut keys = doc.keys();
|
||||
keys.sort();
|
||||
assert_eq!(keys, vec!["array", "map", "text"]);
|
||||
}
|
||||
}
|
||||
17
packages/common/y-octo/node/src/lib.rs
Normal file
17
packages/common/y-octo/node/src/lib.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use anyhow::Result;
|
||||
use napi_derive::napi;
|
||||
|
||||
mod array;
|
||||
mod doc;
|
||||
mod map;
|
||||
mod text;
|
||||
mod utils;
|
||||
|
||||
pub use array::YArray;
|
||||
pub use doc::Doc;
|
||||
pub use map::YMap;
|
||||
pub use text::YText;
|
||||
use utils::{
|
||||
get_any_from_js_object, get_any_from_js_unknown, get_js_unknown_from_any,
|
||||
get_js_unknown_from_value, MixedRefYType, MixedYType,
|
||||
};
|
||||
134
packages/common/y-octo/node/src/map.rs
Normal file
134
packages/common/y-octo/node/src/map.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
use napi::{Env, JsObject, ValueType};
|
||||
use y_octo::{Any, Map, Value};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[napi]
|
||||
pub struct YMap {
|
||||
pub(crate) map: Map,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl YMap {
|
||||
#[allow(clippy::new_without_default)]
|
||||
#[napi(constructor)]
|
||||
pub fn new() -> Self {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub(crate) fn inner_new(map: Map) -> Self {
|
||||
Self { map }
|
||||
}
|
||||
|
||||
#[napi(getter)]
|
||||
pub fn length(&self) -> i64 {
|
||||
self.map.len() as i64
|
||||
}
|
||||
|
||||
#[napi(getter)]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.map.is_empty()
|
||||
}
|
||||
|
||||
#[napi(ts_generic_types = "T = unknown", ts_return_type = "T")]
|
||||
pub fn get(&self, env: Env, key: String) -> Result<MixedYType> {
|
||||
if let Some(value) = self.map.get(&key) {
|
||||
match value {
|
||||
Value::Any(any) => get_js_unknown_from_any(env, any).map(MixedYType::D),
|
||||
Value::Array(array) => Ok(MixedYType::A(YArray::inner_new(array))),
|
||||
Value::Map(map) => Ok(MixedYType::B(YMap::inner_new(map))),
|
||||
Value::Text(text) => Ok(MixedYType::C(YText::inner_new(text))),
|
||||
_ => env.get_null().map(|v| v.into_unknown()).map(MixedYType::D),
|
||||
}
|
||||
.map_err(anyhow::Error::from)
|
||||
} else {
|
||||
Ok(MixedYType::D(env.get_null()?.into_unknown()))
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(
|
||||
ts_args_type = "key: string, value: YArray | YMap | YText | boolean | number | string | \
|
||||
Record<string, any> | null | undefined"
|
||||
)]
|
||||
pub fn set(&mut self, key: String, value: MixedRefYType) -> Result<()> {
|
||||
match value {
|
||||
MixedRefYType::A(array) => self
|
||||
.map
|
||||
.insert(key, array.array.clone())
|
||||
.map_err(anyhow::Error::from),
|
||||
MixedRefYType::B(map) => self
|
||||
.map
|
||||
.insert(key, map.map.clone())
|
||||
.map_err(anyhow::Error::from),
|
||||
MixedRefYType::C(text) => self
|
||||
.map
|
||||
.insert(key, text.text.clone())
|
||||
.map_err(anyhow::Error::from),
|
||||
MixedRefYType::D(unknown) => match unknown.get_type() {
|
||||
Ok(value_type) => match value_type {
|
||||
ValueType::Undefined | ValueType::Null => {
|
||||
self.map.insert(key, Any::Null).map_err(anyhow::Error::from)
|
||||
}
|
||||
ValueType::Boolean => match unknown.coerce_to_bool().and_then(|v| v.get_value()) {
|
||||
Ok(boolean) => self.map.insert(key, boolean).map_err(anyhow::Error::from),
|
||||
Err(e) => Err(anyhow::Error::from(e).context("Failed to coerce value to boolean")),
|
||||
},
|
||||
ValueType::Number => match unknown.coerce_to_number().and_then(|v| v.get_double()) {
|
||||
Ok(number) => self.map.insert(key, number).map_err(anyhow::Error::from),
|
||||
Err(e) => Err(anyhow::Error::from(e).context("Failed to coerce value to number")),
|
||||
},
|
||||
ValueType::String => {
|
||||
match unknown
|
||||
.coerce_to_string()
|
||||
.and_then(|v| v.into_utf8())
|
||||
.and_then(|s| s.as_str().map(|s| s.to_string()))
|
||||
{
|
||||
Ok(string) => self.map.insert(key, string).map_err(anyhow::Error::from),
|
||||
Err(e) => Err(anyhow::Error::from(e).context("Failed to coerce value to string")),
|
||||
}
|
||||
}
|
||||
ValueType::Object => match unknown.coerce_to_object().and_then(get_any_from_js_object) {
|
||||
Ok(any) => self
|
||||
.map
|
||||
.insert(key, Value::Any(any))
|
||||
.map_err(anyhow::Error::from),
|
||||
Err(e) => Err(anyhow::Error::from(e).context("Failed to coerce value to object")),
|
||||
},
|
||||
ValueType::BigInt => Err(anyhow::Error::msg("BigInt values are not supported")),
|
||||
ValueType::Symbol => Err(anyhow::Error::msg("Symbol values are not supported")),
|
||||
ValueType::Function => Err(anyhow::Error::msg("Function values are not supported")),
|
||||
ValueType::External => Err(anyhow::Error::msg("External values are not supported")),
|
||||
ValueType::Unknown => Err(anyhow::Error::msg("Unknown values are not supported")),
|
||||
},
|
||||
Err(e) => Err(anyhow::Error::from(e)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn remove(&mut self, key: String) {
|
||||
self.map.remove(&key);
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn to_json(&self, env: Env) -> Result<JsObject> {
|
||||
let mut js_object = env.create_object()?;
|
||||
for (key, value) in self.map.iter() {
|
||||
js_object.set(key, get_js_unknown_from_value(env, value))?;
|
||||
}
|
||||
Ok(js_object)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_map_init() {
|
||||
let doc = Doc::new(None);
|
||||
let text = doc.get_or_create_map("map".into()).unwrap();
|
||||
assert_eq!(text.length(), 0);
|
||||
}
|
||||
}
|
||||
82
packages/common/y-octo/node/src/text.rs
Normal file
82
packages/common/y-octo/node/src/text.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use y_octo::Text;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[napi]
|
||||
pub struct YText {
|
||||
pub(crate) text: Text,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl YText {
|
||||
#[allow(clippy::new_without_default)]
|
||||
#[napi(constructor)]
|
||||
pub fn new() -> Self {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub(crate) fn inner_new(text: Text) -> Self {
|
||||
Self { text }
|
||||
}
|
||||
|
||||
#[napi(getter)]
|
||||
pub fn len(&self) -> i64 {
|
||||
self.text.len() as i64
|
||||
}
|
||||
|
||||
#[napi(getter)]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.text.is_empty()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn insert(&mut self, index: i64, str: String) -> Result<()> {
|
||||
self
|
||||
.text
|
||||
.insert(index as u64, str)
|
||||
.map_err(anyhow::Error::from)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn remove(&mut self, index: i64, len: i64) -> Result<()> {
|
||||
self
|
||||
.text
|
||||
.remove(index as u64, len as u64)
|
||||
.map_err(anyhow::Error::from)
|
||||
}
|
||||
|
||||
#[napi(getter)]
|
||||
pub fn length(&self) -> i64 {
|
||||
self.text.len() as i64
|
||||
}
|
||||
|
||||
#[allow(clippy::inherent_to_string)]
|
||||
#[napi]
|
||||
pub fn to_string(&self) -> String {
|
||||
self.text.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_text_init() {
|
||||
let doc = Doc::new(None);
|
||||
let text = doc.get_or_create_text("text".into()).unwrap();
|
||||
assert_eq!(text.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_text_edit() {
|
||||
let doc = Doc::new(None);
|
||||
let mut text = doc.get_or_create_text("text".into()).unwrap();
|
||||
text.insert(0, "hello".into()).unwrap();
|
||||
assert_eq!(text.to_string(), "hello");
|
||||
text.insert(5, " world".into()).unwrap();
|
||||
assert_eq!(text.to_string(), "hello world");
|
||||
text.remove(5, 6).unwrap();
|
||||
assert_eq!(text.to_string(), "hello");
|
||||
}
|
||||
}
|
||||
117
packages/common/y-octo/node/src/utils.rs
Normal file
117
packages/common/y-octo/node/src/utils.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
use napi::{bindgen_prelude::Either4, Env, Error, JsObject, JsUnknown, Result, Status, ValueType};
|
||||
use y_octo::{AHashMap, Any, HashMapExt, Value};
|
||||
|
||||
use super::*;
|
||||
|
||||
pub type MixedYType = Either4<YArray, YMap, YText, JsUnknown>;
|
||||
pub type MixedRefYType<'a> = Either4<&'a YArray, &'a YMap, &'a YText, JsUnknown>;
|
||||
|
||||
pub fn get_js_unknown_from_any(env: Env, any: Any) -> Result<JsUnknown> {
|
||||
match any {
|
||||
Any::Null | Any::Undefined => env.get_null().map(|v| v.into_unknown()),
|
||||
Any::True => env.get_boolean(true).map(|v| v.into_unknown()),
|
||||
Any::False => env.get_boolean(false).map(|v| v.into_unknown()),
|
||||
Any::Integer(number) => env.create_int32(number).map(|v| v.into_unknown()),
|
||||
Any::BigInt64(number) => env.create_int64(number).map(|v| v.into_unknown()),
|
||||
Any::Float32(number) => env.create_double(number.0 as f64).map(|v| v.into_unknown()),
|
||||
Any::Float64(number) => env.create_double(number.0).map(|v| v.into_unknown()),
|
||||
Any::String(string) => env.create_string(string.as_str()).map(|v| v.into_unknown()),
|
||||
Any::Array(array) => {
|
||||
let mut js_array = env.create_array_with_length(array.len())?;
|
||||
for (i, value) in array.into_iter().enumerate() {
|
||||
js_array.set_element(i as u32, get_js_unknown_from_any(env, value)?)?;
|
||||
}
|
||||
Ok(js_array.into_unknown())
|
||||
}
|
||||
_ => env.get_null().map(|v| v.into_unknown()),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
// Wait for NAPI-RS External::into_unknown to be stabilized
|
||||
pub fn get_js_unknown_from_value(env: Env, value: Value) -> Result<JsUnknown> {
|
||||
match value {
|
||||
Value::Any(any) => get_js_unknown_from_any(env, any),
|
||||
Value::Array(array) => env
|
||||
.create_external(YArray::inner_new(array), None)
|
||||
.map(|o| o.into_unknown()),
|
||||
Value::Map(map) => env
|
||||
.create_external(YMap::inner_new(map), None)
|
||||
.map(|o| o.into_unknown()),
|
||||
Value::Text(text) => env
|
||||
.create_external(YText::inner_new(text), None)
|
||||
.map(|o| o.into_unknown()),
|
||||
_ => env.get_null().map(|v| v.into_unknown()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_any_from_js_object(object: JsObject) -> Result<Any> {
|
||||
if let Ok(length) = object.get_array_length() {
|
||||
let mut array = Vec::with_capacity(length as usize);
|
||||
for i in 0..length {
|
||||
if let Ok(value) = object.get_element::<JsUnknown>(i) {
|
||||
array.push(get_any_from_js_unknown(value)?);
|
||||
}
|
||||
}
|
||||
Ok(Any::Array(array))
|
||||
} else {
|
||||
let mut map = AHashMap::new();
|
||||
let keys = object.get_property_names()?;
|
||||
if let Ok(length) = keys.get_array_length() {
|
||||
for i in 0..length {
|
||||
if let Ok((obj, key)) = keys.get_element::<JsUnknown>(i).and_then(|o| {
|
||||
o.coerce_to_string().and_then(|obj| {
|
||||
obj
|
||||
.into_utf8()
|
||||
.and_then(|s| s.as_str().map(|s| (obj, s.to_string())))
|
||||
})
|
||||
}) {
|
||||
if let Ok(value) = object.get_property::<_, JsUnknown>(obj) {
|
||||
println!("key: {}", key);
|
||||
map.insert(key, get_any_from_js_unknown(value)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Any::Object(map))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_any_from_js_unknown(js_unknown: JsUnknown) -> Result<Any> {
|
||||
match js_unknown.get_type()? {
|
||||
ValueType::Undefined | ValueType::Null => Ok(Any::Null),
|
||||
ValueType::Boolean => Ok(
|
||||
js_unknown
|
||||
.coerce_to_bool()
|
||||
.and_then(|v| v.get_value())?
|
||||
.into(),
|
||||
),
|
||||
ValueType::Number => Ok(
|
||||
js_unknown
|
||||
.coerce_to_number()
|
||||
.and_then(|v| v.get_double())
|
||||
.map(|v| v.into())?,
|
||||
),
|
||||
ValueType::String => Ok(
|
||||
js_unknown
|
||||
.coerce_to_string()
|
||||
.and_then(|v| v.into_utf8())
|
||||
.and_then(|s| s.as_str().map(|s| s.to_string()))?
|
||||
.into(),
|
||||
),
|
||||
ValueType::Object => {
|
||||
if let Ok(object) = js_unknown.coerce_to_object() {
|
||||
get_any_from_js_object(object)
|
||||
} else {
|
||||
Err(Error::new(
|
||||
Status::InvalidArg,
|
||||
"Failed to coerce value to object",
|
||||
))
|
||||
}
|
||||
}
|
||||
_ => Err(Error::new(
|
||||
Status::InvalidArg,
|
||||
"Failed to coerce value to any",
|
||||
)),
|
||||
}
|
||||
}
|
||||
62
packages/common/y-octo/node/tests/array.spec.mts
Normal file
62
packages/common/y-octo/node/tests/array.spec.mts
Normal file
@@ -0,0 +1,62 @@
|
||||
import assert, { equal, deepEqual } from 'node:assert';
|
||||
import { test } from 'node:test';
|
||||
|
||||
import { Doc, type YArray } from '../index';
|
||||
|
||||
test('array test', { concurrency: false }, async t => {
|
||||
let client_id: number;
|
||||
let doc: Doc;
|
||||
t.beforeEach(async () => {
|
||||
client_id = (Math.random() * 100000) | 0;
|
||||
doc = new Doc(client_id);
|
||||
});
|
||||
|
||||
t.afterEach(async () => {
|
||||
client_id = -1;
|
||||
// @ts-expect-error - doc must not null in next range
|
||||
doc = null;
|
||||
});
|
||||
|
||||
await t.test('array should be created', () => {
|
||||
let arr = doc.getOrCreateArray('arr');
|
||||
deepEqual(doc.keys, ['arr']);
|
||||
equal(arr.length, 0);
|
||||
});
|
||||
|
||||
await t.test('array editing', () => {
|
||||
let arr = doc.getOrCreateArray('arr');
|
||||
arr.insert(0, true);
|
||||
arr.insert(1, false);
|
||||
arr.insert(2, 1);
|
||||
arr.insert(3, 'hello world');
|
||||
equal(arr.length, 4);
|
||||
equal(arr.get(0), true);
|
||||
equal(arr.get(1), false);
|
||||
equal(arr.get(2), 1);
|
||||
equal(arr.get(3), 'hello world');
|
||||
equal(arr.length, 4);
|
||||
arr.remove(1, 1);
|
||||
equal(arr.length, 3);
|
||||
equal(arr.get(2), 'hello world');
|
||||
});
|
||||
|
||||
await t.test('sub array should can edit', () => {
|
||||
let map = doc.getOrCreateMap('map');
|
||||
let sub = doc.createArray();
|
||||
map.set('sub', sub);
|
||||
|
||||
sub.insert(0, true);
|
||||
sub.insert(1, false);
|
||||
sub.insert(2, 1);
|
||||
sub.insert(3, 'hello world');
|
||||
equal(sub.length, 4);
|
||||
|
||||
let sub2 = map.get<YArray>('sub');
|
||||
assert(sub2);
|
||||
equal(sub2.get(0), true);
|
||||
equal(sub2.get(1), false);
|
||||
equal(sub2.get(2), 1);
|
||||
equal(sub2.get(3), 'hello world');
|
||||
equal(sub2.length, 4);
|
||||
});
|
||||
});
|
||||
99
packages/common/y-octo/node/tests/doc.spec.mts
Normal file
99
packages/common/y-octo/node/tests/doc.spec.mts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { equal } from 'node:assert';
|
||||
import { test } from 'node:test';
|
||||
|
||||
import { Doc } from '../index';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
test('doc test', { concurrency: false }, async t => {
|
||||
let client_id: number;
|
||||
let doc: Doc;
|
||||
t.beforeEach(async () => {
|
||||
client_id = (Math.random() * 100000) | 0;
|
||||
doc = new Doc(client_id);
|
||||
});
|
||||
|
||||
t.afterEach(async () => {
|
||||
client_id = -1;
|
||||
// @ts-expect-error - doc must not null in next range
|
||||
doc = null;
|
||||
});
|
||||
|
||||
await t.test('doc id should be set', () => {
|
||||
equal(doc.clientId, client_id);
|
||||
});
|
||||
|
||||
await t.test('y-octo doc update should be apply', () => {
|
||||
let array = doc.getOrCreateArray('array');
|
||||
let map = doc.getOrCreateMap('map');
|
||||
let text = doc.getOrCreateText('text');
|
||||
|
||||
array.insert(0, true);
|
||||
array.insert(1, false);
|
||||
array.insert(2, 1);
|
||||
array.insert(3, 'hello world');
|
||||
map.set('a', true);
|
||||
map.set('b', false);
|
||||
map.set('c', 1);
|
||||
map.set('d', 'hello world');
|
||||
text.insert(0, 'a');
|
||||
text.insert(1, 'b');
|
||||
text.insert(2, 'c');
|
||||
|
||||
let doc2 = new Doc(client_id);
|
||||
doc2.applyUpdate(doc.encodeStateAsUpdateV1());
|
||||
|
||||
let array2 = doc2.getOrCreateArray('array');
|
||||
let map2 = doc2.getOrCreateMap('map');
|
||||
let text2 = doc2.getOrCreateText('text');
|
||||
|
||||
equal(doc2.clientId, client_id);
|
||||
equal(array2.length, 4);
|
||||
equal(array2.get(0), true);
|
||||
equal(array2.get(1), false);
|
||||
equal(array2.get(2), 1);
|
||||
equal(array2.get(3), 'hello world');
|
||||
equal(map2.length, 4);
|
||||
equal(map2.get('a'), true);
|
||||
equal(map2.get('b'), false);
|
||||
equal(map2.get('c'), 1);
|
||||
equal(map2.get('d'), 'hello world');
|
||||
equal(text2.toString(), 'abc');
|
||||
});
|
||||
|
||||
await t.test('yjs doc update should be apply', () => {
|
||||
let doc2 = new Y.Doc();
|
||||
let array2 = doc2.getArray('array');
|
||||
let map2 = doc2.getMap('map');
|
||||
let text2 = doc2.getText('text');
|
||||
|
||||
array2.insert(0, [true]);
|
||||
array2.insert(1, [false]);
|
||||
array2.insert(2, [1]);
|
||||
array2.insert(3, ['hello world']);
|
||||
map2.set('a', true);
|
||||
map2.set('b', false);
|
||||
map2.set('c', 1);
|
||||
map2.set('d', 'hello world');
|
||||
text2.insert(0, 'a');
|
||||
text2.insert(1, 'b');
|
||||
text2.insert(2, 'c');
|
||||
|
||||
doc.applyUpdate(Buffer.from(Y.encodeStateAsUpdate(doc2)));
|
||||
|
||||
let array = doc.getOrCreateArray('array');
|
||||
let map = doc.getOrCreateMap('map');
|
||||
let text = doc.getOrCreateText('text');
|
||||
|
||||
equal(array.length, 4);
|
||||
equal(array.get(0), true);
|
||||
equal(array.get(1), false);
|
||||
equal(array.get(2), 1);
|
||||
equal(array.get(3), 'hello world');
|
||||
equal(map.length, 4);
|
||||
equal(map.get('a'), true);
|
||||
equal(map.get('b'), false);
|
||||
equal(map.get('c'), 1);
|
||||
equal(map.get('d'), 'hello world');
|
||||
equal(text.toString(), 'abc');
|
||||
});
|
||||
});
|
||||
152
packages/common/y-octo/node/tests/map.spec.mts
Normal file
152
packages/common/y-octo/node/tests/map.spec.mts
Normal file
@@ -0,0 +1,152 @@
|
||||
import assert, { equal, deepEqual } from 'node:assert';
|
||||
import { test } from 'node:test';
|
||||
|
||||
import * as Y from 'yjs';
|
||||
import { Doc, type YArray, type YMap, type YText } from '../index';
|
||||
|
||||
test('map test', { concurrency: false }, async t => {
|
||||
let client_id: number;
|
||||
let doc: Doc;
|
||||
t.beforeEach(async () => {
|
||||
client_id = (Math.random() * 100000) | 0;
|
||||
doc = new Doc(client_id);
|
||||
});
|
||||
|
||||
t.afterEach(async () => {
|
||||
client_id = -1;
|
||||
// @ts-expect-error - doc must not null in next range
|
||||
doc = null;
|
||||
});
|
||||
|
||||
await t.test('map should be created', () => {
|
||||
let map = doc.getOrCreateMap('map');
|
||||
deepEqual(doc.keys, ['map']);
|
||||
equal(map.length, 0);
|
||||
});
|
||||
|
||||
await t.test('map editing', () => {
|
||||
let map = doc.getOrCreateMap('map');
|
||||
map.set('a', true);
|
||||
map.set('b', false);
|
||||
map.set('c', 1);
|
||||
map.set('d', 'hello world');
|
||||
equal(map.length, 4);
|
||||
equal(map.get('a'), true);
|
||||
equal(map.get('b'), false);
|
||||
equal(map.get('c'), 1);
|
||||
equal(map.get('d'), 'hello world');
|
||||
equal(map.length, 4);
|
||||
map.remove('b');
|
||||
equal(map.length, 3);
|
||||
equal(map.get('d'), 'hello world');
|
||||
});
|
||||
|
||||
await t.test('map should can be nested', () => {
|
||||
let map = doc.getOrCreateMap('map');
|
||||
let sub = doc.createMap();
|
||||
map.set('sub', sub);
|
||||
|
||||
sub.set('a', true);
|
||||
sub.set('b', false);
|
||||
sub.set('c', 1);
|
||||
sub.set('d', 'hello world');
|
||||
equal(sub.length, 4);
|
||||
|
||||
let sub2 = map.get<YMap>('sub');
|
||||
assert(sub2);
|
||||
equal(sub2.get('a'), true);
|
||||
equal(sub2.get('b'), false);
|
||||
equal(sub2.get('c'), 1);
|
||||
equal(sub2.get('d'), 'hello world');
|
||||
equal(sub2.length, 4);
|
||||
});
|
||||
|
||||
await t.test('y-octo to yjs compatibility test with nested type', () => {
|
||||
let map = doc.getOrCreateMap('map');
|
||||
let sub_array = doc.createArray();
|
||||
let sub_map = doc.createMap();
|
||||
let sub_text = doc.createText();
|
||||
|
||||
map.set('array', sub_array);
|
||||
map.set('map', sub_map);
|
||||
map.set('text', sub_text);
|
||||
|
||||
sub_array.insert(0, true);
|
||||
sub_array.insert(1, false);
|
||||
sub_array.insert(2, 1);
|
||||
sub_array.insert(3, 'hello world');
|
||||
sub_map.set('a', true);
|
||||
sub_map.set('b', false);
|
||||
sub_map.set('c', 1);
|
||||
sub_map.set('d', 'hello world');
|
||||
sub_text.insert(0, 'a');
|
||||
sub_text.insert(1, 'b');
|
||||
sub_text.insert(2, 'c');
|
||||
|
||||
let doc2 = new Y.Doc();
|
||||
Y.applyUpdate(doc2, doc.encodeStateAsUpdateV1());
|
||||
|
||||
let map2 = doc2.getMap<any>('map');
|
||||
let sub_array2 = map2.get('array') as Y.Array<any>;
|
||||
let sub_map2 = map2.get('map') as Y.Map<any>;
|
||||
let sub_text2 = map2.get('text') as Y.Text;
|
||||
|
||||
assert(sub_array2);
|
||||
equal(sub_array2.length, 4);
|
||||
equal(sub_array2.get(0), true);
|
||||
equal(sub_array2.get(1), false);
|
||||
equal(sub_array2.get(2), 1);
|
||||
equal(sub_array2.get(3), 'hello world');
|
||||
assert(sub_map2);
|
||||
equal(sub_map2.get('a'), true);
|
||||
equal(sub_map2.get('b'), false);
|
||||
equal(sub_map2.get('c'), 1);
|
||||
equal(sub_map2.get('d'), 'hello world');
|
||||
assert(sub_text2);
|
||||
equal(sub_text2.toString(), 'abc');
|
||||
});
|
||||
|
||||
await t.test('yjs to y-octo compatibility test with nested type', () => {
|
||||
let doc2 = new Y.Doc();
|
||||
let map2 = doc2.getMap<any>('map');
|
||||
let sub_array2 = new Y.Array<any>();
|
||||
let sub_map2 = new Y.Map<any>();
|
||||
let sub_text2 = new Y.Text();
|
||||
map2.set('array', sub_array2);
|
||||
map2.set('map', sub_map2);
|
||||
map2.set('text', sub_text2);
|
||||
|
||||
sub_array2.insert(0, [true]);
|
||||
sub_array2.insert(1, [false]);
|
||||
sub_array2.insert(2, [1]);
|
||||
sub_array2.insert(3, ['hello world']);
|
||||
sub_map2.set('a', true);
|
||||
sub_map2.set('b', false);
|
||||
sub_map2.set('c', 1);
|
||||
sub_map2.set('d', 'hello world');
|
||||
sub_text2.insert(0, 'a');
|
||||
sub_text2.insert(1, 'b');
|
||||
sub_text2.insert(2, 'c');
|
||||
|
||||
doc.applyUpdate(Buffer.from(Y.encodeStateAsUpdate(doc2)));
|
||||
|
||||
let map = doc.getOrCreateMap('map');
|
||||
let sub_array = map.get<YArray>('array');
|
||||
let sub_map = map.get<YMap>('map');
|
||||
let sub_text = map.get<YText>('text');
|
||||
|
||||
assert(sub_array);
|
||||
equal(sub_array.length, 4);
|
||||
equal(sub_array.get(0), true);
|
||||
equal(sub_array.get(1), false);
|
||||
equal(sub_array.get(2), 1);
|
||||
equal(sub_array.get(3), 'hello world');
|
||||
assert(sub_map);
|
||||
equal(sub_map.get('a'), true);
|
||||
equal(sub_map.get('b'), false);
|
||||
equal(sub_map.get('c'), 1);
|
||||
equal(sub_map.get('d'), 'hello world');
|
||||
assert(sub_text);
|
||||
equal(sub_text.toString(), 'abc');
|
||||
});
|
||||
});
|
||||
54
packages/common/y-octo/node/tests/text.spec.mts
Normal file
54
packages/common/y-octo/node/tests/text.spec.mts
Normal file
@@ -0,0 +1,54 @@
|
||||
import assert, { equal, deepEqual } from 'node:assert';
|
||||
import { test } from 'node:test';
|
||||
|
||||
import { Doc, type YText } from '../index';
|
||||
|
||||
test('text test', { concurrency: false }, async t => {
|
||||
let client_id: number;
|
||||
let doc: Doc;
|
||||
t.beforeEach(async () => {
|
||||
client_id = (Math.random() * 100000) | 0;
|
||||
doc = new Doc(client_id);
|
||||
});
|
||||
|
||||
t.afterEach(async () => {
|
||||
client_id = -1;
|
||||
// @ts-expect-error - doc must not null in next range
|
||||
doc = null;
|
||||
});
|
||||
|
||||
await t.test('text should be created', () => {
|
||||
let text = doc.getOrCreateText('text');
|
||||
deepEqual(doc.keys, ['text']);
|
||||
equal(text.len, 0);
|
||||
});
|
||||
|
||||
await t.test('text editing', () => {
|
||||
let text = doc.getOrCreateText('text');
|
||||
text.insert(0, 'a');
|
||||
text.insert(1, 'b');
|
||||
text.insert(2, 'c');
|
||||
equal(text.toString(), 'abc');
|
||||
text.remove(0, 1);
|
||||
equal(text.toString(), 'bc');
|
||||
text.remove(1, 1);
|
||||
equal(text.toString(), 'b');
|
||||
text.remove(0, 1);
|
||||
equal(text.toString(), '');
|
||||
});
|
||||
|
||||
await t.test('sub text should can edit', () => {
|
||||
let map = doc.getOrCreateMap('map');
|
||||
let sub = doc.createText();
|
||||
map.set('sub', sub);
|
||||
|
||||
sub.insert(0, 'a');
|
||||
sub.insert(1, 'b');
|
||||
sub.insert(2, 'c');
|
||||
equal(sub.toString(), 'abc');
|
||||
|
||||
let sub2 = map.get<YText>('sub');
|
||||
assert(sub2);
|
||||
equal(sub2.toString(), 'abc');
|
||||
});
|
||||
});
|
||||
10
packages/common/y-octo/node/tsconfig.json
Normal file
10
packages/common/y-octo/node/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../../../tsconfig.node.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": false,
|
||||
"outDir": "lib",
|
||||
"composite": true
|
||||
},
|
||||
"include": ["index.d.ts", "tests/**/*.mts"],
|
||||
"references": []
|
||||
}
|
||||
Reference in New Issue
Block a user