Webアプリ間でWebSocket使ってデータをリアルタイムにやりとりするの作ってみたのでメモ

# 概要

ちょっとしたデモを作る時など、PCやスマホ、IoTデバイス等の間でリアルタイムにデータをやりとりしたいときって結構あります。
そんなとき Firestore 使うと便利なんですけど、Internet につながらない環境という場合がけっこうありまして、そんなのときのためにローカル回線の中で簡易 Firestore 的なこと（データを更新したら各WebApp上のデータも自動でリアルタイムに更新されるというもの）を実現してみました。

# 作ったもの

## 同期するデータ(サーバ/クライアントで共有)

### synchroData.ts

```ts
interface AllData {
  aaa: number
  bbb: string
  ccc: string[]
}

const allData: AllData = {
  aaa: 123,
  bbb: 'abc',
  ccc: [],
}

export default {}
export { allData, AllData }
```


## WebSocketサーバ(node-ts)

### package.json

```json
{
  "name": "synchro-data",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "ts-node src/index.ts"
  },
  "license": "MIT",
  "devDependencies": {
    "typescript": "^4.5.4"
  },
  "dependencies": {
    "@types/lodash": "^4.14.178",
    "@types/node": "^17.0.8",
    "@types/ws": "^8.2.2",
    "lodash": "^4.17.21",
    "ts-node": "^10.4.0",
    "ws": "^8.4.0"
  }
}
```

### index.ts

```ts
import { Server } from 'ws'
import { set } from 'lodash'
import { allData, AllData } from './synchroData'

const ws = new Server({ port: 5001 });

ws.on('connection', socket => {
  console.log('connected!')

  // 初期データ受け取り
  socket.send(JSON.stringify({
    allData,
  }))

  socket.on('message', msg => {
    console.log('receive: ' + msg);
    // 自身のデータ更新
    const received = JSON.parse(`${msg}`)
    if (received.method === 'set') {
      set(allData, received.path, received.data)
      console.log('allData', allData)
    }
    // クライアントにリレー
    ws.clients.forEach(client => {
      // console.log(`${msg}`);
      client.send(`${msg}`)
    })
  })

  socket.on('close', () => {
    console.log('good bye.');
  })
})

console.log('Start!')
```

### tsconfig.json

```json
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "ES2021",
    "sourceMap": true,
    "types": [
      "node"
    ]
  },
  "include": [
    "./src/**/*"
  ],
  "exclude": [
    "node_modules"
  ]
}
```

## クライアント(nuxt3)

### package.json

```json
{
  "private": true,
  "scripts": {
    "dev": "nuxi dev",
    "build": "nuxi build",
    "start": "node .output/server/index.mjs"
  },
  "devDependencies": {
    "nuxt3": "latest"
  },
  "dependencies": {
    "@types/lodash": "^4.14.178",
    "lodash": "^4.17.21"
  }
}
```


### nuxt.config.ts

```ts
import { defineNuxtConfig } from 'nuxt3'

// https://v3.nuxtjs.org/docs/directory-structure/nuxt.config
export default defineNuxtConfig({
  ssr: false,
})
```


### plugins/SynchroDataPlugin.ts

```ts
import { reactive, ref } from 'vue'
import { defineNuxtPlugin } from '#app'
import { set as lodashSet } from 'lodash'
import { allData, AllData } from '~/plugins/synchroData'

interface SynchroDataJournal {
  method: 'set' | 'append'
  path: string
  data: any
}

const initSynchroData = <SynchroData extends object> (defaultData: SynchroData) => {
  type ResOfAllData = { allData: SynchroData }

  const isResOfAllData = <SynchroData> (res: any): res is ResOfAllData => {
    return !!res['allData']
  }

  const synchroData = reactive<SynchroData>(defaultData)
  const synchroDataStatus = ref('')

  const mutateSynchroData = (journal: SynchroDataJournal) => {
    if (journal.method === 'set') {
      lodashSet(synchroData, journal.path, journal.data)
      // console.log('allData', allData)
    }
  }

  const ws = new WebSocket('ws://localhost:5001')
  ws.addEventListener('open', (e) => {
    synchroDataStatus.value = 'Socket Success'
  })
  ws.addEventListener('message', (e) => {
    synchroDataStatus.value = `received: ${e.data}`
    const received = JSON.parse(`${e.data}`) as ResOfAllData | SynchroDataJournal
    if (isResOfAllData(received)) {
      Object.assign(synchroData, received.allData)
    } else {
      mutateSynchroData(received)
    }
  })

  const synchroDataEmit = (journal: SynchroDataJournal) => {
    ws.send(JSON.stringify(journal))
  }

  return {
    provide: {
      synchroData,
      synchroDataStatus,
      synchroDataEmit,
    }
  }
}

export default defineNuxtPlugin(nuxtApp => initSynchroData<AllData>(allData))
```

### pages/index.vue

```vue
<template>
  <div>
    <div>{{ $synchroDataStatus }}</div>
    <div>{{ $synchroData }}</div>
    <button @click="dataChange">
      dataChange
    </button>
  </div>
</template>

<script setup lang="ts">
const { $synchroData, $synchroDataStatus, $synchroDataEmit } = useNuxtApp()

const dataChange = () => {
  $synchroDataEmit({
    method: 'set',
    path: 'aaa',
    data: $synchroData.aaa + 1,
  })
}
</script>
```


# 結果

これで $synchroData には常に最新のデータがリアクティブに入っている状態になる。