# \@aptpod/iscp-ts

> iSCP Client for TypeScript は、iSCP version 2 を用いたリアルタイム API にアクセスするためのクライアントライブラリです。

## Installation

npm でインストールする場合は以下を実行します。

```
npm install @aptpod/iscp-ts
```

yarn でインストールする場合は以下を実行します。

```
yarn add @aptpod/iscp-ts
```

## Example

- [アップストリームとダウンストリーム](#アップストリームとダウンストリーム)
- [E2E Call](#e2e-call)

### アップストリームとダウンストリーム

アップストリームとダウンストリームのサンプルを示します。アップストリームで送信したデータポイントをダウンストリームで確認する簡単なサンプルです。

#### 事前準備

本サンプルを動作させるために、必要なパッケージをインポートし、定数を定義します。

```ts
// iscp-tsをインポートします。
import * as iscp from '@aptpod/iscp-ts'

// intdash APIのRESTクライアントを参照します。
// RESTクライアントを生成する手順については、 intdash API/SDK サイトの説明を参照してください。
import { Configuration, BrokerISCPApi } from './intdash'

// デバッグ情報を文字列に変換するため、Node.jsのinspectを使用します。
import { inspect } from 'util'

// iSCPのサーバーアドレス。
const ISCP_ADDRESS = 'localhost:8080'

// WebSocket接続時にTLSを有効にします。
const WEBSOCKET_ENABLE_TLS = true

// REST APIのBASE PATH。
const INTDASH_REST_API_BASE_PATH = 'https://localhost:8080/api'

// アップストリームを行うノードのID。
const UPSTREAM_SOURCE_NODE_ID = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'

// REST APIを使ってiSCP接続用のアクセストークンを取得する関数を定義します。
const tokenSource = async () => {
  const configuration = new Configuration({
    basePath: INTDASH_REST_API_BASE_PATH,
  })
  const api = new BrokerISCPApi(configuration)
  const response = await api.issueISCPTicket()
  return response.data.ticket
}
```

#### アップストリームを行うコードの定義

アップストリームを行うコードのサンプルです。このサンプルでは、基準時刻のメタデータと、文字列型のデータポイントを iSCP サーバーへ送信しています。

```ts
// 指定したミリ秒の時間だけ待機します。
const sleepMs = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))

// 現在の時刻をナノ秒で取得します。
const getNowTimeNano = () => BigInt(Date.now()) * BigInt(1000_000)

const startUpstream = async () => {
  // WebSocketのコネクターを使用します。
  const connector = new iscp.WebSocketConnector({
    enableTLS: WEBSOCKET_ENABLE_TLS,
  })

  // iSCP接続を開始します。
  const conn = await iscp.Conn.connect({
    address: ISCP_ADDRESS,
    connector,
    tokenSource,
    nodeId: UPSTREAM_SOURCE_NODE_ID,
  })

  // アップストリームを開きます。
  const upstream = await conn.openUpstream({
    sessionId: 'sessionId',
  })

  // 基準時刻を送信します。
  const start = getNowTimeNano()
  await sleepMs(1000)
  await conn.sendBaseTime(
    new iscp.BaseTime({
      name: 'manual',
      elapsedTime: 0n,
      baseTime: start,
      priority: 60,
      sessionId: 'sessionId',
    }),
  )

  // 一定時間ごとにデータポイントをアップストリームに書き込みます。
  for (let i = 0; i < 4; i++) {
    await sleepMs(1000)
    await upstream.writeDataPoints(new iscp.DataId({ name: 'greeting', type: 'string' }), [
      new iscp.DataPoint({
        elapsedTime: getNowTimeNano() - start,
        payload: new TextEncoder().encode(`hello: ${i}`),
      }),
    ])
  }

  // 終了を通知するデータポイントをアップストリームに書き込みます。
  await upstream.writeDataPoints(new iscp.DataId({ name: 'end', type: 'string' }), [
    new iscp.DataPoint({
      elapsedTime: getNowTimeNano() - start,
      payload: new TextEncoder().encode('end'),
    }),
  ])

  // 未送信のアップストリームのデータポイントを全て送信します。
  await upstream.flush()

  // アップストリームを閉じます。
  await upstream.close()
  console.log('[startUpstream]', 'closed upstream')

  // iSCPを切断します。
  await conn.close()
  console.log('[startUpstream]', 'closed connection')
}
```

#### ダウンストリームを行うコードの定義

前述のアップストリームで送信されたデータをダウンストリームで受信するコードのサンプルです。

`downstream.ts`

```ts
const DEBUG_LOG_NEW_LINE_SEPARATOR = '\n==================================='

const startDownstream = async () => {
  // WebSocketのコネクターを使用します。
  const connector = new iscp.WebSocketConnector({
    enableTLS: WEBSOCKET_ENABLE_TLS,
  })

  // iSCP接続を開始します。
  const conn = await iscp.Conn.connect({
    address: ISCP_ADDRESS,
    connector,
    tokenSource,
  })

  // ダウンストリームを開きます。
  const downstream = await conn.openDownstream({
    filters: [iscp.DownstreamFilter.allFor(UPSTREAM_SOURCE_NODE_ID)],
  })

  // DownstreamMetadataを受信します。
  downstream.addEventListener(iscp.Downstream.EVENT.METADATA, (metadata) => {
    if (metadata.metadata instanceof iscp.BaseTime) {
      console.log(
        '[startDownstream]',
        'Received BaseTime:',
        inspect(metadata, { depth: Infinity }),
        DEBUG_LOG_NEW_LINE_SEPARATOR,
      )
    }
  })

  // DownstreamChunkを受信します。
  downstream.addEventListener(iscp.Downstream.EVENT.CHUNK, (chunk) => {
    console.log(
      '[startDownstream]',
      'Received DownstreamChunk',
      inspect(chunk, { depth: Infinity }),
      DEBUG_LOG_NEW_LINE_SEPARATOR,
    )

    for (const dataPointGroup of chunk.dataPointGroups) {
      if (dataPointGroup.dataId.name === 'end') {
        console.log('[startDownstream]', 'Received end message', DEBUG_LOG_NEW_LINE_SEPARATOR)
        downstream.close()
      }
    }
  })

  await downstream.waitClosed()
  console.log('[startDownstream]', 'closed downstream')

  await conn.close()
  console.log('[startDownstream]', 'closed connection')
}
```

#### 実行

アップストリームのコードと、ダウンストリームのコードを並列で実行します。

```ts
;(async () => {
  await Promise.all([startDownstream(), startUpstream()])
})()
```

出力結果

```
[startDownstream] Received BaseTime: DownstreamMetadata {
  sourceNodeId: '9fe734e5-2d43-437f-8331-838b1e6c1055',
  metadata: BaseTime3 {
    sessionId: 'sessionId',
    name: 'manual',
    priority: 1000,
    elapsedTime: 0n,
    baseTime: 1673936224024000000n
  }
}
===================================
[startDownstream] Received DownstreamChunk DownstreamChunk {
  upstreamInfo: UpstreamInfo {
    sessionId: 'sessionId',
    streamId: '9af44d42-b8d4-4cf0-91ec-b959818d80ad',
    sourceNodeId: '9fe734e5-2d43-437f-8331-838b1e6c1055'
  },
  sequenceNumber: 1,
  dataPointGroups: [
    DataPointGroup {
      dataId: DataId3 { name: 'greeting', type: 'string' },
      dataPoints: [
        DataPoint3 {
          elapsedTime: 2021000000n,
          payload: Uint8Array(8) [
            104, 101, 108, 108,
            111,  58,  32,  48
          ]
        },
        DataPoint3 {
          elapsedTime: 3024000000n,
          payload: Uint8Array(8) [
            104, 101, 108, 108,
            111,  58,  32,  49
          ]
        }
      ]
    }
  ]
}
===================================
[startDownstream] Received DownstreamChunk DownstreamChunk {
  upstreamInfo: UpstreamInfo {
    sessionId: 'sessionId',
    streamId: '9af44d42-b8d4-4cf0-91ec-b959818d80ad',
    sourceNodeId: '9fe734e5-2d43-437f-8331-838b1e6c1055'
  },
  sequenceNumber: 2,
  dataPointGroups: [
    DataPointGroup {
      dataId: DataId3 { name: 'greeting', type: 'string' },
      dataPoints: [
        DataPoint3 {
          elapsedTime: 4026000000n,
          payload: Uint8Array(8) [
            104, 101, 108, 108,
            111,  58,  32,  50
          ]
        },
        DataPoint3 {
          elapsedTime: 5027000000n,
          payload: Uint8Array(8) [
            104, 101, 108, 108,
            111,  58,  32,  51
          ]
        }
      ]
    },
    DataPointGroup {
      dataId: DataId3 { name: 'end', type: 'string' },
      dataPoints: [
        DataPoint3 {
          elapsedTime: 5027000000n,
          payload: Uint8Array(3) [ 101, 110, 100 ]
        }
      ]
    }
  ]
}
===================================
[startDownstream] Received end message
===================================
[startUpstream] closed upstream
[startUpstream] closed connection
[startDownstream] closed downstream
[startDownstream] closed connection
```

</details>

### E2E Call

E2E コールのサンプルを示します。コントローラノードが対象ノードに対して指示を出し、対象ノードは受信完了のリプライを行う簡単なサンプルです。

#### 事前準備

本サンプルを動作させるために、必要なパッケージをインポートし、定数を定義します。

```ts
// iscp-tsをインポートします。
import * as iscp from '@aptpod/iscp-ts'

// intdash APIのRESTクライアントを参照します。
// RESTクライアントを生成する手順については、 intdash API/SDK サイトの説明を参照してください。
import { Configuration, BrokerISCPApi } from './intdash'

// デバッグ情報を文字列に変換するため、Node.jsのinspectを使用します。
import { inspect } from 'util'

// iSCPのサーバーアドレス。
const ISCP_ADDRESS = 'localhost:8080'

// WebSocket接続時にTLSを有効にします。
const WEBSOCKET_ENABLE_TLS = true

// REST APIのBASE PATH。
const INTDASH_REST_API_BASE_PATH = 'https://localhost:8080/api'

// コントローラーのノードID。
const CONTROLLER_NODE_ID = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'

// コントール対象のノードID。
const TARGET_NODE_ID = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'

// REST APIを使ってiSCP接続用のアクセストークンを取得する関数を定義します。
const tokenSource = async () => {
  const configuration = new Configuration({
    basePath: INTDASH_REST_API_BASE_PATH,
  })
  const api = new BrokerISCPApi(configuration)
  const response = await api.issueISCPTicket()
  return response.data.ticket
}
```

#### コントローラノードの定義

コントローラノードからメッセージを送信するサンプルです。このサンプルでは文字列メッセージを対象ノードに対して送信し、対象ノードからのリプライを待ちます。

```ts
// 指定したミリ秒の時間だけ待機します。
const sleepMs = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))

export const send = async () => {
  // WebSocketのコネクターを使用します。
  const connector = new iscp.WebSocketConnector({
    enableTLS: WEBSOCKET_ENABLE_TLS,
  })

  // iSCP接続を開始します。
  const conn = await iscp.Conn.connect({
    address: ISCP_ADDRESS,
    connector,
    tokenSource,
    nodeId: CONTROLLER_NODE_ID,
  })

  // 対象ノードが起動するまで少し待機します。
  await sleepMs(1000)

  // Callを送信し、replayCallを受信するまで待機します。
  const got = await conn.sendCallAndWaitReplyCall(
    new iscp.UpstreamCall({
      destinationNodeId: TARGET_NODE_ID,
      name: 'greeting',
      type: 'string',
      payload: new TextEncoder().encode('hello'),
    }),
  )
  console.log('[send]', 'Received replay call:', inspect(got, { depth: Infinity }))

  await conn.close()
  console.log('[send]', 'closed connection')
}
```

#### 対象ノードのコード定義

コントローラノードからのコールを受け付け、すぐにリプライするサンプルです。

```ts
const reply = async () => {
  // WebSocketのコネクターを使用します。
  const connector = new iscp.WebSocketConnector({
    enableTLS: WEBSOCKET_ENABLE_TLS,
  })

  // iSCP接続を開始します。
  const conn = await iscp.Conn.connect({
    address: ISCP_ADDRESS,
    connector,
    tokenSource,
    nodeId: TARGET_NODE_ID,
  })

  // DownstreamCallの受信を監視し、受信したらすぐにreplyCallを送信します。
  conn.addEventListener(iscp.Conn.EVENT.CALL, (call) => {
    console.log('[reply]', 'Received call:', inspect(call, { depth: Infinity }))

    conn
      .sendReplyCall(
        new iscp.UpstreamReplyCall({
          requestCallId: call.callId,
          destinationNodeId: call.sourceNodeId,
          name: 'reply_greeting',
          type: 'string',
          payload: new TextEncoder().encode('world'),
        }),
      )
      .finally(() => {
        // replayCallを送信したらiSCPを切断します。
        conn.close()
      })
  })

  await conn.waitClosed()
  console.log('[reply]', 'closed connection')
}
```

#### 実行

```ts
;(async () => {
  await Promise.all([send(), reply()])
})()
```

実行結果

```
[reply] Received call: DownstreamCall {
  callId: '57ea53d2-f65b-4552-8367-4db83069241c',
  sourceNodeId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
  name: 'greeting',
  type: 'string',
  payload: Uint8Array(5) [ 104, 101, 108, 108, 111 ]
}
[reply] closed connection
[send] Received replay call: DownstreamReplyCall {
  requestCalId: '57ea53d2-f65b-4552-8367-4db83069241c',
  sourceNodeId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
  name: 'reply_greeting',
  type: 'string',
  payload: Uint8Array(5) [ 119, 111, 114, 108, 100 ]
}
[send] closed connection
```

## Proxy Server

当 SDK をブラウザと Node.js のどちらで実行するかによって設定方法が異なります。

### ブラウザで実行する場合

各ブラウザのプロキシの設定方法を参照ください。

### Node.js で実行する場合

以下環境変数を設定することで Proxy Server を経由して iSCP サーバーに接続することができます。

環境変数は大文字、または小文字で指定可能です。全ての環境変数を必ず指定する必要はありません。ご利用の環境に応じて、必要な環境変数を設定してください。

| 環境変数                           | 説明                                                                                                                                                                                                  | 設定例                                                              |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `HTTP_PROXY` または `http_proxy`   | iscp.WebSocketConnector で enableTLS を false で設定したときに使用する Proxy Server の URL です。                                                                                                     | `http://proxy.example.com`、または `https://proxy.example.com:3128` |
| `HTTPS_PROXY` または `https_proxy` | iscp.WebSocketConnector で enableTLS を true で設定したときに使用する Proxy Server の URL です。                                                                                                      | `http://proxy.example.com`、または `https://proxy.example.com:3128` |
| `NO_PROXY` または `no_proxy`       | ホスト名をカンマ(,)区切りのリストで指定します。iscp.Conn.connect で指定する address （iSCP サーバーのアドレス）がいずれかに一致する場合、 Proxy Server を使用せずに直接 iSCP サーバーと通信します。　 | `no-proxy.example.com,*.example.com`                                |

Proxy Server にユーザー認証が必要な場合、以下のように設定してください。

- `HTTP_PROXY=http://username:password@proxy.example.com`
- `HTTPS_PROXY=http://username:password@proxy.example.com`

## References

詳細については以下を参照してください。

- [API リファレンス](https://docs.intdash.jp/api/intdash-sdk/typescript/latest)
  - 過去のバージョンのリファレンスは [こちら](https://docs.intdash.jp/api/intdash-sdk/typescript-versions)
- [npm](https://www.npmjs.com/package/@aptpod/iscp-ts)

## Version history

### v1.1.1 (2025-02-18)

#### Security

- 依存パッケージ [`elliptic`](https://www.npmjs.com/package/elliptic) を削除し、[脆弱性](https://www.npmjs.com/advisories/1102309) への対策を実施しました。

### v1.1.0 (2024-10-17)

#### Features

- サポートする Node.js のバージョンに 22 を追加しました。

#### Bug Fixes

- Downstream 処理において、DownstreamChunk を取得する際に、参照メモリが開放されない不具合を修正しました。
- Upstream を開く際に、FlushPolicy に intervalOnly、bufferSizeOnly、intervalOrBufferSize を指定した場合に、参照メモリが開放されない不具合を修正しました。

### v1.0.0 (2024-04-16)

#### Security

- サポートする Node.js のバージョンを 18.12.0 以上にしました。

#### Documentation

- README に記載しているアップストリームを行うコードにおいて、BaseTime の Priority を 255 以下の数値で設定するように修正しました。

### v0.12.1 (2024-03-19)

#### Bug Fixes

- ブラウザ、または Node.js の[ストリーム API](https://developer.mozilla.org/ja/docs/Web/API/Streams_API) が [web-streams-polyfill](https://www.npmjs.com/package/web-streams-polyfill) で置換されないように修正しました。

### v0.12.0 (2024-03-13)

#### Features

- Node.js で実行する場合に、環境変数を使用して Proxy Server を設定する機能を追加しました。

### v0.11.1 (2023-05-23)

#### Security

- Node.js v14 のサポートを終了しました。

#### Bug Fixes

- Upstream で Ack を全て受信済みの状態で Close した時に、Close Timeout の時間が経過するまで Close の処理が完了しない不具合を修正しました。

### v0.11.0 (2023-04-13)

#### Features

- Conn に Reconnecting、Reconnected イベントを追加しました。
- ISCPFailedMessageError に resultCode、resultString のプロパティを追加しました。
- DownstreamConfig、Downstream クラスに omitEmptyChunk のプロパティを追加しました。

#### Bug Fixes

- Conn.connect で autoReconnect を指定した時に再接続が正常に動作しない不具合を修正しました。

### v0.10.0 (2023-01-27)

#### Features

- Upstream の機能を追加しました。
- E2E Call の機能を追加しました。
- WebSocketConnector の enableTLS のデフォルトを true に変更しました。
- Conn クラスにデフォルト値の static プロパティを追加しました。
- EventListenerOptions から timeout, onTimeout のプロパティを削除しました。(Braking Change)
- README に「アップストリームとダウンストリーム」のサンプルを追加しました。
- README に「E2E Call」のサンプルを追加しました。
- API Document の Index の Category を変更しました。

#### Bug Fixes

- Downstream で例外が発生した時に Close されない不具合を修正しました。
- Disconnect メッセージを受信した時に再接続しないように修正しました。

### v0.9.1 (2022-11-17)

#### Features

- README にアクセストークンを取得するサンプルを追加しました。
- README に Version history を追加しました。

#### Bug Fixes

- 依存パッケージの指定に[web-streams-polyfill](https://www.npmjs.com/package/web-streams-polyfill)が含まれていない不具合を修正しました。
- WebTransportConnector を使用して iSCP に接続できない不具合を修正しました。

### v0.9.0 (2022-11-08)

#### Features

- ベータバージョン初回リリース。
