# DolphinDB JavaScript API

<p align='center'>
    <img src='./ddb.svg' alt='DolphinDB' width='256'>
</p>

<p align='center'>
    <a href='https://www.npmjs.com/package/dolphindb' target='_blank'>
        <img alt='npm version' src='https://img.shields.io/npm/v/dolphindb.svg?style=flat-square&color=brightgreen' />
    </a>
    <a href='https://www.npmjs.com/package/dolphindb' target='_blank'>
        <img alt='npm downloads' src='https://img.shields.io/npm/dt/dolphindb?style=flat-square&color=brightgreen' />
    </a>
</p>

## [English](./README.md) | 中文

## 简介
DolphinDB JavaScript API 是一个 JavaScript 库，封装了操作 DolphinDB 数据库的能力，如：连接数据库、执行脚本、调用函数、上传变量等

https://www.npmjs.com/package/dolphindb

## 特性
- 使用 WebSocket 与 DolphinDB 数据库通信，用二进制格式进行数据交换，支持流数据实时推送
- 支持在浏览器环境和 Node.js 环境中运行
- 使用了 JavaScript 中的 Int32Array 等 TypedArray 处理二进制数据，性能较高
- 单次调用支持最大 2 GB 数据的序列化上传，下载数据量不受限制

## 用法
### 初始化并连接到 DolphinDB

#### 方法一：在浏览器中直接使用构建好的 CDN 版本

保存以下内容到 `example.html` 文件，用浏览器打开即可运行，F12 打开调试控制台可以看到日志

```html
<!doctype html>
<html>
    <head>
        <title>DolphinDB</title>
        <meta charset='utf-8' />
    </head>
    <body>
        <script type="module">
            import { DDB } from 'https://cdn.dolphindb.cn/assets/api.js'
            
            let ddb = new DDB('ws://127.0.0.1:8848')
            
            await ddb.connect()
            
            console.log(
                await ddb.eval('1 + 1')
            )
        </script>
    </body>
</html>
```

#### 方法二：在项目中安装 npm 包并导入

##### 1. 安装

1.1. 在机器上安装最新版的 Node.js 及浏览器。
    - windows: https://nodejs.org/en/download/current/
    - linux: https://github.com/nodesource/distributions?tab=readme-ov-file#debian-and-ubuntu-based-distributions
1.2. （可选）使用以下命令创建新项目。如果已有项目，可跳过此步。
    ```bash
    mkdir dolphindb-example
    cd dolphindb-example
    npm init --yes
    ```
1.3. 用编辑器打开 package.json 文件，在 `"main": "./index.js"` 下方加入一行 "type": "module", 这样能够启用 ECMAScript modules，在后面代码中可以使用 `import { DDB } from 'dolphindb'` 导入 npm 包。
1.4. 在项目中安装 npm 包。
    ```bash
    npm install dolphindb
    ```

##### 2. 使用

```ts
// 2.1 在浏览器环境中用下面的方法导入
import { DDB } from 'dolphindb/browser.js'

// 2.1 在 Node.js 环境中用下面的方法导入
// import { DDB } from 'dolphindb'
// 已有的使用 CommonJS 模块的项目的导入方法为 const { DDB } = await import('dolphindb')

// 2.2 使用 WebSocket URL 初始化连接到 DolphinDB 的实例（不建立实际的网络连接）
let ddb = new DDB('ws://127.0.0.1:8848')

// 使用 HTTPS 加密
// let ddb = new DDB('wss://dolphindb.com')

// 2.3 建立到 DolphinDB 的连接（要求 DolphinDB 数据库版本不低于 1.30.16 或 2.00.4）
await ddb.connect()
```

#### 代码补全、函数提示数据
- https://cdn.dolphindb.cn/assets/docs.zh.json  
- https://cdn.dolphindb.cn/assets/docs.en.json


#### DDB 连接选项

```ts
let ddb = new DDB('ws://127.0.0.1:8848', {
    // 是否在建立连接后自动登录，默认 `true`
    autologin: true,
    
    // DolphinDB 登录用户名，默认 `'admin'`
    username: 'admin',
    
    // DolphinDB 登录密码，默认 `'123456'`
    password: '123456',
    
    // 设置 python session flag，默认 `false`
    python: false,
    
    // 设置当前会话执行的 sql 标准, 请使用 SqlStandard 枚举进行传参，默认 `DolphinDB`
    // sql: SqlStandard.MySQL,
    // sql: SqlStandard.Oracle,
    
    // 设置该选项后，该数据库连接只用于流数据，详细用法见后文 `5. 流数据`
    streaming: undefined
})
```


### 调用函数
#### 例子
```ts
import { DdbInt } from 'dolphindb'

const result = await ddb.call('add', [new DdbInt(1), new DdbInt(1)])
// TypeScript: const result = await ddb.call<DdbInt>('add', [new DdbInt(1), new DdbInt(1)])

console.log(result.value === 2)  // true
```

##### 用 DdbObj 对象来表示 DolphinDB 中的数据类型

上面例子中，上传了两个参数 1 (对应 DolphinDB 中的 int 类型) 到 DolphinDB 数据库，作为 add 函数的参数，并接收函数调用的结果 result

`<DdbInt>` 用于 TypeScript 推断返回值的类型

- result 是一个 `DdbInt`，也是 `DdbObj<number>`
- result.form 是 `DdbForm.scalar`
- result.type 是 `DdbType.int`
- result.value 是 JavaScript 中原生的 `number` (int 的取值范围及精度可以用 JavaScript 的 number 准确表示)

建议先了解一下 JavaScript 中的 TypedArray 相关的概念，可以参考:  
https://stackoverflow.com/questions/42416783/where-to-use-arraybuffer-vs-typed-array-in-javascript  
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray  

```ts
/** 可以表示所有 DolphinDB 数据库中的数据类型 */
class DdbObj <T extends DdbValue = DdbValue> {
    /** 是否为小端 (little endian) */
    le: boolean
    
    /** 数据形式 https://www.dolphindb.cn/cn/help/DataTypesandStructures/DataForms/index.html */
    form: DdbForm
    
    /** 数据类型 https://www.dolphindb.cn/cn/help/DataTypesandStructures/DataTypes/index.html */
    type: DdbType
    
    /** 占用 parse 时传入的 buf 的长度 */
    length: number
    
    /** table name / column name */
    name?: string
    
    /**
        最低维、第 1 维
        - vector: rows = n, cols = 1
        - pair:   rows = 2, cols = 1
        - matrix: rows = n, cols = m
        - set:    同 vector
        - dict:   包含 keys, values 向量
        - table:  同 matrix
    */
    rows?: number
    
    /** 第 2 维 */
    cols?: number
    
    /** 实际数据。不同的 DdbForm, DdbType 使用 DdbValue 中不同的类型来表示实际数据 */
    value: T
    
    /** 原始二进制数据，仅在 parse_object 为 false 时通过 parse_message 生成的顶层对象有这个属性 */
    buffer?: Uint8Array
    
    constructor (data: Partial<DdbObj> & { form: DdbForm, type: DdbType, length: number }) {
        Object.assign(this, data)
    }
}

class DdbInt extends DdbObj<number> {
    constructor (value: number) {
        super({
            form: DdbForm.scalar,
            type: DdbType.int,
            length: 4,
            value
        })
    }
}

// ... 还有很多快捷类，如 DdbString, DdbLong, DdbDouble, DdbVectorDouble, DdbVectorAny 等

type DdbValue = 
    null | boolean | number | [number, number] | bigint | string | string[] | 
    Uint8Array | Int16Array | Int32Array | Float32Array | Float64Array | BigInt64Array | Uint8Array[] | 
    DdbObj[] | DdbFunctionDefValue | DdbSymbolExtendedValue
    

enum DdbForm {
    scalar = 0,
    vector = 1,
    pair = 2,
    matrix = 3,
    set = 4,
    dict = 5,
    table = 6,
    chart = 7,
    chunk = 8,
}


enum DdbType {
    void = 0,
    bool = 1,
    char = 2,
    short = 3,
    int = 4,
    long = 5,
    // ...
    timestamp = 12,
    // ...
    double = 16,
    symbol = 17,
    string = 18,
    // ...
}
```

##### 无快捷类的类型

对于没有快捷类的类型，可指定 form 和 type 手动创建 DdbObj 对象

```ts
// 通过 DdbDateTime 快捷类创建
new DdbDateTime(1644573600)

// 等价于手动通过 DdbObj 创建 form = scalar, type = datetime 的对象
const obj = new DdbObj({
    form: DdbForm.scalar,
    type: DdbType.datetime,
    value: 1644573600,
    length: 0
})


// value 在 js 中对应类型及取值可以参考 ddb.eval 返回的结果 (见后文 `eval` 方法声明)
const obj = await ddb.eval('2022.02.11 10:00:00')
console.log(obj.form === DdbForm.scalar)
console.log(obj.type === DdbType.datetime)
console.log(obj.value)

// 再比如创建一个 set
// 参考 ddb.eval
// const obj = await ddb.eval('set([1, 2, 3])')
// console.log(obj.value)
const obj = new DdbObj({
    form: DdbForm.set,
    type: DdbType.int,
    value: Int32Array.of(1, 2, 3),
    length: 0
})

// 使用快捷类较为简单
const obj = new DdbSetInt(
    new Set([1, 2, 3])
)
```

#### scalar 形式的 NULL 对象

scalar 形式的 NULL 对象，其对应 DdbObj 的 value 为 JavaScript 中的 null：

```ts
;(await ddb.eval('double()')).value === null

// 创建 NULL 对象
new DdbInt(null)
new DdbDouble(null)
```


#### `call` 方法声明
```ts
async call <T extends DdbObj> (
    /** 函数名 */
    func: string,
    
    /** 调用参数 (传入的原生 string 和 boolean 会被自动转换为 DdbObj<string> 和 DdbObj<boolean>) */
    args?: (DdbObj | string | boolean)[] = [ ],
    
    /** 调用选项 */
    options?: {
        /** 紧急 flag。使用 urgent worker 执行，防止被其它作业阻塞 */
        urgent?: boolean
        
        /** 设置结点 alias 时发送到集群中对应的结点执行 (使用 DolphinDB 中的 rpc 方法) */
        node?: string
        
        /** 设置多个结点 alias 时发送到集群中对应的多个结点执行 (使用 DolphinDB 中的 pnodeRun 方法) */
        nodes?: string[]
        
        /** 设置 node 参数时必传，需指定函数类型，其它情况下不传 */
        func_type?: DdbFunctionType
        
        /** 设置 nodes 参数时选传，其它情况不传 */
        add_node_alias?: boolean
    } = { }
): Promise<T>
```


### 执行脚本
#### 例子
```ts
const result = await ddb.eval(
    'def foo (a, b) {\n' +
    '    return a + b\n' +
    '}\n' +
    'foo(1l, 1l)\n'
)

// TypeScript:
// import type { DdbLong } from 'dolphindb'
// const result = await ddb.eval<DdbLong>(...)

console.log(result.value === 2n)  // true
```

上面例子中，通过字符串上传了一段脚本到 DolphinDB 数据库执行，并接收最后一条语句 `foo(1l, 1l)` 执行结果 result

`<DdbLong>` 用于 TypeScript 推断返回值的类型

- result 是一个 `DdbLong`，也是 `DdbObj<bigint>`
- result.form 是 `DdbForm.scalar`
- result.type 是 `DdbType.long`
- result.value 是 JavaScript 中原生的 `bigint` (long 的精度不能用 JavaScript 的 number 准确表示，但可以用 bigint 表示)

只要 WebSocket 连接不断开，在后续的会话中 `foo` 这个自定义函数会一直存在，可复用，比如后续通过 `await ddb.call<DdbInt>('foo', [new DdbInt(1), new DdbInt(1)])` 调用这个自定义函数

#### `eval` 方法声明
```ts
async eval <T extends DdbObj> (
    /** 执行的脚本 */
    script: string,
    
    /** 执行选项 */
    options: {
        /** 紧急 flag，确保提交的脚本使用 urgent worker 处理，防止被其它作业阻塞 */
        urgent?: boolean
    } = { }
): Promise<T>
```


### 上传变量
#### 例子
```ts
import { DdbVectorDouble } from 'dolphindb'

let a = new Array(10000)
a.fill(1.0)

ddb.upload(['bar1', 'bar2'], [new DdbVectorDouble(a), new DdbVectorDouble(a)])
```

上面的例子中，上传了 `bar1`, `bar2` 两个变量，变量值是长度为 10000 的 double 向量

只要 WebSocket 连接不断开，在后续的会话中 `bar1`, `bar2` 这些变量会一直存在，可复用

#### `upload` 方法声明
```ts
async upload (
    /** 上传的变量名 */
    vars: string[],
    
    /** 上传的变量值 */
    args: (DdbObj | string | boolean)[]
): Promise<void>
```


### 其他例子

```ts
import { nulls, DdbInt, timestamp2str, DdbVectorSymbol, DdbTable, DdbVectorDouble } from 'dolphindb'

// 将 DolphinDB 中的 timestamp 格式化为 string
timestamp2str(
    (
        await ddb.call('now', [false])
        // TypeScript: await ddb.call<DdbObj<bigint>>('now', [false])
    ).value
) === '2022.02.23 17:23:13.494'

// 创建 symbol vector
new DdbVectorSymbol(['aaa', 'aaa', 'aaa', 'aaa', 'aaa', 'bbb'])

// 使用 JavaScript 原生数组创建含有 NULL 值的 double vector
new DdbVectorDouble([0.1, null, 0.3])

// 使用 JavaScript TypedArray 更加高效且节约内存的创建 double vector
let av = new Float64Array(3)
av[0] = 0.1
av[1] = nulls.double
av[2] = 0.3
new DdbVectorDouble(av)

// 创建 DdbTable
new DdbTable(
    [
        new DdbVectorDouble([0.1, 0.2, null], 'col0'),
        new DdbVectorSymbol(['a', 'b', 'c'], 'col1')
    ],
    'mytable'
)
```


### 流数据

```ts
// 新建流数据连接配置
let sddb = new DDB('ws://192.168.0.43:8800', {
    autologin: true,
    username: 'admin',
    password: '123456',
    streaming: {
        table: '要订阅的流表名称',
        
        // 流数据处理回调, message 的类型是 StreamingData
        handler (message) {
            console.log(message)
        }
    }
})

// 建立连接
await sddb.connect()
```

连接建立后接收到的流数据会作为 message 参数调用 handler, message 的类型是 StreamingData, 如下:

```ts
export interface StreamingParams {
    table: string
    action?: string
    
    handler (message: StreamingData): any
}

export interface StreamingData extends StreamingParams {
    /**
        server 发送消息的时间 (nano seconds since epoch)  
        std::chrono::system_clock::now().time_since_epoch() / std::chrono::nanoseconds(1)
    */
    time: bigint
    
    /** message id */
    id: bigint
    
    colnames: string[]
    
    /** 订阅主题，即一个订阅的名称。
        它是一个字符串，由订阅表所在节点的别名、流数据表名称和订阅任务名称（如果指定了 actionName）组合而成，使用 `/` 分隔
    */
    topic: string
    
    /** 流数据，类型是 any vector, 其中的每一个元素对应被订阅表的一个列 (没有 name)，列 (DdbObj<DdbVectorValue>) 中的内容是新增的数据值 */
    data: DdbObj<DdbVectorObj[]>
    
    /** 新增的流数据行数 */
    rows: number
    
    window: {
        /** 建立连接开始 offset = 0, 随着 window 的移动逐渐增加 */
        offset: number
        
        /** segments 中 segment.row 的总和 */
        rows: number
        
        /** 每次接收到的 data 组成的数组 */
        segments: DdbObj<DdbVectorObj[]>[]
    }
    
    /** 成功订阅后，后续推送过来的 message 解析错误，则会设置 error 并调用 handler */
    error?: Error
}
```


### 开发方法

```shell
# 安装最新版的 nodejs (见上文)

# 安装 pnpm 包管理器
npm install -g pnpm

git clone https://github.com/dolphindb/api-javascript.git

cd api-javascript

# 安装项目依赖
pnpm install

# 将 .vscode/settings.template.json 复制为 .vscode/settings.json
cp .vscode/settings.template.json .vscode/settings.json

# 参考 package.json 中的 scripts

# 构建
pnpm run build

# lint
pnpm run lint

# 测试
pnpm run test

# 扫描词条
pnpm run scan
# 手动补全未翻译词条
# 再次运行扫描以更新词典文件 dict.json
pnpm run scan
```
